<commit_before>var _ = require('lodash');
var url_utils = require('url');

var utils = require('../utils');
var BaseError = utils.BaseError;
var Extendable = utils.Extendable;

var events = require('../events');
var Eventable = events.Eventable;

var HttpApiError = BaseError.extend(function(self) {
    /**class:HttpApiError()

    Thrown when an error occurs while making and checking an HTTP request and
    the corresponding API reply.
    */
    self.name = "HttpApiError";
});

var HttpRequestError = BaseError.extend(function(self, request, reason) {
    /**class:HttpRequestError(request, reason)

    Thrown when an error occurs while making and checking an HTTP request.

    :param HttpRequest request: the request.
    :param string reason: the reason for the failure. Optional.
    */
    self.name = "HttpRequestError";
    self.request = request;
    self.reason = reason || null;

    if (self.reason) {
        self.message = [self.reason, self.request].join(' ');
    } else {
        self.message = self.request.toString();
    }
});

var HttpResponseError = BaseError.extend(function(self, response, reason) {
    /**class:HttpResponseError(response, reason)

    Thrown when an error response is returned by an HTTP request or if the HTTP
    response body cannot be parsed.

    :param HttpResponse response: the response.
    :param string reason: the reason for the failure. Optional.
    */
    self.name = "HttpResponseError";
    self.response = response;
    self.reason = reason || null;

    if (self.reason) {
        self.message = [self.reason, self.response].join(' ');
    } else {
        self.message = self.response.toString();
    }
});

var HttpRequest = Extendable.extend(function(self, method, url, opts) {
    /**class:HttpRequest(request, code, opts)

    Encapsulates information about an HTTP request made by the
    :class:`HttpApi`. Once :meth:`HttpRequest.encode` has been invoked,
    the request's data is encoded and made available as the request's body.

    :param string method:
        the HTTP request method.
    :param string url:
        the url to send the request to.
    :param string opts.data:
        the request's data to be encoded as the request's body. Optional.
    :param opts.params:
        An object of key-value pairs to append to the URL as query
        parameters. Can be in any form accepted by node.js's querystring
        module
    :param string opts.verify_options:
        A list of options to verify when doing an HTTPS request. Optional.
    :param string opts.ssl_method:
        A request for a specific ssl method to be attempted. Optional.
    */
    Extendable.call(self);

    opts = _.defaults(opts || {}, {
        headers: null,
        body: null,
        data: null,
        params: {},
        encoder: _.identity,
        verify_options: null,
        ssl_method: null
    });

    self.url = url;
    if (_.isString(self.url)) {
        self.url = url_utils.format(url_utils.parse(url));
    }

    self.method = method.toUpperCase();
    self.headers = opts.headers;
    self.data = opts.data;
    self.body = opts.body;
    self.params = opts.params;
    self.encoder = opts.encoder;
    self.verify_options = opts.verify_options;
    self.ssl_method = opts.ssl_method;

    self.encode = function() {
        /**:HttpRequest.encode()

        Encodes the request data (if available).
        */
        if (self.data !== null) {
            try {
                self.body = self.encoder(self.data);
            }
            catch (e) {
                throw new HttpRequestError(
                    self, "Could not parse request (" + e.toString() + ")");
            }
        }
    };

    self.to_cmd = function() {
        /**:HttpRequest.to_cmd()

        Returns a sandbox API command that can be used to initiate this request
        via the sandbox API.
        */
        var cmd = {};
        cmd.data = {};
        cmd.name = 'http.' + self.method.toLowerCase();

        var url_data = url_utils.parse(self.url);
        url_data.query = self.params;
        cmd.data.url = url_utils.format(url_data);

        if (self.headers !== null) {
            cmd.data.headers = self.headers;
        }

        if (self.body !== null) {
            cmd.data.data = self.body;
        }

        if (self.verify_options !== null) {
            cmd.data.verify_options = self.verify_options;
        }

        if (self.ssl_method !== null) {
            cmd.data.ssl_method = self.ssl_method;
        }

        return cmd;
    };

    self.toString = function() {
        var parts = [
            'HttpRequest:',
            self.method,
            self.url];

        if (self.body !== null) {
            parts.push('(body: ' + self.body + ')');
        }

        if (_.size(self.params)) {
            parts.push('(params: ' + JSON.stringify(self.params) + ')');
        }

        return '[' + parts.join(' ') + ']';
    };

});

var HttpResponse = Extendable.extend(function(self, request, code, opts) {
    /**class:HttpResponse(request, code, opts)

    Encapsulates information about an HTTP response given to the
    :class:`HttpApi`. Once :meth:`HttpResponse.decode` has been invoked,
    the response's body is decoded and made available as the response's data.

    :param HttpRequest request:
        the request that caused the response.
    :param integer code:
        the status code for the HTTP response.
    :param string opts.body:
        the response's body to be decoded as the response's data. Optional.
    */
    Extendable.call(self);

    opts = _.defaults(opts || {}, {
        data: null,
        body: null,
        decoder: _.identity
    });

    self.request = request;
    self.code = code;
    self.data = opts.data;
    self.body = opts.body;
    self.decoder = opts.decoder;

    self.decode = function() {
        /**:HttpResponse.decode()

        Decodes the responses body (if available).
        */
        if (self.body !== null) {

            try {
                self.data = self.decoder(self.body);
            }
            catch (e) {
                throw new HttpResponseError(
                    self, "Could not parse response (" + e.toString() + ")");
            }
        }
    };

    self.toString = function() {
        var parts = [
            'HttpResponse:',
            self.code];

        if (self.body !== null) {
            parts.push('(body: ' + self.body + ')');
        }

        parts.push('(request: ' + self.request + ')');
        return parts.join(' ');
    };
});

var HttpApi = Eventable.extend(function(self, im, opts) {
    /**class:HttpApi(im, opts)

    A helper class for making HTTP requests via the HTTP sandbox resource.

    :param InteractionMachine im:
        The interaction machine to use when making requests.
    :param object opts.headers:
        Default headers to use in HTTP requests.
    :param object opts.auth:
        Adds a HTTP Basic authentication to the default headers. Should
        contain ``username`` and ``password`` attributes.
    :param string opts.verify_options:
        The default list of options to verify when doing HTTPS requests.
        Optional.
    :param string opts.ssl_method:
       The default ssl method to attempt for HTTPS requests. Optional.
    */
    opts = _.defaults(opts || {}, {headers: {}});

    self.im = im;
    self.defaults = _.pick({
        headers: opts.headers,
        auth: opts.auth,
        verify_options: opts.verify_options,
        ssl_method: opts.ssl_method
    }, utils.exists);

    self.make_auth = utils.basic_auth;

    self.decode_response_body = function(body) {
        /**:HttpApi.decode_response_body(body)

        :param string body:
            The body to decode.

        Sub-classes should override this to decode the response body and throw
        an exception if the body cannot be parsed. This base implementation
        returns the body as-is (i.e. decoding is left to the code calling the
        :class:`HttpApi`).
        */
        return body;
    };

    self.encode_request_data = function(data) {
        /**:HttpApi.encode_request_data(data)

        :param object data:
            The data to enoptscode.

        Sub-classes should override this to encode the request body and throw
        an exception if the data cannot be encoded. This base implementation
        returns the data as-is (i.e. encoding is left to code calling the
        :class:`HttpApi`).
        */
        return data;
    };

    self.set_defaults = function(opts) {
        opts = _.defaults(opts || {}, {headers: {}});

        var headers = _.extend({}, self.defaults.headers);

        if (self.defaults.auth) {
            headers.Authorization = [self.make_auth(
                self.defaults.auth.username,
                self.defaults.auth.password)];
        }

        return _.defaults(opts, _.omit(self.defaults, 'headers', 'auth'), {
            encoder: self.encode_request_data,
            headers: _.defaults(opts.headers, headers)
        });
    };

    self.parse_reply = function(reply, request) {
        /**:HttpApi.parse_reply(reply, request)

        Check an HTTP reply and throw an :class:`HttpRequestError` if the
        sandbox API command was unsuccessful, or otherwise parse the sandbox's
        reply into a response.  If the response status code is not in the 200
        range or the reply body cannot be decoded, throw an
        :class:`HttpResponseError`.

        Logs an error via the sandbox logging resource in an error is raised.

        :param object reply:
            Raw response to the sandbox API command.
        :param HttpRequest request:
            The request that initiated the sandbox API command.

        Returns an :class:`HttpResponse` or throws an :class:`HttpApiError`
        (either the :class:`HttpRequestError` or :class:`HttpResponseError`
        derivative, depending on what error occured).
        */
        var response = new HttpResponse(request, reply.code, {
            body: reply.body,
            decoder: self.decode_response_body
        });

        response.decode();

        if (response.code < 200 || response.code >= 300) {
            throw new HttpResponseError(response);
        }

        return response;
    };

    self.request = function(method, url, opts) {
        /**:HttpApi.request(method, url, opts)

        :param string method:
            The HTTP method to use (e.g. `GET`, `POST`).
        :param string url:
            The URL to make the request to. If you pass in query parameters
            using ``opts.params``, don't include any in the URL itself.
        :param opts.params:
            An object of key-value pairs to append to the URL as query
            parameters. Can be in any form accepted by node.js's querystring
            module
        :param object opts.data:
            Data to pass as the request body. Will be encoded using
            :func:`HttpApi.encode_request_data` before being sent.
        :param object opts.headers:
            Additional headers to add to the default headers.

        Returns a :class:`HttpResponse` via a promise. Failures while making
        and checking the request will be thrown as :class:`HttpApiError`s, and
        can be caught with a Q errback. See :meth:`HttpApi.parse_reply` for
        more on the response parsing and error throwing.
        */
        var request = new HttpRequest(method, url, self.set_defaults(opts));
        request.encode();

        var cmd = request.to_cmd();
        return self
            .im.api_request(cmd.name, cmd.data)
            .then(
                function(reply) {
                    return self.parse_reply(reply, request);
                }, function(e) {
                    throw new HttpRequestError(request, e.reply.reason);
                });

    };

    self.get = function(url, opts) {
        /**:HttpApi.get(url, opts)

        Make an HTTP GET request.

        :param string url:
            The URL to make the request to.
        :param object opts:
            Options to pass to :func:`HttpApi.request`.

        Returns a promise which fires with the decoded value of the
        response body or an object with an error attribute containing
        the error message.
        */
        return self.request('GET', url, opts);
    };

    self.head = function(url, opts) {
        /**:HttpApi.head(url, opts)

        Make an HTTP HEAD request.

        :param string url:
            The URL to make the request to.
        :param object opts:
            Options to pass to :func:`HttpApi.request`.

        Returns a promise which fires with the decoded value of the
        response body or an object with an error attribute containing
        the error message.
        */
        return self.request('HEAD', url, opts);
    };

    self.post = function(url, opts) {
        /**:HttpApi.post(url, opts)

        Make an HTTP POST request.

        :param string url:
            The URL to make the request to.
        :param object opts:
            Options to pass to :func:`HttpApi.request`.

        Returns a promise which fires with the decoded value of the
        response body or an object with an error attribute containing
        the error message.
        */
        return self.request('POST', url, opts);
    };

    self.put = function(url, opts) {
        /**:HttpApi.put(url, opts)

        Make an HTTP PUT request.

        :param string url:
            The URL to make the request to.
        :param object opts:
            Options to pass to :func:`HttpApi.request`.

        Returns a promise which fires with the decoded value of the
        response body or an object with an error attribute containing
        the error message.
        */
        return self.request('PUT', url, opts);
    };

    self.delete = function(url, opts) {
        /**:HttpApi.delete(url, opts)

        Make an HTTP DELETE request.

        :param string url:
            The URL to make the request to.
        :param object opts:
            Options to pass to :func:`HttpApi.request`.

        Returns a promise which fires with the decoded value of the
        response body or an object with an error attribute containing
        the error message.
        */
        return self.request('DELETE', url, opts);
    };
});

var JsonApi = HttpApi.extend(function(self, im, opts) {
    /**class:JsonApi(im, opts)

    A helper class for making HTTP requests that send and receive JSON
    encoded data.

    :param InteractionMachine im:
        The interaction machine to use when making requests.
    :param object opts.headers:
        Default headers to use in HTTP requests. The ``Content-Type``
        header is overridden to be ``application/json; charset=utf-8``.
    :param object opts.auth:
        Adds a HTTP Basic authentication to the default headers. Should
        contain ``username`` and ``password`` attributes.
    */
    HttpApi.call(self, im, opts);

    self.defaults.headers['Content-Type'] = ['application/json; charset=utf-8'];

    self.decode_response_body = function(body) {
        /**:JsonApi.decode_response_body(body)

        Decode an HTTP response body using ``JSON.parse()``.

        :param string body:
            Raw HTTP response body to parse.

        Returns the decoded response body.
        */
        return JSON.parse(body);
    };

    self.encode_request_data = function(data) {
        /**:JsonApi.encode_request_data(data)

        Encode an object as JSON using ``JSON.stringify()``.

        :param object data:
            Object to encode to JSON.

        Returns the serialized object as a string.
        */
        return JSON.stringify(data);
    };
});

this.HttpApi = HttpApi;
this.JsonApi = JsonApi;

this.HttpApiError = HttpApiError;
this.HttpRequestError = HttpRequestError;
this.HttpResponseError = HttpResponseError;
this.HttpRequest = HttpRequest;
this.HttpResponse = HttpResponse;
<commit_msg>Rename HttpApi.set_defaults to HttpApi.parse_opts<commit_after>var _ = require('lodash');
var url_utils = require('url');

var utils = require('../utils');
var BaseError = utils.BaseError;
var Extendable = utils.Extendable;

var events = require('../events');
var Eventable = events.Eventable;

var HttpApiError = BaseError.extend(function(self) {
    /**class:HttpApiError()

    Thrown when an error occurs while making and checking an HTTP request and
    the corresponding API reply.
    */
    self.name = "HttpApiError";
});

var HttpRequestError = BaseError.extend(function(self, request, reason) {
    /**class:HttpRequestError(request, reason)

    Thrown when an error occurs while making and checking an HTTP request.

    :param HttpRequest request: the request.
    :param string reason: the reason for the failure. Optional.
    */
    self.name = "HttpRequestError";
    self.request = request;
    self.reason = reason || null;

    if (self.reason) {
        self.message = [self.reason, self.request].join(' ');
    } else {
        self.message = self.request.toString();
    }
});

var HttpResponseError = BaseError.extend(function(self, response, reason) {
    /**class:HttpResponseError(response, reason)

    Thrown when an error response is returned by an HTTP request or if the HTTP
    response body cannot be parsed.

    :param HttpResponse response: the response.
    :param string reason: the reason for the failure. Optional.
    */
    self.name = "HttpResponseError";
    self.response = response;
    self.reason = reason || null;

    if (self.reason) {
        self.message = [self.reason, self.response].join(' ');
    } else {
        self.message = self.response.toString();
    }
});

var HttpRequest = Extendable.extend(function(self, method, url, opts) {
    /**class:HttpRequest(request, code, opts)

    Encapsulates information about an HTTP request made by the
    :class:`HttpApi`. Once :meth:`HttpRequest.encode` has been invoked,
    the request's data is encoded and made available as the request's body.

    :param string method:
        the HTTP request method.
    :param string url:
        the url to send the request to.
    :param string opts.data:
        the request's data to be encoded as the request's body. Optional.
    :param opts.params:
        An object of key-value pairs to append to the URL as query
        parameters. Can be in any form accepted by node.js's querystring
        module
    :param string opts.verify_options:
        A list of options to verify when doing an HTTPS request. Optional.
    :param string opts.ssl_method:
        A request for a specific ssl method to be attempted. Optional.
    */
    Extendable.call(self);

    opts = _.defaults(opts || {}, {
        headers: null,
        body: null,
        data: null,
        params: {},
        encoder: _.identity,
        verify_options: null,
        ssl_method: null
    });

    self.url = url;
    if (_.isString(self.url)) {
        self.url = url_utils.format(url_utils.parse(url));
    }

    self.method = method.toUpperCase();
    self.headers = opts.headers;
    self.data = opts.data;
    self.body = opts.body;
    self.params = opts.params;
    self.encoder = opts.encoder;
    self.verify_options = opts.verify_options;
    self.ssl_method = opts.ssl_method;

    self.encode = function() {
        /**:HttpRequest.encode()

        Encodes the request data (if available).
        */
        if (self.data !== null) {
            try {
                self.body = self.encoder(self.data);
            }
            catch (e) {
                throw new HttpRequestError(
                    self, "Could not parse request (" + e.toString() + ")");
            }
        }
    };

    self.to_cmd = function() {
        /**:HttpRequest.to_cmd()

        Returns a sandbox API command that can be used to initiate this request
        via the sandbox API.
        */
        var cmd = {};
        cmd.data = {};
        cmd.name = 'http.' + self.method.toLowerCase();

        var url_data = url_utils.parse(self.url);
        url_data.query = self.params;
        cmd.data.url = url_utils.format(url_data);

        if (self.headers !== null) {
            cmd.data.headers = self.headers;
        }

        if (self.body !== null) {
            cmd.data.data = self.body;
        }

        if (self.verify_options !== null) {
            cmd.data.verify_options = self.verify_options;
        }

        if (self.ssl_method !== null) {
            cmd.data.ssl_method = self.ssl_method;
        }

        return cmd;
    };

    self.toString = function() {
        var parts = [
            'HttpRequest:',
            self.method,
            self.url];

        if (self.body !== null) {
            parts.push('(body: ' + self.body + ')');
        }

        if (_.size(self.params)) {
            parts.push('(params: ' + JSON.stringify(self.params) + ')');
        }

        return '[' + parts.join(' ') + ']';
    };

});

var HttpResponse = Extendable.extend(function(self, request, code, opts) {
    /**class:HttpResponse(request, code, opts)

    Encapsulates information about an HTTP response given to the
    :class:`HttpApi`. Once :meth:`HttpResponse.decode` has been invoked,
    the response's body is decoded and made available as the response's data.

    :param HttpRequest request:
        the request that caused the response.
    :param integer code:
        the status code for the HTTP response.
    :param string opts.body:
        the response's body to be decoded as the response's data. Optional.
    */
    Extendable.call(self);

    opts = _.defaults(opts || {}, {
        data: null,
        body: null,
        decoder: _.identity
    });

    self.request = request;
    self.code = code;
    self.data = opts.data;
    self.body = opts.body;
    self.decoder = opts.decoder;

    self.decode = function() {
        /**:HttpResponse.decode()

        Decodes the responses body (if available).
        */
        if (self.body !== null) {

            try {
                self.data = self.decoder(self.body);
            }
            catch (e) {
                throw new HttpResponseError(
                    self, "Could not parse response (" + e.toString() + ")");
            }
        }
    };

    self.toString = function() {
        var parts = [
            'HttpResponse:',
            self.code];

        if (self.body !== null) {
            parts.push('(body: ' + self.body + ')');
        }

        parts.push('(request: ' + self.request + ')');
        return parts.join(' ');
    };
});

var HttpApi = Eventable.extend(function(self, im, opts) {
    /**class:HttpApi(im, opts)

    A helper class for making HTTP requests via the HTTP sandbox resource.

    :param InteractionMachine im:
        The interaction machine to use when making requests.
    :param object opts.headers:
        Default headers to use in HTTP requests.
    :param object opts.auth:
        Adds a HTTP Basic authentication to the default headers. Should
        contain ``username`` and ``password`` attributes.
    :param string opts.verify_options:
        The default list of options to verify when doing HTTPS requests.
        Optional.
    :param string opts.ssl_method:
       The default ssl method to attempt for HTTPS requests. Optional.
    */
    opts = _.defaults(opts || {}, {headers: {}});

    self.im = im;
    self.defaults = _.pick({
        headers: opts.headers,
        auth: opts.auth,
        verify_options: opts.verify_options,
        ssl_method: opts.ssl_method
    }, utils.exists);

    self.make_auth = utils.basic_auth;

    self.decode_response_body = function(body) {
        /**:HttpApi.decode_response_body(body)

        :param string body:
            The body to decode.

        Sub-classes should override this to decode the response body and throw
        an exception if the body cannot be parsed. This base implementation
        returns the body as-is (i.e. decoding is left to the code calling the
        :class:`HttpApi`).
        */
        return body;
    };

    self.encode_request_data = function(data) {
        /**:HttpApi.encode_request_data(data)

        :param object data:
            The data to enoptscode.

        Sub-classes should override this to encode the request body and throw
        an exception if the data cannot be encoded. This base implementation
        returns the data as-is (i.e. encoding is left to code calling the
        :class:`HttpApi`).
        */
        return data;
    };

    self.parse_opts = function(opts) {
        opts = _.defaults(opts || {}, {headers: {}});

        var headers = _.extend({}, self.defaults.headers);

        if (self.defaults.auth) {
            headers.Authorization = [self.make_auth(
                self.defaults.auth.username,
                self.defaults.auth.password)];
        }

        return _.defaults(opts, _.omit(self.defaults, 'headers', 'auth'), {
            encoder: self.encode_request_data,
            headers: _.defaults(opts.headers, headers)
        });
    };

    self.parse_reply = function(reply, request) {
        /**:HttpApi.parse_reply(reply, request)

        Check an HTTP reply and throw an :class:`HttpRequestError` if the
        sandbox API command was unsuccessful, or otherwise parse the sandbox's
        reply into a response.  If the response status code is not in the 200
        range or the reply body cannot be decoded, throw an
        :class:`HttpResponseError`.

        Logs an error via the sandbox logging resource in an error is raised.

        :param object reply:
            Raw response to the sandbox API command.
        :param HttpRequest request:
            The request that initiated the sandbox API command.

        Returns an :class:`HttpResponse` or throws an :class:`HttpApiError`
        (either the :class:`HttpRequestError` or :class:`HttpResponseError`
        derivative, depending on what error occured).
        */
        var response = new HttpResponse(request, reply.code, {
            body: reply.body,
            decoder: self.decode_response_body
        });

        response.decode();

        if (response.code < 200 || response.code >= 300) {
            throw new HttpResponseError(response);
        }

        return response;
    };

    self.request = function(method, url, opts) {
        /**:HttpApi.request(method, url, opts)

        :param string method:
            The HTTP method to use (e.g. `GET`, `POST`).
        :param string url:
            The URL to make the request to. If you pass in query parameters
            using ``opts.params``, don't include any in the URL itself.
        :param opts.params:
            An object of key-value pairs to append to the URL as query
            parameters. Can be in any form accepted by node.js's querystring
            module
        :param object opts.data:
            Data to pass as the request body. Will be encoded using
            :func:`HttpApi.encode_request_data` before being sent.
        :param object opts.headers:
            Additional headers to add to the default headers.

        Returns a :class:`HttpResponse` via a promise. Failures while making
        and checking the request will be thrown as :class:`HttpApiError`s, and
        can be caught with a Q errback. See :meth:`HttpApi.parse_reply` for
        more on the response parsing and error throwing.
        */
        var request = new HttpRequest(method, url, self.parse_opts(opts));
        request.encode();

        var cmd = request.to_cmd();
        return self
            .im.api_request(cmd.name, cmd.data)
            .then(
                function(reply) {
                    return self.parse_reply(reply, request);
                }, function(e) {
                    throw new HttpRequestError(request, e.reply.reason);
                });

    };

    self.get = function(url, opts) {
        /**:HttpApi.get(url, opts)

        Make an HTTP GET request.

        :param string url:
            The URL to make the request to.
        :param object opts:
            Options to pass to :func:`HttpApi.request`.

        Returns a promise which fires with the decoded value of the
        response body or an object with an error attribute containing
        the error message.
        */
        return self.request('GET', url, opts);
    };

    self.head = function(url, opts) {
        /**:HttpApi.head(url, opts)

        Make an HTTP HEAD request.

        :param string url:
            The URL to make the request to.
        :param object opts:
            Options to pass to :func:`HttpApi.request`.

        Returns a promise which fires with the decoded value of the
        response body or an object with an error attribute containing
        the error message.
        */
        return self.request('HEAD', url, opts);
    };

    self.post = function(url, opts) {
        /**:HttpApi.post(url, opts)

        Make an HTTP POST request.

        :param string url:
            The URL to make the request to.
        :param object opts:
            Options to pass to :func:`HttpApi.request`.

        Returns a promise which fires with the decoded value of the
        response body or an object with an error attribute containing
        the error message.
        */
        return self.request('POST', url, opts);
    };

    self.put = function(url, opts) {
        /**:HttpApi.put(url, opts)

        Make an HTTP PUT request.

        :param string url:
            The URL to make the request to.
        :param object opts:
            Options to pass to :func:`HttpApi.request`.

        Returns a promise which fires with the decoded value of the
        response body or an object with an error attribute containing
        the error message.
        */
        return self.request('PUT', url, opts);
    };

    self.delete = function(url, opts) {
        /**:HttpApi.delete(url, opts)

        Make an HTTP DELETE request.

        :param string url:
            The URL to make the request to.
        :param object opts:
            Options to pass to :func:`HttpApi.request`.

        Returns a promise which fires with the decoded value of the
        response body or an object with an error attribute containing
        the error message.
        */
        return self.request('DELETE', url, opts);
    };
});

var JsonApi = HttpApi.extend(function(self, im, opts) {
    /**class:JsonApi(im, opts)

    A helper class for making HTTP requests that send and receive JSON
    encoded data.

    :param InteractionMachine im:
        The interaction machine to use when making requests.
    :param object opts.headers:
        Default headers to use in HTTP requests. The ``Content-Type``
        header is overridden to be ``application/json; charset=utf-8``.
    :param object opts.auth:
        Adds a HTTP Basic authentication to the default headers. Should
        contain ``username`` and ``password`` attributes.
    */
    HttpApi.call(self, im, opts);

    self.defaults.headers['Content-Type'] = ['application/json; charset=utf-8'];

    self.decode_response_body = function(body) {
        /**:JsonApi.decode_response_body(body)

        Decode an HTTP response body using ``JSON.parse()``.

        :param string body:
            Raw HTTP response body to parse.

        Returns the decoded response body.
        */
        return JSON.parse(body);
    };

    self.encode_request_data = function(data) {
        /**:JsonApi.encode_request_data(data)

        Encode an object as JSON using ``JSON.stringify()``.

        :param object data:
            Object to encode to JSON.

        Returns the serialized object as a string.
        */
        return JSON.stringify(data);
    };
});

this.HttpApi = HttpApi;
this.JsonApi = JsonApi;

this.HttpApiError = HttpApiError;
this.HttpRequestError = HttpRequestError;
this.HttpResponseError = HttpResponseError;
this.HttpRequest = HttpRequest;
this.HttpResponse = HttpResponse;
<|endoftext|>


<commit_before>"use strict"
var userToken = process.env.AGENDOR_TOKEN;
var agendorUrl = "https://api.agendor.com.br/v1/";
var request = require('request');
var moment = require('moment');

module.exports.nextTask = function nextTask(sendTextMessage, sender, req, res) {
  request({
    url: agendorUrl + "tasks",
    headers: {"Authorization": "Token " + userToken},
    qs: {page:1, per_page:1},
    method: 'GET'
  }, function(error, response, body) {
    if (error) {
      console.log('Error sending messages: ', error)
    } else if (response.body.error) {
      console.log('Error: ', response.body.error)
    }
    if (!error && response.statusCode == 200) {
      var text = "🐶 au au! Fui lá no agendor e achei! \n\n " + formatTask(JSON.parse(body)[0]);
      sendTextMessage(sender, text);
      res.sendStatus(200);
    }
  });
};

module.exports.createTask = function createTask(params, sendTextMessage, sender, req, res) {
  request({
    url: agendorUrl + "tasks",
    headers: {"Authorization": "Token " + userToken},
    body: JSON.stringify(params),
    method: 'POST'
  }, function(error, response, body) {
    console.log(body);
    if (error) {
      console.log('Error sending messages: ', error)
    } else if (response.body.error) {
      console.log('Error: ', response.body.error)
    }
    if (!error && response.statusCode == 200) {
      var text = "🐶 au au! Deu certo! Cadastrei esta tarefa!";
      sendTextMessage(sender, text);
      req.session = null;
      res.sendStatus(200);
    }
  });
};

function formatTask(task) {
  var entity;
  var entityDesc;
  if (task.deal && task.deal.name) {
    entityDesc = "o negócio";
    entity = task.deal.name;
  } else if (task.organization && task.organization.name) {
    entityDesc = "a empresa";
    entity = task.organization.name;
  } else if (task.person && task.person.name) {
    entityDesc = "a pessoa";
    entity = task.person.name;
  }

  var text = task.text;
  var dueDate = task.dueDate;

  var textMessage = "Você possuí uma tarefa para " + entityDesc + ": " + entity + " ";
  if (dueDate) {
    textMessage += "às " + moment(dueDate).format("DD/MM/YYYY hh:mm");
  }
  if (text) {
    textMessage += "\n Descrição:\n" + text;
  }
  return textMessage;
}
<commit_msg>Add Logs<commit_after>"use strict"
var userToken = process.env.AGENDOR_TOKEN;
var agendorUrl = "https://api.agendor.com.br/v1/";
var request = require('request');
var moment = require('moment');

module.exports.nextTask = function nextTask(sendTextMessage, sender, req, res) {
  request({
    url: agendorUrl + "tasks",
    headers: {"Authorization": "Token " + userToken},
    qs: {page:1, per_page:1},
    method: 'GET'
  }, function(error, response, body) {
    if (error) {
      console.log('Error sending messages: ', error)
    } else if (response.body.error) {
      console.log('Error: ', response.body.error)
    }
    if (!error && response.statusCode == 200) {
      var text = "🐶 au au! Fui lá no agendor e achei! \n\n " + formatTask(JSON.parse(body)[0]);
      sendTextMessage(sender, text);
      res.sendStatus(200);
    }
  });
};

module.exports.createTask = function createTask(params, sendTextMessage, sender, req, res) {
  request({
    url: agendorUrl + "tasks",
    headers: {"Authorization": "Token " + userToken},
    body: JSON.stringify(params),
    method: 'POST'
  }, function(error, response, body) {
    console.log(body);
    if (error) {
      console.log('Error sending messages: ', error)
    } else if (response.body.error) {
      console.log('Error: ', response.body.error)
    }
    if (!error && response.statusCode == 200) {
      var text = "🐶 au au! Deu certo! Cadastrei esta tarefa!";
      sendTextMessage(sender, text);
      res.sendStatus(200);
    }
  });
};

function formatTask(task) {
  var entity;
  var entityDesc;
  if (task.deal && task.deal.name) {
    entityDesc = "o negócio";
    entity = task.deal.name;
  } else if (task.organization && task.organization.name) {
    entityDesc = "a empresa";
    entity = task.organization.name;
  } else if (task.person && task.person.name) {
    entityDesc = "a pessoa";
    entity = task.person.name;
  }

  var text = task.text;
  var dueDate = task.dueDate;

  var textMessage = "Você possuí uma tarefa para " + entityDesc + ": " + entity + " ";
  if (dueDate) {
    textMessage += "às " + moment(dueDate).format("DD/MM/YYYY hh:mm");
  }
  if (text) {
    textMessage += "\n Descrição:\n" + text;
  }
  return textMessage;
}
<|endoftext|>


<commit_before>/**
 * Copyright 2012-2013 Amazon.com, Inc. or its affiliates. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"). You
 * may not use this file except in compliance with the License. A copy of
 * the License is located at
 *
 *     http://aws.amazon.com/apache2.0/
 *
 * or in the "license" file accompanying this file. This file is
 * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
 * ANY KIND, either express or implied. See the License for the specific
 * language governing permissions and limitations under the License.
 */

var AWS = require('../core');
var EventEmitter = require('events').EventEmitter;
require('../http');

/**
 * @api private
 */
AWS.XHRClient = AWS.util.inherit({
  handleRequest: function handleRequest(httpRequest, httpOptions, callback, errCallback) {
    var self = this;
    var endpoint = httpRequest.endpoint;
    var xhr = new XMLHttpRequest();
    var emitter = new EventEmitter();
    var href = endpoint.protocol + '//' + endpoint.host;
    if (endpoint.port != 80 && endpoint.port != 443) {
      href += ':' + endpoint.port;
    }
    href += httpRequest.path;

    if (httpOptions.timeout) {
      xhr.timeout = httpOptions.timeout;
    }

    xhr.addEventListener('readystatechange', function() {
      if (this.readyState === this.HEADERS_RECEIVED) {
        xhr.responseType = 'arraybuffer';
        emitter.statusCode = xhr.status;
        emitter.headers = self.parseHeaders(xhr.getAllResponseHeaders());
        callback(emitter);
      } else if (this.readyState === this.DONE) {
        var ab = xhr.response;
        if (ab) {
          var buffer = new Buffer(ab.byteLength);
          var view = new Uint8Array(ab);
          for (var i = 0; i < buffer.length; ++i) {
            buffer[i] = view[i];
          }

          emitter.emit('data', buffer);
        }

        emitter.emit('end');
      }
    }, false);
    xhr.addEventListener('timeout', function () {
      errCallback(AWS.util.error(new Error('Timeout'), {code: 'TimeoutError'}));
    }, false);
    xhr.addEventListener('error', function () {
      errCallback(new Error('Network Failure'));
    }, false);

    xhr.open(httpRequest.method, href, true);
    AWS.util.each(httpRequest.headers, function (key, value) {
      if (key !== 'Content-Length' && key !== 'User-Agent' && key !== 'Host') {
        xhr.setRequestHeader(key, value);
      }
    });
    xhr.send(httpRequest.body);

    return emitter;
  },

  parseHeaders: function parseHeaders(rawHeaders) {
    var headers = {};
    AWS.util.arrayEach(rawHeaders.split(/\r?\n/), function (line) {
      var key = line.split(':', 1)[0];
      var value = line.substring(key.length + 2);
      headers[key] = value;
    });
    return headers;
  }
});

/**
 * @api private
 */
AWS.HttpClient.prototype = AWS.XHRClient.prototype;

/**
 * @api private
 */
AWS.HttpClient.streamsApiVersion = 1;
<commit_msg>Ensure typed arrays get sent as ArrayBuffers<commit_after>/**
 * Copyright 2012-2013 Amazon.com, Inc. or its affiliates. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"). You
 * may not use this file except in compliance with the License. A copy of
 * the License is located at
 *
 *     http://aws.amazon.com/apache2.0/
 *
 * or in the "license" file accompanying this file. This file is
 * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
 * ANY KIND, either express or implied. See the License for the specific
 * language governing permissions and limitations under the License.
 */

var AWS = require('../core');
var EventEmitter = require('events').EventEmitter;
require('../http');

/**
 * @api private
 */
AWS.XHRClient = AWS.util.inherit({
  handleRequest: function handleRequest(httpRequest, httpOptions, callback, errCallback) {
    var self = this;
    var endpoint = httpRequest.endpoint;
    var xhr = new XMLHttpRequest();
    var emitter = new EventEmitter();
    var href = endpoint.protocol + '//' + endpoint.host;
    if (endpoint.port != 80 && endpoint.port != 443) {
      href += ':' + endpoint.port;
    }
    href += httpRequest.path;

    if (httpOptions.timeout) {
      xhr.timeout = httpOptions.timeout;
    }

    xhr.addEventListener('readystatechange', function() {
      if (this.readyState === this.HEADERS_RECEIVED) {
        xhr.responseType = 'arraybuffer';
        emitter.statusCode = xhr.status;
        emitter.headers = self.parseHeaders(xhr.getAllResponseHeaders());
        callback(emitter);
      } else if (this.readyState === this.DONE) {
        var ab = xhr.response;
        if (ab) {
          var buffer = new Buffer(ab.byteLength);
          var view = new Uint8Array(ab);
          for (var i = 0; i < buffer.length; ++i) {
            buffer[i] = view[i];
          }

          emitter.emit('data', buffer);
        }

        emitter.emit('end');
      }
    }, false);
    xhr.addEventListener('timeout', function () {
      errCallback(AWS.util.error(new Error('Timeout'), {code: 'TimeoutError'}));
    }, false);
    xhr.addEventListener('error', function () {
      errCallback(new Error('Network Failure'));
    }, false);

    xhr.open(httpRequest.method, href, true);
    AWS.util.each(httpRequest.headers, function (key, value) {
      if (key !== 'Content-Length' && key !== 'User-Agent' && key !== 'Host') {
        xhr.setRequestHeader(key, value);
      }
    });

    if (httpRequest.body && typeof httpRequest.body.buffer === 'object') {
      xhr.send(httpRequest.body.buffer); // typed arrays sent as ArrayBuffer
    } else {
      xhr.send(httpRequest.body);
    }

    return emitter;
  },

  parseHeaders: function parseHeaders(rawHeaders) {
    var headers = {};
    AWS.util.arrayEach(rawHeaders.split(/\r?\n/), function (line) {
      var key = line.split(':', 1)[0];
      var value = line.substring(key.length + 2);
      headers[key] = value;
    });
    return headers;
  }
});

/**
 * @api private
 */
AWS.HttpClient.prototype = AWS.XHRClient.prototype;

/**
 * @api private
 */
AWS.HttpClient.streamsApiVersion = 1;
<|endoftext|>


<commit_before>import { inject as service } from '@ember/service';
import Component from '@ember/component';
import RSVP from 'rsvp';
import EmberObject, { computed } from '@ember/object';
import { isEmpty } from '@ember/utils';
import layout from '../templates/components/dashboard-calendar';
import moment from 'moment';
import momentFormat from 'ember-moment/computeds/format';

const { all, map } = RSVP;

export default Component.extend({
  layout,
  init() {
    this._super(...arguments);
    //do these on setup otherwise tests were failing because
    //the old filter value hung around
    this.set('selectedSessionTypes', []);
    this.set('selectedCourseLevels', []);
    this.set('selectedCohorts', []);
    this.set('selectedCourses', []);
    this.set('selectedTerms', []);
  },
  userEvents: service(),
  schoolEvents: service(),
  currentUser: service(),
  store: service(),
  i18n: service(),
  classNames: ['dashboard-calendar'],
  selectedSchool: null,
  selectedDate: null,
  selectedView: null,
  selectedCohorts: null,
  selectedCourseLevels: null,
  selectedCourses: null,
  selectedSessionTypes: null,
  selectedAcademicYear: null,
  selectedTerms: null,

  dayTranslation: computed('i18n.locale', function(){
    return this.get('i18n').t('general.day');
  }),
  weekTranslation: computed('i18n.locale', function(){
    return this.get('i18n').t('general.week');
  }),
  monthTranslation: computed('i18n.locale', function(){
    return this.get('i18n').t('general.month');
  }),
  loadingEventsTranslation: computed('i18n.locale', function(){
    return this.get('i18n').t('general.loadingEvents');
  }),
  icsInstructionsTranslation: computed('i18n.locale', function(){
    return this.get('i18n').t('general.icsInstructions');
  }),
  fromTimeStamp: computed('selectedDate', 'selectedView', function(){
    return moment(this.get('selectedDate')).startOf(this.get('selectedView')).subtract(this.get('skew'), 'days').unix();
  }),
  toTimeStamp: computed('selectedDate', 'selectedView', function(){
    return moment(this.get('selectedDate')).endOf(this.get('selectedView')).add(this.get('skew'), 'days').unix();
  }),
  clockSkew: computed('selectedView', function(){
    if(this.set('selectedView') === 'month'){
      return 6;
    }

    return 1;
  }),
  calendarDate: momentFormat('selectedDate', 'YYYY-MM-DD'),

  /**
   * @property academicYears
   * @type {Ember.computed}
   * @public
   */
  academicYears: computed('allAcademicYears.[]', async function(){
    const years = await this.get('allAcademicYears');
    return years.sortBy('title');
  }),

  /**
   * @property cohorts
   * @type {Ember.computed}
   * @public
   */
  cohorts: computed('bestSelectedSchool', 'bestSelectedAcademicYear', async function(){
    const school = await this.get('bestSelectedSchool');
    const year = await this.get('bestSelectedAcademicYear');
    const cohorts = await school.getCohortsForYear(year.get('title'));
    const cohortProxies = await map(cohorts.toArray(), async cohort => {
      let displayTitle = cohort.get('title');
      if (isEmpty(displayTitle)) {
        const i18n = this.get('i18n');
        const classOfYear = await cohort.get('classOfYear');
        displayTitle = i18n.t('general.classOf', { year: classOfYear });
      }

      return EmberObject.create({
        cohort,
        displayTitle
      });
    });

    return cohortProxies.sortBy('displayTitle').mapBy('cohort');
  }),

  /**
   * @property courseLevels
   * @type {Array}
   * @public
   */
  courseLevels: [1, 2, 3, 4, 5],

  /**
   * @property courses
   * @type {Ember.computed}
   * @public
   */
  courses: computed('bestSelectedSchool', 'bestSelectedAcademicYear', async function(){
    const school = await this.get('bestSelectedSchool');
    const year = await this.get('bestSelectedAcademicYear');
    const courses = await this.get('store').query('course', {
      filters: {
        school: school.get('id'),
        year: year.get('title')
      },
    });
    return courses.sortBy('title');
  }),

  /**
   * @property filteredEvents
   * @type {Ember.computed}
   * @public
   */
  filteredEvents: computed(
    'ourEvents.[]',
    'eventsWithSelectedSessionTypes.[]',
    'eventsWithSelectedCourseLevels.[]',
    'eventsWithSelectedCohorts.[]',
    'eventsWithSelectedCourses.[]',
    'eventsWithSelectedTerms.[]',
    async function() {
      const eventTypes = [
        'eventsWithSelectedSessionTypes',
        'eventsWithSelectedCourseLevels',
        'eventsWithSelectedCohorts',
        'eventsWithSelectedCourses',
        'eventsWithSelectedTerms',
      ];
      const allFilteredEvents = await map(eventTypes, async name => {
        return await this.get(name);
      });

      const events = await this.get('ourEvents');

      return events.filter(event => {
        return allFilteredEvents.every(arr => {
          return arr.includes(event);
        });
      });
    }
  ),

  /**
   * @property filterTags
   * @type {Ember.computed}
   * @public
   */
  filterTags: computed('activeFilters.[]', async function() {
    const activeFilters = this.get('activeFilters');

    return map(activeFilters, async (filter) => {
      let hash = { filter };

      if (typeof filter === 'number') {
        hash.class = 'tag-course-level';
        hash.name = `Course Level ${filter}`;
      } else {
        let model = filter.get('constructor.modelName');
        switch (model) {
        case 'session-type':
          hash.class = 'tag-session-type';
          hash.name = filter.get('title');
          break;
        case 'cohort': {
          hash.class = 'tag-cohort';
          let displayTitle = filter.get('title');
          const i18n = this.get('i18n');
          const classOfYear = await filter.get('classOfYear');
          if (isEmpty(displayTitle)) {
            displayTitle = i18n.t('general.classOf', {year: classOfYear});
          }
          const program = await filter.get('programYear.program');
          hash.name = `${displayTitle} ${program.get('title')}`;
          break;
        }
        case 'term': {
          hash.class = 'tag-term';
          const allTitles = await filter.get('titleWithParentTitles');
          const vocabulary = await filter.get('vocabulary');
          const title = vocabulary.get('title');
          hash.name = `${title} > ${allTitles}`;
          break;
        }
        case 'course':
          hash.class = 'tag-course';
          hash.name = filter.get('title');
          break;
        }
      }
      return hash;
    });
  }),

  hasMoreThanOneSchool: computed('schools.[]', async function() {
    const schools = await this.get('schools');
    return (schools.length > 1);
  }),

  /**
   * @property selectedSchool
   * @type {Ember.computed}
   * @public
   */
  bestSelectedSchool: computed('selectedSchool', 'currentUser.model.school', async function(){
    const selectedSchool = this.get('selectedSchool');
    if (selectedSchool)  {
      return selectedSchool;
    }
    const user = await this.get('currentUser').get('model');
    return await user.get('school');
  }),

  /**
   * @property schools
   * @type {Ember.computed}
   * @public
   */
  schools: computed('allSchools.[]', async function(){
    const schools = await this.get('allSchools');
    return schools.sortBy('title');
  }),

  /**
   * @property bestSelectedAcademicYear
   * @type {Ember.computed}
   * @public
   */
  bestSelectedAcademicYear: computed('selectedAcademicYear', 'allAcademicYears.[]', async function(){
    const selectedAcademicYear = this.get('selectedAcademicYear');
    if (selectedAcademicYear) {
      return selectedAcademicYear;
    }
    const years = await this.get('allAcademicYears');
    return years.sortBy('title').get('lastObject');
  }),

  /**
   * @property sessionTypes
   * @type {Ember.computed}
   * @public
   */
  sessionTypes: computed('bestSelectedSchool.sessionTypes.[]', 'selectedSessionTypes.[]', async function(){
    const school = await this.get('bestSelectedSchool');
    const types = await school.get('sessionTypes');
    return types.toArray().sortBy('title');
  }),

  /**
   * @property vocabularies
   * @type {Ember.computed}
   * @public
   */
  vocabularies: computed('bestSelectedSchool.vocabularies.[]', async function(){
    const school = await this.get('bestSelectedSchool');
    const vocabularies = await school.get('vocabularies');
    await all(vocabularies.mapBy('terms'));
    return vocabularies.toArray().sortBy('title');
  }),

  /**
   * @property showClearFilters
   * @type {Ember.computed}
   * @public
   */
  activeFilters: computed('selectedCourses.[]', 'selectedSessionTypes.[]', 'selectedCourseLevels.[]', 'selectedCohorts.[]', 'selectedTerms.[]', function () {
    const a = this.get('selectedSessionTypes');
    const b = this.get('selectedCourseLevels');
    const c = this.get('selectedCohorts');
    const d = this.get('selectedCourses');
    const e = this.get('selectedTerms');

    return [].concat(a, b, c, d, e);
  }),

  /**
   * @property ourEvents
   * @type {Ember.computed}
   * @protected
   */
  ourEvents: computed('mySchedule', 'fromTimeStamp', 'toTimeStamp', 'bestSelectedSchool', 'selectedView', async function(){
    if(this.get('mySchedule')) {
      return await this.get('userEvents').getEvents(this.get('fromTimeStamp'), this.get('toTimeStamp'));
    }
    const school = await this.get('bestSelectedSchool');
    return await this.get('schoolEvents').getEvents(school.get('id'), this.get('fromTimeStamp'), this.get('toTimeStamp'));
  }),

  /**
   * @property eventsWithSelectedSessionTypes
   * @type {Ember.computed}
   * @protected
   */
  eventsWithSelectedSessionTypes: computed('ourEvents.[]', 'selectedSessionTypes.[]', async function(){
    const events = await this.get('ourEvents');
    const selectedSessionTypes = this.get('selectedSessionTypes').mapBy('id');
    if(isEmpty(selectedSessionTypes)) {
      return events;
    }
    const matchingEvents = await map(events, async event => {
      if(event.ilmSession || event.offering) {
        const id = await this.get('userEvents').getSessionTypeIdForEvent(event);
        if (selectedSessionTypes.includes(id)) {
          return event;
        }
      }
      return null;
    });

    return matchingEvents.filter(event => {
      return ! isEmpty(event);
    });
  }),

  /**
   * @property eventsWithSelectedCourseLevels
   * @type {Ember.computed}
   * @protected
   */
  eventsWithSelectedCourseLevels: computed('ourEvents.[]', 'selectedCourseLevels.[]', async function(){
    const events = await this.get('ourEvents');
    const selectedCourseLevels = this.get('selectedCourseLevels');
    if(isEmpty(selectedCourseLevels)) {
      return events;
    }
    const matchingEvents = await map(events, async event => {
      if(event.ilmSession || event.offering) {
        const level = await this.get('userEvents').getCourseLevelForEvent(event);
        if (selectedCourseLevels.includes(level)) {
          return event;
        }
      }
      return null;
    });

    return matchingEvents.filter(event => {
      return ! isEmpty(event);
    });
  }),

  /**
   * @property eventsWithSelectedCohorts
   * @type {Ember.computed}
   * @protected
   */
  eventsWithSelectedCohorts: computed('ourEvents.[]', 'selectedCohorts.[]', async function(){
    const events = await this.get('ourEvents');
    const selectedCohorts = this.get('selectedCohorts').mapBy('id');
    if(isEmpty(selectedCohorts)) {
      return events;
    }
    const matchingEvents = await map(events, async event => {
      if (event.ilmSession || event.offering) {
        const cohorts = await this.get('userEvents').getCohortIdsForEvent(event);
        if(cohorts.any(cohortId => {
          return selectedCohorts.includes(cohortId);
        })) {
          return event;
        }
      }
      return null;
    });

    return matchingEvents.filter(event => {
      return ! isEmpty(event);
    });
  }),

  /**
   * @property eventsWithSelectedCourses
   * @type {Ember.computed}
   * @protected
   */
  eventsWithSelectedCourses: computed('ourEvents.[]', 'selectedCourses.[]', async function(){
    const events = await this.get('ourEvents');
    const selectedCourses = this.get('selectedCourses').mapBy('id');
    if(isEmpty(selectedCourses)) {
      return events;
    }

    const matchingEvents = await map(events, async event => {
      if(event.ilmSession || event.offering) {
        const courseId = await this.get('userEvents').getCourseIdForEvent(event);
        if(selectedCourses.includes(courseId)) {
          return event;
        }
      }
      return null;
    });

    return matchingEvents.filter(event => {
      return ! isEmpty(event);
    });
  }),

  /**
   * @property eventsWithSelectedTerms
   * @type {Ember.computed}
   * @protected
   */
  eventsWithSelectedTerms: computed('ourEvents.[]', 'selectedTerms.[]', async function(){
    const events = await this.get('ourEvents');
    const selectedTerms = this.get('selectedTerms').mapBy('id');
    if(isEmpty(selectedTerms)) {
      return events;
    }
    const matchingEvents = await map(events, async event => {
      if (event.ilmSession || event.offering) {

        const termIds = await this.get('userEvents').getTermIdsForEvent(event);
        if(termIds.any(termId => {
          return selectedTerms.includes(termId);
        })) {
          return event;
        }
      }
      return null;
    });

    return matchingEvents.filter(event => {
      return ! isEmpty(event);
    });
  }),

  /**
   * @property allSchools
   * @type {Ember.computed}
   * @protected
   */
  allSchools: computed(async function(){
    const user = await this.get('currentUser.model');
    return await user.get('schools');
  }),

  /**
   * @property allAcademicYears
   * @type {Ember.computed}
   * @protected
   */
  allAcademicYears: computed(function(){
    return this.get('store').findAll('academic-year');
  }),

  actions: {
    toggleSessionType(sessionType){
      if(this.get('selectedSessionTypes').includes(sessionType)){
        this.get('selectedSessionTypes').removeObject(sessionType);
      } else {
        this.get('selectedSessionTypes').pushObject(sessionType);
      }
    },
    toggleCourseLevel(level){
      if(this.get('selectedCourseLevels').includes(level)){
        this.get('selectedCourseLevels').removeObject(level);
      } else {
        this.get('selectedCourseLevels').pushObject(level);
      }
    },
    toggleCohort(cohort){
      if(this.get('selectedCohorts').includes(cohort)){
        this.get('selectedCohorts').removeObject(cohort);
      } else {
        this.get('selectedCohorts').pushObject(cohort);
      }
    },
    toggleCourse(course){
      if(this.get('selectedCourses').includes(course)){
        this.get('selectedCourses').removeObject(course);
      } else {
        this.get('selectedCourses').pushObject(course);
      }
    },
    toggleTerm(term){
      if(this.get('selectedTerms').includes(term)){
        this.get('selectedTerms').removeObject(term);
      } else {
        this.get('selectedTerms').pushObject(term);
      }
    },

    clearFilters() {
      const selectedCourses = [];
      const selectedSessionTypes = [];
      const selectedCourseLevels = [];
      const selectedCohorts = [];

      this.setProperties({ selectedCourses, selectedSessionTypes, selectedCourseLevels, selectedCohorts });
    },

    removeFilter(filter) {
      if (typeof filter === 'number') {
        this.send('toggleCourseLevel', filter);
      } else {
        let model = filter.get('constructor.modelName');

        switch (model) {
        case 'session-type':
          this.send('toggleSessionType', filter);
          break;
        case 'cohort':
          this.send('toggleCohort', filter);
          break;
        case 'course':
          this.send('toggleCourse', filter);
          break;
        }
      }
    }
  }
});
<commit_msg>Clear term filter correctly<commit_after>import { inject as service } from '@ember/service';
import Component from '@ember/component';
import RSVP from 'rsvp';
import EmberObject, { computed } from '@ember/object';
import { isEmpty } from '@ember/utils';
import layout from '../templates/components/dashboard-calendar';
import moment from 'moment';
import momentFormat from 'ember-moment/computeds/format';

const { all, map } = RSVP;

export default Component.extend({
  layout,
  init() {
    this._super(...arguments);
    //do these on setup otherwise tests were failing because
    //the old filter value hung around
    this.set('selectedSessionTypes', []);
    this.set('selectedCourseLevels', []);
    this.set('selectedCohorts', []);
    this.set('selectedCourses', []);
    this.set('selectedTerms', []);
  },
  userEvents: service(),
  schoolEvents: service(),
  currentUser: service(),
  store: service(),
  i18n: service(),
  classNames: ['dashboard-calendar'],
  selectedSchool: null,
  selectedDate: null,
  selectedView: null,
  selectedCohorts: null,
  selectedCourseLevels: null,
  selectedCourses: null,
  selectedSessionTypes: null,
  selectedAcademicYear: null,
  selectedTerms: null,

  dayTranslation: computed('i18n.locale', function(){
    return this.get('i18n').t('general.day');
  }),
  weekTranslation: computed('i18n.locale', function(){
    return this.get('i18n').t('general.week');
  }),
  monthTranslation: computed('i18n.locale', function(){
    return this.get('i18n').t('general.month');
  }),
  loadingEventsTranslation: computed('i18n.locale', function(){
    return this.get('i18n').t('general.loadingEvents');
  }),
  icsInstructionsTranslation: computed('i18n.locale', function(){
    return this.get('i18n').t('general.icsInstructions');
  }),
  fromTimeStamp: computed('selectedDate', 'selectedView', function(){
    return moment(this.get('selectedDate')).startOf(this.get('selectedView')).subtract(this.get('skew'), 'days').unix();
  }),
  toTimeStamp: computed('selectedDate', 'selectedView', function(){
    return moment(this.get('selectedDate')).endOf(this.get('selectedView')).add(this.get('skew'), 'days').unix();
  }),
  clockSkew: computed('selectedView', function(){
    if(this.set('selectedView') === 'month'){
      return 6;
    }

    return 1;
  }),
  calendarDate: momentFormat('selectedDate', 'YYYY-MM-DD'),

  /**
   * @property academicYears
   * @type {Ember.computed}
   * @public
   */
  academicYears: computed('allAcademicYears.[]', async function(){
    const years = await this.get('allAcademicYears');
    return years.sortBy('title');
  }),

  /**
   * @property cohorts
   * @type {Ember.computed}
   * @public
   */
  cohorts: computed('bestSelectedSchool', 'bestSelectedAcademicYear', async function(){
    const school = await this.get('bestSelectedSchool');
    const year = await this.get('bestSelectedAcademicYear');
    const cohorts = await school.getCohortsForYear(year.get('title'));
    const cohortProxies = await map(cohorts.toArray(), async cohort => {
      let displayTitle = cohort.get('title');
      if (isEmpty(displayTitle)) {
        const i18n = this.get('i18n');
        const classOfYear = await cohort.get('classOfYear');
        displayTitle = i18n.t('general.classOf', { year: classOfYear });
      }

      return EmberObject.create({
        cohort,
        displayTitle
      });
    });

    return cohortProxies.sortBy('displayTitle').mapBy('cohort');
  }),

  /**
   * @property courseLevels
   * @type {Array}
   * @public
   */
  courseLevels: [1, 2, 3, 4, 5],

  /**
   * @property courses
   * @type {Ember.computed}
   * @public
   */
  courses: computed('bestSelectedSchool', 'bestSelectedAcademicYear', async function(){
    const school = await this.get('bestSelectedSchool');
    const year = await this.get('bestSelectedAcademicYear');
    const courses = await this.get('store').query('course', {
      filters: {
        school: school.get('id'),
        year: year.get('title')
      },
    });
    return courses.sortBy('title');
  }),

  /**
   * @property filteredEvents
   * @type {Ember.computed}
   * @public
   */
  filteredEvents: computed(
    'ourEvents.[]',
    'eventsWithSelectedSessionTypes.[]',
    'eventsWithSelectedCourseLevels.[]',
    'eventsWithSelectedCohorts.[]',
    'eventsWithSelectedCourses.[]',
    'eventsWithSelectedTerms.[]',
    async function() {
      const eventTypes = [
        'eventsWithSelectedSessionTypes',
        'eventsWithSelectedCourseLevels',
        'eventsWithSelectedCohorts',
        'eventsWithSelectedCourses',
        'eventsWithSelectedTerms',
      ];
      const allFilteredEvents = await map(eventTypes, async name => {
        return await this.get(name);
      });

      const events = await this.get('ourEvents');

      return events.filter(event => {
        return allFilteredEvents.every(arr => {
          return arr.includes(event);
        });
      });
    }
  ),

  /**
   * @property filterTags
   * @type {Ember.computed}
   * @public
   */
  filterTags: computed('activeFilters.[]', async function() {
    const activeFilters = this.get('activeFilters');

    return map(activeFilters, async (filter) => {
      let hash = { filter };

      if (typeof filter === 'number') {
        hash.class = 'tag-course-level';
        hash.name = `Course Level ${filter}`;
      } else {
        let model = filter.get('constructor.modelName');
        switch (model) {
        case 'session-type':
          hash.class = 'tag-session-type';
          hash.name = filter.get('title');
          break;
        case 'cohort': {
          hash.class = 'tag-cohort';
          let displayTitle = filter.get('title');
          const i18n = this.get('i18n');
          const classOfYear = await filter.get('classOfYear');
          if (isEmpty(displayTitle)) {
            displayTitle = i18n.t('general.classOf', {year: classOfYear});
          }
          const program = await filter.get('programYear.program');
          hash.name = `${displayTitle} ${program.get('title')}`;
          break;
        }
        case 'term': {
          hash.class = 'tag-term';
          const allTitles = await filter.get('titleWithParentTitles');
          const vocabulary = await filter.get('vocabulary');
          const title = vocabulary.get('title');
          hash.name = `${title} > ${allTitles}`;
          break;
        }
        case 'course':
          hash.class = 'tag-course';
          hash.name = filter.get('title');
          break;
        }
      }
      return hash;
    });
  }),

  hasMoreThanOneSchool: computed('schools.[]', async function() {
    const schools = await this.get('schools');
    return (schools.length > 1);
  }),

  /**
   * @property selectedSchool
   * @type {Ember.computed}
   * @public
   */
  bestSelectedSchool: computed('selectedSchool', 'currentUser.model.school', async function(){
    const selectedSchool = this.get('selectedSchool');
    if (selectedSchool)  {
      return selectedSchool;
    }
    const user = await this.get('currentUser').get('model');
    return await user.get('school');
  }),

  /**
   * @property schools
   * @type {Ember.computed}
   * @public
   */
  schools: computed('allSchools.[]', async function(){
    const schools = await this.get('allSchools');
    return schools.sortBy('title');
  }),

  /**
   * @property bestSelectedAcademicYear
   * @type {Ember.computed}
   * @public
   */
  bestSelectedAcademicYear: computed('selectedAcademicYear', 'allAcademicYears.[]', async function(){
    const selectedAcademicYear = this.get('selectedAcademicYear');
    if (selectedAcademicYear) {
      return selectedAcademicYear;
    }
    const years = await this.get('allAcademicYears');
    return years.sortBy('title').get('lastObject');
  }),

  /**
   * @property sessionTypes
   * @type {Ember.computed}
   * @public
   */
  sessionTypes: computed('bestSelectedSchool.sessionTypes.[]', 'selectedSessionTypes.[]', async function(){
    const school = await this.get('bestSelectedSchool');
    const types = await school.get('sessionTypes');
    return types.toArray().sortBy('title');
  }),

  /**
   * @property vocabularies
   * @type {Ember.computed}
   * @public
   */
  vocabularies: computed('bestSelectedSchool.vocabularies.[]', async function(){
    const school = await this.get('bestSelectedSchool');
    const vocabularies = await school.get('vocabularies');
    await all(vocabularies.mapBy('terms'));
    return vocabularies.toArray().sortBy('title');
  }),

  /**
   * @property showClearFilters
   * @type {Ember.computed}
   * @public
   */
  activeFilters: computed('selectedCourses.[]', 'selectedSessionTypes.[]', 'selectedCourseLevels.[]', 'selectedCohorts.[]', 'selectedTerms.[]', function () {
    const a = this.get('selectedSessionTypes');
    const b = this.get('selectedCourseLevels');
    const c = this.get('selectedCohorts');
    const d = this.get('selectedCourses');
    const e = this.get('selectedTerms');

    return [].concat(a, b, c, d, e);
  }),

  /**
   * @property ourEvents
   * @type {Ember.computed}
   * @protected
   */
  ourEvents: computed('mySchedule', 'fromTimeStamp', 'toTimeStamp', 'bestSelectedSchool', 'selectedView', async function(){
    if(this.get('mySchedule')) {
      return await this.get('userEvents').getEvents(this.get('fromTimeStamp'), this.get('toTimeStamp'));
    }
    const school = await this.get('bestSelectedSchool');
    return await this.get('schoolEvents').getEvents(school.get('id'), this.get('fromTimeStamp'), this.get('toTimeStamp'));
  }),

  /**
   * @property eventsWithSelectedSessionTypes
   * @type {Ember.computed}
   * @protected
   */
  eventsWithSelectedSessionTypes: computed('ourEvents.[]', 'selectedSessionTypes.[]', async function(){
    const events = await this.get('ourEvents');
    const selectedSessionTypes = this.get('selectedSessionTypes').mapBy('id');
    if(isEmpty(selectedSessionTypes)) {
      return events;
    }
    const matchingEvents = await map(events, async event => {
      if(event.ilmSession || event.offering) {
        const id = await this.get('userEvents').getSessionTypeIdForEvent(event);
        if (selectedSessionTypes.includes(id)) {
          return event;
        }
      }
      return null;
    });

    return matchingEvents.filter(event => {
      return ! isEmpty(event);
    });
  }),

  /**
   * @property eventsWithSelectedCourseLevels
   * @type {Ember.computed}
   * @protected
   */
  eventsWithSelectedCourseLevels: computed('ourEvents.[]', 'selectedCourseLevels.[]', async function(){
    const events = await this.get('ourEvents');
    const selectedCourseLevels = this.get('selectedCourseLevels');
    if(isEmpty(selectedCourseLevels)) {
      return events;
    }
    const matchingEvents = await map(events, async event => {
      if(event.ilmSession || event.offering) {
        const level = await this.get('userEvents').getCourseLevelForEvent(event);
        if (selectedCourseLevels.includes(level)) {
          return event;
        }
      }
      return null;
    });

    return matchingEvents.filter(event => {
      return ! isEmpty(event);
    });
  }),

  /**
   * @property eventsWithSelectedCohorts
   * @type {Ember.computed}
   * @protected
   */
  eventsWithSelectedCohorts: computed('ourEvents.[]', 'selectedCohorts.[]', async function(){
    const events = await this.get('ourEvents');
    const selectedCohorts = this.get('selectedCohorts').mapBy('id');
    if(isEmpty(selectedCohorts)) {
      return events;
    }
    const matchingEvents = await map(events, async event => {
      if (event.ilmSession || event.offering) {
        const cohorts = await this.get('userEvents').getCohortIdsForEvent(event);
        if(cohorts.any(cohortId => {
          return selectedCohorts.includes(cohortId);
        })) {
          return event;
        }
      }
      return null;
    });

    return matchingEvents.filter(event => {
      return ! isEmpty(event);
    });
  }),

  /**
   * @property eventsWithSelectedCourses
   * @type {Ember.computed}
   * @protected
   */
  eventsWithSelectedCourses: computed('ourEvents.[]', 'selectedCourses.[]', async function(){
    const events = await this.get('ourEvents');
    const selectedCourses = this.get('selectedCourses').mapBy('id');
    if(isEmpty(selectedCourses)) {
      return events;
    }

    const matchingEvents = await map(events, async event => {
      if(event.ilmSession || event.offering) {
        const courseId = await this.get('userEvents').getCourseIdForEvent(event);
        if(selectedCourses.includes(courseId)) {
          return event;
        }
      }
      return null;
    });

    return matchingEvents.filter(event => {
      return ! isEmpty(event);
    });
  }),

  /**
   * @property eventsWithSelectedTerms
   * @type {Ember.computed}
   * @protected
   */
  eventsWithSelectedTerms: computed('ourEvents.[]', 'selectedTerms.[]', async function(){
    const events = await this.get('ourEvents');
    const selectedTerms = this.get('selectedTerms').mapBy('id');
    if(isEmpty(selectedTerms)) {
      return events;
    }
    const matchingEvents = await map(events, async event => {
      if (event.ilmSession || event.offering) {

        const termIds = await this.get('userEvents').getTermIdsForEvent(event);
        if(termIds.any(termId => {
          return selectedTerms.includes(termId);
        })) {
          return event;
        }
      }
      return null;
    });

    return matchingEvents.filter(event => {
      return ! isEmpty(event);
    });
  }),

  /**
   * @property allSchools
   * @type {Ember.computed}
   * @protected
   */
  allSchools: computed(async function(){
    const user = await this.get('currentUser.model');
    return await user.get('schools');
  }),

  /**
   * @property allAcademicYears
   * @type {Ember.computed}
   * @protected
   */
  allAcademicYears: computed(function(){
    return this.get('store').findAll('academic-year');
  }),

  actions: {
    toggleSessionType(sessionType){
      if(this.get('selectedSessionTypes').includes(sessionType)){
        this.get('selectedSessionTypes').removeObject(sessionType);
      } else {
        this.get('selectedSessionTypes').pushObject(sessionType);
      }
    },
    toggleCourseLevel(level){
      if(this.get('selectedCourseLevels').includes(level)){
        this.get('selectedCourseLevels').removeObject(level);
      } else {
        this.get('selectedCourseLevels').pushObject(level);
      }
    },
    toggleCohort(cohort){
      if(this.get('selectedCohorts').includes(cohort)){
        this.get('selectedCohorts').removeObject(cohort);
      } else {
        this.get('selectedCohorts').pushObject(cohort);
      }
    },
    toggleCourse(course){
      if(this.get('selectedCourses').includes(course)){
        this.get('selectedCourses').removeObject(course);
      } else {
        this.get('selectedCourses').pushObject(course);
      }
    },
    toggleTerm(term){
      if(this.get('selectedTerms').includes(term)){
        this.get('selectedTerms').removeObject(term);
      } else {
        this.get('selectedTerms').pushObject(term);
      }
    },

    clearFilters() {
      const selectedCourses = [];
      const selectedSessionTypes = [];
      const selectedCourseLevels = [];
      const selectedCohorts = [];
      const selectedTerms = [];

      this.setProperties({ selectedCourses, selectedSessionTypes, selectedCourseLevels, selectedCohorts, selectedTerms });
    },

    removeFilter(filter) {
      if (typeof filter === 'number') {
        this.send('toggleCourseLevel', filter);
      } else {
        let model = filter.get('constructor.modelName');

        switch (model) {
        case 'session-type':
          this.send('toggleSessionType', filter);
          break;
        case 'cohort':
          this.send('toggleCohort', filter);
          break;
        case 'course':
          this.send('toggleCourse', filter);
          break;
        case 'term':
          this.send('toggleTerm', filter);
          break;
        }
      }
    }
  }
});
<|endoftext|>


<commit_before>// IdentityJS -> strict equality & identity checking of javascript types
;(function(module) {

	'use strict';

	var

	//+ _type :: any -> string
	_type = function(entity) {
		return Object.prototype.toString.call(entity).slice(8, -1);
	},

	//+ _is :: (any, string) -> boolean
	_is = function(entity, type) {
		return _type(entity) === type;
	},

	//+ _truthy :: any -> object -> boolean
	_truthy = function(entity) {

		var _members = {

			'object': function() {
				return (_is(entity, 'Object') && !!Object.keys(entity).length);
			},

			'string': function() {
				return (_is(entity, 'String') && !!entity.length);
			},

			'number': function() {
				return (!!entity && _is(entity, 'Number'));
			},

			'function': function() {
				return _is(entity, 'Function');
			},

			'array': function() {
				return (_is(entity, 'Array') && !!entity.length);
			}
		};

		return _members;
	},

	//+ _keysLength :: object -> number
	_keysLength = function(object) {
		return Object.keys(object).length;
	},

	// identity/equality checks for suspect against witness
	_check = {

		'String': function(suspect, witness) {

			var isString = _is(suspect, 'String'),
			areEqual = suspect === witness;

			return isString && areEqual;
		},

		'Array': function(suspect, witness) {

			var isArray = _is(suspect, 'Array'),
			equalLen = suspect.length === witness.length;

			if(!(isArray && equalLen)) return false;

			return true;
		},

		'Function': function(suspect, witness) {
			var isFunction = _is(suspect, 'Function');

			if(!(isFunction)) return false;

			return true;
		},

		'Object': function(suspect, witness) {

			var isObject = _is(suspect, 'Object');

			if(!(isObject)) return false;

			var suspectKeyLen = _keysLength(suspect),
			witnessKeyLen = _keysLength(witness);

			if(suspectKeyLen != witnessKeyLen) {
				return false;
			}

			return true;
		}
	},

	// suspect will be compared against witness and treated as that type
	_identity = function(suspect, witness) {

		if(typeof suspect !== 'undefined' && typeof witness !== 'undefined') {

			var expectedType = _check[_type(witness)];

			if(expectedType) {
				return expectedType(suspect, witness);
			} else {
				throw new TypeError('Could not find identity of param[2]');
			}
		}

	};

	module.identity = _identity;

})(typeof module !== 'undefined' && module.exports? module.exports : window);<commit_msg>object, array deep checking imperatively<commit_after>// IdentityJS -> strict equality & identity checking of javascript types
;(function(module) {

	'use strict';

	var

	//+ _type :: any -> string
	_type = function(entity) {
		return Object.prototype.toString.call(entity).slice(8, -1);
	},

	//+ _is :: (any, string) -> boolean
	_is = function(entity, type) {
		return _type(entity) === type;
	},

	//+ _truthy :: any -> object -> boolean
	_truthy = function(entity) {

		var _members = {

			'object': function() {
				return (_is(entity, 'Object') && !!Object.keys(entity).length);
			},

			'string': function() {
				return (_is(entity, 'String') && !!entity.length);
			},

			'number': function() {
				return (!!entity && _is(entity, 'Number'));
			},

			'function': function() {
				return _is(entity, 'Function');
			},

			'array': function() {
				return (_is(entity, 'Array') && !!entity.length);
			}
		};

		return _members;
	},

	//+ _asString :: any -> string
	_asString = function(entity) {
		return entity.toString();
	},

	//+ _keysLength :: object -> number
	_keysLength = function(object) {
		return Object.keys(object).length;
	},

	// identity/equality checks for suspect against witness
	// TODO: possible to make more generic?
	_check = {

		'Number': function(suspect, witness) {

			var isNum = _is(suspect, 'Number'),
			areEqual = suspect === witness;

			return isNum && areEqual;
		},

		'String': function(suspect, witness) {

			var isString = _is(suspect, 'String'),
			areEqual = suspect === witness;

			return isString && areEqual;
		},

		'Array': function(suspect, witness) {

			// holds the current pass state of check
			var status = false,

			// is suspect of type array
			isArray = _is(suspect, 'Array'),

			// compare length
			susLen = suspect.length,
			witLen = witness.length,
			equalLen = susLen === witLen;

			if(!(isArray && equalLen)) return false;

			for(var ith = 0; ith < susLen; ith++) {

				var type = _check[_type(witness[ith])];

				status = type? type(suspect[ith], witness[ith]) : false;

				if(!status) break;
			}

			return status;
		},

		'Function': function(suspect, witness) {
			var isFunction = _is(suspect, 'Function');

			if(!(isFunction)) return false;

			var susString = _asString(suspect),
			witString = _asString(witness);

			return susString === witString;
		},

		'Object': function(suspect, witness) {

			var status = false,

			isObject = _is(suspect, 'Object');

			if(!(isObject)) return false;

			var suspectKeyLen = _keysLength(suspect),
			witnessKeyLen = _keysLength(witness);

			// are keys array both of equal length
			if(suspectKeyLen != witnessKeyLen) {
				return false;
			}

			for(var ith in suspect) {

				// suspect has ith directly
				if(suspect.hasOwnProperty(ith)) {

					// does witness have ith too
					if(witness.hasOwnProperty(ith)) {

						var type = _check[_type(witness[ith])];

						status = type? type(suspect[ith], witness[ith]) : false;

					} else {

						status = false;
					}

					if(!status) break;
				}
			}

			return status;
		}
	},

	// suspect will be compared against witness and treated as that type
	_identity = function(suspect, witness) {

		if(typeof suspect !== 'undefined' && typeof witness !== 'undefined') {

			var expectedType = _check[_type(witness)];

			if(expectedType) {
				return expectedType(suspect, witness);
			} else {
				throw new TypeError('Could not find identity of param[2]');
			}
		}

	};

	module.identity = _identity;

})(typeof module !== 'undefined' && module.exports? module.exports : window);<|endoftext|>


<commit_before>import Ember from 'ember';
import HasBlockMixin from '../mixins/hasblock-mixin';
import { promiseArray } from 'ember-paper/utils/promise-proxies';

function isString(item) {
  return typeof item === 'string' || item instanceof String;
}

/**
 * @name paper-autocomplete
 *
 * @description
 *     Provides material design autocomplete.
 *
 *
 * ## Dependencies
 * - paper-autocomplete-item
 * - paper-autocomplete-list
 * - paper-input
 * - paper-button
 * - input
 */
export default Ember.Component.extend(HasBlockMixin, {
  util: Ember.inject.service(),
  constants: Ember.inject.service(),

  tagName: 'md-autocomplete',
  classNameBindings: ['notFloating:md-default-theme'],
  attributeBindings: ['floating:md-floating-label', 'showDisabled:disabled'],

  // Internal
  hidden: true,
  selectedIndex: 0,
  messages: [],
  noBlur: false,
  hasFocus: false,
  searchText: '',

  // wrap in a computed property so that cache
  // isn't shared among autocomplete instances
  itemCache: Ember.computed(function() {
    return {};
  }),

  // Public
  disabled: null,
  required: null,
  lookupKey: null,
  placeholder: '',
  delay: 0,
  minLength: 1,
  allowNonExisting: false,
  noCache: false,
  notFoundMessage: 'No matches found for \'%@\'.',

  init() {
    this._super(...arguments);
    if (this.get('model')) {
      this.set('searchText', this.lookupLabelOfItem(this.get('model')));
      this.set('debouncedSearchText', this.lookupLabelOfItem(this.get('model')));
    }
  },

  notFloating: Ember.computed.not('floating'),
  notHidden: Ember.computed.not('hidden'),

  autocompleteWrapperId: Ember.computed('elementId', function() {
    return 'autocomplete-wrapper-' + this.get('elementId');
  }),

  sections: {
    itemTemplate: {isItemTemplate: true},
    notFoundTemplate: {isNotFoundTemplate: true}
  },

  notFoundMsg: Ember.computed('searchText', 'notFoundMessage', function() {
    return Ember.String.fmt(this.get('notFoundMessage'), [this.get('searchText')]);
  }),

  /**
   * Needed because of false = disabled='false'.
   */
  showDisabled: Ember.computed('disabled', function() {
    if (this.get('disabled')) {
      return true;
    }
  }),

  showLoadingBar: Ember.computed('loading', 'allowNonExisting', 'debouncingState', function() {
    return !this.get('loading') && !this.get('allowNonExisting') && !this.get('debouncingState');
  }),

  enableClearButton: Ember.computed('searchText', 'disabled', function() {
    return this.get('searchText') && !this.get('disabled');
  }),

  /**
   * Source filtering logic
   */

  debounceSearchText: Ember.observer('searchText', function() {
    var searchText = this.get('searchText');
    if (searchText !== this.get('previousSearchText')) {
      if (!this.get('allowNonExisting')) {
        this.set('model', null);
      } else {
        this.set('model', searchText);
      }

      this.sendAction('update-filter', searchText);

      this.set('debouncingState', true);
      Ember.run.debounce(this, this.setDebouncedSearchText, this.get('delay'));
      this.set('previousSearchText', searchText);
    }
  }),

  setDebouncedSearchText() {
    var searchText = this.get('searchText');
    if (this.get('isMinLengthMet')) {
      this.sendAction('debounced-update-filter', searchText);
      if (!this.cacheGet(searchText)) {
        this.sendAction('cache-miss', searchText);
      } else {
        this.sendAction('cache-hit', searchText);
      }
      this.set('debouncedSearchText', searchText);
      this.set('hidden', false);
    } else {
      this.set('hidden', true);
    }
    this.set('debouncingState', false);
  },

  loading: Ember.computed.bool('sourcePromiseArray.isPending').readOnly(),

  //coalesces all promises into PromiseArrays or Arrays
  sourcePromiseArray: Ember.computed('source', function() {
    var source = this.get('source');
    if (source && source.then) {
      //coalesce into promise array
      return promiseArray(source);
    } else if (Ember.isArray(source)) {
      //return array
      return Ember.A(source);
    } else {
      //Unknown source type
      Ember.assert('The provided \'source\' for paper-autocomplete must be an Array or a Promise.', !Ember.isPresent(source));
      return Ember.A();
    }
  }).readOnly(),

  suggestions: Ember.computed('debouncedSearchText', 'sourcePromiseArray.@each', function() {
    var source = this.get('sourcePromiseArray');
    var lookupKey = this.get('lookupKey');
    var searchText = (this.get('debouncedSearchText') || '').toLowerCase();
    var cachedItems = this.cacheGet(searchText);
    var suggestions;

    if (cachedItems) {
      //We have cached results
      suggestions = cachedItems;
    } else {
      //no cache

      var data = this.filterArray(source, searchText, lookupKey);
      if (source.then && source.get('isFulfilled')) {
        //cache when we have a PromiseArray
        this.cacheSet(searchText, data);
      }
      suggestions = Ember.A(data);
    }
    // If we have no item suggestions, and allowNonExisting is enabled
    // We need to close the paper-autocomplete-list so all mouse events get activated again.
    if (suggestions.length === 0 && this.get('allowNonExisting')){
      this.set('hidden', true);
    }
    return suggestions;
  }).readOnly(),

  filterArray(array, searchText, lookupKey) {
    return array.filter(function(item) {
      Ember.assert('You have not defined \'lookupKey\' on paper-autocomplete, when source contained ' +
        'items that are not of type String. To fix this error provide a ' +
        'lookupKey=\'key to lookup from source item\'.', isString(item) || Ember.isPresent(lookupKey));

      Ember.assert('You specified \'' + lookupKey + '\' as a lookupKey on paper-autocomplete, ' +
        'but at least one of its values is not of type String. To fix this error make sure that every \'' + lookupKey +
        '\' value is a string.', isString(item) || (Ember.isPresent(lookupKey) && isString(Ember.get(item, lookupKey))) );

      var search = isString(item) ? item.toLowerCase() : Ember.get(item, lookupKey).toLowerCase();
      return search.indexOf(searchText) === 0;
    });
  },

  //TODO move cache to service? Components are not singletons.
  cacheGet(text) {
    return !this.get('noCache') && this.get('itemCache')[text];
  },

  cacheSet(text, data) {
    this.get('itemCache')[text] = data;
  },

  shouldHide: Ember.computed.not('isMinLengthMet'),

  isMinLengthMet: Ember.computed('searchText', 'minLength', function() {
   return this.get('searchText').length >= this.get('minLength');
  }),

  /**
   * Returns the default index based on whether or not autoselect is enabled.
   * @returns {number}
   */
  defaultIndex: Ember.computed('autoselect', function() {
    return this.get('autoselect') ? 0 : -1;
  }),

  lookupLabelOfItem(model) {
    return this.get('lookupKey') ? Ember.get(model, this.get('lookupKey')) : model;
  },

  actions: {
    clear() {
      this.set('searchText', '');
      this.set('selectedIndex', -1);
      this.set('model', null);
      this.set('hidden', this.get('shouldHide'));
    },

    pickModel(model) {
      this.set('model', model);
      var value = this.lookupLabelOfItem(model);
      // First set previousSearchText then searchText ( do not trigger observer only update value! ).
      this.set('previousSearchText', value);
      this.set('searchText', value);
      this.set('hidden', true);
    },

    inputFocusOut() {
      this.set('hasFocus', false);
      if (this.get('noBlur') === false) {
        this.set('hidden', true);
      }
    },

    inputFocusIn() {
      this.set('hasFocus', true);
      this.set('hidden', this.get('shouldHide'));
    },

    inputKeyDown(value, event) {
      switch (event.keyCode) {
        case this.get('constants').KEYCODE.DOWN_ARROW:
          if (this.get('loading')) {
            return;
          }
          this.set('selectedIndex', Math.min(this.get('selectedIndex') + 1, this.get('suggestions').length - 1));
          break;
        case this.get('constants').KEYCODE.UP_ARROW:
          if (this.get('loading')) {
            return;
          }
          this.set('selectedIndex', this.get('selectedIndex') < 0 ? this.get('suggestions').length - 1 : Math.max(0, this.get('selectedIndex') - 1));
          break;
        case this.get('constants').KEYCODE.TAB:
        case this.get('constants').KEYCODE.ENTER:
          if (this.get('hidden') || this.get('loading') || this.get('selectedIndex') < 0 || this.get('suggestions').length < 1) {
            return;
          }
          this.send('pickModel', this.get('suggestions').get(this.get('selectedIndex')));
          break;
        case this.get('constants').KEYCODE.ESCAPE:
          this.set('searchText', '');
          this.set('selectedIndex', this.get('defaultIndex'));
          this.set('model', null);
          this.set('hidden', this.get('shouldHide'));
          break;
        default:
          break;
      }
    },

    listMouseEnter() {
      this.set('noBlur', true);
    },

    listMouseLeave() {
      this.set('noBlur', false);
      if (this.get('hasFocus') === false) {
        this.set('hidden', true);
      }
    },

    listMouseUp() {
      this.$().find('input').focus();
    }
  }

});
<commit_msg>Fire observer on init, but only toggle suggestion list if we want to open it ( Not on initial render. ).<commit_after>import Ember from 'ember';
import HasBlockMixin from '../mixins/hasblock-mixin';
import { promiseArray } from 'ember-paper/utils/promise-proxies';

function isString(item) {
  return typeof item === 'string' || item instanceof String;
}

/**
 * @name paper-autocomplete
 *
 * @description
 *     Provides material design autocomplete.
 *
 *
 * ## Dependencies
 * - paper-autocomplete-item
 * - paper-autocomplete-list
 * - paper-input
 * - paper-button
 * - input
 */
export default Ember.Component.extend(HasBlockMixin, {
  util: Ember.inject.service(),
  constants: Ember.inject.service(),

  tagName: 'md-autocomplete',
  classNameBindings: ['notFloating:md-default-theme'],
  attributeBindings: ['floating:md-floating-label', 'showDisabled:disabled'],

  // Internal
  hidden: true,
  selectedIndex: 0,
  messages: [],
  noBlur: false,
  hasFocus: false,
  searchText: '',
  // wrap in a computed property so that cache
  // isn't shared among autocomplete instances
  itemCache: Ember.computed(function() {
    return {};
  }),

  // Public
  disabled: null,
  required: null,
  lookupKey: null,
  placeholder: '',
  delay: 0,
  minLength: 1,
  allowNonExisting: false,
  noCache: false,
  notFoundMessage: 'No matches found for \'%@\'.',

  // do not use init(): we need observer to fire.
  setSearchText: Ember.on('init', function () {
    if (this.get('model')) {
      this.set('searchText', this.lookupLabelOfItem(this.get('model')));
    }
  }),

  notFloating: Ember.computed.not('floating'),
  notHidden: Ember.computed.not('hidden'),

  autocompleteWrapperId: Ember.computed('elementId', function() {
    return 'autocomplete-wrapper-' + this.get('elementId');
  }),

  sections: {
    itemTemplate: {isItemTemplate: true},
    notFoundTemplate: {isNotFoundTemplate: true}
  },

  notFoundMsg: Ember.computed('searchText', 'notFoundMessage', function() {
    return Ember.String.fmt(this.get('notFoundMessage'), [this.get('searchText')]);
  }),

  /**
   * Needed because of false = disabled='false'.
   */
  showDisabled: Ember.computed('disabled', function() {
    if (this.get('disabled')) {
      return true;
    }
  }),

  showLoadingBar: Ember.computed('loading', 'allowNonExisting', 'debouncingState', function() {
    return !this.get('loading') && !this.get('allowNonExisting') && !this.get('debouncingState');
  }),

  enableClearButton: Ember.computed('searchText', 'disabled', function() {
    return this.get('searchText') && !this.get('disabled');
  }),

  /**
   * Source filtering logic
   */

  debounceSearchText: Ember.observer('searchText', function() {
    var searchText = this.get('searchText');
    if (searchText !== this.get('previousSearchText')) {
      if (!this.get('allowNonExisting')) {
        this.set('model', null);
      } else {
        this.set('model', searchText);
      }

      this.sendAction('update-filter', searchText);

      this.set('debouncingState', true);
      Ember.run.debounce(this, this.setDebouncedSearchText, this.get('delay'));
      this.set('previousSearchText', searchText);
    }
  }),

  setDebouncedSearchText() {
    var searchText = this.get('searchText');
    if (this.get('isMinLengthMet')) {
      this.sendAction('debounced-update-filter', searchText);
      if (!this.cacheGet(searchText)) {
        this.sendAction('cache-miss', searchText);
      } else {
        this.sendAction('cache-hit', searchText);
      }
      this.set('debouncedSearchText', searchText);

      // If the autocomplete is being triggered by a human / not on initial render.
      if (this.get('hasFocus') || this.get('noBlur')) {
        this.set('hidden', false);
      }
    } else {
      this.set('hidden', true);
    }
    this.set('debouncingState', false);
  },

  loading: Ember.computed.bool('sourcePromiseArray.isPending').readOnly(),

  //coalesces all promises into PromiseArrays or Arrays
  sourcePromiseArray: Ember.computed('source', function() {
    var source = this.get('source');
    if (source && source.then) {
      //coalesce into promise array
      return promiseArray(source);
    } else if (Ember.isArray(source)) {
      //return array
      return Ember.A(source);
    } else {
      //Unknown source type
      Ember.assert('The provided \'source\' for paper-autocomplete must be an Array or a Promise.', !Ember.isPresent(source));
      return Ember.A();
    }
  }).readOnly(),

  suggestions: Ember.computed('debouncedSearchText', 'sourcePromiseArray.@each', function() {
    var source = this.get('sourcePromiseArray');
    var lookupKey = this.get('lookupKey');
    var searchText = (this.get('debouncedSearchText') || '').toLowerCase();
    var cachedItems = this.cacheGet(searchText);
    var suggestions;

    if (cachedItems) {
      //We have cached results
      suggestions = cachedItems;
    } else {
      //no cache

      var data = this.filterArray(source, searchText, lookupKey);
      if (source.then && source.get('isFulfilled')) {
        //cache when we have a PromiseArray
        this.cacheSet(searchText, data);
      }
      suggestions = Ember.A(data);
    }
    // If we have no item suggestions, and allowNonExisting is enabled
    // We need to close the paper-autocomplete-list so all mouse events get activated again.
    if (suggestions.length === 0 && this.get('allowNonExisting')){
      this.set('hidden', true);
    }
    return suggestions;
  }).readOnly(),

  filterArray(array, searchText, lookupKey) {
    return array.filter(function(item) {
      Ember.assert('You have not defined \'lookupKey\' on paper-autocomplete, when source contained ' +
        'items that are not of type String. To fix this error provide a ' +
        'lookupKey=\'key to lookup from source item\'.', isString(item) || Ember.isPresent(lookupKey));

      Ember.assert('You specified \'' + lookupKey + '\' as a lookupKey on paper-autocomplete, ' +
        'but at least one of its values is not of type String. To fix this error make sure that every \'' + lookupKey +
        '\' value is a string.', isString(item) || (Ember.isPresent(lookupKey) && isString(Ember.get(item, lookupKey))) );

      var search = isString(item) ? item.toLowerCase() : Ember.get(item, lookupKey).toLowerCase();
      return search.indexOf(searchText) === 0;
    });
  },

  //TODO move cache to service? Components are not singletons.
  cacheGet(text) {
    return !this.get('noCache') && this.get('itemCache')[text];
  },

  cacheSet(text, data) {
    this.get('itemCache')[text] = data;
  },

  shouldHide: Ember.computed.not('isMinLengthMet'),

  isMinLengthMet: Ember.computed('searchText', 'minLength', function() {
   return this.get('searchText').length >= this.get('minLength');
  }),

  /**
   * Returns the default index based on whether or not autoselect is enabled.
   * @returns {number}
   */
  defaultIndex: Ember.computed('autoselect', function() {
    return this.get('autoselect') ? 0 : -1;
  }),

  lookupLabelOfItem(model) {
    return this.get('lookupKey') ? Ember.get(model, this.get('lookupKey')) : model;
  },

  actions: {
    clear() {
      this.set('searchText', '');
      this.set('selectedIndex', -1);
      this.set('model', null);
      this.set('hidden', this.get('shouldHide'));
    },

    pickModel(model) {
      this.set('model', model);
      var value = this.lookupLabelOfItem(model);
      // First set previousSearchText then searchText ( do not trigger observer only update value! ).
      this.set('previousSearchText', value);
      this.set('searchText', value);
      this.set('hidden', true);
    },

    inputFocusOut() {
      this.set('hasFocus', false);
      if (this.get('noBlur') === false) {
        this.set('hidden', true);
      }
    },

    inputFocusIn() {
      this.set('hasFocus', true);
      this.set('hidden', this.get('shouldHide'));
    },

    inputKeyDown(value, event) {
      switch (event.keyCode) {
        case this.get('constants').KEYCODE.DOWN_ARROW:
          if (this.get('loading')) {
            return;
          }
          this.set('selectedIndex', Math.min(this.get('selectedIndex') + 1, this.get('suggestions').length - 1));
          break;
        case this.get('constants').KEYCODE.UP_ARROW:
          if (this.get('loading')) {
            return;
          }
          this.set('selectedIndex', this.get('selectedIndex') < 0 ? this.get('suggestions').length - 1 : Math.max(0, this.get('selectedIndex') - 1));
          break;
        case this.get('constants').KEYCODE.TAB:
        case this.get('constants').KEYCODE.ENTER:
          if (this.get('hidden') || this.get('loading') || this.get('selectedIndex') < 0 || this.get('suggestions').length < 1) {
            return;
          }
          this.send('pickModel', this.get('suggestions').get(this.get('selectedIndex')));
          break;
        case this.get('constants').KEYCODE.ESCAPE:
          this.set('searchText', '');
          this.set('selectedIndex', this.get('defaultIndex'));
          this.set('model', null);
          this.set('hidden', this.get('shouldHide'));
          break;
        default:
          break;
      }
    },

    listMouseEnter() {
      this.set('noBlur', true);
    },

    listMouseLeave() {
      this.set('noBlur', false);
      if (this.get('hasFocus') === false) {
        this.set('hidden', true);
      }
    },

    listMouseUp() {
      this.$().find('input').focus();
    }
  }

});
<|endoftext|>