<commit_before>/*!
 * Copyright 2012 Sakai Foundation (SF) Licensed under the
 * Educational Community License, Version 2.0 (the "License"); you may
 * not use this file except in compliance with the License. You may
 * obtain a copy of the License at
 *
 *     http://www.osedu.org/licenses/ECL-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License 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 _ = require('underscore');

var ContentConstants = require('oae-content/lib/constants').ContentConstants;
var log = require('oae-logger').logger('oae-preview-processor');
var MQ = require('oae-util/lib/mq');
var RestAPI = require('oae-rest');
var Telemetry = require('oae-telemetry').telemetry('preview-processor');
var Validator = require('oae-util/lib/validator').Validator;

var PreviewConstants = require('./constants');
var PreviewContext = require('./model').PreviewContext;
var PreviewImages = require('./api.images');
var PreviewOffice = require('./api.office');
var PreviewPDF = require('./api.pdf');

var _config = null;

// A hash of registered processors.
// The first level is the content type, the second level is the mimetype for which the processor should run.
// If the content type has no mimetype, the default `application/octet-stream` will be used.
// The value for the second level is an array of processor objects sorted on their ranking.
var _processors = {};

/**
 * Registers the default processors and starts listening for preview tasks.
 *
 * @param {Function}    [callback]      Standard callback method.
 * @param {Object}      [callback.err]  Standard error object (if any.)
 */
var enable = module.exports.enable = function(callback) {
    callback = callback || function(err) { /* error is logged within the implementation */ };
    registerProcessor('oae-image-processor', 'file', PreviewConstants.TYPES.IMAGE, 10, PreviewImages.generatePreviews);
    registerProcessor('oae-office-processor', 'file', PreviewConstants.TYPES.OFFICE, 10, PreviewOffice.generatePreviews);
    registerProcessor('oae-pdf-processor', 'file', PreviewConstants.TYPES.PDF, 10, PreviewPDF.generatePreviews);

    // Set up the message queue and start listenening for preview tasks.
    var options = {
        'subscribe': {
            'prefetchCount': 1
        }
    };
    MQ.bind(PreviewConstants.MQ.TASK_GENERATE_PREVIEWS, _handleGeneratePreviewsTask, options, function(err) {
        if (err) {
            log().error({'err': err}, 'Could not bind to the previews queue.');
            return callback(err);
        }
        log().info('Bound the preview processor to the message queue.');
        callback();
    });
};

/**
 * Unregisters the default processors and remove the listener for preview tasks.
 *
 * @param {Function}    [callback]      Standard callback method.
 * @param {Object}      [callback.err]  Standard error object (if any.)
 */
var disable = module.exports.disable = function(callback) {
    callback = callback || function(err) { /* error is logged within the implementation */ };
    unregisterProcessor('oae-image-processor');
    unregisterProcessor('oae-office-processor');
    unregisterProcessor('oae-pdf-processor');

    MQ.unbind(PreviewConstants.MQ.TASK_GENERATE_PREVIEWS, function(err) {
        if (err) {
            log().error({'err': err}, 'Could not unbind from the previews queue.');
            return callback(err);
        }
        log().info('Unbound the preview processor to the message queue.');
        callback();
    });
};

/**
 * Refresh the preview processor configuration with the given options.
 *
 * @param  {Object}     config      The main configuration object as defined in `config.js`. The full config object should be passed in.
 * @param  {Function}   callback    Invoked when configuration has been refreshed
 */
var refreshPreviewConfiguration = module.exports.refreshPreviewConfiguration = function(config, callback) {
    // Disable the current processor.
    disable(function(err) {
        if (err) {
            return callback(err);
        }

        // Enable with the new config.
        _config = config;

        // Only initialize the processors/listeners
        // if we have explicitly set the PP enabled flag to true.
        if (config.previews.enabled) {

            // Initialize those default processors that need configuration.
            PreviewOffice.init(config, function(err) {
                if (err) {
                    return callback(err);
                }

                PreviewPDF.init(config, function(err) {
                    if (err) {
                        return callback(err);
                    }

                    // Defer binding to the MQ listener
                    // See https://github.com/sakaiproject/Hilary/issues/314
                    callback();
                    setTimeout(enable, 5000);
                });
            });
        } else {
            callback();
        }
    });
};

/**
 * @return  {Object}    The configuration object that is currently in use.
 */
var getConfiguration = module.exports.getConfiguration = function() {
    return _config;
};

/**
 * Register a preview processor for a certain content type and mime type.
 * Each processor will be registered with a ranking, when a piece of content
 * needs processing and multiple processors are available, the one with the highest ranking will be selected.
 * The default processors are registered with a ranking of 10.
 *
 * @param {String}          processorId                     A unique identifier for this processor. This identifier can be used to remove a processor.
 * @param {String}          contentType                     The type of content for which this processor should be registered. Usually one of `file`, `sakaidoc`, ..
 * @param {String[]}        [mimeTypes]                     In case the `contentType` is a file, an array of mimetypes can be passed along to further scope which files this processor should receive.
 * @param {Number}          ranking                         The ranking for this processor. The default OAE processors have a ranking of 10.
 * @param {Function}        processorCallback               The method that can be used to perform the processing.
 * @param {PreviewContext}  processorCallback.ctx           The preview context associated to the piece of content that needs processing. This will have an authenticated global administrator attached to it.
 * @param {Function}        processorCallback.callback      Standard callback method for when the processor is done processing.
 */
var registerProcessor = module.exports.registerProcessor = function(processorId, contentType, mimeTypes, ranking, processorCallback) {
    var validator = new Validator();
    validator.check(processorId, 'Missing processor ID.').notEmpty();
    validator.check(contentType, 'Missing content type').notEmpty();
    validator.check(ranking, 'Missing or invalid ranking').isInt();
    validator.check(processorCallback, 'Missing callback method').notNull();
    if (validator.hasErrors()) {
        throw new Error(validator.getFirstError());
    }

    // Default the mime type in case it was not provided
    if (!mimeTypes || mimeTypes.length === 0) {
        mimeTypes = [ PreviewConstants.TYPES.DEFAULT ];
    }

    // Our processor object that will be inserted in the correct places in the `_processors` hash.
    var processor = { 'id': processorId, 'processor': processorCallback, 'ranking': ranking };

    // We sort the array of processors on their ranking, highest first.
    var sortProcessors = function(a, b) {
        return a.ranking < b.ranking;
    };

    // Get or create the first level hash.
    _processors[contentType] = _processors[contentType] || {};

    // Get or create the second level hash.
    for (var i = 0; i < mimeTypes.length; i++) {
        _processors[contentType][mimeTypes[i]] = _processors[contentType][mimeTypes[i]] || [];
        _processors[contentType][mimeTypes[i]].push(processor);
        _processors[contentType][mimeTypes[i]].sort(sortProcessors);
    }
};

/**
 * Unregisters a preview processor.
 *
 * @param {String}  processorId     The ID of the processor that should be unregistered.
 */
var unregisterProcessor = module.exports.unregisterProcessor = function(processorId) {
    if (!processorId) {
        throw new Error('The processor id must be specified');
    }

    // Loop over the processor object and remove the processor from each mimetype.
    _.each(_processors, function(mimetypes, contentType) {
        _.each(mimetypes, function(processors, mimeType) {
            var newProcessors = [];
            _.each(processors, function(processor) {
                if (processor.id !== processorId) {
                    newProcessors.push(processor);
                }
            });
            _processors[contentType][mimeType] = newProcessors;
        });
    });
};

/**
 * Returns the processor that is associated with a type of content.
 *
 * @param  {String}     contentType     The type of content. Typically, 'file' or 'sakaidoc'
 * @param  {String}     [mimeType]      The mimetype.
 * @return {Function}                   The function that is associated to this certain type of content item, null will be returned if nothing could be found.
 */
var getProcessor = module.exports.getProcessor = function(contentType, mimeType) {
    mimeType = mimeType || PreviewConstants.TYPES.DEFAULT;
    if (contentType && mimeType && _processors[contentType] && _processors[contentType][mimeType] && _processors[contentType][mimeType].length > 0) {
        return _processors[contentType][mimeType][0].processor;
    } else {
        return null;
    }
};

/**
 * Submits a piece of content to the preview generation queue where it can then
 * be picked up by one of the preview processors.
 *
 * @param   {String}    contentId   The ID of the piece of the content that needs new preview items.
 */
var submitForProcessing = module.exports.submitForProcessing = function(contentId) {
    MQ.submit(PreviewConstants.MQ.TASK_GENERATE_PREVIEWS, {'id': contentId});
};

/**
 * When bound to am MQ generate preview task, this method will generate the appropriate
 * preview images
 *
 * @param   {Object}    data           The task data
 * @param   {String}    data.id        The ID for the piece of content that needs new preview items.
 * @param   {Function}  callback       Invoked when generating previews has completed
 * @param   {Object}    callback.err   An error that occurred, if any
 * @api private
 */
var _handleGeneratePreviewsTask = function(data, callback) {
    callback = callback || function(err) {
        if (err) {
            log().error({'err': err, 'data': data}, 'Error handling preview generation.');
        }
    };

    if (!data.id) {
        log().error({ 'data': data }, 'An invalid task was submitted to the generate previews task queue.');
        callback({'code': 400, 'msg': 'An invalid task was submitted to the generate previews task queue.'});
    } else {
        generatePreviews(data.id, callback);
    }
};

/**
 * Generates previews for a piece of content.
 *
 * @param {String}      contentId       The content ID for which the previews should be generated.
 * @param {Function}    callback        Standard callback method.
 * @param {Object}      callback.err    Standard error object (if any.)
 */
var generatePreviews = module.exports.generatePreviews = function(contentId, callback) {
    var start = Date.now();
    log().info({'contentId': contentId}, 'Starting preview generation process.');
    var ctx = new PreviewContext(_config, contentId);

    // Generate a context for this preview process and
    // login to the tenant of this content item and start processing.
    ctx.login(function(err) {
        if (err) {
            // If we can't login, we cannot call cleanCallback as we won't have a session cookie
            // to set a status.
            ctx.cleanup();
            Telemetry.appendDuration('process', start);
            Telemetry.incr('error');
            return callback(err);
        }

        // Generate the actual preview images.
        _generatePreviews(ctx, function(err) {
            ctx.cleanup();
            Telemetry.appendDuration('process', start);
            if (err) {
                log().error({'err': err, 'contentId': contentId}, 'Error when trying to process this file.');
                Telemetry.incr('error');
            } else {
                // We're done.
                log().info({'contentId': contentId}, 'Preview processing done', contentId);
                Telemetry.incr('ok');
                callback();
            }
        });
    });
};

/**
 * Generates previews for a piece of content.
 *
 * @param {PreviewContext}      ctx             The preview context associated to this file. This context should have an authenticated global administrator against it.
 * @param {Function}            callback        Standard callback method.
 * @param {Object}              callback.err    Standard error object (if any.)
 * @api private
 */
var _generatePreviews = function(ctx, callback) {
    // Get the content profile.
    RestAPI.Content.getContent(ctx.tenantRestContext, ctx.contentId, function(err, content) {
        if (err) {
            log().error({'err': err, 'contentId': ctx.contentId}, 'Could not get the content profile.');
            return callback(err);
        }

        // Stick the piece of content on the context.
        // This allows for easy retrieval in later methods.
        ctx.content = content;

        var mimeType = content.mime || PreviewConstants.TYPES.DEFAULT;
        var processor = getProcessor(content.contentType, mimeType);
        if (processor) {
            processor(ctx, function(err) {
                if (err) {
                    ctx.setStatus('error', callback);
                } else {
                    ctx.savePreviews(callback);
                }
            });
        } else {
            log().info({'contentId': ctx.contentId}, 'Ignoring as the type %s is not supported for now.', content.contentType);
            ctx.setStatus('ignored', callback);
        }
    });
};
<commit_msg>(Paris2013) Call the callback if there is an error generating<commit_after>/*!
 * Copyright 2012 Sakai Foundation (SF) Licensed under the
 * Educational Community License, Version 2.0 (the "License"); you may
 * not use this file except in compliance with the License. You may
 * obtain a copy of the License at
 *
 *     http://www.osedu.org/licenses/ECL-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License 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 _ = require('underscore');

var ContentConstants = require('oae-content/lib/constants').ContentConstants;
var log = require('oae-logger').logger('oae-preview-processor');
var MQ = require('oae-util/lib/mq');
var RestAPI = require('oae-rest');
var Telemetry = require('oae-telemetry').telemetry('preview-processor');
var Validator = require('oae-util/lib/validator').Validator;

var PreviewConstants = require('./constants');
var PreviewContext = require('./model').PreviewContext;
var PreviewImages = require('./api.images');
var PreviewOffice = require('./api.office');
var PreviewPDF = require('./api.pdf');

var _config = null;

// A hash of registered processors.
// The first level is the content type, the second level is the mimetype for which the processor should run.
// If the content type has no mimetype, the default `application/octet-stream` will be used.
// The value for the second level is an array of processor objects sorted on their ranking.
var _processors = {};

/**
 * Registers the default processors and starts listening for preview tasks.
 *
 * @param {Function}    [callback]      Standard callback method.
 * @param {Object}      [callback.err]  Standard error object (if any.)
 */
var enable = module.exports.enable = function(callback) {
    callback = callback || function(err) { /* error is logged within the implementation */ };
    registerProcessor('oae-image-processor', 'file', PreviewConstants.TYPES.IMAGE, 10, PreviewImages.generatePreviews);
    registerProcessor('oae-office-processor', 'file', PreviewConstants.TYPES.OFFICE, 10, PreviewOffice.generatePreviews);
    registerProcessor('oae-pdf-processor', 'file', PreviewConstants.TYPES.PDF, 10, PreviewPDF.generatePreviews);

    // Set up the message queue and start listenening for preview tasks.
    var options = {
        'subscribe': {
            'prefetchCount': 1
        }
    };
    MQ.bind(PreviewConstants.MQ.TASK_GENERATE_PREVIEWS, _handleGeneratePreviewsTask, options, function(err) {
        if (err) {
            log().error({'err': err}, 'Could not bind to the previews queue.');
            return callback(err);
        }
        log().info('Bound the preview processor to the message queue.');
        callback();
    });
};

/**
 * Unregisters the default processors and remove the listener for preview tasks.
 *
 * @param {Function}    [callback]      Standard callback method.
 * @param {Object}      [callback.err]  Standard error object (if any.)
 */
var disable = module.exports.disable = function(callback) {
    callback = callback || function(err) { /* error is logged within the implementation */ };
    unregisterProcessor('oae-image-processor');
    unregisterProcessor('oae-office-processor');
    unregisterProcessor('oae-pdf-processor');

    MQ.unbind(PreviewConstants.MQ.TASK_GENERATE_PREVIEWS, function(err) {
        if (err) {
            log().error({'err': err}, 'Could not unbind from the previews queue.');
            return callback(err);
        }
        log().info('Unbound the preview processor to the message queue.');
        callback();
    });
};

/**
 * Refresh the preview processor configuration with the given options.
 *
 * @param  {Object}     config      The main configuration object as defined in `config.js`. The full config object should be passed in.
 * @param  {Function}   callback    Invoked when configuration has been refreshed
 */
var refreshPreviewConfiguration = module.exports.refreshPreviewConfiguration = function(config, callback) {
    // Disable the current processor.
    disable(function(err) {
        if (err) {
            return callback(err);
        }

        // Enable with the new config.
        _config = config;

        // Only initialize the processors/listeners
        // if we have explicitly set the PP enabled flag to true.
        if (config.previews.enabled) {

            // Initialize those default processors that need configuration.
            PreviewOffice.init(config, function(err) {
                if (err) {
                    return callback(err);
                }

                PreviewPDF.init(config, function(err) {
                    if (err) {
                        return callback(err);
                    }

                    // Defer binding to the MQ listener
                    // See https://github.com/sakaiproject/Hilary/issues/314
                    callback();
                    setTimeout(enable, 5000);
                });
            });
        } else {
            callback();
        }
    });
};

/**
 * @return  {Object}    The configuration object that is currently in use.
 */
var getConfiguration = module.exports.getConfiguration = function() {
    return _config;
};

/**
 * Register a preview processor for a certain content type and mime type.
 * Each processor will be registered with a ranking, when a piece of content
 * needs processing and multiple processors are available, the one with the highest ranking will be selected.
 * The default processors are registered with a ranking of 10.
 *
 * @param {String}          processorId                     A unique identifier for this processor. This identifier can be used to remove a processor.
 * @param {String}          contentType                     The type of content for which this processor should be registered. Usually one of `file`, `sakaidoc`, ..
 * @param {String[]}        [mimeTypes]                     In case the `contentType` is a file, an array of mimetypes can be passed along to further scope which files this processor should receive.
 * @param {Number}          ranking                         The ranking for this processor. The default OAE processors have a ranking of 10.
 * @param {Function}        processorCallback               The method that can be used to perform the processing.
 * @param {PreviewContext}  processorCallback.ctx           The preview context associated to the piece of content that needs processing. This will have an authenticated global administrator attached to it.
 * @param {Function}        processorCallback.callback      Standard callback method for when the processor is done processing.
 */
var registerProcessor = module.exports.registerProcessor = function(processorId, contentType, mimeTypes, ranking, processorCallback) {
    var validator = new Validator();
    validator.check(processorId, 'Missing processor ID.').notEmpty();
    validator.check(contentType, 'Missing content type').notEmpty();
    validator.check(ranking, 'Missing or invalid ranking').isInt();
    validator.check(processorCallback, 'Missing callback method').notNull();
    if (validator.hasErrors()) {
        throw new Error(validator.getFirstError());
    }

    // Default the mime type in case it was not provided
    if (!mimeTypes || mimeTypes.length === 0) {
        mimeTypes = [ PreviewConstants.TYPES.DEFAULT ];
    }

    // Our processor object that will be inserted in the correct places in the `_processors` hash.
    var processor = { 'id': processorId, 'processor': processorCallback, 'ranking': ranking };

    // We sort the array of processors on their ranking, highest first.
    var sortProcessors = function(a, b) {
        return a.ranking < b.ranking;
    };

    // Get or create the first level hash.
    _processors[contentType] = _processors[contentType] || {};

    // Get or create the second level hash.
    for (var i = 0; i < mimeTypes.length; i++) {
        _processors[contentType][mimeTypes[i]] = _processors[contentType][mimeTypes[i]] || [];
        _processors[contentType][mimeTypes[i]].push(processor);
        _processors[contentType][mimeTypes[i]].sort(sortProcessors);
    }
};

/**
 * Unregisters a preview processor.
 *
 * @param {String}  processorId     The ID of the processor that should be unregistered.
 */
var unregisterProcessor = module.exports.unregisterProcessor = function(processorId) {
    if (!processorId) {
        throw new Error('The processor id must be specified');
    }

    // Loop over the processor object and remove the processor from each mimetype.
    _.each(_processors, function(mimetypes, contentType) {
        _.each(mimetypes, function(processors, mimeType) {
            var newProcessors = [];
            _.each(processors, function(processor) {
                if (processor.id !== processorId) {
                    newProcessors.push(processor);
                }
            });
            _processors[contentType][mimeType] = newProcessors;
        });
    });
};

/**
 * Returns the processor that is associated with a type of content.
 *
 * @param  {String}     contentType     The type of content. Typically, 'file' or 'sakaidoc'
 * @param  {String}     [mimeType]      The mimetype.
 * @return {Function}                   The function that is associated to this certain type of content item, null will be returned if nothing could be found.
 */
var getProcessor = module.exports.getProcessor = function(contentType, mimeType) {
    mimeType = mimeType || PreviewConstants.TYPES.DEFAULT;
    if (contentType && mimeType && _processors[contentType] && _processors[contentType][mimeType] && _processors[contentType][mimeType].length > 0) {
        return _processors[contentType][mimeType][0].processor;
    } else {
        return null;
    }
};

/**
 * Submits a piece of content to the preview generation queue where it can then
 * be picked up by one of the preview processors.
 *
 * @param   {String}    contentId   The ID of the piece of the content that needs new preview items.
 */
var submitForProcessing = module.exports.submitForProcessing = function(contentId) {
    MQ.submit(PreviewConstants.MQ.TASK_GENERATE_PREVIEWS, {'id': contentId});
};

/**
 * When bound to am MQ generate preview task, this method will generate the appropriate
 * preview images
 *
 * @param   {Object}    data           The task data
 * @param   {String}    data.id        The ID for the piece of content that needs new preview items.
 * @param   {Function}  callback       Invoked when generating previews has completed
 * @param   {Object}    callback.err   An error that occurred, if any
 * @api private
 */
var _handleGeneratePreviewsTask = function(data, callback) {
    callback = callback || function(err) {
        if (err) {
            log().error({'err': err, 'data': data}, 'Error handling preview generation.');
        }
    };

    if (!data.id) {
        log().error({ 'data': data }, 'An invalid task was submitted to the generate previews task queue.');
        callback({'code': 400, 'msg': 'An invalid task was submitted to the generate previews task queue.'});
    } else {
        generatePreviews(data.id, callback);
    }
};

/**
 * Generates previews for a piece of content.
 *
 * @param {String}      contentId       The content ID for which the previews should be generated.
 * @param {Function}    callback        Standard callback method.
 * @param {Object}      callback.err    Standard error object (if any.)
 */
var generatePreviews = module.exports.generatePreviews = function(contentId, callback) {
    var start = Date.now();
    log().info({'contentId': contentId}, 'Starting preview generation process.');
    var ctx = new PreviewContext(_config, contentId);

    // Generate a context for this preview process and
    // login to the tenant of this content item and start processing.
    ctx.login(function(err) {
        if (err) {
            // If we can't login, we cannot call cleanCallback as we won't have a session cookie
            // to set a status.
            ctx.cleanup();
            Telemetry.appendDuration('process', start);
            Telemetry.incr('error');
            return callback(err);
        }

        // Generate the actual preview images.
        _generatePreviews(ctx, function(err) {
            ctx.cleanup();
            Telemetry.appendDuration('process', start);
            if (err) {
                log().error({'err': err, 'contentId': contentId}, 'Error when trying to process this file.');
                Telemetry.incr('error');
                return callback(err);
            } else {
                // We're done.
                log().info({'contentId': contentId}, 'Preview processing done', contentId);
                Telemetry.incr('ok');
                return callback();
            }
        });
    });
};

/**
 * Generates previews for a piece of content.
 *
 * @param {PreviewContext}      ctx             The preview context associated to this file. This context should have an authenticated global administrator against it.
 * @param {Function}            callback        Standard callback method.
 * @param {Object}              callback.err    Standard error object (if any.)
 * @api private
 */
var _generatePreviews = function(ctx, callback) {
    // Get the content profile.
    RestAPI.Content.getContent(ctx.tenantRestContext, ctx.contentId, function(err, content) {
        if (err) {
            log().error({'err': err, 'contentId': ctx.contentId}, 'Could not get the content profile.');
            return callback(err);
        }

        // Stick the piece of content on the context.
        // This allows for easy retrieval in later methods.
        ctx.content = content;

        var mimeType = content.mime || PreviewConstants.TYPES.DEFAULT;
        var processor = getProcessor(content.contentType, mimeType);
        if (processor) {
            processor(ctx, function(err) {
                if (err) {
                    ctx.setStatus('error', callback);
                } else {
                    ctx.savePreviews(callback);
                }
            });
        } else {
            log().info({'contentId': ctx.contentId}, 'Ignoring as the type %s is not supported for now.', content.contentType);
            ctx.setStatus('ignored', callback);
        }
    });
};
<|endoftext|>


<commit_before>import React from 'react';
import { mount } from 'enzyme';
import Upload from '..';
import Form from '../../form';
import { errorRequest, successRequest } from './requests';
import { setup, teardown } from './mock';

const delay = timeout => new Promise(resolve => setTimeout(resolve, timeout));

const fileList = [{
  uid: '-1',
  name: 'xxx.png',
  status: 'done',
  url: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png',
  thumbUrl: 'https://zos.alipayobjects.com/rmsportal/IQKRngzUuFzJzGzRJXUs.png',
}, {
  uid: '-2',
  name: 'yyy.png',
  status: 'done',
  url: 'https://zos.alipayobjects.com/rmsportal/IQKRngzUuFzJzGzRJXUs.png',
  thumbUrl: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png',
}];

describe('Upload List', () => {
  beforeEach(() => setup());
  afterEach(() => teardown());

  // https://github.com/ant-design/ant-design/issues/4653
  it('should use file.thumbUrl for <img /> in priority', () => {
    const wrapper = mount(
      <Upload defaultFileList={fileList} listType="picture">
        <button type="button">upload</button>
      </Upload>
    );
    fileList.forEach((file, i) => {
      const linkNode = wrapper.find('.ant-upload-list-item-thumbnail').at(i);
      const imgNode = wrapper.find('.ant-upload-list-item-thumbnail img').at(i);
      expect(linkNode.prop('href')).toBe(file.url);
      expect(imgNode.prop('src')).toBe(file.thumbUrl);
    });
  });

  // https://github.com/ant-design/ant-design/issues/7269
  it('should remove correct item when uid is 0', async () => {
    const list = [{
      uid: '0',
      name: 'xxx.png',
      status: 'done',
      url: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png',
      thumbUrl: 'https://zos.alipayobjects.com/rmsportal/IQKRngzUuFzJzGzRJXUs.png',
    }, {
      uid: '1',
      name: 'xxx.png',
      status: 'done',
      url: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png',
      thumbUrl: 'https://zos.alipayobjects.com/rmsportal/IQKRngzUuFzJzGzRJXUs.png',
    }];
    const wrapper = mount(
      <Upload defaultFileList={list}>
        <button type="button">upload</button>
      </Upload>
    );
    expect(wrapper.find('.ant-upload-list-item').length).toBe(2);
    wrapper.find('.ant-upload-list-item').at(0).find('.anticon-cross').simulate('click');
    await delay(400);
    wrapper.update();
    expect(wrapper.find('.ant-upload-list-item').hostNodes().length).toBe(1);
  });

  it('should be uploading when upload a file', (done) => {
    let wrapper;
    const onChange = ({ file }) => {
      if (file.status === 'uploading') {
        expect(wrapper.render()).toMatchSnapshot();
      }
      if (file.status === 'done') {
        expect(wrapper.render()).toMatchSnapshot();
        done();
      }
    };
    wrapper = mount(
      <Upload
        action="http://jsonplaceholder.typicode.com/posts/"
        onChange={onChange}
        customRequest={successRequest}
      >
        <button type="button">upload</button>
      </Upload>
    );
    wrapper.find('input').simulate('change', {
      target: {
        files: [
          { name: 'foo.png' },
        ],
      },
    });
  });

  it('handle error', (done) => {
    let wrapper;
    const onChange = ({ file }) => {
      if (file.status !== 'uploading') {
        expect(wrapper.render()).toMatchSnapshot();
        done();
      }
    };
    wrapper = mount(
      <Upload
        action="http://jsonplaceholder.typicode.com/posts/"
        onChange={onChange}
        customRequest={errorRequest}
      >
        <button type="button">upload</button>
      </Upload>
    );
    wrapper.find('input').simulate('change', {
      target: {
        files: [
          { name: 'foo.png' },
        ],
      },
    });
  });

  it('does concat filelist when beforeUpload returns false', () => {
    const handleChange = jest.fn();
    const wrapper = mount(
      <Upload
        listType="picture"
        defaultFileList={fileList}
        onChange={handleChange}
        beforeUpload={() => false}
      >
        <button type="button">upload</button>
      </Upload>
    );

    wrapper.find('input').simulate('change', {
      target: {
        files: [
          { name: 'foo.png' },
        ],
      },
    });

    expect(wrapper.state().fileList.length).toBe(fileList.length + 1);
    expect(handleChange.mock.calls[0][0].fileList).toHaveLength(3);
  });

  // https://github.com/ant-design/ant-design/issues/7762
  it('work with form validation', () => {
    let errors;
    class TestForm extends React.Component {
      handleSubmit = () => {
        const { form: { validateFields } } = this.props;
        validateFields((err) => {
          errors = err;
        });
      }

      render() {
        const { form: { getFieldDecorator } } = this.props;
        return (
          <Form onSubmit={this.handleSubmit}>
            <Form.Item>
              {getFieldDecorator('file', {
                valuePropname: 'fileList',
                getValueFromEvent: e => e.fileList,
                rules: [
                  {
                    required: true,
                    validator: (rule, value, callback) => {
                      if (!value || value.length === 0) {
                        callback('file required');
                      } else {
                        callback();
                      }
                    },
                  },
                ],
              })(
                <Upload
                  beforeUpload={() => false}
                >
                  <button type="button">upload</button>
                </Upload>
              )}
            </Form.Item>
          </Form>
        );
      }
    }

    const App = Form.create()(TestForm);
    const wrapper = mount(<App />);
    wrapper.find(Form).simulate('submit');
    expect(errors.file.errors).toEqual([{ message: 'file required', field: 'file' }]);

    wrapper.find('input').simulate('change', {
      target: {
        files: [
          { name: 'foo.png' },
        ],
      },
    });
    wrapper.find(Form).simulate('submit');
    expect(errors).toBeNull();
  });

  it('should support onPreview', () => {
    const handlePreview = jest.fn();
    const wrapper = mount(
      <Upload
        listType="picture-card"
        defaultFileList={fileList}
        onPreview={handlePreview}
      >
        <button type="button">upload</button>
      </Upload>
    );
    wrapper.find('.anticon-eye-o').at(0).simulate('click');
    expect(handlePreview).toBeCalledWith(fileList[0]);
    wrapper.find('.anticon-eye-o').at(1).simulate('click');
    expect(handlePreview).toBeCalledWith(fileList[1]);
  });

  it('should support onRemove', async () => {
    const handleRemove = jest.fn();
    const handleChange = jest.fn();
    const wrapper = mount(
      <Upload
        listType="picture-card"
        defaultFileList={fileList}
        onRemove={handleRemove}
        onChange={handleChange}
      >
        <button type="button">upload</button>
      </Upload>
    );
    wrapper.find('.anticon-delete').at(0).simulate('click');
    expect(handleRemove).toBeCalledWith(fileList[0]);
    wrapper.find('.anticon-delete').at(1).simulate('click');
    expect(handleRemove).toBeCalledWith(fileList[1]);
    await delay(0);
    expect(handleChange.mock.calls.length).toBe(2);
  });

  it('should generate thumbUrl from file', async () => {
    const handlePreview = jest.fn();
    const newFileList = [...fileList];
    const newFile = { ...fileList[0], uid: '-3', originFileObj: new File([], 'xxx.png') };
    delete newFile.thumbUrl;
    newFileList.push(newFile);
    const wrapper = mount(
      <Upload
        listType="picture-card"
        defaultFileList={newFileList}
        onPreview={handlePreview}
      >
        <button type="button">upload</button>
      </Upload>
    );
    wrapper.setState({});
    await delay(200);
    expect(wrapper.state().fileList[2].thumbUrl).not.toBeFalsy();
  });

  it('should non-image format file preview', () => {
    const list = [
      {
        name: 'not-image',
        status: 'done',
        uid: '-3',
        url: 'https://cdn.xxx.com/aaa.zip',
        thumbUrl: 'data:application/zip;base64,UEsDBAoAAAAAADYZYkwAAAAAAAAAAAAAAAAdAAk',
        originFileObj: new File([], 'aaa.zip'),
      },
      {
        name: 'image',
        status: 'done',
        uid: '-4',
        url: 'https://cdn.xxx.com/aaa',
      },
      {
        name: 'not-image',
        status: 'done',
        uid: '-5',
        url: 'https://cdn.xxx.com/aaa.xx',
      },
      {
        name: 'not-image',
        status: 'done',
        uid: '-6',
        url: 'https://cdn.xxx.com/aaa.png/xx.xx',
      },
      {
        name: 'image',
        status: 'done',
        uid: '-7',
        url: 'https://cdn.xxx.com/xx.xx/aaa.png',
      },
      {
        name: 'image',
        status: 'done',
        uid: '-8',
        url: 'https://cdn.xxx.com/xx.xx/aaa.png',
        thumbUrl: 'data:image/png;base64,UEsDBAoAAAAAADYZYkwAAAAAAAAAAAAAAAAdAAk',
      },
      {
        name: 'image',
        status: 'done',
        uid: '-9',
        url: 'https://cdn.xxx.com/xx.xx/aaa.png?query=123',
      },
      {
        name: 'image',
        status: 'done',
        uid: '-10',
        url: 'https://cdn.xxx.com/xx.xx/aaa.png#anchor',
      },
      {
        name: 'image',
        status: 'done',
        uid: '-11',
        url: 'https://cdn.xxx.com/xx.xx/aaa.png?query=some.query.with.dot',
      },
      {
        name: 'image',
        status: 'done',
        uid: '-12',
        url: 'https://publish-pic-cpu.baidu.com/1296beb3-50d9-4276-885f-52645cbb378e.jpeg@w_228%2ch_152',
        type: 'image',
      },
    ];

    const wrapper = mount(
      <Upload
        listType="picture"
        defaultFileList={list}
      >
        <button type="button">upload</button>
      </Upload>
    );
    expect(wrapper.render()).toMatchSnapshot();
  });
});
<commit_msg>fix test case failed often<commit_after>import React from 'react';
import { mount } from 'enzyme';
import Upload from '..';
import Form from '../../form';
import { errorRequest, successRequest } from './requests';
import { setup, teardown } from './mock';

const delay = timeout => new Promise(resolve => setTimeout(resolve, timeout));

const fileList = [{
  uid: '-1',
  name: 'xxx.png',
  status: 'done',
  url: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png',
  thumbUrl: 'https://zos.alipayobjects.com/rmsportal/IQKRngzUuFzJzGzRJXUs.png',
}, {
  uid: '-2',
  name: 'yyy.png',
  status: 'done',
  url: 'https://zos.alipayobjects.com/rmsportal/IQKRngzUuFzJzGzRJXUs.png',
  thumbUrl: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png',
}];

describe('Upload List', () => {
  beforeEach(() => setup());
  afterEach(() => teardown());

  // https://github.com/ant-design/ant-design/issues/4653
  it('should use file.thumbUrl for <img /> in priority', () => {
    const wrapper = mount(
      <Upload defaultFileList={fileList} listType="picture">
        <button type="button">upload</button>
      </Upload>
    );
    fileList.forEach((file, i) => {
      const linkNode = wrapper.find('.ant-upload-list-item-thumbnail').at(i);
      const imgNode = wrapper.find('.ant-upload-list-item-thumbnail img').at(i);
      expect(linkNode.prop('href')).toBe(file.url);
      expect(imgNode.prop('src')).toBe(file.thumbUrl);
    });
  });

  // https://github.com/ant-design/ant-design/issues/7269
  it('should remove correct item when uid is 0', async () => {
    const list = [{
      uid: '0',
      name: 'xxx.png',
      status: 'done',
      url: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png',
      thumbUrl: 'https://zos.alipayobjects.com/rmsportal/IQKRngzUuFzJzGzRJXUs.png',
    }, {
      uid: '1',
      name: 'xxx.png',
      status: 'done',
      url: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png',
      thumbUrl: 'https://zos.alipayobjects.com/rmsportal/IQKRngzUuFzJzGzRJXUs.png',
    }];
    const wrapper = mount(
      <Upload defaultFileList={list}>
        <button type="button">upload</button>
      </Upload>
    );
    expect(wrapper.find('.ant-upload-list-item').length).toBe(2);
    wrapper.find('.ant-upload-list-item').at(0).find('.anticon-cross').simulate('click');
    await delay(400);
    wrapper.update();
    expect(wrapper.find('.ant-upload-list-item').hostNodes().length).toBe(1);
  });

  it('should be uploading when upload a file', (done) => {
    let wrapper;
    const onChange = ({ file }) => {
      if (file.status === 'uploading') {
        expect(wrapper.render()).toMatchSnapshot();
      }
      if (file.status === 'done') {
        expect(wrapper.render()).toMatchSnapshot();
        done();
      }
    };
    wrapper = mount(
      <Upload
        action="http://jsonplaceholder.typicode.com/posts/"
        onChange={onChange}
        customRequest={successRequest}
      >
        <button type="button">upload</button>
      </Upload>
    );
    wrapper.find('input').simulate('change', {
      target: {
        files: [
          { name: 'foo.png' },
        ],
      },
    });
  });

  it('handle error', (done) => {
    let wrapper;
    const onChange = ({ file }) => {
      if (file.status !== 'uploading') {
        expect(wrapper.render()).toMatchSnapshot();
        done();
      }
    };
    wrapper = mount(
      <Upload
        action="http://jsonplaceholder.typicode.com/posts/"
        onChange={onChange}
        customRequest={errorRequest}
      >
        <button type="button">upload</button>
      </Upload>
    );
    wrapper.find('input').simulate('change', {
      target: {
        files: [
          { name: 'foo.png' },
        ],
      },
    });
  });

  it('does concat filelist when beforeUpload returns false', () => {
    const handleChange = jest.fn();
    const wrapper = mount(
      <Upload
        listType="picture"
        defaultFileList={fileList}
        onChange={handleChange}
        beforeUpload={() => false}
      >
        <button type="button">upload</button>
      </Upload>
    );

    wrapper.find('input').simulate('change', {
      target: {
        files: [
          { name: 'foo.png' },
        ],
      },
    });

    expect(wrapper.state().fileList.length).toBe(fileList.length + 1);
    expect(handleChange.mock.calls[0][0].fileList).toHaveLength(3);
  });

  // https://github.com/ant-design/ant-design/issues/7762
  it('work with form validation', () => {
    let errors;
    class TestForm extends React.Component {
      handleSubmit = () => {
        const { form: { validateFields } } = this.props;
        validateFields((err) => {
          errors = err;
        });
      }

      render() {
        const { form: { getFieldDecorator } } = this.props;
        return (
          <Form onSubmit={this.handleSubmit}>
            <Form.Item>
              {getFieldDecorator('file', {
                valuePropname: 'fileList',
                getValueFromEvent: e => e.fileList,
                rules: [
                  {
                    required: true,
                    validator: (rule, value, callback) => {
                      if (!value || value.length === 0) {
                        callback('file required');
                      } else {
                        callback();
                      }
                    },
                  },
                ],
              })(
                <Upload
                  beforeUpload={() => false}
                >
                  <button type="button">upload</button>
                </Upload>
              )}
            </Form.Item>
          </Form>
        );
      }
    }

    const App = Form.create()(TestForm);
    const wrapper = mount(<App />);
    wrapper.find(Form).simulate('submit');
    expect(errors.file.errors).toEqual([{ message: 'file required', field: 'file' }]);

    wrapper.find('input').simulate('change', {
      target: {
        files: [
          { name: 'foo.png' },
        ],
      },
    });
    wrapper.find(Form).simulate('submit');
    expect(errors).toBeNull();
  });

  it('should support onPreview', () => {
    const handlePreview = jest.fn();
    const wrapper = mount(
      <Upload
        listType="picture-card"
        defaultFileList={fileList}
        onPreview={handlePreview}
      >
        <button type="button">upload</button>
      </Upload>
    );
    wrapper.find('.anticon-eye-o').at(0).simulate('click');
    expect(handlePreview).toBeCalledWith(fileList[0]);
    wrapper.find('.anticon-eye-o').at(1).simulate('click');
    expect(handlePreview).toBeCalledWith(fileList[1]);
  });

  it('should support onRemove', async () => {
    const handleRemove = jest.fn();
    const handleChange = jest.fn();
    const wrapper = mount(
      <Upload
        listType="picture-card"
        defaultFileList={fileList}
        onRemove={handleRemove}
        onChange={handleChange}
      >
        <button type="button">upload</button>
      </Upload>
    );
    wrapper.find('.anticon-delete').at(0).simulate('click');
    expect(handleRemove).toBeCalledWith(fileList[0]);
    wrapper.find('.anticon-delete').at(1).simulate('click');
    expect(handleRemove).toBeCalledWith(fileList[1]);
    await delay(0);
    expect(handleChange.mock.calls.length).toBe(2);
  });

  it('should generate thumbUrl from file', async () => {
    const handlePreview = jest.fn();
    const newFileList = [...fileList];
    const newFile = { ...fileList[0], uid: '-3', originFileObj: new File([], 'xxx.png') };
    delete newFile.thumbUrl;
    newFileList.push(newFile);
    const wrapper = mount(
      <Upload
        listType="picture-card"
        defaultFileList={newFileList}
        onPreview={handlePreview}
      >
        <button type="button">upload</button>
      </Upload>
    );
    wrapper.setState({});
    await delay(0);
    expect(wrapper.state().fileList[2].thumbUrl).not.toBe(undefined);
  });

  it('should non-image format file preview', () => {
    const list = [
      {
        name: 'not-image',
        status: 'done',
        uid: '-3',
        url: 'https://cdn.xxx.com/aaa.zip',
        thumbUrl: 'data:application/zip;base64,UEsDBAoAAAAAADYZYkwAAAAAAAAAAAAAAAAdAAk',
        originFileObj: new File([], 'aaa.zip'),
      },
      {
        name: 'image',
        status: 'done',
        uid: '-4',
        url: 'https://cdn.xxx.com/aaa',
      },
      {
        name: 'not-image',
        status: 'done',
        uid: '-5',
        url: 'https://cdn.xxx.com/aaa.xx',
      },
      {
        name: 'not-image',
        status: 'done',
        uid: '-6',
        url: 'https://cdn.xxx.com/aaa.png/xx.xx',
      },
      {
        name: 'image',
        status: 'done',
        uid: '-7',
        url: 'https://cdn.xxx.com/xx.xx/aaa.png',
      },
      {
        name: 'image',
        status: 'done',
        uid: '-8',
        url: 'https://cdn.xxx.com/xx.xx/aaa.png',
        thumbUrl: 'data:image/png;base64,UEsDBAoAAAAAADYZYkwAAAAAAAAAAAAAAAAdAAk',
      },
      {
        name: 'image',
        status: 'done',
        uid: '-9',
        url: 'https://cdn.xxx.com/xx.xx/aaa.png?query=123',
      },
      {
        name: 'image',
        status: 'done',
        uid: '-10',
        url: 'https://cdn.xxx.com/xx.xx/aaa.png#anchor',
      },
      {
        name: 'image',
        status: 'done',
        uid: '-11',
        url: 'https://cdn.xxx.com/xx.xx/aaa.png?query=some.query.with.dot',
      },
      {
        name: 'image',
        status: 'done',
        uid: '-12',
        url: 'https://publish-pic-cpu.baidu.com/1296beb3-50d9-4276-885f-52645cbb378e.jpeg@w_228%2ch_152',
        type: 'image',
      },
    ];

    const wrapper = mount(
      <Upload
        listType="picture"
        defaultFileList={list}
      >
        <button type="button">upload</button>
      </Upload>
    );
    expect(wrapper.render()).toMatchSnapshot();
  });
});
<|endoftext|>


<commit_before>JSAN.use('DOM.Events');

if ( typeof Silki == "undefined" ) {
    Silki = {};
}

if ( typeof Silki.QuickSearch == "undefined" ) {
    Silki.QuickSearch = {};
}

Silki.QuickSearch = function () {
    var input = $("quick-search-input");

    if ( ! input ) {
        return;
    }

    var match = input.className.match( /js-default-text-(\w+)/ );
    var default_val = match[1];

    DOM.Events.addListener( input,
                            "focus",
                            function () {
                                if ( input.value == default_val ) {
                                    input.value = "";
                                }
                            }
                          );
};<commit_msg>Remove useless assignment<commit_after>JSAN.use('DOM.Events');

if ( typeof Silki == "undefined" ) {
    Silki = {};
}

Silki.QuickSearch = function () {
    var input = $("quick-search-input");

    if ( ! input ) {
        return;
    }

    var match = input.className.match( /js-default-text-(\w+)/ );
    var default_val = match[1];

    DOM.Events.addListener( input,
                            "focus",
                            function () {
                                if ( input.value == default_val ) {
                                    input.value = "";
                                }
                            }
                          );
};<|endoftext|>


<commit_before>const EXTENT_HOURS = 72;
const GAP_HOURS = 6;
const STALE_DATA_CUTOFF = new Date(new Date() - 1000 * 60 * 60 * EXTENT_HOURS);

function initMap() {
    var defaultZoomLevel = 18;
    var latitude = parseFloat($('#site-latitude').val());
    var longitude = parseFloat($('#site-longitude').val());
    var sitePosition = {lat: latitude, lng: longitude};

    var map = new google.maps.Map(document.getElementById('map'), {
        center: sitePosition,
        gestureHandling: 'greedy',
        zoom: defaultZoomLevel,
        mapTypeId: google.maps.MapTypeId.HYBRID
    });

    map.setOptions({minZoom: 3, maxZoom: 18});

    var marker = new google.maps.Marker({
        position: sitePosition,
        map: map
    });
}

function format_date(date) {
    year = String(date.getFullYear()).padStart(4, '0');
    month = String(date.getMonth()+1).padStart(2, '0');
    day = String(date.getDate()).padStart(2, '0');
    hour = String(date.getHours()).padStart(2, '0');
    minute = String(date.getMinutes()).padStart(2, '0');
    second = String(date.getSeconds()).padStart(2, '0');
    return `${year}-${month}-${day} ${hour}:${minute}:${second}`;
}

function fillValueTable(table, data) {
    var rows = data.map(function (dataValue) {
        //looks to be 1 hour offset between python datetime integer and JS
        date = new Date(dataValue.valuedatetime - 3600000);
        var row_string = "
" +
            format_date(date) + "	" +
            dataValue.valuedatetimeutcoffset + "	" +
            dataValue.datavalue + "
";
        return row_string;
    });
    table.append($(rows.join('')));
}

function drawSparklineOnResize(seriesInfo, seriesData) {
    var resizeTimer;
    window.addEventListener('resize', function (event) {
        clearTimeout(resizeTimer);
        resizeTimer = setTimeout(function () {
            drawSparklinePlot(seriesInfo, seriesData);
        }, 500);
    });
}

function drawSparklinePlot(seriesInfo, seriesData) {
    var card = $('div.plot_box[data-result-id="' + seriesInfo['resultId'] + '"]');
    var plotBox = card.find(".graph-container");

    plotBox.empty();

    var margin = {top: 5, right: 1, bottom: 5, left: 1};
    var width = plotBox.width() - margin.left - margin.right;
    var height = plotBox.height() - margin.top - margin.bottom;

    if (seriesData.length === 0) {
        card.find(".table-trigger").toggleClass("disabled", true);
        card.find(".download-trigger").toggleClass("disabled", true);
        card.find(".tsa-trigger").toggleClass("disabled", true);

        // Append message when there is no data
        d3.select(plotBox.get(0)).append("svg")
            .attr("width", width + margin.left + margin.right)
            .attr("height", height + margin.top + margin.bottom)
            .append("text")
                .text("No data exist for this variable.")
                .attr("font-size", "12px")
                .attr("fill", "#AAA")
                .attr("text-anchor", "left")
                .attr("transform", "translate(" + (margin.left + 10) + "," + (margin.top + 20) + ")");
        return;
    }

    var lastRead = Math.max.apply(Math, seriesData.map(function(value){
        return new Date(value.valuedatetime);
    }));

    var dataTimeOffset = Math.min.apply(Math, seriesData.map(function(value){
        return new Date(value.valuedatetime);
    }));

    var xAxis = d3.scaleTime().range([0, width]);
    var yAxis = d3.scaleLinear().range([height, 0]);

    var yDomain = d3.extent(seriesData, function(d) {
        return parseFloat(d.datavalue);
    });
    var yPadding = (yDomain[1] - yDomain[0]) / 20;  // 5% padding
    yDomain[0] -= yPadding;
    yDomain[1] += yPadding;

    xAxis.domain([dataTimeOffset, lastRead]);
    yAxis.domain(yDomain);

    var line = d3.line()
        .x(function(d) {
            var date = new Date(d.valuedatetime);
            return xAxis(date);
        })
        .y(function(d) {
            return yAxis(d.datavalue);
        });

    var svg = d3.select(plotBox.get(0)).append("svg")
        .attr("width", width + margin.left + margin.right)
        .attr("class", function() {
            if (lastRead <= STALE_DATA_CUTOFF) {
                return "stale";
            }

            return "not-stale";
        })
        .attr("height", height + margin.top + margin.bottom)
        .append("g")
        .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

    // Rendering the paths
    var gapOffset;  // The minimum date required before being considered a gap.
    var previousDate;
    var start = 0;  // Latest start detected after a gap. Initially set to the start of the list.
    var paths = [];

    for (var i = 0; i < seriesData.length; i++) {
        var currentDate = new Date(seriesData[i].valuedatetime);

        if (previousDate) {
            gapOffset = new Date(currentDate - 1000 * 60 * 60 * GAP_HOURS);
        }

        if (previousDate && previousDate < gapOffset) {
            paths.push(seriesData.slice(start, i));
            start = i;
        }
        previousDate = currentDate;
    }

    if (start > 0) {
        paths.push(seriesData.slice(start, seriesData.length));
    }
    else {
        paths.push(seriesData); // No gaps were detected. Just plot the entire original data.
    }

    // Plot all paths separately to display gaps between them.
    for (var i = 0; i < paths.length; i++) {
        if (paths[i].length == 1) {
            svg.append("circle")
                .attr("r", 2)
                .style("fill", "steelblue")
                .attr("transform", "translate(" + xAxis(new Date(paths[i][0].valuedatetime)) + ", " + yAxis(paths[i][0].datavalue) + ")")
        }
        else {
            svg.append("path")
            .data([paths[i]])
            .attr("class", "line").attr("d", line)
            .attr("stroke", "steelblue");
        }
    }
}

function getTimeSeriesData(sensorInfo) {
    request_data = {method:'get_result_timeseries', 'resultid': sensorInfo['resultId']}
    $.ajax({
        url: '../../dataloader/ajax/',
        data: {request_data: JSON.stringify(request_data)},
        method: 'POST',
        success: function(data) {
            response_data = JSON.parse(data)
            $table = $('table.data-values[data-result-id=' + sensorInfo['resultId'] + ']');
            fillValueTable($table, response_data);
            drawSparklineOnResize(sensorInfo, response_data);
            drawSparklinePlot(sensorInfo, response_data);
            /*
            var resultSet = influx_data.results ? influx_data.results.shift() : null;
            if (resultSet && resultSet.series && resultSet.series.length) {
                var influxSeries = resultSet.series.shift();
                var indexes = {
                    time: influxSeries.columns.indexOf("time"),
                    value: influxSeries.columns.indexOf("DataValue"),
                    offset: influxSeries.columns.indexOf("UTCOffset")
                };
                var values = influxSeries.values.map(function(influxValue) {
                    return {
                        DateTime: influxValue[indexes.time].match(/^(\d{4}\-\d\d\-\d\d([tT][\d:]*)?)/).shift(),
                        Value: influxValue[indexes.value],
                        TimeOffset: influxValue[indexes.offset]
                    }
                });

                fillValueTable($('table.data-values[data-result-id=' + sensorInfo['resultId'] + ']'), values);
                drawSparklineOnResize(sensorInfo, values);
                drawSparklinePlot(sensorInfo, values);
            } else {
                console.log('No data values were found for this site');
                drawSparklinePlot(sensorInfo, []);  // Will just render the empty message
                // console.info(series.getdatainflux);
            }
            */
        },
        fail: function() {
            drawSparklinePlot(sensorInfo, []);  // Will just render the empty message
            console.log('data failed to load.');
        }
    });
}

$(document).ready(function () {
    var dialog = $('#data-table-dialog');
    $("#chkFollow").on("change", function () {
        var statusContainer = $(".follow-status");
        var followForm = $("#follow-site-form");
        var following = !$(this).prop("checked");

        $.ajax({
            url: $('#follow-site-api').val(),
            type: 'post',
            data: {
                csrfmiddlewaretoken: followForm.find('input[name="csrfmiddlewaretoken"]').val(),
                sampling_feature_code: followForm.find('input[name="sampling_feature_code"]').val(),
                action: (following) ? 'unfollow' : 'follow'
            }
        }).done(function () {
            statusContainer.toggleClass("following");
            var message = !following ? 'You are now following this site.' : 'This site has been unfollowed.';
            snackbarMsg(message);
        });
    });

    $(".table-trigger").click(function () {
        var box = $(this).parents('.plot_box');
        var id = box.data('result-id');
        var tables = $('table.data-values');
        tables.hide();

        tables.filter('[data-result-id="' + id + '"]').show();
        var title = box.data('variable-name') + ' (' + box.data('variable-code') + ')';
        dialog.find('.mdl-dialog__title').text(title);
        dialog.find('.mdl-dialog__title').attr("title", title);

        dialog.modal('show');
    });

    $('nav .menu-sites-list').addClass('active');

    var sensors = document.querySelectorAll('.sparkline-plots .plot_box');
    for (var index = 0; index < sensors.length; index++) {
        var sensorInfo = sensors[index].dataset;
        getTimeSeriesData(sensorInfo);
    }
});
<commit_msg>Corrected UTC to local conversion<commit_after>const EXTENT_HOURS = 72;
const GAP_HOURS = 6;
const STALE_DATA_CUTOFF = new Date(new Date() - 1000 * 60 * 60 * EXTENT_HOURS);

const LOCAL_UTC_OFFSET = new Date().getTimezoneOffset() / 60; //in hours

function initMap() {
    var defaultZoomLevel = 18;
    var latitude = parseFloat($('#site-latitude').val());
    var longitude = parseFloat($('#site-longitude').val());
    var sitePosition = {lat: latitude, lng: longitude};

    var map = new google.maps.Map(document.getElementById('map'), {
        center: sitePosition,
        gestureHandling: 'greedy',
        zoom: defaultZoomLevel,
        mapTypeId: google.maps.MapTypeId.HYBRID
    });

    map.setOptions({minZoom: 3, maxZoom: 18});

    var marker = new google.maps.Marker({
        position: sitePosition,
        map: map
    });
}

function format_date(date) {
    year = String(date.getFullYear()).padStart(4, '0');
    month = String(date.getMonth()+1).padStart(2, '0');
    day = String(date.getDate()).padStart(2, '0');
    hour = String(date.getHours()).padStart(2, '0');
    minute = String(date.getMinutes()).padStart(2, '0');
    second = String(date.getSeconds()).padStart(2, '0');
    return `${year}-${month}-${day} ${hour}:${minute}:${second}`;
}

function fillValueTable(table, data) {
    var rows = data.map(function (dataValue) {
        //looks to be 1 hour offset between python datetime integer and JS
        date = new Date(dataValue.valuedatetime + (dataValue.valuedatetimeutcoffset + LOCAL_UTC_OFFSET) * 3600000);
        var row_string = "
" +
            format_date(date) + "	" +
            dataValue.valuedatetimeutcoffset + "	" +
            dataValue.datavalue + "
";
        return row_string;
    });
    table.append($(rows.join('')));
}

function drawSparklineOnResize(seriesInfo, seriesData) {
    var resizeTimer;
    window.addEventListener('resize', function (event) {
        clearTimeout(resizeTimer);
        resizeTimer = setTimeout(function () {
            drawSparklinePlot(seriesInfo, seriesData);
        }, 500);
    });
}

function drawSparklinePlot(seriesInfo, seriesData) {
    var card = $('div.plot_box[data-result-id="' + seriesInfo['resultId'] + '"]');
    var plotBox = card.find(".graph-container");

    plotBox.empty();

    var margin = {top: 5, right: 1, bottom: 5, left: 1};
    var width = plotBox.width() - margin.left - margin.right;
    var height = plotBox.height() - margin.top - margin.bottom;

    if (seriesData.length === 0) {
        card.find(".table-trigger").toggleClass("disabled", true);
        card.find(".download-trigger").toggleClass("disabled", true);
        card.find(".tsa-trigger").toggleClass("disabled", true);

        // Append message when there is no data
        d3.select(plotBox.get(0)).append("svg")
            .attr("width", width + margin.left + margin.right)
            .attr("height", height + margin.top + margin.bottom)
            .append("text")
                .text("No data exist for this variable.")
                .attr("font-size", "12px")
                .attr("fill", "#AAA")
                .attr("text-anchor", "left")
                .attr("transform", "translate(" + (margin.left + 10) + "," + (margin.top + 20) + ")");
        return;
    }

    var lastRead = Math.max.apply(Math, seriesData.map(function(value){
        return new Date(value.valuedatetime);
    }));

    var dataTimeOffset = Math.min.apply(Math, seriesData.map(function(value){
        return new Date(value.valuedatetime);
    }));

    var xAxis = d3.scaleTime().range([0, width]);
    var yAxis = d3.scaleLinear().range([height, 0]);

    var yDomain = d3.extent(seriesData, function(d) {
        return parseFloat(d.datavalue);
    });
    var yPadding = (yDomain[1] - yDomain[0]) / 20;  // 5% padding
    yDomain[0] -= yPadding;
    yDomain[1] += yPadding;

    xAxis.domain([dataTimeOffset, lastRead]);
    yAxis.domain(yDomain);

    var line = d3.line()
        .x(function(d) {
            var date = new Date(d.valuedatetime);
            return xAxis(date);
        })
        .y(function(d) {
            return yAxis(d.datavalue);
        });

    var svg = d3.select(plotBox.get(0)).append("svg")
        .attr("width", width + margin.left + margin.right)
        .attr("class", function() {
            if (lastRead <= STALE_DATA_CUTOFF) {
                return "stale";
            }

            return "not-stale";
        })
        .attr("height", height + margin.top + margin.bottom)
        .append("g")
        .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

    // Rendering the paths
    var gapOffset;  // The minimum date required before being considered a gap.
    var previousDate;
    var start = 0;  // Latest start detected after a gap. Initially set to the start of the list.
    var paths = [];

    for (var i = 0; i < seriesData.length; i++) {
        var currentDate = new Date(seriesData[i].valuedatetime);

        if (previousDate) {
            gapOffset = new Date(currentDate - 1000 * 60 * 60 * GAP_HOURS);
        }

        if (previousDate && previousDate < gapOffset) {
            paths.push(seriesData.slice(start, i));
            start = i;
        }
        previousDate = currentDate;
    }

    if (start > 0) {
        paths.push(seriesData.slice(start, seriesData.length));
    }
    else {
        paths.push(seriesData); // No gaps were detected. Just plot the entire original data.
    }

    // Plot all paths separately to display gaps between them.
    for (var i = 0; i < paths.length; i++) {
        if (paths[i].length == 1) {
            svg.append("circle")
                .attr("r", 2)
                .style("fill", "steelblue")
                .attr("transform", "translate(" + xAxis(new Date(paths[i][0].valuedatetime)) + ", " + yAxis(paths[i][0].datavalue) + ")")
        }
        else {
            svg.append("path")
            .data([paths[i]])
            .attr("class", "line").attr("d", line)
            .attr("stroke", "steelblue");
        }
    }
}

function getTimeSeriesData(sensorInfo) {
    request_data = {method:'get_result_timeseries', 'resultid': sensorInfo['resultId']}
    $.ajax({
        url: '../../dataloader/ajax/',
        data: {request_data: JSON.stringify(request_data)},
        method: 'POST',
        success: function(data) {
            response_data = JSON.parse(data)
            $table = $('table.data-values[data-result-id=' + sensorInfo['resultId'] + ']');
            fillValueTable($table, response_data);
            drawSparklineOnResize(sensorInfo, response_data);
            drawSparklinePlot(sensorInfo, response_data);
            /*
            var resultSet = influx_data.results ? influx_data.results.shift() : null;
            if (resultSet && resultSet.series && resultSet.series.length) {
                var influxSeries = resultSet.series.shift();
                var indexes = {
                    time: influxSeries.columns.indexOf("time"),
                    value: influxSeries.columns.indexOf("DataValue"),
                    offset: influxSeries.columns.indexOf("UTCOffset")
                };
                var values = influxSeries.values.map(function(influxValue) {
                    return {
                        DateTime: influxValue[indexes.time].match(/^(\d{4}\-\d\d\-\d\d([tT][\d:]*)?)/).shift(),
                        Value: influxValue[indexes.value],
                        TimeOffset: influxValue[indexes.offset]
                    }
                });

                fillValueTable($('table.data-values[data-result-id=' + sensorInfo['resultId'] + ']'), values);
                drawSparklineOnResize(sensorInfo, values);
                drawSparklinePlot(sensorInfo, values);
            } else {
                console.log('No data values were found for this site');
                drawSparklinePlot(sensorInfo, []);  // Will just render the empty message
                // console.info(series.getdatainflux);
            }
            */
        },
        fail: function() {
            drawSparklinePlot(sensorInfo, []);  // Will just render the empty message
            console.log('data failed to load.');
        }
    });
}

$(document).ready(function () {
    var dialog = $('#data-table-dialog');
    $("#chkFollow").on("change", function () {
        var statusContainer = $(".follow-status");
        var followForm = $("#follow-site-form");
        var following = !$(this).prop("checked");

        $.ajax({
            url: $('#follow-site-api').val(),
            type: 'post',
            data: {
                csrfmiddlewaretoken: followForm.find('input[name="csrfmiddlewaretoken"]').val(),
                sampling_feature_code: followForm.find('input[name="sampling_feature_code"]').val(),
                action: (following) ? 'unfollow' : 'follow'
            }
        }).done(function () {
            statusContainer.toggleClass("following");
            var message = !following ? 'You are now following this site.' : 'This site has been unfollowed.';
            snackbarMsg(message);
        });
    });

    $(".table-trigger").click(function () {
        var box = $(this).parents('.plot_box');
        var id = box.data('result-id');
        var tables = $('table.data-values');
        tables.hide();

        tables.filter('[data-result-id="' + id + '"]').show();
        var title = box.data('variable-name') + ' (' + box.data('variable-code') + ')';
        dialog.find('.mdl-dialog__title').text(title);
        dialog.find('.mdl-dialog__title').attr("title", title);

        dialog.modal('show');
    });

    $('nav .menu-sites-list').addClass('active');

    var sensors = document.querySelectorAll('.sparkline-plots .plot_box');
    for (var index = 0; index < sensors.length; index++) {
        var sensorInfo = sensors[index].dataset;
        getTimeSeriesData(sensorInfo);
    }
});
<|endoftext|>


<commit_before>'use strict';
//List of errors
const errors = [
  //Unknown errors
  'UNKNOWN_ERROR', //ONLY FOR USE IF THE ERROR IS COMPLETELY UNKNOWN
  //Database errors
  'DATABASE_GENERIC', //Generic database error
  'DATABASE_NOT_FOUND', //Document not found
  //Generic errors
  'GENERIC_PAYLOAD_MISSING', //For use anywhere, if the payload is missing
  'GENERIC_PARAMETERS_MISSING', //For use anywhere where parameters are missing
  'GENERIC_MISSING_SECURITY_INFORMATION', //If security information is missing
  //Handshake stage errors
  'STAGE_HANDSHAKE_GENERIC', //Generic handshake stage error
  'STAGE_HANDSHAKE_KEY_MISSING', //Missing key
  'STAGE_HANDSHAKE_POST_COMPLETE_FAILURE', //Handshake failure in another area
  //Verification stage errors
  'STAGE_VERIFICATION_GENERIC', //Generic verification stage error
  'STAGE_VERIFICATION_NOT_EXECUTED', //For when the user isn't verified
  //Request stage errors
  'STAGE_REQUEST_NO_WORK', //No work remaining
  'STAGE_REQUEST_EXISTING_WORKER', //Already a worker of this group
  'STAGE_REQUEST_PENDING_WORK', //Work is already pending
  //Submit stage errors
  'STAGE_SUBMIT_NO_DATA', //No work data was sent to the server
  //Security errors
  'SECURITY_INVALID_KEY', //Invalid key error
  'SECURITY_KEY_GENERATION_FAILURE', //Key generation error
  'SECURITY_ENCRYPTION_FAILURE', //Encryption error
  'SECURITY_DECRYPTION_FAILURE', //Decryption error
  'SECURITY_SIGNING_FAILURE', //Signing error
  'SECURITY_VERIFICATION_FAILURE' //Signed message verification error
];
//Declare error verifying function
let verifyError = function(error, type) {
  //Make sure the string is UpperCase
  error = error.toUpperCase();
  //Verify that the error exists. If so, return the error, else return
  //"UNKNOWN_ERROR"
  return errors.indexOf(error) !== -1 ? error : errors[0];
};
//Send error helper function
module.exports.sendError = function(socket, error, disconnect) {
  //Tell the user about the error
  try {
    socket.sendError(new Error(verifyError(error)),
      function(error) {
      //If we wanted to disconnect, do so to prevent further communications
      if(disconnect) {
        socket.destroy();
      }
    });
  } catch(e) {
    //Destroy socket
    socket.destroy();
    return;
  }
};
<commit_msg>Revert accidental commit of half broken new errorHandler<commit_after>'use strict';
//List of errors
const errors = [
  //Unknown errors
  'UNKNOWN_ERROR', //ONLY FOR USE IF THE ERROR IS COMPLETELY UNKNOWN
  //Database errors
  'DATABASE_GENERIC', //Generic database error
  'DATABASE_NOT_FOUND', //Document not found
  //Generic errors
  'GENERIC_PAYLOAD_MISSING', //For use anywhere, if the payload is missing
  'GENERIC_PARAMETERS_MISSING', //For use anywhere where parameters are missing
  'GENERIC_MISSING_SECURITY_INFORMATION', //If security information is missing
  //Handshake stage errors
  'STAGE_HANDSHAKE_GENERIC', //Generic handshake stage error
  'STAGE_HANDSHAKE_KEY_MISSING', //Missing key
  'STAGE_HANDSHAKE_POST_COMPLETE_FAILURE', //Handshake failure in another area
  //Verification stage errors
  'STAGE_VERIFICATION_GENERIC', //Generic verification stage error
  'STAGE_VERIFICATION_NOT_EXECUTED', //For when the user isn't verified
  //Request stage errors
  'STAGE_REQUEST_NO_WORK', //No work remaining
  'STAGE_REQUEST_EXISTING_WORKER', //Already a worker of this group
  'STAGE_REQUEST_PENDING_WORK', //Work is already pending
  //Submit stage errors
  'STAGE_SUBMIT_NO_DATA', //No work data was sent to the server
  //Security errors
  'SECURITY_INVALID_KEY', //Invalid key error
  'SECURITY_KEY_GENERATION_FAILURE', //Key generation error
  'SECURITY_ENCRYPTION_FAILURE', //Encryption error
  'SECURITY_DECRYPTION_FAILURE', //Decryption error
  'SECURITY_SIGNING_FAILURE', //Signing error
  'SECURITY_VERIFICATION_FAILURE' //Signed message verification error
];
//Export error finding function
module.exports.findError = function(error, type) {
  //If the error is undefined, return undefined
  if(typeof error === 'undefined') {
    return undefined;
  }
  //If the received type of error is a number
  if(typeof error === 'number') {
    //Then check if the type we wanted is a number, if so return error
    if(type === 'number') {
      return error;
    }
    //Else, find and return the error name
    return errors[error] ? errors[error] : errors[0];
  } else {
    //Make sure the string is UpperCase
    error = error.toUpperCase();
    //Otherwise, check if the type we wanted is a string, and if so return that
    if(type === 'string') {
      return error;
    }
    //Otherwise find the error number. If the error is unknown, we return the
    //index 0
    return errors.indexOf(error) !== -1 ? errors.indexOf(error) : 0;
  }
};
//Send error helper function
module.exports.sendError = function(socket, error, disconnect) {
  //Tell the user about the error
  try {
    socket.sendMessage({'error': module.exports.findError(error, 'number')},
      function(error) {
      //If we wanted to disconnect, do so to prevent further communications
      if(disconnect) {
        socket.destroy();
      }
    });
  } catch(e) {
    //Destroy socket
    socket.destroy();
    return;
  }
};
<|endoftext|>


<commit_before>// Client side code for the Problem Builder Dashboard XBlock
// So far, this code is only used to generate a downloadable report.
function ExportBase(runtime, element, initData) {
    "use strict";

    var reportTemplate = initData.reportTemplate;

    var generateDataUriFromImageURL = function(imgURL) {
        // Given the URL to an image, IF the image has already been cached by the browser,
        // returns a data: URI with the contents of the image (image will be converted to PNG)

        // Expand relative urls and urls without an explicit protocol into absolute urls
        var a = document.createElement('a');
        a.href = imgURL;
        imgURL = a.href;

        // If the image is from another domain, just return its URL. We can't
        // create a data URL from cross-domain images:
        // https://html.spec.whatwg.org/multipage/scripting.html#dom-canvas-todataurl
        if (a.origin !== window.location.origin)
            return imgURL;

        var img = new Image();
        img.src = imgURL;
        if (!img.complete)
            return imgURL;

        // Create an in-memory canvas from which we can extract a data URL:
        var canvas = document.createElement("canvas");
        canvas.width = img.naturalWidth;
        canvas.height = img.naturalHeight;
        // Draw the image onto our temporary canvas:
        canvas.getContext('2d').drawImage(img, 0, 0);
        return canvas.toDataURL("image/png");
    };

    var unicodeStringToBase64 = function(str) {
        // Convert string to base64. A bit weird in order to support unicode, per
        // https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/btoa
        return window.btoa(unescape(encodeURIComponent(str)));
    };

    var downloadReport = function(ev) {
        // Download Report:
        // Change the URL to a data: URI before continuing with the click event.
        if ($(this).attr('href').charAt(0) == '#') {
            var $report = $(initData.reportContentSelector, element).clone();
            // Convert all images in $report to data URIs:
            $report.find('image').each(function() {
                var origURL = $(this).attr('xlink:href');
                $(this).attr('xlink:href', generateDataUriFromImageURL(origURL));
            });
            // Take the resulting HTML and put it into the template we have:

            var wrapperHTML = reportTemplate.replace('REPORT_GOES_HERE', $report.html());
            console.log($.parseHTML(wrapperHTML, document));
            console.log($.parseHTML(wrapperHTML, document)[9]);
            console.log(typeof $.parseHTML(wrapperHTML, document)[9]);
            console.log($report);
            console.log(typeof $report);
            console.log($report.context);
            console.log(typeof $report.context);
            var doc = new jsPDF();
            doc.fromHTML($.parseHTML($.parseHTML(wrapperHTML, document)[9]).context, 15, 15, {
                'width': 170
            });

            doc.save('report.pdf');

            var dataURI = "data:text/html;base64," + unicodeStringToBase64(wrapperHTML);
            $(this).attr('href', dataURI);
        }
    };

    var $downloadLink = $('.report-download-link', element);
    $downloadLink.on('click', downloadReport);
}

function PBDashboardBlock(runtime, element, initData) {
    new ExportBase(runtime, element, initData);
}

function MentoringTableBlock(runtime, element, initData) {
    // Display an excerpt for long answers, with a "more" link to display the full text

    var $element = $(element),
        $shareButton = $element.find('.mentoring-share-button'),
        $doShareButton = $element.find('.do-share-button'),
        $shareMenu = $element.find('.mentoring-share-with'),
        $displayDropdown = $element.find('.mentoring-display-dropdown'),
        $errorHolder = $element.find('.share-errors'),
        $deleteShareButton = $element.find('.remove-share'),
        $newShareContainer = $($element.find('.new-share-container')[0]),
        $addShareField = $($element.find('.add-share-field')[0]),
        $notification = $($element.find('.share-notification')),
        $closeNotification = $($element.find('.notification-close')),
        tableLoadURL = runtime.handlerUrl(element, 'table_render'),
        deleteShareUrl = runtime.handlerUrl(element, 'remove_share'),
        sharedListLoadUrl = runtime.handlerUrl(element, 'get_shared_list'),
        clearNotificationUrl = runtime.handlerUrl(element, 'clear_notification'),
        shareResultsUrl = runtime.handlerUrl(element, 'share_results');

    function loadTable(data) {
        $element.find('.mentoring-table-target').html(data['content']);
        $('.answer-table', element).shorten({
            moreText: 'more',
            lessText: 'less',
            showChars: '500'
        });
    }

    function errorMessage(event) {
        $errorHolder.text(JSON.parse(event.responseText)['error'])
    }

    function sharedRefresh(data) {
        $element.find('.shared-with-container').html(data['content']);
        $deleteShareButton = $($deleteShareButton.selector);
        $deleteShareButton.on('click', deleteShare);
    }

    function postShareRefresh(data) {
        sharedRefresh(data);
        $element.find(".new-share-container").each(function(index, container) {
            if (index === 0) {
                var $container = $(container);
                $container.find('.add-share-username').val('');
                $container.find('.add-share-field').show();
                return;
            }
            $(container).remove()
        });
        $errorHolder.html('');
    }

    function postShare() {
        $.ajax({
            type: "POST",
            url: sharedListLoadUrl,
            data: JSON.stringify({}),
            success: postShareRefresh,
            error: errorMessage
        });
    }

    function updateShare() {
        var usernames = [];
        $element.find('.add-share-username').each(function(index, username) {
            usernames.push($(username).val())
        });
        $.ajax({
            type: "POST",
            url: shareResultsUrl,
            data: JSON.stringify({'usernames': usernames}),
            success: postShare,
            error: errorMessage
        });
    }

    function menuHider(event) {
        if (!$(event.target).closest($shareMenu).length) {
            // We're clicking outside of the menu, so hide it.
            $shareMenu.hide();
            $(document).off('click.mentoring_share_menu_hide');
        }
    }

    $shareButton.on('click', function (event) {
        if (!$shareMenu.is(':visible')){
            event.stopPropagation();
            $(document).on('click.mentoring_share_menu_hide', menuHider);
            $shareMenu.show();
        }
    });
    $doShareButton.on('click', updateShare);

    function postLoad(data) {
        loadTable(data);
        new ExportBase(runtime, element, initData);
    }

    $.ajax({
        type: "POST",
        url: tableLoadURL,
        data: JSON.stringify({'target_username': $displayDropdown.val()}),
        success: postLoad
    });

    $.ajax({
        type: "POST",
        url: sharedListLoadUrl,
        data: JSON.stringify({}),
        success: sharedRefresh
    });

    $displayDropdown.on('change', function () {
        if ($displayDropdown[0].selectedIndex !== 0) {
            $shareButton.prop('disabled', true);
            $element.find('.report-download-container').hide();
        } else {
            $shareButton.prop('disabled', false);
            $element.find('.report-download-container').show();
        }
        $.ajax({
            type: "POST",
            url: tableLoadURL,
            data: JSON.stringify({'target_username': $displayDropdown.val()}),
            success: loadTable
        })
    });

    function addShare() {
        var container = $newShareContainer.clone();
        container.find('.add-share-username').val('');
        container.insertAfter($element.find('.new-share-container').last());
        container.find('.add-share-field').on('click', addShare);
        var buttons = $element.find('.new-share-container .add-share-field');
        buttons.hide();
        buttons.last().show();
    }

    function deleteShare(event) {
        event.preventDefault();
        $.ajax({
            type: "POST",
            url: deleteShareUrl,
            data: JSON.stringify({'username': $(event.target).parent().prev()[0].innerHTML}),
            success: function () {
                $(event.target).parent().parent().remove();
                $errorHolder.html('');
            },
            error: errorMessage
        });
    }

    $closeNotification.on('click', function () {
        // Don't need server approval to hide it.
        $notification.hide();
        $.ajax({
            type: "POST",
            url: clearNotificationUrl,
            data: JSON.stringify({'usernames': $notification.data('shared')})
        })
    });

    $addShareField.on('click', addShare);
}
<commit_msg>innerHTML<commit_after>// Client side code for the Problem Builder Dashboard XBlock
// So far, this code is only used to generate a downloadable report.
function ExportBase(runtime, element, initData) {
    "use strict";

    var reportTemplate = initData.reportTemplate;

    var generateDataUriFromImageURL = function(imgURL) {
        // Given the URL to an image, IF the image has already been cached by the browser,
        // returns a data: URI with the contents of the image (image will be converted to PNG)

        // Expand relative urls and urls without an explicit protocol into absolute urls
        var a = document.createElement('a');
        a.href = imgURL;
        imgURL = a.href;

        // If the image is from another domain, just return its URL. We can't
        // create a data URL from cross-domain images:
        // https://html.spec.whatwg.org/multipage/scripting.html#dom-canvas-todataurl
        if (a.origin !== window.location.origin)
            return imgURL;

        var img = new Image();
        img.src = imgURL;
        if (!img.complete)
            return imgURL;

        // Create an in-memory canvas from which we can extract a data URL:
        var canvas = document.createElement("canvas");
        canvas.width = img.naturalWidth;
        canvas.height = img.naturalHeight;
        // Draw the image onto our temporary canvas:
        canvas.getContext('2d').drawImage(img, 0, 0);
        return canvas.toDataURL("image/png");
    };

    var unicodeStringToBase64 = function(str) {
        // Convert string to base64. A bit weird in order to support unicode, per
        // https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/btoa
        return window.btoa(unescape(encodeURIComponent(str)));
    };

    var downloadReport = function(ev) {
        // Download Report:
        // Change the URL to a data: URI before continuing with the click event.
        if ($(this).attr('href').charAt(0) == '#') {
            var $report = $(initData.reportContentSelector, element).clone();
            // Convert all images in $report to data URIs:
            $report.find('image').each(function() {
                var origURL = $(this).attr('xlink:href');
                $(this).attr('xlink:href', generateDataUriFromImageURL(origURL));
            });
            // Take the resulting HTML and put it into the template we have:

            var wrapperHTML = reportTemplate.replace('REPORT_GOES_HERE', $report.html());
            console.log($.parseHTML(wrapperHTML, document));
            console.log($.parseHTML(wrapperHTML, document)[9]);
            // console.log(typeof $.parseHTML(wrapperHTML, document)[9]);
            // console.log($report);
            // console.log(typeof $report);
            // console.log($report.context);
            // console.log(typeof $report.context);
            console.log($.parseHTML($.parseHTML(wrapperHTML, document)[9].innerHTML));
            var doc = new jsPDF();
            doc.fromHTML($.parseHTML($.parseHTML(wrapperHTML, document)[9].innerHTML).context, 15, 15, {
                'width': 170
            });

            doc.save('report.pdf');

            var dataURI = "data:text/html;base64," + unicodeStringToBase64(wrapperHTML);
            $(this).attr('href', dataURI);
        }
    };

    var $downloadLink = $('.report-download-link', element);
    $downloadLink.on('click', downloadReport);
}

function PBDashboardBlock(runtime, element, initData) {
    new ExportBase(runtime, element, initData);
}

function MentoringTableBlock(runtime, element, initData) {
    // Display an excerpt for long answers, with a "more" link to display the full text

    var $element = $(element),
        $shareButton = $element.find('.mentoring-share-button'),
        $doShareButton = $element.find('.do-share-button'),
        $shareMenu = $element.find('.mentoring-share-with'),
        $displayDropdown = $element.find('.mentoring-display-dropdown'),
        $errorHolder = $element.find('.share-errors'),
        $deleteShareButton = $element.find('.remove-share'),
        $newShareContainer = $($element.find('.new-share-container')[0]),
        $addShareField = $($element.find('.add-share-field')[0]),
        $notification = $($element.find('.share-notification')),
        $closeNotification = $($element.find('.notification-close')),
        tableLoadURL = runtime.handlerUrl(element, 'table_render'),
        deleteShareUrl = runtime.handlerUrl(element, 'remove_share'),
        sharedListLoadUrl = runtime.handlerUrl(element, 'get_shared_list'),
        clearNotificationUrl = runtime.handlerUrl(element, 'clear_notification'),
        shareResultsUrl = runtime.handlerUrl(element, 'share_results');

    function loadTable(data) {
        $element.find('.mentoring-table-target').html(data['content']);
        $('.answer-table', element).shorten({
            moreText: 'more',
            lessText: 'less',
            showChars: '500'
        });
    }

    function errorMessage(event) {
        $errorHolder.text(JSON.parse(event.responseText)['error'])
    }

    function sharedRefresh(data) {
        $element.find('.shared-with-container').html(data['content']);
        $deleteShareButton = $($deleteShareButton.selector);
        $deleteShareButton.on('click', deleteShare);
    }

    function postShareRefresh(data) {
        sharedRefresh(data);
        $element.find(".new-share-container").each(function(index, container) {
            if (index === 0) {
                var $container = $(container);
                $container.find('.add-share-username').val('');
                $container.find('.add-share-field').show();
                return;
            }
            $(container).remove()
        });
        $errorHolder.html('');
    }

    function postShare() {
        $.ajax({
            type: "POST",
            url: sharedListLoadUrl,
            data: JSON.stringify({}),
            success: postShareRefresh,
            error: errorMessage
        });
    }

    function updateShare() {
        var usernames = [];
        $element.find('.add-share-username').each(function(index, username) {
            usernames.push($(username).val())
        });
        $.ajax({
            type: "POST",
            url: shareResultsUrl,
            data: JSON.stringify({'usernames': usernames}),
            success: postShare,
            error: errorMessage
        });
    }

    function menuHider(event) {
        if (!$(event.target).closest($shareMenu).length) {
            // We're clicking outside of the menu, so hide it.
            $shareMenu.hide();
            $(document).off('click.mentoring_share_menu_hide');
        }
    }

    $shareButton.on('click', function (event) {
        if (!$shareMenu.is(':visible')){
            event.stopPropagation();
            $(document).on('click.mentoring_share_menu_hide', menuHider);
            $shareMenu.show();
        }
    });
    $doShareButton.on('click', updateShare);

    function postLoad(data) {
        loadTable(data);
        new ExportBase(runtime, element, initData);
    }

    $.ajax({
        type: "POST",
        url: tableLoadURL,
        data: JSON.stringify({'target_username': $displayDropdown.val()}),
        success: postLoad
    });

    $.ajax({
        type: "POST",
        url: sharedListLoadUrl,
        data: JSON.stringify({}),
        success: sharedRefresh
    });

    $displayDropdown.on('change', function () {
        if ($displayDropdown[0].selectedIndex !== 0) {
            $shareButton.prop('disabled', true);
            $element.find('.report-download-container').hide();
        } else {
            $shareButton.prop('disabled', false);
            $element.find('.report-download-container').show();
        }
        $.ajax({
            type: "POST",
            url: tableLoadURL,
            data: JSON.stringify({'target_username': $displayDropdown.val()}),
            success: loadTable
        })
    });

    function addShare() {
        var container = $newShareContainer.clone();
        container.find('.add-share-username').val('');
        container.insertAfter($element.find('.new-share-container').last());
        container.find('.add-share-field').on('click', addShare);
        var buttons = $element.find('.new-share-container .add-share-field');
        buttons.hide();
        buttons.last().show();
    }

    function deleteShare(event) {
        event.preventDefault();
        $.ajax({
            type: "POST",
            url: deleteShareUrl,
            data: JSON.stringify({'username': $(event.target).parent().prev()[0].innerHTML}),
            success: function () {
                $(event.target).parent().parent().remove();
                $errorHolder.html('');
            },
            error: errorMessage
        });
    }

    $closeNotification.on('click', function () {
        // Don't need server approval to hide it.
        $notification.hide();
        $.ajax({
            type: "POST",
            url: clearNotificationUrl,
            data: JSON.stringify({'usernames': $notification.data('shared')})
        })
    });

    $addShareField.on('click', addShare);
}
<|endoftext|>