<commit_before>import React from 'react';
import PropTypes from 'prop-types';
import { Autobind } from 'es-decorators';
import { toastr } from 'react-redux-toastr';

// Redux
import { connect } from 'react-redux';

import { setFilters, setColor, setCategory, setValue, setSize, setOrderBy,
  setAggregateFunction, setLimit, setChartType } from 'redactions/widgetEditor';

// Services
import WidgetService from 'services/WidgetService';

// Components
import Spinner from 'components/ui/Spinner';
import WidgetEditor from 'components/widgets/WidgetEditor';
import Button from 'components/ui/Button';
import Input from 'components/form/Input';
import Field from 'components/form/Field';

// utils
import { getChartConfig } from 'utils/widgets/WidgetHelper';

const FORM_ELEMENTS = {
  elements: {
  },
  validate() {
    const elements = this.elements;
    Object.keys(elements).forEach((k) => {
      elements[k].validate();
    });
  },
  isValid() {
    const elements = this.elements;
    const valid = Object.keys(elements)
      .map(k => elements[k].isValid())
      .filter(v => v !== null)
      .every(element => element);

    return valid;
  }
};

class WidgetsEdit extends React.Component {

  constructor(props) {
    super(props);

    this.state = {
      loading: true,
      submitting: false,
      widget: null
    };

    // Services
    this.widgetService = new WidgetService(this.props.id,
      { apiURL: process.env.CONTROL_TOWER_URL });
  }

  componentWillMount() {
    this.widgetService.fetchData().then((data) => {
      this.setState({
        widget: data,
        loading: false
      }, () => {
        this.loadWidgetIntoRedux();
      });
    });
  }

  @Autobind
  async onSubmit(event) {
    if (event) {
      event.preventDefault();
    }

    this.setState({
      loading: true
    });
    const { widget } = this.state;
    const widgetAtts = widget.attributes;
    const dataset = widgetAtts.dataset;
    const { widgetEditor, tableName, user } = this.props;
    const { limit, value, category, color, size, orderBy, aggregateFunction,
      chartType, filters, areaIntersection } = widgetEditor;

    let chartConfig;
    try {
      chartConfig = await getChartConfig(widgetEditor, tableName, dataset);
    } catch (err) {
      this.setState({
        saved: false,
        error: true,
        errorMessage: 'Unable to generate the configuration of the chart'
      });

      return;
    }

    const widgetConfig = {
      widgetConfig: Object.assign(
        {},
        {
          paramsConfig: {
            limit,
            value,
            category,
            color,
            size,
            orderBy,
            aggregateFunction,
            chartType,
            filters,
            areaIntersection
          }
        },
        chartConfig
      )
    };

    const widgetObj = Object.assign(
      {},
      {
        id: widget.id,
        application: widgetAtts.application,
        name: widgetAtts.name,
        description: widgetAtts.description,
        authors: widgetAtts.authors,
        source: widgetAtts.source,
        sourceUrl: widgetAtts.sourceUrl
      },
      widgetConfig
    );

    this.widgetService.updateUserWidget(widgetObj, dataset, user.token)
      .then((response) => {
        if (response.errors) {
          const errorMessage = response.errors[0].detail;
          this.setState({
            saved: false,
            loading: false,
            error: true,
            errorMessage
          });
          toastr('Error', errorMessage);
        } else {
          this.setState({
            saved: true,
            loading: false,
            error: false
          });
          toastr('Success', 'Widget updated successfully!');
        }
      }).catch((err) => {
        this.setState({
          saved: false,
          error: true
        });
        toastr('Error', err);
      });
  }

  loadWidgetIntoRedux() {
    const { paramsConfig } = this.state.widget.attributes.widgetConfig;
    const {
        value,
        category,
        color,
        size,
        aggregateFunction,
        orderBy,
        filters,
        limit,
        chartType
      } = paramsConfig;

    if (aggregateFunction) {
      this.props.setAggregateFunction(aggregateFunction);
    }
    if (value) {
      this.props.setValue(value);
    }
    if (size) {
      this.props.setSize(size);
    }
    if (color) {
      this.props.setColor(color);
    }
    if (orderBy) {
      this.props.setOrderBy(orderBy);
    }
    if (category) {
      this.props.setCategory(category);
    }
    if (filters) {
      this.props.setFilters(filters);
    }
    if (limit) {
      this.props.setLimit(limit);
    }
    if (chartType) {
      this.props.setChartType(chartType);
    }
  }

  @Autobind
  handleChange(value) {
    const newWidgetAtts = Object.assign({}, this.state.widget.attributes, value);
    const newWidgetObj = Object.assign({}, this.state.widget, { attributes: newWidgetAtts });
    this.setState({ widget: newWidgetObj });
  }

  render() {
    const { loading, widget, submitting } = this.state;
    const widgetAtts = widget && widget.attributes;

    return (

        <Spinner
          className="-relative -light"
          isLoading={loading}
        />
        {widget &&

          <WidgetEditor
            widget={widget}
            dataset={widget.attributes.dataset}
            availableVisualizations={['chart', 'table']}
            mode="widget"
            onUpdateWidget={this.onSubmit}
            showSaveButton
          />

            <form className="form-container" onSubmit={this.onSubmit}>
              <fieldset className="c-field-container">
                <Field
                  ref={(c) => { if (c) FORM_ELEMENTS.elements.title = c; }}
                  onChange={value => this.handleChange({ name: value })}
                  validations={['required']}
                  properties={{
                    title: 'title',
                    label: 'Title',
                    type: 'text',
                    required: true,
                    default: widgetAtts.name,
                    placeholder: 'Widget title'
                  }}
                >
                  {Input}
                </Field>
                <Field
                  ref={(c) => { if (c) FORM_ELEMENTS.elements.description = c; }}
                  onChange={value => this.handleChange({ description: value })}
                  properties={{
                    title: 'description',
                    label: 'Description',
                    type: 'text',
                    default: widgetAtts.description,
                    placeholder: 'Widget description'
                  }}
                >
                  {Input}
                </Field>
                <Field
                  ref={(c) => { if (c) FORM_ELEMENTS.elements.authors = c; }}
                  onChange={value => this.handleChange({ authors: value })}
                  properties={{
                    title: 'authors',
                    label: 'Authors',
                    type: 'text',
                    default: widgetAtts.authors,
                    placeholder: 'Author name'
                  }}
                >
                  {Input}
                </Field>

                  <Field
                    ref={(c) => { if (c) FORM_ELEMENTS.elements.source = c; }}
                    onChange={value => this.handleChange({ source: value })}
                    properties={{
                      title: 'source',
                      label: 'Source name',
                      type: 'text',
                      default: widgetAtts.source,
                      placeholder: 'Source name'
                    }}
                  >
                    {Input}
                  </Field>
                  <Field
                    ref={(c) => { if (c) FORM_ELEMENTS.elements.sourceUrl = c; }}
                    onChange={value => this.handleChange({ sourceUrl: value })}
                    properties={{
                      title: 'sourceUrl',
                      label: 'Source URL',
                      type: 'text',
                      default: widgetAtts.sourceUrl,
                      placeholder: 'Paste a URL here'
                    }}
                  >
                    {Input}
                  </Field>

              </fieldset>

                <Button
                  properties={{
                    type: 'submit',
                    disabled: submitting,
                    className: '-secondary'
                  }}
                >
                  Save
                </Button>

            </form>

        }

    );
  }
}

WidgetsEdit.propTypes = {
  id: PropTypes.string.isRequired,
  // Store
  user: PropTypes.object.isRequired,
  // ACTIONS
  setFilters: PropTypes.func.isRequired,
  setSize: PropTypes.func.isRequired,
  setColor: PropTypes.func.isRequired,
  setCategory: PropTypes.func.isRequired,
  setValue: PropTypes.func.isRequired,
  setOrderBy: PropTypes.func.isRequired,
  setAggregateFunction: PropTypes.func.isRequired,
  setLimit: PropTypes.func.isRequired,
  setChartType: PropTypes.func.isRequired
};

const mapStateToProps = state => ({
  user: state.user,
  widgetEditor: state.widgetEditor
});

const mapDispatchToProps = dispatch => ({
  setFilters: (filter) => {
    dispatch(setFilters(filter));
  },
  setColor: (color) => {
    dispatch(setColor(color));
  },
  setSize: (size) => {
    dispatch(setSize(size));
  },
  setCategory: (category) => {
    dispatch(setCategory(category));
  },
  setValue: (value) => {
    dispatch(setValue(value));
  },
  setOrderBy: (value) => {
    dispatch(setOrderBy(value));
  },
  setAggregateFunction: (value) => {
    dispatch(setAggregateFunction(value));
  },
  setLimit: (value) => {
    dispatch(setLimit(value));
  },
  setChartType: (value) => {
    dispatch(setChartType(value));
  }
});

export default connect(mapStateToProps, mapDispatchToProps)(WidgetsEdit);
<commit_msg>fix toastr calls<commit_after>import React from 'react';
import PropTypes from 'prop-types';
import { Autobind } from 'es-decorators';
import { toastr } from 'react-redux-toastr';

// Redux
import { connect } from 'react-redux';

import { setFilters, setColor, setCategory, setValue, setSize, setOrderBy,
  setAggregateFunction, setLimit, setChartType } from 'redactions/widgetEditor';

// Services
import WidgetService from 'services/WidgetService';
import DatasetService from 'services/DatasetService';

// Components
import Spinner from 'components/ui/Spinner';
import WidgetEditor from 'components/widgets/WidgetEditor';
import Button from 'components/ui/Button';
import Input from 'components/form/Input';
import Field from 'components/form/Field';

// utils
import { getChartConfig } from 'utils/widgets/WidgetHelper';

const FORM_ELEMENTS = {
  elements: {
  },
  validate() {
    const elements = this.elements;
    Object.keys(elements).forEach((k) => {
      elements[k].validate();
    });
  },
  isValid() {
    const elements = this.elements;
    const valid = Object.keys(elements)
      .map(k => elements[k].isValid())
      .filter(v => v !== null)
      .every(element => element);

    return valid;
  }
};

class WidgetsEdit extends React.Component {

  constructor(props) {
    super(props);

    this.state = {
      loading: true,
      submitting: false,
      widget: null,
      tableName: null
    };

    // Services
    this.widgetService = new WidgetService(this.props.id,
      { apiURL: process.env.CONTROL_TOWER_URL });
  }

  componentWillMount() {
    this.widgetService.fetchData().then((data) => {
      this.datasetService = new DatasetService(data.attributes.dataset,
        { apiURL: process.env.CONTROL_TOWER_URL });
      this.datasetService.fetchData().then((response) => {
        this.setState({
          widget: data,
          loading: false,
          tableName: response.attributes.tableName
        }, () => {
          this.loadWidgetIntoRedux();
        });
      });
    });
  }

  @Autobind
  async onSubmit(event) {
    if (event) {
      event.preventDefault();
    }

    this.setState({
      loading: true
    });
    const { widget, tableName } = this.state;
    const widgetAtts = widget.attributes;
    const dataset = widgetAtts.dataset;
    const { widgetEditor, user } = this.props;
    const { limit, value, category, color, size, orderBy, aggregateFunction,
      chartType, filters, areaIntersection } = widgetEditor;

    let chartConfig;
    try {
      chartConfig = await getChartConfig(widgetEditor, tableName, dataset);
    } catch (err) {
      this.setState({
        saved: false,
        error: true,
        errorMessage: 'Unable to generate the configuration of the chart'
      });

      return;
    }

    const widgetConfig = {
      widgetConfig: Object.assign(
        {},
        {
          paramsConfig: {
            limit,
            value,
            category,
            color,
            size,
            orderBy,
            aggregateFunction,
            chartType,
            filters,
            areaIntersection
          }
        },
        chartConfig
      )
    };

    const widgetObj = Object.assign(
      {},
      {
        id: widget.id,
        application: widgetAtts.application,
        name: widgetAtts.name,
        description: widgetAtts.description,
        authors: widgetAtts.authors,
        source: widgetAtts.source,
        sourceUrl: widgetAtts.sourceUrl
      },
      widgetConfig
    );

    this.widgetService.updateUserWidget(widgetObj, dataset, user.token)
      .then((response) => {
        if (response.errors) {
          const errorMessage = response.errors[0].detail;
          this.setState({
            saved: false,
            loading: false,
            error: true,
            errorMessage
          });
          toastr.error('Error', errorMessage);
        } else {
          this.setState({
            saved: true,
            loading: false,
            error: false
          });
          toastr.success('Success', 'Widget updated successfully!');
        }
      }).catch((err) => {
        this.setState({
          saved: false,
          error: true
        });
        toastr.error('Error', err);
      });
  }

  loadWidgetIntoRedux() {
    const { paramsConfig } = this.state.widget.attributes.widgetConfig;
    const {
        value,
        category,
        color,
        size,
        aggregateFunction,
        orderBy,
        filters,
        limit,
        chartType
      } = paramsConfig;

    if (aggregateFunction) {
      this.props.setAggregateFunction(aggregateFunction);
    }
    if (value) {
      this.props.setValue(value);
    }
    if (size) {
      this.props.setSize(size);
    }
    if (color) {
      this.props.setColor(color);
    }
    if (orderBy) {
      this.props.setOrderBy(orderBy);
    }
    if (category) {
      this.props.setCategory(category);
    }
    if (filters) {
      this.props.setFilters(filters);
    }
    if (limit) {
      this.props.setLimit(limit);
    }
    if (chartType) {
      this.props.setChartType(chartType);
    }
  }

  @Autobind
  handleChange(value) {
    const newWidgetAtts = Object.assign({}, this.state.widget.attributes, value);
    const newWidgetObj = Object.assign({}, this.state.widget, { attributes: newWidgetAtts });
    this.setState({ widget: newWidgetObj });
  }

  render() {
    const { loading, widget, submitting, tableName } = this.state;
    const widgetAtts = widget && widget.attributes;

    return (

        <Spinner
          className="-relative -light"
          isLoading={loading}
        />
        {widget && tableName &&

          <WidgetEditor
            widget={widget}
            dataset={widget.attributes.dataset}
            availableVisualizations={['chart', 'table']}
            mode="widget"
            onUpdateWidget={this.onSubmit}
            showSaveButton
            showShareEmbedButton={false}
            tableName={tableName}
          />

            <form className="form-container" onSubmit={this.onSubmit}>
              <fieldset className="c-field-container">
                <Field
                  ref={(c) => { if (c) FORM_ELEMENTS.elements.title = c; }}
                  onChange={value => this.handleChange({ name: value })}
                  validations={['required']}
                  properties={{
                    title: 'title',
                    label: 'Title',
                    type: 'text',
                    required: true,
                    default: widgetAtts.name,
                    placeholder: 'Widget title'
                  }}
                >
                  {Input}
                </Field>
                <Field
                  ref={(c) => { if (c) FORM_ELEMENTS.elements.description = c; }}
                  onChange={value => this.handleChange({ description: value })}
                  properties={{
                    title: 'description',
                    label: 'Description',
                    type: 'text',
                    default: widgetAtts.description,
                    placeholder: 'Widget description'
                  }}
                >
                  {Input}
                </Field>
                <Field
                  ref={(c) => { if (c) FORM_ELEMENTS.elements.authors = c; }}
                  onChange={value => this.handleChange({ authors: value })}
                  properties={{
                    title: 'authors',
                    label: 'Authors',
                    type: 'text',
                    default: widgetAtts.authors,
                    placeholder: 'Author name'
                  }}
                >
                  {Input}
                </Field>

                  <Field
                    ref={(c) => { if (c) FORM_ELEMENTS.elements.source = c; }}
                    onChange={value => this.handleChange({ source: value })}
                    properties={{
                      title: 'source',
                      label: 'Source name',
                      type: 'text',
                      default: widgetAtts.source,
                      placeholder: 'Source name'
                    }}
                  >
                    {Input}
                  </Field>
                  <Field
                    ref={(c) => { if (c) FORM_ELEMENTS.elements.sourceUrl = c; }}
                    onChange={value => this.handleChange({ sourceUrl: value })}
                    properties={{
                      title: 'sourceUrl',
                      label: 'Source URL',
                      type: 'text',
                      default: widgetAtts.sourceUrl,
                      placeholder: 'Paste a URL here'
                    }}
                  >
                    {Input}
                  </Field>

              </fieldset>

                <Button
                  properties={{
                    type: 'submit',
                    disabled: submitting,
                    className: '-secondary'
                  }}
                >
                  Save
                </Button>

            </form>

        }

    );
  }
}

WidgetsEdit.propTypes = {
  id: PropTypes.string.isRequired,
  // Store
  user: PropTypes.object.isRequired,
  // ACTIONS
  setFilters: PropTypes.func.isRequired,
  setSize: PropTypes.func.isRequired,
  setColor: PropTypes.func.isRequired,
  setCategory: PropTypes.func.isRequired,
  setValue: PropTypes.func.isRequired,
  setOrderBy: PropTypes.func.isRequired,
  setAggregateFunction: PropTypes.func.isRequired,
  setLimit: PropTypes.func.isRequired,
  setChartType: PropTypes.func.isRequired
};

const mapStateToProps = state => ({
  user: state.user,
  widgetEditor: state.widgetEditor
});

const mapDispatchToProps = dispatch => ({
  setFilters: (filter) => {
    dispatch(setFilters(filter));
  },
  setColor: (color) => {
    dispatch(setColor(color));
  },
  setSize: (size) => {
    dispatch(setSize(size));
  },
  setCategory: (category) => {
    dispatch(setCategory(category));
  },
  setValue: (value) => {
    dispatch(setValue(value));
  },
  setOrderBy: (value) => {
    dispatch(setOrderBy(value));
  },
  setAggregateFunction: (value) => {
    dispatch(setAggregateFunction(value));
  },
  setLimit: (value) => {
    dispatch(setLimit(value));
  },
  setChartType: (value) => {
    dispatch(setChartType(value));
  }
});

export default connect(mapStateToProps, mapDispatchToProps)(WidgetsEdit);
<|endoftext|>


<commit_before>class ConfigService {
  time = true
  code = false
  partials = false
  size = 1

  editSizes = [[128, 128], [256, 256], [384, 384], [512, 512]];
  editSizeIndex = 1;
  editSize = this.editSizes[this.editSizeIndex];

  thumbnailSizes = [[64, 64], [128, 32], [128, 128], [192, 48], [192, 192], [256, 64], [256, 256], [384, 96]]
  thumbnailSizeIndex = 2
  thumbnailSize = this.thumbnailSizes[this.thumbnailSizeIndex]
}

export default new ConfigService()
<commit_msg>glt-vue<commit_after>class ConfigService {
  time = true
  code = false
  partials = false
  size = 1

  editSizes = [[256, 256], [384, 384], [512, 512]];
  editSizeIndex = 0;
  editSize = this.editSizes[this.editSizeIndex];

  thumbnailSizes = [[64, 64], [128, 32], [128, 128], [192, 48], [192, 192], [256, 64], [256, 256], [384, 96]]
  thumbnailSizeIndex = 2
  thumbnailSize = this.thumbnailSizes[this.thumbnailSizeIndex]
}

export default new ConfigService()
<|endoftext|>


<commit_before>/**
  @module ember-data
*/

import Ember from 'ember';
import { assert } from "ember-data/-private/debug";
import normalizeModelName from "ember-data/-private/system/normalize-model-name";
import isArrayLike from "ember-data/-private/system/is-array-like";

/**
  `DS.hasMany` is used to define One-To-Many and Many-To-Many
  relationships on a [DS.Model](/api/data/classes/DS.Model.html).

  `DS.hasMany` takes an optional hash as a second parameter, currently
  supported options are:

  - `async`: A boolean value used to explicitly declare this to be an async relationship.
  - `inverse`: A string used to identify the inverse property on a related model.

  #### One-To-Many
  To declare a one-to-many relationship between two models, use
  `DS.belongsTo` in combination with `DS.hasMany`, like this:

  ```app/models/post.js
  import DS from 'ember-data';

  export default DS.Model.extend({
    comments: DS.hasMany('comment')
  });
  ```

  ```app/models/comment.js
  import DS from 'ember-data';

  export default DS.Model.extend({
    post: DS.belongsTo('post')
  });
  ```

  #### Many-To-Many
  To declare a many-to-many relationship between two models, use
  `DS.hasMany`:

  ```app/models/post.js
  import DS from 'ember-data';

  export default DS.Model.extend({
    tags: DS.hasMany('tag')
  });
  ```

  ```app/models/tag.js
  import DS from 'ember-data';

  export default DS.Model.extend({
    posts: DS.hasMany('post')
  });
  ```

  You can avoid passing a string as the first parameter. In that case Ember Data
  will infer the type from the singularized key name.

  ```app/models/post.js
  import DS from 'ember-data';

  export default DS.Model.extend({
    tags: DS.hasMany()
  });
  ```

  will lookup for a Tag type.

  #### Explicit Inverses

  Ember Data will do its best to discover which relationships map to
  one another. In the one-to-many code above, for example, Ember Data
  can figure out that changing the `comments` relationship should update
  the `post` relationship on the inverse because post is the only
  relationship to that model.

  However, sometimes you may have multiple `belongsTo`/`hasManys` for the
  same type. You can specify which property on the related model is
  the inverse using `DS.hasMany`'s `inverse` option:

  ```app/models/comment.js
  import DS from 'ember-data';

  export default DS.Model.extend({
    onePost: DS.belongsTo('post'),
    twoPost: DS.belongsTo('post'),
    redPost: DS.belongsTo('post'),
    bluePost: DS.belongsTo('post')
  });
  ```

  ```app/models/post.js
  import DS from 'ember-data';

  export default DS.Model.extend({
    comments: DS.hasMany('comment', {
      inverse: 'redPost'
    })
  });
  ```

  You can also specify an inverse on a `belongsTo`, which works how
  you'd expect.

  @namespace
  @method hasMany
  @for DS
  @param {String} type (optional) type of the relationship
  @param {Object} options (optional) a hash of options
  @return {Ember.computed} relationship
*/
export default function hasMany(type, options) {
  if (typeof type === 'object') {
    options = type;
    type = undefined;
  }

  assert("The first argument to DS.hasMany must be a string representing a model type key, not an instance of " + Ember.inspect(type) + ". E.g., to define a relation to the Comment model, use DS.hasMany('comment')", typeof type === 'string' || typeof type === 'undefined');

  options = options || {};

  if (typeof type === 'string') {
    type = normalizeModelName(type);
  }

  // Metadata about relationships is stored on the meta of
  // the relationship. This is used for introspection and
  // serialization. Note that `key` is populated lazily
  // the first time the CP is called.
  var meta = {
    type: type,
    isRelationship: true,
    options: options,
    kind: 'hasMany',
    key: null
  };

  return Ember.computed({
    get(key) {
      var relationship = this._internalModel._relationships.get(key);
      return relationship.getRecords();
    },
    set(key, records) {
      var Model = require('ember-data/model').default;
      assert("You must pass an array of records to set a hasMany relationship", isArrayLike(records));
      assert(`All elements of a hasMany relationship must be instances of DS.Model, you passed ${Ember.inspect(records)}`, (function() {
        return Ember.A(records).every((record) => Model.detectInstance(record) );
      })());

      var relationship = this._internalModel._relationships.get(key);
      relationship.clear();
      relationship.addRecords(Ember.A(records).mapBy('_internalModel'));
      return relationship.getRecords();
    }
  }).meta(meta);
}

export const HasManyMixin = Ember.Mixin.create({
  notifyHasManyAdded(key) {
    //We need to notifyPropertyChange in the adding case because we need to make sure
    //we fetch the newly added record in case it is unloaded
    //TODO(Igor): Consider whether we could do this only if the record state is unloaded

    //Goes away once hasMany is double promisified
    this.notifyPropertyChange(key);
  }
});
<commit_msg>Fix global require<commit_after>/**
  @module ember-data
*/

import Ember from 'ember';
import { assert } from "ember-data/-private/debug";
import normalizeModelName from "ember-data/-private/system/normalize-model-name";
import isArrayLike from "ember-data/-private/system/is-array-like";

/**
  `DS.hasMany` is used to define One-To-Many and Many-To-Many
  relationships on a [DS.Model](/api/data/classes/DS.Model.html).

  `DS.hasMany` takes an optional hash as a second parameter, currently
  supported options are:

  - `async`: A boolean value used to explicitly declare this to be an async relationship.
  - `inverse`: A string used to identify the inverse property on a related model.

  #### One-To-Many
  To declare a one-to-many relationship between two models, use
  `DS.belongsTo` in combination with `DS.hasMany`, like this:

  ```app/models/post.js
  import DS from 'ember-data';

  export default DS.Model.extend({
    comments: DS.hasMany('comment')
  });
  ```

  ```app/models/comment.js
  import DS from 'ember-data';

  export default DS.Model.extend({
    post: DS.belongsTo('post')
  });
  ```

  #### Many-To-Many
  To declare a many-to-many relationship between two models, use
  `DS.hasMany`:

  ```app/models/post.js
  import DS from 'ember-data';

  export default DS.Model.extend({
    tags: DS.hasMany('tag')
  });
  ```

  ```app/models/tag.js
  import DS from 'ember-data';

  export default DS.Model.extend({
    posts: DS.hasMany('post')
  });
  ```

  You can avoid passing a string as the first parameter. In that case Ember Data
  will infer the type from the singularized key name.

  ```app/models/post.js
  import DS from 'ember-data';

  export default DS.Model.extend({
    tags: DS.hasMany()
  });
  ```

  will lookup for a Tag type.

  #### Explicit Inverses

  Ember Data will do its best to discover which relationships map to
  one another. In the one-to-many code above, for example, Ember Data
  can figure out that changing the `comments` relationship should update
  the `post` relationship on the inverse because post is the only
  relationship to that model.

  However, sometimes you may have multiple `belongsTo`/`hasManys` for the
  same type. You can specify which property on the related model is
  the inverse using `DS.hasMany`'s `inverse` option:

  ```app/models/comment.js
  import DS from 'ember-data';

  export default DS.Model.extend({
    onePost: DS.belongsTo('post'),
    twoPost: DS.belongsTo('post'),
    redPost: DS.belongsTo('post'),
    bluePost: DS.belongsTo('post')
  });
  ```

  ```app/models/post.js
  import DS from 'ember-data';

  export default DS.Model.extend({
    comments: DS.hasMany('comment', {
      inverse: 'redPost'
    })
  });
  ```

  You can also specify an inverse on a `belongsTo`, which works how
  you'd expect.

  @namespace
  @method hasMany
  @for DS
  @param {String} type (optional) type of the relationship
  @param {Object} options (optional) a hash of options
  @return {Ember.computed} relationship
*/
export default function hasMany(type, options) {
  if (typeof type === 'object') {
    options = type;
    type = undefined;
  }

  assert("The first argument to DS.hasMany must be a string representing a model type key, not an instance of " + Ember.inspect(type) + ". E.g., to define a relation to the Comment model, use DS.hasMany('comment')", typeof type === 'string' || typeof type === 'undefined');

  options = options || {};

  if (typeof type === 'string') {
    type = normalizeModelName(type);
  }

  // Metadata about relationships is stored on the meta of
  // the relationship. This is used for introspection and
  // serialization. Note that `key` is populated lazily
  // the first time the CP is called.
  var meta = {
    type: type,
    isRelationship: true,
    options: options,
    kind: 'hasMany',
    key: null
  };

  return Ember.computed({
    get(key) {
      var relationship = this._internalModel._relationships.get(key);
      return relationship.getRecords();
    },
    set(key, records) {
      assert("You must pass an array of records to set a hasMany relationship", isArrayLike(records));
      assert(`All elements of a hasMany relationship must be instances of DS.Model, you passed ${Ember.inspect(records)}`, (function() {
        return Ember.A(records).every((record) => record.hasOwnProperty('_internalModel') === true);
      })());

      var relationship = this._internalModel._relationships.get(key);
      relationship.clear();
      relationship.addRecords(Ember.A(records).mapBy('_internalModel'));
      return relationship.getRecords();
    }
  }).meta(meta);
}

export const HasManyMixin = Ember.Mixin.create({
  notifyHasManyAdded(key) {
    //We need to notifyPropertyChange in the adding case because we need to make sure
    //we fetch the newly added record in case it is unloaded
    //TODO(Igor): Consider whether we could do this only if the record state is unloaded

    //Goes away once hasMany is double promisified
    this.notifyPropertyChange(key);
  }
});
<|endoftext|>


<commit_before>openerp.web_dashboard = function(openerp) {

QWeb.add_template('/web_dashboard/static/src/xml/web_dashboard.xml');

openerp.web.form.DashBoard = openerp.web.form.Widget.extend({
    init: function(view, node) {
        this._super(view, node);
        this.template = 'DashBoard';
        this.actions_attrs = {};
        this.action_managers = [];
    },
    start: function() {
        var self = this;
        this._super.apply(this, arguments);

        this.$element.find('.oe-dashboard-column').sortable({
            connectWith: '.oe-dashboard-column',
            handle: '.oe-dashboard-action-header',
            scroll: false
        }).disableSelection().bind('sortstop', self.do_save_dashboard);

        // Events
        this.$element.find('.oe-dashboard-link-undo').click(this.on_undo);
        this.$element.find('.oe-dashboard-link-reset').click(this.on_reset);
        this.$element.find('.oe-dashboard-link-add_widget').click(this.on_add_widget);
        this.$element.find('.oe-dashboard-link-change_layout').click(this.on_change_layout);

        this.$element.delegate('.oe-dashboard-column .oe-dashboard-fold', 'click', this.on_fold_action);
        this.$element.delegate('.oe-dashboard-column .ui-icon-closethick', 'click', this.on_close_action);

        this.actions_attrs = {};
        // Init actions
        _.each(this.node.children, function(column) {
            _.each(column.children, function(action) {
                delete(action.attrs.width);
                delete(action.attrs.height);
                delete(action.attrs.colspan);
                self.actions_attrs[action.attrs.name] = action.attrs;
                self.rpc('/web/action/load', {
                    action_id: parseInt(action.attrs.name, 10)
                }, self.on_load_action);
            });
        });

        //this.$element.find('a.oe-dashboard-action-rename').live('click', this.on_rename);
        this.$element.find('.oe-dashboard-action').live('mouseover mouseout', function(event) {
            $(this).find('.oe-dashboard-action-header .ui-icon, .oe-dashboard-action-header .oe-dashboard-action-rename').toggle(event.type == 'mouseover');
        });
    },
    on_undo: function() {
        this.rpc('/web/view/undo_custom', {
            view_id: this.view.fields_view.view_id
        }, this.do_reload);
    },
    on_reset: function() {
        this.rpc('/web/view/undo_custom', {
            view_id: this.view.fields_view.view_id,
            reset: true
        }, this.do_reload);
    },
    on_add_widget: function() {
        var self = this;
        var action_manager = new openerp.web.ActionManager(this);
        var dialog = new openerp.web.Dialog(this, {
            title : 'Actions',
            width: 800,
            height: 600,
            buttons : {
                Cancel : function() {
                    $(this).dialog('destroy');
                },
                Add : function() {
                    self.do_add_widget(action_manager.inner_viewmanager.views.list.controller);
                    $(this).dialog('destroy');
                }
            }
        }).start().open();
        action_manager.appendTo(dialog.$element);
        action_manager.do_action({
            res_model : 'ir.actions.actions',
            views : [[false, 'list']],
            type : 'ir.actions.act_window',
            limit : 80,
            auto_search : true,
            flags : {
                sidebar : false,
                views_switcher : false,
                action_buttons : false
            }
        });
        // TODO: should bind ListView#select_record in order to catch record clicking
    },
    do_add_widget : function(listview) {
        var self = this,
            actions = listview.groups.get_selection().ids,
            results = [],
            qdict = { view : this.view };
        // TODO: should load multiple actions at once
        _.each(actions, function(aid) {
            self.rpc('/web/action/load', {
                action_id: aid
            }, function(result) {
                self.actions_attrs[aid] = {
                    name: aid,
                    string: _.trim(result.result.name)
                }
                qdict.action = {
                    attrs : self.actions_attrs[aid]
                }
                self.$element.find('.oe-dashboard-column:first').prepend(QWeb.render('DashBoard.action', qdict));
                self.do_save_dashboard();
                self.on_load_action(result)
            });
        });
    },
    on_rename : function(e) {
        var self = this,
            id = parseInt($(e.currentTarget).parents('.oe-dashboard-action:first').attr('data-id'), 10),
            $header = $(e.currentTarget).parents('.oe-dashboard-action-header:first'),
            $rename = $header.find('a.oe-dashboard-action-rename').hide(),
            $title = $header.find('span.oe-dashboard-action-title').hide(),
            $input = $header.find('input[name=title]');
        $input.val($title.text()).show().focus().bind('keydown', function(e) {
            if (e.which == 13 || e.which == 27) {
                if (e.which == 13) { //enter
                    var val = $input.val();
                    if (!val) {
                        return false;
                    }
                    $title.text(val);
                    self.actions_attrs[id].string = val;
                    self.do_save_dashboard();
                }
                $input.unbind('keydown').hide();
                $rename.show();
                $title.show();
            }
        });
    },
    on_change_layout: function() {
        var self = this;
        var qdict = {
            current_layout : this.$element.find('.oe-dashboard').attr('data-layout')
        };
        var $dialog = $('<div>').dialog({
                            modal: true,
                            title: 'Edit Layout',
                            width: 'auto',
                            height: 'auto'
                        }).html(QWeb.render('DashBoard.layouts', qdict));
        $dialog.find('li').click(function() {
            var layout = $(this).attr('data-layout');
            $dialog.dialog('destroy');
            self.do_change_layout(layout);
        });
    },
    do_change_layout: function(new_layout) {
        var $dashboard = this.$element.find('.oe-dashboard');
        var current_layout = $dashboard.attr('data-layout');
        if (current_layout != new_layout) {
            var clayout = current_layout.split('-').length,
                nlayout = new_layout.split('-').length,
                column_diff = clayout - nlayout;
            if (column_diff > 0) {
                var $last_column = $();
                $dashboard.find('.oe-dashboard-column').each(function(k, v) {
                    if (k >= nlayout) {
                        $(v).find('.oe-dashboard-action').appendTo($last_column);
                    } else {
                        $last_column = $(v);
                    }
                });
            }
            $dashboard.toggleClass('oe-dashboard-layout_' + current_layout + ' oe-dashboard-layout_' + new_layout);
            $dashboard.attr('data-layout', new_layout);
            this.do_save_dashboard();
        }
    },
    on_fold_action: function(e) {
        var $e = $(e.currentTarget),
            $action = $e.parents('.oe-dashboard-action:first'),
            id = parseInt($action.attr('data-id'), 10);
        if ($e.is('.ui-icon-minusthick')) {
            this.actions_attrs[id].fold = '1';
        } else {
            delete(this.actions_attrs[id].fold);
        }
        $e.toggleClass('ui-icon-minusthick ui-icon-plusthick');
        $action.find('.oe-dashboard-action-content').toggle();
        this.do_save_dashboard();
    },
    on_close_action: function(e) {
        $(e.currentTarget).parents('.oe-dashboard-action:first').remove();
        this.do_save_dashboard();
    },
    do_save_dashboard: function() {
        var self = this;
        var board = {
                form_title : this.view.fields_view.arch.attrs.string,
                style : this.$element.find('.oe-dashboard').attr('data-layout'),
                columns : []
            };
        this.$element.find('.oe-dashboard-column').each(function() {
            var actions = [];
            $(this).find('.oe-dashboard-action').each(function() {
                var action_id = $(this).attr('data-id');
                actions.push(self.actions_attrs[action_id]);
            });
            board.columns.push(actions);
        });
        var arch = QWeb.render('DashBoard.xml', board);
        this.rpc('/web/view/add_custom', {
            view_id: this.view.fields_view.view_id,
            arch: arch
        }, function() {
            self.$element.find('.oe-dashboard-link-undo, .oe-dashboard-link-reset').show();
        });
    },
    on_load_action: function(result) {
        var action = result.result;
        action.flags = {
            search_view : false,
            sidebar : false,
            views_switcher : false,
            action_buttons : false,
            pager: false
        };
        var am = new openerp.web.ActionManager(this);
        this.action_managers.push(am);
        am.appendTo($("#"+this.view.element_id + '_action_' + action.id));
        am.do_action(action);
    },
    render: function() {
        // We should start with three columns available
        for (var i = this.node.children.length; i < 3; i++) {
            this.node.children.push({
                tag: 'column',
                attrs: {},
                children: []
            });
        }
        return QWeb.render(this.template, this);
    },
    do_reload: function() {
        _.each(this.action_managers, function(am) {
            am.stop();
        });
        this.action_managers = [];
        this.view.stop();
        this.view.start();
    }
});
openerp.web.form.DashBoardLegacy = openerp.web.form.DashBoard.extend({
    render: function() {
        if (this.node.tag == 'hpaned') {
            this.node.attrs.style = '1-1';
        } else if (this.node.tag == 'vpaned') {
            this.node.attrs.style = '1';
        }
        this.node.tag = 'board';
        _.each(this.node.children, function(child) {
            if (child.tag.indexOf('child') == 0) {
                child.tag = 'column';
                var actions = [], first_child = child.children[0];
                if (first_child && first_child.tag == 'vpaned') {
                    _.each(first_child.children, function(subchild) {
                        actions.push.apply(actions, subchild.children);
                    });
                    child.children = actions;
                }
            }
        });
        return this._super(this, arguments);
    }
});

openerp.web.form.widgets.add('hpaned', 'openerp.web.form.DashBoardLegacy');
openerp.web.form.widgets.add('vpaned', 'openerp.web.form.DashBoardLegacy');
openerp.web.form.widgets.add('board', 'openerp.web.form.DashBoard');

openerp.web.client_actions.add(
    'board.config.overview', 'openerp.web_dashboard.ConfigOverview'
);
if (!openerp.web_dashboard) {
    openerp.web_dashboard = {};
}
openerp.web_dashboard.ConfigOverview = openerp.web.View.extend({
    template: 'ConfigOverview',
    init: function (parent, element_id) {
        this._super(parent, element_id);
        this.dataset = new openerp.web.DataSetSearch(
                this, 'ir.actions.todo');
        this.dataset.domain = [['type', '=', 'manual']];
    },
    start: function () {
        $.when(this.dataset.read_slice(['state', 'action_id', 'category_id']),
               this.dataset.call('progress'))
            .then(this.on_records_loaded);
    },
    on_records_loaded: function (read_response, progress_response) {
        var records = read_response[0].records,
           progress = progress_response[0];

        var grouped_todos = _(records).chain()
            .map(function (record) {
                return {
                    id: record.id,
                    name: record.action_id[1],
                    done: record.state !== 'open',
                    to_do: record.state === 'open',
                    category: record['category_id'][1] || "Uncategorized"
                }
            })
            .groupBy(function (record) {return record.category})
            .value();
        this.$element.html(QWeb.render('ConfigOverview.content', {
            completion: 100 * progress.done / progress.total,
            groups: grouped_todos
        }));
        var $progress = this.$element.find('div.oe-config-progress-bar');
        $progress.progressbar({value: $progress.data('completion')});

        var self = this;
        this.$element.find('dl')
            .delegate('input', 'click', function (e) {
                // switch todo status
                e.stopImmediatePropagation();
                var new_state = this.checked ? 'done' : 'open',
                      todo_id = parseInt($(this).val(), 10);
                self.dataset.write(todo_id, {state: new_state}, {}, function () {
                    self.start();
                });
            })
            .delegate('li:not(.oe-done)', 'click', function () {
                self.execute_action({
                        type: 'object',
                        name: 'action_launch'
                    }, self.dataset,
                    $(this).data('id'), function () {
                        // after action popup closed, refresh configuration
                        // thingie
                        self.start();
                    });
            });
    }
});

openerp.web.client_actions.add(
    'board.home.applications', 'openerp.web_dashboard.ApplicationTiles');
openerp.web_dashboard.ApplicationTiles = openerp.web.View.extend({
    template: 'ApplicationTiles',
    start: function () {
        var self = this;
        return new openerp.web.DataSetSearch(
                this, 'ir.ui.menu', null, [['parent_id', '=', false]])
            .read_slice( ['name', 'web_icon_data', 'web_icon_hover_data'], {}, function (applications) {
                // Create a matrix of 3*x applications
                var rows = [];
                while (applications.length) {
                    rows.push(applications.splice(0, 3));
                }
                self.$element
                    .append(QWeb.render(
                        'ApplicationTiles.content', {rows: rows}))
                    .find('.oe-dashboard-home-tile')
                        .click(function () {
                            var $this = $(this);
                            $this.closest('.openerp')
                                 .find('.menu a[data-menu=' + $this.data('menuid') + ']')
                                 .click();});
            });
    }
});
openerp.web.client_actions.add(
    'board.home.widgets', 'openerp.web_dashboard.Widget');
openerp.web_dashboard.Widget = openerp.web.View.extend(/** @lends openerp.web_dashboard.Widgets# */{
    template: 'HomeWidget',
    /**
     * Initializes a "HomeWidget" client widget: handles the display of a given
     * res.widget objects in an OpenERP view (mainly a dashboard).
     *
     * @constructs
     * @extends openerp.web.View
     * @param {Object} parent
     * @param {Object} options
     * @param {Number} options.widget_id
     */
    init: function (parent, options) {
        this._super(parent);
        this.widget_id = options.widget_id;
    },
    start: function () {
        return new openerp.web.DataSet(this, 'res.widget').read_ids(
                [this.widget_id], ['title'], this.on_widget_loaded);
    },
    on_widget_loaded: function (widgets) {
        var widget = widgets[0];
        var url = _.sprintf(
            '/web_dashboard/widgets/content?session_id=%s&widget_id=%d',
            this.session.session_id, widget.id);
        this.$element.html(QWeb.render('HomeWidget.content', {
            widget: widget,
            url: url
        }));
    }
});
};
<commit_msg>[FIX] configuration overview actions should not open in its dashboard section<commit_after>openerp.web_dashboard = function(openerp) {

QWeb.add_template('/web_dashboard/static/src/xml/web_dashboard.xml');

openerp.web.form.DashBoard = openerp.web.form.Widget.extend({
    init: function(view, node) {
        this._super(view, node);
        this.template = 'DashBoard';
        this.actions_attrs = {};
        this.action_managers = [];
    },
    start: function() {
        var self = this;
        this._super.apply(this, arguments);

        this.$element.find('.oe-dashboard-column').sortable({
            connectWith: '.oe-dashboard-column',
            handle: '.oe-dashboard-action-header',
            scroll: false
        }).disableSelection().bind('sortstop', self.do_save_dashboard);

        // Events
        this.$element.find('.oe-dashboard-link-undo').click(this.on_undo);
        this.$element.find('.oe-dashboard-link-reset').click(this.on_reset);
        this.$element.find('.oe-dashboard-link-add_widget').click(this.on_add_widget);
        this.$element.find('.oe-dashboard-link-change_layout').click(this.on_change_layout);

        this.$element.delegate('.oe-dashboard-column .oe-dashboard-fold', 'click', this.on_fold_action);
        this.$element.delegate('.oe-dashboard-column .ui-icon-closethick', 'click', this.on_close_action);

        this.actions_attrs = {};
        // Init actions
        _.each(this.node.children, function(column) {
            _.each(column.children, function(action) {
                delete(action.attrs.width);
                delete(action.attrs.height);
                delete(action.attrs.colspan);
                self.actions_attrs[action.attrs.name] = action.attrs;
                self.rpc('/web/action/load', {
                    action_id: parseInt(action.attrs.name, 10)
                }, self.on_load_action);
            });
        });

        //this.$element.find('a.oe-dashboard-action-rename').live('click', this.on_rename);
        this.$element.find('.oe-dashboard-action').live('mouseover mouseout', function(event) {
            $(this).find('.oe-dashboard-action-header .ui-icon, .oe-dashboard-action-header .oe-dashboard-action-rename').toggle(event.type == 'mouseover');
        });
    },
    on_undo: function() {
        this.rpc('/web/view/undo_custom', {
            view_id: this.view.fields_view.view_id
        }, this.do_reload);
    },
    on_reset: function() {
        this.rpc('/web/view/undo_custom', {
            view_id: this.view.fields_view.view_id,
            reset: true
        }, this.do_reload);
    },
    on_add_widget: function() {
        var self = this;
        var action_manager = new openerp.web.ActionManager(this);
        var dialog = new openerp.web.Dialog(this, {
            title : 'Actions',
            width: 800,
            height: 600,
            buttons : {
                Cancel : function() {
                    $(this).dialog('destroy');
                },
                Add : function() {
                    self.do_add_widget(action_manager.inner_viewmanager.views.list.controller);
                    $(this).dialog('destroy');
                }
            }
        }).start().open();
        action_manager.appendTo(dialog.$element);
        action_manager.do_action({
            res_model : 'ir.actions.actions',
            views : [[false, 'list']],
            type : 'ir.actions.act_window',
            limit : 80,
            auto_search : true,
            flags : {
                sidebar : false,
                views_switcher : false,
                action_buttons : false
            }
        });
        // TODO: should bind ListView#select_record in order to catch record clicking
    },
    do_add_widget : function(listview) {
        var self = this,
            actions = listview.groups.get_selection().ids,
            results = [],
            qdict = { view : this.view };
        // TODO: should load multiple actions at once
        _.each(actions, function(aid) {
            self.rpc('/web/action/load', {
                action_id: aid
            }, function(result) {
                self.actions_attrs[aid] = {
                    name: aid,
                    string: _.trim(result.result.name)
                }
                qdict.action = {
                    attrs : self.actions_attrs[aid]
                }
                self.$element.find('.oe-dashboard-column:first').prepend(QWeb.render('DashBoard.action', qdict));
                self.do_save_dashboard();
                self.on_load_action(result)
            });
        });
    },
    on_rename : function(e) {
        var self = this,
            id = parseInt($(e.currentTarget).parents('.oe-dashboard-action:first').attr('data-id'), 10),
            $header = $(e.currentTarget).parents('.oe-dashboard-action-header:first'),
            $rename = $header.find('a.oe-dashboard-action-rename').hide(),
            $title = $header.find('span.oe-dashboard-action-title').hide(),
            $input = $header.find('input[name=title]');
        $input.val($title.text()).show().focus().bind('keydown', function(e) {
            if (e.which == 13 || e.which == 27) {
                if (e.which == 13) { //enter
                    var val = $input.val();
                    if (!val) {
                        return false;
                    }
                    $title.text(val);
                    self.actions_attrs[id].string = val;
                    self.do_save_dashboard();
                }
                $input.unbind('keydown').hide();
                $rename.show();
                $title.show();
            }
        });
    },
    on_change_layout: function() {
        var self = this;
        var qdict = {
            current_layout : this.$element.find('.oe-dashboard').attr('data-layout')
        };
        var $dialog = $('<div>').dialog({
                            modal: true,
                            title: 'Edit Layout',
                            width: 'auto',
                            height: 'auto'
                        }).html(QWeb.render('DashBoard.layouts', qdict));
        $dialog.find('li').click(function() {
            var layout = $(this).attr('data-layout');
            $dialog.dialog('destroy');
            self.do_change_layout(layout);
        });
    },
    do_change_layout: function(new_layout) {
        var $dashboard = this.$element.find('.oe-dashboard');
        var current_layout = $dashboard.attr('data-layout');
        if (current_layout != new_layout) {
            var clayout = current_layout.split('-').length,
                nlayout = new_layout.split('-').length,
                column_diff = clayout - nlayout;
            if (column_diff > 0) {
                var $last_column = $();
                $dashboard.find('.oe-dashboard-column').each(function(k, v) {
                    if (k >= nlayout) {
                        $(v).find('.oe-dashboard-action').appendTo($last_column);
                    } else {
                        $last_column = $(v);
                    }
                });
            }
            $dashboard.toggleClass('oe-dashboard-layout_' + current_layout + ' oe-dashboard-layout_' + new_layout);
            $dashboard.attr('data-layout', new_layout);
            this.do_save_dashboard();
        }
    },
    on_fold_action: function(e) {
        var $e = $(e.currentTarget),
            $action = $e.parents('.oe-dashboard-action:first'),
            id = parseInt($action.attr('data-id'), 10);
        if ($e.is('.ui-icon-minusthick')) {
            this.actions_attrs[id].fold = '1';
        } else {
            delete(this.actions_attrs[id].fold);
        }
        $e.toggleClass('ui-icon-minusthick ui-icon-plusthick');
        $action.find('.oe-dashboard-action-content').toggle();
        this.do_save_dashboard();
    },
    on_close_action: function(e) {
        $(e.currentTarget).parents('.oe-dashboard-action:first').remove();
        this.do_save_dashboard();
    },
    do_save_dashboard: function() {
        var self = this;
        var board = {
                form_title : this.view.fields_view.arch.attrs.string,
                style : this.$element.find('.oe-dashboard').attr('data-layout'),
                columns : []
            };
        this.$element.find('.oe-dashboard-column').each(function() {
            var actions = [];
            $(this).find('.oe-dashboard-action').each(function() {
                var action_id = $(this).attr('data-id');
                actions.push(self.actions_attrs[action_id]);
            });
            board.columns.push(actions);
        });
        var arch = QWeb.render('DashBoard.xml', board);
        this.rpc('/web/view/add_custom', {
            view_id: this.view.fields_view.view_id,
            arch: arch
        }, function() {
            self.$element.find('.oe-dashboard-link-undo, .oe-dashboard-link-reset').show();
        });
    },
    on_load_action: function(result) {
        var action = result.result;
        action.flags = {
            search_view : false,
            sidebar : false,
            views_switcher : false,
            action_buttons : false,
            pager: false
        };
        var am = new openerp.web.ActionManager(this);
        this.action_managers.push(am);
        am.appendTo($("#"+this.view.element_id + '_action_' + action.id));
        am.do_action(action);
    },
    render: function() {
        // We should start with three columns available
        for (var i = this.node.children.length; i < 3; i++) {
            this.node.children.push({
                tag: 'column',
                attrs: {},
                children: []
            });
        }
        return QWeb.render(this.template, this);
    },
    do_reload: function() {
        _.each(this.action_managers, function(am) {
            am.stop();
        });
        this.action_managers = [];
        this.view.stop();
        this.view.start();
    }
});
openerp.web.form.DashBoardLegacy = openerp.web.form.DashBoard.extend({
    render: function() {
        if (this.node.tag == 'hpaned') {
            this.node.attrs.style = '1-1';
        } else if (this.node.tag == 'vpaned') {
            this.node.attrs.style = '1';
        }
        this.node.tag = 'board';
        _.each(this.node.children, function(child) {
            if (child.tag.indexOf('child') == 0) {
                child.tag = 'column';
                var actions = [], first_child = child.children[0];
                if (first_child && first_child.tag == 'vpaned') {
                    _.each(first_child.children, function(subchild) {
                        actions.push.apply(actions, subchild.children);
                    });
                    child.children = actions;
                }
            }
        });
        return this._super(this, arguments);
    }
});

openerp.web.form.widgets.add('hpaned', 'openerp.web.form.DashBoardLegacy');
openerp.web.form.widgets.add('vpaned', 'openerp.web.form.DashBoardLegacy');
openerp.web.form.widgets.add('board', 'openerp.web.form.DashBoard');

openerp.web.client_actions.add(
    'board.config.overview', 'openerp.web_dashboard.ConfigOverview'
);
if (!openerp.web_dashboard) {
    openerp.web_dashboard = {};
}
openerp.web_dashboard.ConfigOverview = openerp.web.View.extend({
    template: 'ConfigOverview',
    init: function (parent, element_id) {
        this._super(parent, element_id);
        this.dataset = new openerp.web.DataSetSearch(
                this, 'ir.actions.todo');
        this.dataset.domain = [['type', '=', 'manual']];
    },
    start: function () {
        $.when(this.dataset.read_slice(['state', 'action_id', 'category_id']),
               this.dataset.call('progress'))
            .then(this.on_records_loaded);
    },
    on_records_loaded: function (read_response, progress_response) {
        var records = read_response[0].records,
           progress = progress_response[0];

        var grouped_todos = _(records).chain()
            .map(function (record) {
                return {
                    id: record.id,
                    name: record.action_id[1],
                    done: record.state !== 'open',
                    to_do: record.state === 'open',
                    category: record['category_id'][1] || "Uncategorized"
                }
            })
            .groupBy(function (record) {return record.category})
            .value();
        this.$element.html(QWeb.render('ConfigOverview.content', {
            completion: 100 * progress.done / progress.total,
            groups: grouped_todos
        }));
        var $progress = this.$element.find('div.oe-config-progress-bar');
        $progress.progressbar({value: $progress.data('completion')});

        var self = this;
        this.$element.find('dl')
            .delegate('input', 'click', function (e) {
                // switch todo status
                e.stopImmediatePropagation();
                var new_state = this.checked ? 'done' : 'open',
                      todo_id = parseInt($(this).val(), 10);
                self.dataset.write(todo_id, {state: new_state}, {}, function () {
                    self.start();
                });
            })
            .delegate('li:not(.oe-done)', 'click', function () {
                self.widget_parent.widget_parent.widget_parent.execute_action({
                        type: 'object',
                        name: 'action_launch'
                    }, self.dataset,
                    $(this).data('id'), function () {
                        // after action popup closed, refresh configuration
                        // thingie
                        self.start();
                    });
            });
    }
});

openerp.web.client_actions.add(
    'board.home.applications', 'openerp.web_dashboard.ApplicationTiles');
openerp.web_dashboard.ApplicationTiles = openerp.web.View.extend({
    template: 'ApplicationTiles',
    start: function () {
        var self = this;
        return new openerp.web.DataSetSearch(
                this, 'ir.ui.menu', null, [['parent_id', '=', false]])
            .read_slice( ['name', 'web_icon_data', 'web_icon_hover_data'], {}, function (applications) {
                // Create a matrix of 3*x applications
                var rows = [];
                while (applications.length) {
                    rows.push(applications.splice(0, 3));
                }
                self.$element
                    .append(QWeb.render(
                        'ApplicationTiles.content', {rows: rows}))
                    .find('.oe-dashboard-home-tile')
                        .click(function () {
                            var $this = $(this);
                            $this.closest('.openerp')
                                 .find('.menu a[data-menu=' + $this.data('menuid') + ']')
                                 .click();});
            });
    }
});
openerp.web.client_actions.add(
    'board.home.widgets', 'openerp.web_dashboard.Widget');
openerp.web_dashboard.Widget = openerp.web.View.extend(/** @lends openerp.web_dashboard.Widgets# */{
    template: 'HomeWidget',
    /**
     * Initializes a "HomeWidget" client widget: handles the display of a given
     * res.widget objects in an OpenERP view (mainly a dashboard).
     *
     * @constructs
     * @extends openerp.web.View
     * @param {Object} parent
     * @param {Object} options
     * @param {Number} options.widget_id
     */
    init: function (parent, options) {
        this._super(parent);
        this.widget_id = options.widget_id;
    },
    start: function () {
        return new openerp.web.DataSet(this, 'res.widget').read_ids(
                [this.widget_id], ['title'], this.on_widget_loaded);
    },
    on_widget_loaded: function (widgets) {
        var widget = widgets[0];
        var url = _.sprintf(
            '/web_dashboard/widgets/content?session_id=%s&widget_id=%d',
            this.session.session_id, widget.id);
        this.$element.html(QWeb.render('HomeWidget.content', {
            widget: widget,
            url: url
        }));
    }
});
};
<|endoftext|>


<commit_before>import React from "react";
import PropTypes from "prop-types";
import styled from "styled-components";
import { DateTime } from "luxon";

import Button from "@agir/front/genericComponents/Button";
import { useGlobalContext } from "@agir/front/genericComponents/GobalContext";
import style from "@agir/front/genericComponents/style.scss";
import { Hide } from "@agir/front/genericComponents/grid";
import CSRFProtectedForm from "@agir/front/genericComponents/CSRFProtectedForm";

const dateFormat = {
  weekday: "long",
  month: "long",
  day: "numeric",
  hour: "numeric",
  minute: "2-digit",
};

const EventHeaderContainer = styled.div`
  > * {
    margin: 0.5rem 0;
  }
`;

const EventTitle = styled.h1`
  font-size: 1.75rem;
  line-height: 1.4;
  font-weight: 800;
  margin-bottom: 0.5rem;
`;

const EventDate = styled.div`
  margin: 0.5rem 0;
`;

const SmallText = styled.div`
  font-size: 0.81rem;
  font-color: ${style.black500};
`;

/* Bouton qui prend 100 % de la largeur en petits écrans */
const ActionButton = styled(Button)`
  margin: 0.5rem 0;
  display: block;

  @media only screen and (min-width: 501px) {
    display: inline-block;
    & + & {
      margin-left: 0.5rem;
    }
  }
`;

const ActionButtons = ({
  hasSubscriptionForm,
  past,
  rsvped,
  logged,
  isOrganizer,
  routes,
}) => {
  if (past) {
    return <Button color="unavailable">Événement terminé</Button>;
  }

  if (logged) {
    if (rsvped) {
      return (
        <>
          <ActionButton icon="check-circle" color="confirmed">
            Je participe
          </ActionButton>
          {isOrganizer && (
            <ActionButton as="a" href={routes.manage}>
              Modifier l'événement
            </ActionButton>
          )}
        </>
      );
    } else {
      if (hasSubscriptionForm) {
        return (
          <ActionButton as="a" color="secondary" href={routes.attend}>
            Participer à l'événement
          </ActionButton>
        );
      } else {
        return (
          <CSRFProtectedForm method="post" action={routes.attend}>
            <ActionButton type="submit" color="secondary">
              Participer à l'événement
            </ActionButton>
          </CSRFProtectedForm>
        );
      }
    }
  } else {
    return (
      <ActionButton color="secondary" disabled={true}>
        Participer à l'événement
      </ActionButton>
    );
  }
};

const AdditionalMessage = ({ logged, rsvped, price, routes }) => {
  if (logged) {
    if (rsvped) {
      return (
        <SmallText>
          Annuler ma participation
        </SmallText>
      );
    } else if (price) {
      return (
        <SmallText>
          Entrée :
          {price}
        </SmallText>
      );
    } else {
      return (
        <SmallText>
          Votre email sera communiquée à l'organisateur.rice
        </SmallText>
      );
    }
  } else {
    return (

        Je me connecte ou{" "}
        je m'inscris pour participer à l'événement

    );
  }
};

const EventHeader = ({
  name,
  rsvp,
  options,
  startTime,
  routes,
  isOrganizer,
  hasSubscriptionForm,
}) => {
  const config = useGlobalContext();
  const logged = config.user !== null;
  const rsvped = !!rsvp;
  const now = DateTime.local();
  const past = now > startTime;
  let eventString = startTime.toLocaleString(dateFormat);
  eventString = eventString.slice(0, 1).toUpperCase() + eventString.slice(1);

  return (
    <EventHeaderContainer>
      <EventTitle>{name}</EventTitle>
      <Hide under>
        <EventDate>{eventString}</EventDate>
      </Hide>
      <ActionButtons
        hasSubscriptionForm={hasSubscriptionForm}
        past={past}
        logged={logged}
        rsvped={rsvped}
        routes={routes}
        isOrganizer={isOrganizer}
      />
      {!past && (
        <AdditionalMessage
          past={past}
          logged={logged}
          rsvped={rsvped}
          price={options.price}
          routes={{ ...routes, ...config.routes }}
        />
      )}
    </EventHeaderContainer>
  );
};

EventHeader.propTypes = {
  name: PropTypes.string,
  startTime: PropTypes.instanceOf(DateTime),
  hasSubscriptionForm: PropTypes.bool,
  isOrganizer: PropTypes.bool,
  options: PropTypes.shape({
    price: PropTypes.string,
  }),
  rsvp: PropTypes.shape({ status: PropTypes.string }),
  routes: PropTypes.shape({
    page: PropTypes.string,
    join: PropTypes.string,
    cancel: PropTypes.string,
  }),
};

export default EventHeader;
<commit_msg>feat: icône sur le bouton "gérer l'événement"<commit_after>import React from "react";
import PropTypes from "prop-types";
import styled from "styled-components";
import { DateTime } from "luxon";

import Button from "@agir/front/genericComponents/Button";
import { useGlobalContext } from "@agir/front/genericComponents/GobalContext";
import style from "@agir/front/genericComponents/style.scss";
import { Hide } from "@agir/front/genericComponents/grid";
import CSRFProtectedForm from "@agir/front/genericComponents/CSRFProtectedForm";

const dateFormat = {
  weekday: "long",
  month: "long",
  day: "numeric",
  hour: "numeric",
  minute: "2-digit",
};

const EventHeaderContainer = styled.div`
  > * {
    margin: 0.5rem 0;
  }
`;

const EventTitle = styled.h1`
  font-size: 1.75rem;
  line-height: 1.4;
  font-weight: 800;
  margin-bottom: 0.5rem;
`;

const EventDate = styled.div`
  margin: 0.5rem 0;
`;

const SmallText = styled.div`
  font-size: 0.81rem;
  font-color: ${style.black500};
`;

/* Bouton qui prend 100 % de la largeur en petits écrans */
const ActionButton = styled(Button)`
  margin: 0.5rem 0;
  display: block;

  @media only screen and (min-width: 501px) {
    display: inline-block;
    & + & {
      margin-left: 0.5rem;
    }
  }
`;

const ActionButtons = ({
  hasSubscriptionForm,
  past,
  rsvped,
  logged,
  isOrganizer,
  routes,
}) => {
  if (past) {
    return <Button color="unavailable">Événement terminé</Button>;
  }

  if (logged) {
    if (rsvped) {
      return (
        <>
          <ActionButton icon="check-circle" color="confirmed">
            Je participe
          </ActionButton>
          {isOrganizer && (
            <ActionButton icon="settings" as="a" href={routes.manage}>
              Gérer l'événement
            </ActionButton>
          )}
        </>
      );
    } else {
      if (hasSubscriptionForm) {
        return (
          <ActionButton as="a" color="secondary" href={routes.attend}>
            Participer à l'événement
          </ActionButton>
        );
      } else {
        return (
          <CSRFProtectedForm method="post" action={routes.attend}>
            <ActionButton type="submit" color="secondary">
              Participer à l'événement
            </ActionButton>
          </CSRFProtectedForm>
        );
      }
    }
  } else {
    return (
      <ActionButton color="secondary" disabled={true}>
        Participer à l'événement
      </ActionButton>
    );
  }
};

const AdditionalMessage = ({ logged, rsvped, price, routes }) => {
  if (logged) {
    if (rsvped) {
      return (
        <SmallText>
          Annuler ma participation
        </SmallText>
      );
    } else if (price) {
      return (
        <SmallText>
          Entrée :
          {price}
        </SmallText>
      );
    } else {
      return (
        <SmallText>
          Votre email sera communiquée à l'organisateur.rice
        </SmallText>
      );
    }
  } else {
    return (

        Je me connecte ou{" "}
        je m'inscris pour participer à l'événement

    );
  }
};

const EventHeader = ({
  name,
  rsvp,
  options,
  startTime,
  routes,
  isOrganizer,
  hasSubscriptionForm,
}) => {
  const config = useGlobalContext();
  const logged = config.user !== null;
  const rsvped = !!rsvp;
  const now = DateTime.local();
  const past = now > startTime;
  let eventString = startTime.toLocaleString(dateFormat);
  eventString = eventString.slice(0, 1).toUpperCase() + eventString.slice(1);

  return (
    <EventHeaderContainer>
      <EventTitle>{name}</EventTitle>
      <Hide under>
        <EventDate>{eventString}</EventDate>
      </Hide>
      <ActionButtons
        hasSubscriptionForm={hasSubscriptionForm}
        past={past}
        logged={logged}
        rsvped={rsvped}
        routes={routes}
        isOrganizer={isOrganizer}
      />
      {!past && (
        <AdditionalMessage
          past={past}
          logged={logged}
          rsvped={rsvped}
          price={options.price}
          routes={{ ...routes, ...config.routes }}
        />
      )}
    </EventHeaderContainer>
  );
};

EventHeader.propTypes = {
  name: PropTypes.string,
  startTime: PropTypes.instanceOf(DateTime),
  hasSubscriptionForm: PropTypes.bool,
  isOrganizer: PropTypes.bool,
  options: PropTypes.shape({
    price: PropTypes.string,
  }),
  rsvp: PropTypes.shape({ status: PropTypes.string }),
  routes: PropTypes.shape({
    page: PropTypes.string,
    join: PropTypes.string,
    cancel: PropTypes.string,
  }),
};

export default EventHeader;
<|endoftext|>


<commit_before>// Copyright (c) 2009 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

// -----------------------------------------------------------------------------
// NOTE: If you change this file you need to touch renderer_resources.grd to
// have your change take effect.
// -----------------------------------------------------------------------------

//==============================================================================
// This file contains a class that implements a subset of JSON Schema.
// See: http://www.json.com/json-schema-proposal/ for more details.
//
// The following features of JSON Schema are not implemented:
// - requires
// - unique
// - disallow
// - union types (but replaced with 'choices')
//
// The following properties are not applicable to the interface exposed by
// this class:
// - options
// - readonly
// - title
// - description
// - format
// - default
// - transient
// - hidden
//
// There are also these departures from the JSON Schema proposal:
// - function and undefined types are supported
// - null counts as 'unspecified' for optional values
// - added the 'choices' property, to allow specifying a list of possible types
//   for a value
// - made additionalProperties default to false
//==============================================================================

var chromium = chromium || {};

/**
 * Validates an instance against a schema and accumulates errors. Usage:
 *
 * var validator = new chromium.JSONSchemaValidator();
 * validator.validate(inst, schema);
 * if (validator.errors.length == 0)
 *   console.log("Valid!");
 * else
 *   console.log(validator.errors);
 *
 * The errors property contains a list of objects. Each object has two
 * properties: "path" and "message". The "path" property contains the path to
 * the key that had the problem, and the "message" property contains a sentence
 * describing the error.
 */
chromium.JSONSchemaValidator = function() {
  this.errors = [];
};

chromium.JSONSchemaValidator.messages = {
  invalidEnum: "Value must be one of: [*].",
  propertyRequired: "Property is required.",
  unexpectedProperty: "Unexpected property.",
  arrayMinItems: "Array must have at least * items.",
  arrayMaxItems: "Array must not have more than * items.",
  itemRequired: "Item is required.",
  stringMinLength: "String must be at least * characters long.",
  stringMaxLength: "String must not be more than * characters long.",
  stringPattern: "String must match the pattern: *.",
  numberMinValue: "Value must not be less than *.",
  numberMaxValue: "Value must not be greater than *.",
  numberMaxDecimal: "Value must not have more than * decimal places.",
  invalidType: "Expected '*' but got '*'.",
  invalidChoice: "Value does not match any valid type choices."
};

/**
 * Builds an error message. Key is the property in the |errors| object, and
 * |opt_replacements| is an array of values to replace "*" characters with.
 */
chromium.JSONSchemaValidator.formatError = function(key, opt_replacements) {
  var message = this.messages[key];
  if (opt_replacements) {
    for (var i = 0; i < opt_replacements.length; i++) {
      message = message.replace("*", opt_replacements[i]);
    }
  }
  return message;
};

/**
 * Classifies a value as one of the JSON schema primitive types. Note that we
 * don't explicitly disallow 'function', because we want to allow functions in
 * the input values.
 */
chromium.JSONSchemaValidator.getType = function(value) {
  var s = typeof value;

  if (s == "object") {
    if (value === null) {
      return "null";
    } else if (value instanceof Array ||
               Object.prototype.toString.call(value) == "[Object Array]") {
      return "array";
    }
  } else if (s == "number") {
    if (value % 1 == 0) {
      return "integer";
    }
  }

  return s;
};

/**
 * Validates an instance against a schema. The instance can be any JavaScript
 * value and will be validated recursively. When this method returns, the
 * |errors| property will contain a list of errors, if any.
 */
chromium.JSONSchemaValidator.prototype.validate = function(instance, schema,
                                                           opt_path) {
  var path = opt_path || "";

  // If the schema has an extends property, the instance must validate against
  // that schema too.
  if (schema.extends)
    this.validate(instance, schema.extends, path);

  // If the schema has a choices property, the instance must validate against at
  // least one of the items in that array.
  if (schema.choices) {
    this.validateChoices(instance, schema, path);
    return;
  }

  // If the schema has an enum property, the instance must be one of those
  // values.
  if (schema.enum) {
    if (!this.validateEnum(instance, schema, path))
      return;
  }

  if (schema.type && schema.type != "any") {
    if (!this.validateType(instance, schema, path))
      return;

    // Type-specific validation.
    switch (schema.type) {
      case "object":
        this.validateObject(instance, schema, path);
        break;
      case "array":
        this.validateArray(instance, schema, path);
        break;
      case "string":
        this.validateString(instance, schema, path);
        break;
      case "number":
      case "integer":
        this.validateNumber(instance, schema, path);
        break;
    }
  }
};

/**
 * Validates an instance against a choices schema. The instance must match at
 * least one of the provided choices.
 */
chromium.JSONSchemaValidator.prototype.validateChoices = function(instance,
                                                                  schema,
                                                                  path) {
  var originalErrors = this.errors;

  for (var i = 0; i < schema.choices.length; i++) {
    this.errors = [];
    this.validate(instance, schema.choices[i], path);
    if (this.errors.length == 0) {
      this.errors = originalErrors;
      return;
    }
  }

  this.errors = originalErrors;
  this.addError(path, "invalidChoice");
};

/**
 * Validates an instance against a schema with an enum type. Populates the
 * |errors| property, and returns a boolean indicating whether the instance
 * validates.
 */
chromium.JSONSchemaValidator.prototype.validateEnum = function(instance, schema,
                                                               path) {
  for (var i = 0; i < schema.enum.length; i++) {
    if (instance === schema.enum[i])
      return true;
  }

  this.addError(path, "invalidEnum", [schema.enum.join(", ")]);
  return false;
};

/**
 * Validates an instance against an object schema and populates the errors
 * property.
 */
chromium.JSONSchemaValidator.prototype.validateObject = function(instance,
                                                                 schema, path) {
  for (var prop in schema.properties) {
    var propPath = path ? path + "." + prop : prop;
    if (prop in instance && instance[prop] !== null &&
        instance[prop] !== undefined) {
      this.validate(instance[prop], schema.properties[prop], propPath);
    } else if (!schema.properties[prop].optional) {
      this.addError(propPath, "propertyRequired");
    }
  }

  // By default, additional properties are not allowed on instance objects. This
  // can be overridden by setting the additionalProperties property to a schema
  // which any additional properties must validate against.
  for (var prop in instance) {
    if (prop in schema.properties)
      continue;

    var propPath = path ? path + "." + prop : prop;
    if (schema.additionalProperties)
      this.validate(instance[prop], schema.additionalProperties, propPath);
    else
      this.addError(propPath, "unexpectedProperty");
  }
};

/**
 * Validates an instance against an array schema and populates the errors
 * property.
 */
chromium.JSONSchemaValidator.prototype.validateArray = function(instance,
                                                                schema, path) {
  var typeOfItems = chromium.JSONSchemaValidator.getType(schema.items);

  if (typeOfItems == 'object') {
    if (schema.minItems && instance.length < schema.minItems) {
      this.addError(path, "arrayMinItems", [schema.minItems]);
    }

    if (typeof schema.maxItems != "undefined" &&
        instance.length > schema.maxItems) {
      this.addError(path, "arrayMaxItems", [schema.maxItems]);
    }

    // If the items property is a single schema, each item in the array must
    // have that schema.
    for (var i = 0; i < instance.length; i++) {
      this.validate(instance[i], schema.items, path + "." + i);
    }
  } else if (typeOfItems == 'array') {
    // If the items property is an array of schemas, each item in the array must
    // validate against the corresponding schema.
    for (var i = 0; i < schema.items.length; i++) {
      var itemPath = path ? path + "." + i : String(i);
      if (i in instance && instance[i] !== null && instance[i] !== undefined) {
        this.validate(instance[i], schema.items[i], itemPath);
      } else if (!schema.items[i].optional) {
        this.addError(itemPath, "itemRequired");
      }
    }

    if (schema.additionalProperties) {
      for (var i = schema.items.length; i < instance.length; i++) {
        var itemPath = path ? path + "." + i : String(i);
        this.validate(instance[i], schema.additionalProperties, itemPath);
      }
    } else {
      if (instance.length > schema.items.length) {
        this.addError(path, "arrayMaxItems", [schema.items.length]);
      }
    }
  }
};

/**
 * Validates a string and populates the errors property.
 */
chromium.JSONSchemaValidator.prototype.validateString = function(instance,
                                                                 schema, path) {
  if (schema.minLength && instance.length < schema.minLength)
    this.addError(path, "stringMinLength", [schema.minLength]);

  if (schema.maxLength && instance.length > schema.maxLength)
    this.addError(path, "stringMaxLength", [schema.maxLength]);

  if (schema.pattern && !schema.pattern.test(instance))
    this.addError(path, "stringPattern", [schema.pattern]);
};

/**
 * Validates a number and populates the errors property. The instance is
 * assumed to be a number.
 */
chromium.JSONSchemaValidator.prototype.validateNumber = function(instance,
                                                                 schema, path) {
  if (schema.minimum && instance < schema.minimum)
    this.addError(path, "numberMinValue", [schema.minimum]);

  if (schema.maximum && instance > schema.maximum)
    this.addError(path, "numberMaxValue", [schema.maximum]);

  if (schema.maxDecimal && instance * Math.pow(10, schema.maxDecimal) % 1)
    this.addError(path, "numberMaxDecimal", [schema.maxDecimal]);
};

/**
 * Validates the primitive type of an instance and populates the errors
 * property. Returns true if the instance validates, false otherwise.
 */
chromium.JSONSchemaValidator.prototype.validateType = function(instance, schema,
                                                               path) {
  var actualType = chromium.JSONSchemaValidator.getType(instance);
  if (schema.type != actualType && !(schema.type == "number" &&
      actualType == "integer")) {
    this.addError(path, "invalidType", [schema.type, actualType]);
    return false;
  }

  return true;
};

/**
 * Adds an error message. |key| is an index into the |messages| object.
 * |replacements| is an array of values to replace '*' characters in the
 * message.
 */
chromium.JSONSchemaValidator.prototype.addError = function(path, key,
                                                           replacements) {
  this.errors.push({
    path: path,
    message: chromium.JSONSchemaValidator.formatError(key, replacements)
  });
};

// Set up chromium.types with some commonly used types...
(function() {
  function extend(base, ext) {
    var result = {};
    for (var p in base)
      result[p] = base[p];
    for (var p in ext)
      result[p] = ext[p];
    return result;
  }

  var types = {};
  types.opt = {optional: true};
  types.bool = {type: "boolean"};
  types.int = {type: "integer"};
  types.str = {type: "string"};
  types.fun = {type: "function"};
  types.pInt = extend(types.int, {minimum: 0});
  types.optBool = extend(types.bool, types.opt);
  types.optInt = extend(types.int, types.opt);
  types.optStr = extend(types.str, types.opt);
  types.optFun = extend(types.fun, types.opt);
  types.optPInt = extend(types.pInt, types.opt);
  types.singleOrListOf = function(type) {
    return {
      choice: [
        type,
        { type: "array",  item: type }
      ]
    };
  };

  chromium.types = types;
})();
<commit_msg>Add a few extra error messages to help catch typos in schemas.<commit_after>// Copyright (c) 2009 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

// -----------------------------------------------------------------------------
// NOTE: If you change this file you need to touch renderer_resources.grd to
// have your change take effect.
// -----------------------------------------------------------------------------

//==============================================================================
// This file contains a class that implements a subset of JSON Schema.
// See: http://www.json.com/json-schema-proposal/ for more details.
//
// The following features of JSON Schema are not implemented:
// - requires
// - unique
// - disallow
// - union types (but replaced with 'choices')
//
// The following properties are not applicable to the interface exposed by
// this class:
// - options
// - readonly
// - title
// - description
// - format
// - default
// - transient
// - hidden
//
// There are also these departures from the JSON Schema proposal:
// - function and undefined types are supported
// - null counts as 'unspecified' for optional values
// - added the 'choices' property, to allow specifying a list of possible types
//   for a value
// - made additionalProperties default to false
//==============================================================================

var chromium = chromium || {};

/**
 * Validates an instance against a schema and accumulates errors. Usage:
 *
 * var validator = new chromium.JSONSchemaValidator();
 * validator.validate(inst, schema);
 * if (validator.errors.length == 0)
 *   console.log("Valid!");
 * else
 *   console.log(validator.errors);
 *
 * The errors property contains a list of objects. Each object has two
 * properties: "path" and "message". The "path" property contains the path to
 * the key that had the problem, and the "message" property contains a sentence
 * describing the error.
 */
chromium.JSONSchemaValidator = function() {
  this.errors = [];
};

chromium.JSONSchemaValidator.messages = {
  invalidEnum: "Value must be one of: [*].",
  propertyRequired: "Property is required.",
  unexpectedProperty: "Unexpected property.",
  arrayMinItems: "Array must have at least * items.",
  arrayMaxItems: "Array must not have more than * items.",
  itemRequired: "Item is required.",
  stringMinLength: "String must be at least * characters long.",
  stringMaxLength: "String must not be more than * characters long.",
  stringPattern: "String must match the pattern: *.",
  numberMinValue: "Value must not be less than *.",
  numberMaxValue: "Value must not be greater than *.",
  numberMaxDecimal: "Value must not have more than * decimal places.",
  invalidType: "Expected '*' but got '*'.",
  invalidChoice: "Value does not match any valid type choices.",
  invalidPropertyType: "Missing property type.",
  schemaRequired: "Schema value required.",
};

/**
 * Builds an error message. Key is the property in the |errors| object, and
 * |opt_replacements| is an array of values to replace "*" characters with.
 */
chromium.JSONSchemaValidator.formatError = function(key, opt_replacements) {
  var message = this.messages[key];
  if (opt_replacements) {
    for (var i = 0; i < opt_replacements.length; i++) {
      message = message.replace("*", opt_replacements[i]);
    }
  }
  return message;
};

/**
 * Classifies a value as one of the JSON schema primitive types. Note that we
 * don't explicitly disallow 'function', because we want to allow functions in
 * the input values.
 */
chromium.JSONSchemaValidator.getType = function(value) {
  var s = typeof value;

  if (s == "object") {
    if (value === null) {
      return "null";
    } else if (value instanceof Array ||
               Object.prototype.toString.call(value) == "[Object Array]") {
      return "array";
    }
  } else if (s == "number") {
    if (value % 1 == 0) {
      return "integer";
    }
  }

  return s;
};

/**
 * Validates an instance against a schema. The instance can be any JavaScript
 * value and will be validated recursively. When this method returns, the
 * |errors| property will contain a list of errors, if any.
 */
chromium.JSONSchemaValidator.prototype.validate = function(instance, schema,
                                                           opt_path) {
  var path = opt_path || "";

  if (!schema) {
    this.addError(path, "schemaRequired");
    return;
  }

  // If the schema has an extends property, the instance must validate against
  // that schema too.
  if (schema.extends)
    this.validate(instance, schema.extends, path);

  // If the schema has a choices property, the instance must validate against at
  // least one of the items in that array.
  if (schema.choices) {
    this.validateChoices(instance, schema, path);
    return;
  }

  // If the schema has an enum property, the instance must be one of those
  // values.
  if (schema.enum) {
    if (!this.validateEnum(instance, schema, path))
      return;
  }

  if (schema.type && schema.type != "any") {
    if (!this.validateType(instance, schema, path))
      return;

    // Type-specific validation.
    switch (schema.type) {
      case "object":
        this.validateObject(instance, schema, path);
        break;
      case "array":
        this.validateArray(instance, schema, path);
        break;
      case "string":
        this.validateString(instance, schema, path);
        break;
      case "number":
      case "integer":
        this.validateNumber(instance, schema, path);
        break;
    }
  }
};

/**
 * Validates an instance against a choices schema. The instance must match at
 * least one of the provided choices.
 */
chromium.JSONSchemaValidator.prototype.validateChoices = function(instance,
                                                                  schema,
                                                                  path) {
  var originalErrors = this.errors;

  for (var i = 0; i < schema.choices.length; i++) {
    this.errors = [];
    this.validate(instance, schema.choices[i], path);
    if (this.errors.length == 0) {
      this.errors = originalErrors;
      return;
    }
  }

  this.errors = originalErrors;
  this.addError(path, "invalidChoice");
};

/**
 * Validates an instance against a schema with an enum type. Populates the
 * |errors| property, and returns a boolean indicating whether the instance
 * validates.
 */
chromium.JSONSchemaValidator.prototype.validateEnum = function(instance, schema,
                                                               path) {
  for (var i = 0; i < schema.enum.length; i++) {
    if (instance === schema.enum[i])
      return true;
  }

  this.addError(path, "invalidEnum", [schema.enum.join(", ")]);
  return false;
};

/**
 * Validates an instance against an object schema and populates the errors
 * property.
 */
chromium.JSONSchemaValidator.prototype.validateObject = function(instance,
                                                                 schema, path) {
  for (var prop in schema.properties) {
    var propPath = path ? path + "." + prop : prop;
    if (schema.properties[prop] == undefined) {
      this.addError(propPath, "invalidPropertyType");
    } else if (prop in instance && instance[prop] !== null &&
        instance[prop] !== undefined) {
      this.validate(instance[prop], schema.properties[prop], propPath);
    } else if (!schema.properties[prop].optional) {
      this.addError(propPath, "propertyRequired");
    }
  }

  // By default, additional properties are not allowed on instance objects. This
  // can be overridden by setting the additionalProperties property to a schema
  // which any additional properties must validate against.
  for (var prop in instance) {
    if (prop in schema.properties)
      continue;

    var propPath = path ? path + "." + prop : prop;
    if (schema.additionalProperties)
      this.validate(instance[prop], schema.additionalProperties, propPath);
    else
      this.addError(propPath, "unexpectedProperty");
  }
};

/**
 * Validates an instance against an array schema and populates the errors
 * property.
 */
chromium.JSONSchemaValidator.prototype.validateArray = function(instance,
                                                                schema, path) {
  var typeOfItems = chromium.JSONSchemaValidator.getType(schema.items);

  if (typeOfItems == 'object') {
    if (schema.minItems && instance.length < schema.minItems) {
      this.addError(path, "arrayMinItems", [schema.minItems]);
    }

    if (typeof schema.maxItems != "undefined" &&
        instance.length > schema.maxItems) {
      this.addError(path, "arrayMaxItems", [schema.maxItems]);
    }

    // If the items property is a single schema, each item in the array must
    // have that schema.
    for (var i = 0; i < instance.length; i++) {
      this.validate(instance[i], schema.items, path + "." + i);
    }
  } else if (typeOfItems == 'array') {
    // If the items property is an array of schemas, each item in the array must
    // validate against the corresponding schema.
    for (var i = 0; i < schema.items.length; i++) {
      var itemPath = path ? path + "." + i : String(i);
      if (i in instance && instance[i] !== null && instance[i] !== undefined) {
        this.validate(instance[i], schema.items[i], itemPath);
      } else if (!schema.items[i].optional) {
        this.addError(itemPath, "itemRequired");
      }
    }

    if (schema.additionalProperties) {
      for (var i = schema.items.length; i < instance.length; i++) {
        var itemPath = path ? path + "." + i : String(i);
        this.validate(instance[i], schema.additionalProperties, itemPath);
      }
    } else {
      if (instance.length > schema.items.length) {
        this.addError(path, "arrayMaxItems", [schema.items.length]);
      }
    }
  }
};

/**
 * Validates a string and populates the errors property.
 */
chromium.JSONSchemaValidator.prototype.validateString = function(instance,
                                                                 schema, path) {
  if (schema.minLength && instance.length < schema.minLength)
    this.addError(path, "stringMinLength", [schema.minLength]);

  if (schema.maxLength && instance.length > schema.maxLength)
    this.addError(path, "stringMaxLength", [schema.maxLength]);

  if (schema.pattern && !schema.pattern.test(instance))
    this.addError(path, "stringPattern", [schema.pattern]);
};

/**
 * Validates a number and populates the errors property. The instance is
 * assumed to be a number.
 */
chromium.JSONSchemaValidator.prototype.validateNumber = function(instance,
                                                                 schema, path) {
  if (schema.minimum && instance < schema.minimum)
    this.addError(path, "numberMinValue", [schema.minimum]);

  if (schema.maximum && instance > schema.maximum)
    this.addError(path, "numberMaxValue", [schema.maximum]);

  if (schema.maxDecimal && instance * Math.pow(10, schema.maxDecimal) % 1)
    this.addError(path, "numberMaxDecimal", [schema.maxDecimal]);
};

/**
 * Validates the primitive type of an instance and populates the errors
 * property. Returns true if the instance validates, false otherwise.
 */
chromium.JSONSchemaValidator.prototype.validateType = function(instance, schema,
                                                               path) {
  var actualType = chromium.JSONSchemaValidator.getType(instance);
  if (schema.type != actualType && !(schema.type == "number" &&
      actualType == "integer")) {
    this.addError(path, "invalidType", [schema.type, actualType]);
    return false;
  }

  return true;
};

/**
 * Adds an error message. |key| is an index into the |messages| object.
 * |replacements| is an array of values to replace '*' characters in the
 * message.
 */
chromium.JSONSchemaValidator.prototype.addError = function(path, key,
                                                           replacements) {
  this.errors.push({
    path: path,
    message: chromium.JSONSchemaValidator.formatError(key, replacements)
  });
};

// Set up chromium.types with some commonly used types...
(function() {
  function extend(base, ext) {
    var result = {};
    for (var p in base)
      result[p] = base[p];
    for (var p in ext)
      result[p] = ext[p];
    return result;
  }

  var types = {};
  types.opt = {optional: true};
  types.bool = {type: "boolean"};
  types.int = {type: "integer"};
  types.str = {type: "string"};
  types.fun = {type: "function"};
  types.pInt = extend(types.int, {minimum: 0});
  types.optBool = extend(types.bool, types.opt);
  types.optInt = extend(types.int, types.opt);
  types.optStr = extend(types.str, types.opt);
  types.optFun = extend(types.fun, types.opt);
  types.optPInt = extend(types.pInt, types.opt);
  types.singleOrListOf = function(type) {
    return {
      choice: [
        type,
        { type: "array",  item: type }
      ]
    };
  };

  chromium.types = types;
})();
<|endoftext|>