<commit_before>import {Subject} from 'rxjs'
import {getLogger} from 'log'
import _ from 'lodash'

const log = getLogger('sepalMap')

export class SepalMap {
    constructor(google, googleMap) {
        log.debug('creating new SepalMap')
        this.google = google
        this.googleMap = googleMap

        this.cursor = new google.maps.Marker({
            clickable: false,
            draggable: false,
            icon: {
                path: `
                    M 1 -3 L 0 -2 L -1 -3 H -1 V -15 H 1 V -3 Z
                    M 3 -1 L 2 0 L 3 1 V 1 H 15 V -1 H 15 Z
                    M 1 3 L 0 2 L -1 3 H -1 V 15 H 1 Z
                    M -3 1 L -2 0 L -3 -1 V -1 H -15 V 1 Z
                    M -15 -15 H -7 V -13 H -13 V -7 H -15 V -7 Z
                    M 15 -15 H 7 V -13 H 13 V -7 H 15 V -12 Z
                    M 15 15 V 7 H 13 V 13 H 7 V 15 H 7 Z
                    M -15 15 H -7 V 13 H -13 V 7 H -15 V 7 Z
                `,
                fillColor: 'white',
                fillOpacity: 1,
                strokeColor: 'black',
                strokeOpacity: 1
            }
        })
        this.cursor.setMap(googleMap)

        this.crosshair = new google.maps.Marker({
            icon: {
                path: `
                    M 0 0 L 100 0
                    M 0 0 L 0 -100
                    M 0 0 L -100 0
                    M 0 0 L 0 100
                `,
                // fillColor: 'white',
                // fillOpacity: 1,
                strokeColor: 'white',
                strokeOpacity: 1
            }
        })
        this.crosshair.setMap(googleMap)

        this.overlay = new google.maps.OverlayView()
        this.overlay.draw = () => {}
        this.overlay.setMap(googleMap)

    }

    layerById = {}
    hiddenLayerById = {}
    removeLayer$ = new Subject()
    zoomArea$ = new Subject()

    drawingOptions = {
        fillColor: '#FBFAF2',
        fillOpacity: 0.07,
        strokeColor: '#c5b397',
        strokeOpacity: 1,
        strokeWeight: 2,
        clickable: false,
        editable: false,
        zIndex: 1
    }
    drawingManager = null
    drawingPolygon = null

    getGoogle() {
        return {
            google: this.google,
            googleMap: this.googleMap
        }
    }

    addListener(event, listener) {
        const {google, googleMap} = this
        const listenerId = googleMap.addListener(event, listener)
        return {
            remove: () => google.maps.event.removeListener(listenerId)
        }
    }

    addClickListener(listener) {
        const {google, googleMap} = this
        const listenerId = googleMap.addListener('click', ({latLng: {lat, lng}}) => listener({lat: lat(), lng: lng()}))
        googleMap.setOptions({draggableCursor: 'crosshair'})
        return {
            remove: () => {
                googleMap.setOptions({draggableCursor: 'pointer'})
                return google.maps.event.removeListener(listenerId)
            }
        }
    }

    disableDrawingMode() {
        const {google} = this
        if (this.drawingManager) {
            this.drawingManager.setMap(null)
            google.maps.event.clearListeners(this.drawingManager, 'overlaycomplete')
        }
    }

    // Cursor

    setCursor(latLng) {
        this.cursor.setPosition(latLng)
    }

    setcrosshair(latLng) {
        this.crosshair.setPosition(latLng)
    }

    latLngToPixel(latLng) {
        const projection = this.overlay.getProjection()
        return projection ? projection.fromLatLngToContainerPixel(latLng) : {}
    }

    // View

    getScale(center, zoom) {
        return 156543.03392 * Math.cos(center.lat * Math.PI / 180) / Math.pow(2, zoom)
    }

    getView() {
        const {googleMap} = this
        const center = this.getCenter()
        const zoom = this.getZoom()
        const scale = this.getScale(center, zoom)
        const minZoom = googleMap.minZoom
        const maxZoom = googleMap.maxZoom
        const isMinZoom = zoom === minZoom
        const isMaxZoom = zoom === maxZoom
        return {center, zoom, scale, minZoom, maxZoom, isMinZoom, isMaxZoom}
    }

    setView({center, zoom}) {
        this.setCenter(center)
        this.setZoom(zoom)
    }

    // Center

    toGoogleLocation(latLng) {
        const {google} = this
        return latLng instanceof google.maps.LatLng
            ? latLng
            : new google.maps.LatLng(latLng)
    }

    getCenter() {
        const {googleMap} = this
        const lngLatCenter = googleMap.getCenter()
        return {lat: lngLatCenter.lat(), lng: lngLatCenter.lng()}
    }

    setCenter(center) {
        const {googleMap} = this
        const lngLatCenter = this.toGoogleLocation(center)
        if (!googleMap.getCenter().equals(lngLatCenter)) {
            googleMap.setCenter(lngLatCenter)
        }
    }

    // Zooming

    getZoom() {
        const {googleMap} = this
        const zoom = googleMap.getZoom()
        const sanitizedZoom = Math.min(googleMap.maxZoom, Math.max(googleMap.minZoom, zoom))
        if (sanitizedZoom !== zoom) {
            log.debug(`getZoom: zoom adjusted to fall within range [${googleMap.minZoom} - ${googleMap.maxZoom}]: ${zoom} -> ${sanitizedZoom}`)
        }
        return sanitizedZoom
    }

    setZoom(zoom) {
        const {googleMap} = this
        const sanitizedZoom = Math.min(googleMap.maxZoom, Math.max(googleMap.minZoom, zoom))
        if (sanitizedZoom !== zoom) {
            log.debug(`setZoom: zoom adjusted to fall within range [${googleMap.minZoom} - ${googleMap.maxZoom}]: ${zoom} -> ${sanitizedZoom}`)
        }
        if (googleMap.getZoom() !== sanitizedZoom) {
            googleMap.setZoom(sanitizedZoom)
        }
    }

    zoomIn() {
        const {googleMap} = this
        this.setZoom(googleMap.getZoom() + 1)
    }

    zoomOut() {
        const {googleMap} = this
        this.setZoom(googleMap.getZoom() - 1)
    }

    enableZoomArea() {
        const {google, googleMap} = this
        this.drawingManager = new google.maps.drawing.DrawingManager({
            drawingMode: google.maps.drawing.OverlayType.RECTANGLE,
            drawingControl: false,
            rectangleOptions: this.drawingOptions
        })
        const zoomAreaComplete = e => {
            const rectangle = e.overlay
            rectangle.setMap(null)
            googleMap.fitBounds(rectangle.bounds)
            this.cancelZoomArea()
            this.zoomArea$.next()
        }
        google.maps.event.addListener(this.drawingManager, 'overlaycomplete', zoomAreaComplete)
        this.drawingManager.setMap(googleMap)
    }

    cancelZoomArea() {
        this.disableDrawingMode()
    }

    getZoomArea$() {
        return this.zoomArea$
    }

    // Bounds

    fromGoogleBounds(bounds) {
        const {google} = this
        if (bounds && bounds instanceof google.maps.LatLngBounds) {
            const sw = bounds.getSouthWest()
            const ne = bounds.getNorthEast()
            return [
                [sw.lng(), sw.lat()],
                [ne.lng(), ne.lat()]
            ]
        } else {
            return bounds
        }
    }

    toGoogleBounds(bounds) {
        const {google} = this
        if (bounds && bounds instanceof google.maps.LatLngBounds) {
            return bounds
        } else {
            return new google.maps.LatLngBounds(
                {lng: bounds[0][0], lat: bounds[0][1]},
                {lng: bounds[1][0], lat: bounds[1][1]}
            )
        }
    }

    fitBounds(bounds) {
        const PADDING = 2 // compensate for attribution masking
        const {googleMap} = this
        const nextBounds = this.toGoogleBounds(bounds)
        const currentBounds = googleMap.getBounds()
        const boundsChanged = !currentBounds || !currentBounds.equals(nextBounds)
        if (boundsChanged) {
            googleMap.fitBounds(nextBounds, PADDING)
        }
    }

    getBounds() {
        const {googleMap} = this
        return this.fromGoogleBounds(googleMap.getBounds())
    }

    // Markers

    setLocationMarker(options, onRemove) {
        const {google, googleMap} = this
        const marker = new google.maps.Marker({
            label: 'X',
            ...options
        })
        const remove = () => marker.setMap(null)
        marker.addListener('click', onRemove || remove)
        marker.setMap(googleMap)
        return {
            remove
        }
    }

    setAreaMarker(options, onRemove) {
        const {google, googleMap} = this
        const rectangle = new google.maps.Rectangle({
            ...this.drawingOptions,
            fillOpacity: 0,
            strokeOpacity: .5,
            ...options
        })
        const closeMarker = new google.maps.Marker({
            position: options.bounds.getNorthEast(),
            icon: {
                path: 'M12 2C6.47 2 2 6.47 2 12s4.47 10 10 10 10-4.47 10-10S17.53 2 12 2zm5 13.59L15.59 17 12 13.41 8.41 17 7 15.59 10.59 12 7 8.41 8.41 7 12 10.59 15.59 7 17 8.41 13.41 12 17 15.59z',
                fillColor: '#c5b397',
                fillOpacity: 1,
                anchor: new google.maps.Point(12, 12),
                scale: 1
            },
            title: options.title
        })
        const remove = () => {
            closeMarker.setMap(null)
            rectangle.setMap(null)
        }
        closeMarker.addListener('click', onRemove || remove)
        rectangle.setMap(googleMap)
        closeMarker.setMap(googleMap)
        return {
            remove
        }
    }

    // Polygon

    drawPolygon(id, callback) {
        this.drawingPolygon = {id, callback}
        this.redrawPolygon()
    }

    redrawPolygon() {
        const {google, googleMap} = this
        if (this.drawingPolygon) {
            this.disableDrawingMode()
            const {callback} = this.drawingPolygon
            this.drawingManager = new google.maps.drawing.DrawingManager({
                drawingMode: google.maps.drawing.OverlayType.POLYGON,
                drawingControl: false,
                drawingControlOptions: {
                    position: google.maps.ControlPosition.TOP_CENTER,
                    drawingModes: ['polygon']
                },
                circleOptions: this.drawingOptions,
                polygonOptions: this.drawingOptions,
                rectangleOptions: this.drawingOptions
            })
            const drawingListener = e => {
                const polygon = e.overlay
                polygon.setMap(null)
                const toPolygonPath = polygon => polygon.getPaths().getArray()[0].getArray().map(latLng =>
                    [latLng.lng(), latLng.lat()]
                )
                callback(toPolygonPath(polygon))
            }
            google.maps.event.addListener(this.drawingManager, 'overlaycomplete', drawingListener)
            this.drawingManager.setMap(googleMap)
        }
    }

    // Layers

    getLayer(id) {
        return this.layerById[id]
    }

    setLayer({id, layer}) {
        const existingLayer = this.getLayer(id)
        const unchanged = layer === existingLayer || (existingLayer && existingLayer.equals(layer))
        if (unchanged) {
            return false
        }
        this.removeLayer(id)
        if (layer) {
            this.layerById[id] = layer
            layer.initialize()
        }
        return true
    }

    removeLayer(id) {
        const layer = this.getLayer(id)
        if (layer) {
            layer.removeFromMap()
            delete this.layerById[id]
        }
    }

    removeAllLayers() {
        _.forEach(this.layerById, layer => this.removeLayer(layer))
    }

    hideLayer(id, hidden) {
        const layer = this.getLayer(id)
        this.hiddenLayerById[id] = hidden
        if (layer) {
            layer.hide(hidden)
        }
    }

    isHiddenLayer(id) {
        return this.hiddenLayerById[id]
    }

    isLayerInitialized(id) {
        const layer = this.getLayer(id)
        return !!(layer && layer.isInitialized())
    }

    toggleableLayers() {
        return _.orderBy(Object.values(this.layerById).filter(layer => layer.toggleable), ['layerIndex'])
    }

    setVisibility(visible) {
        log.debug(`Visibility ${visible ? 'on' : 'off'}`)
        _.forEach(this.layerById, (layer, id) =>
            layer.hide && layer.hide(visible ? this.isHiddenLayer(id) : true)
        )
    }

    interactive(enabled) {
        const {googleMap} = this
        googleMap.setOptions({gestureHandling: enabled ? 'greedy' : 'none'})
    }
}
<commit_msg>Refactoring.<commit_after>import {Subject} from 'rxjs'
import {getLogger} from 'log'
import _ from 'lodash'

const log = getLogger('sepalMap')

export class SepalMap {
    constructor(google, googleMap) {
        log.debug('creating new SepalMap')
        this.google = google
        this.googleMap = googleMap

        this.cursor = new google.maps.Marker({
            clickable: false,
            draggable: false,
            icon: {
                path: `
                    M 1 -3 L 0 -2 L -1 -3 H -1 V -15 H 1 V -3 Z
                    M 3 -1 L 2 0 L 3 1 V 1 H 15 V -1 H 15 Z
                    M 1 3 L 0 2 L -1 3 H -1 V 15 H 1 Z
                    M -3 1 L -2 0 L -3 -1 V -1 H -15 V 1 Z
                    M -15 -15 H -7 V -13 H -13 V -7 H -15 V -7 Z
                    M 15 -15 H 7 V -13 H 13 V -7 H 15 V -12 Z
                    M 15 15 V 7 H 13 V 13 H 7 V 15 H 7 Z
                    M -15 15 H -7 V 13 H -13 V 7 H -15 V 7 Z
                `,
                fillColor: 'white',
                fillOpacity: 1,
                strokeColor: 'black',
                strokeOpacity: 1
            }
        })
        this.cursor.setMap(googleMap)

        this.crosshair = new google.maps.Marker({
            icon: {
                path: `
                    M 0 0 L 100 0
                    M 0 0 L 0 -100
                    M 0 0 L -100 0
                    M 0 0 L 0 100
                `,
                // fillColor: 'white',
                // fillOpacity: 1,
                strokeColor: 'white',
                strokeOpacity: 1
            }
        })
        this.crosshair.setMap(googleMap)

        this.overlay = new google.maps.OverlayView()
        this.overlay.draw = () => {}
        this.overlay.setMap(googleMap)

    }

    layerById = {}
    hiddenLayerById = {}
    removeLayer$ = new Subject()
    zoomArea$ = new Subject()

    drawingOptions = {
        fillColor: '#FBFAF2',
        fillOpacity: 0.07,
        strokeColor: '#c5b397',
        strokeOpacity: 1,
        strokeWeight: 2,
        clickable: false,
        editable: false,
        zIndex: 1
    }
    drawingManager = null
    drawingPolygon = null

    getGoogle() {
        return {
            google: this.google,
            googleMap: this.googleMap
        }
    }

    addListener(event, listener) {
        const {google, googleMap} = this
        const listenerId = googleMap.addListener(event, listener)
        return {
            remove: () => google.maps.event.removeListener(listenerId)
        }
    }

    addClickListener(listener) {
        const {google, googleMap} = this
        const listenerId = googleMap.addListener('click', ({latLng: {lat, lng}}) => listener({lat: lat(), lng: lng()}))
        googleMap.setOptions({draggableCursor: 'crosshair'})
        return {
            remove: () => {
                googleMap.setOptions({draggableCursor: 'pointer'})
                return google.maps.event.removeListener(listenerId)
            }
        }
    }

    // Drawing mode

    enableDrawingMode(options, overlayCompleteCallback) {
        const {google, googleMap} = this
        this.disableDrawingMode()
        this.drawingManager = new google.maps.drawing.DrawingManager(options)
        google.maps.event.addListener(this.drawingManager, 'overlaycomplete', overlayCompleteCallback)
        this.drawingManager.setMap(googleMap)
    }

    disableDrawingMode() {
        const {google} = this
        if (this.drawingManager) {
            this.drawingManager.setMap(null)
            google.maps.event.clearListeners(this.drawingManager, 'overlaycomplete')
        }
    }

    // Cursor

    setCursor(latLng) {
        this.cursor.setPosition(latLng)
    }

    setcrosshair(latLng) {
        this.crosshair.setPosition(latLng)
    }

    latLngToPixel(latLng) {
        const projection = this.overlay.getProjection()
        return projection ? projection.fromLatLngToContainerPixel(latLng) : {}
    }

    // View

    getScale(center, zoom) {
        return 156543.03392 * Math.cos(center.lat * Math.PI / 180) / Math.pow(2, zoom)
    }

    getView() {
        const {googleMap} = this
        const center = this.getCenter()
        const zoom = this.getZoom()
        const scale = this.getScale(center, zoom)
        const minZoom = googleMap.minZoom
        const maxZoom = googleMap.maxZoom
        const isMinZoom = zoom === minZoom
        const isMaxZoom = zoom === maxZoom
        return {center, zoom, scale, minZoom, maxZoom, isMinZoom, isMaxZoom}
    }

    setView({center, zoom}) {
        this.setCenter(center)
        this.setZoom(zoom)
    }

    // Center

    toGoogleLocation(latLng) {
        const {google} = this
        return latLng instanceof google.maps.LatLng
            ? latLng
            : new google.maps.LatLng(latLng)
    }

    getCenter() {
        const {googleMap} = this
        const lngLatCenter = googleMap.getCenter()
        return {lat: lngLatCenter.lat(), lng: lngLatCenter.lng()}
    }

    setCenter(center) {
        const {googleMap} = this
        const lngLatCenter = this.toGoogleLocation(center)
        if (!googleMap.getCenter().equals(lngLatCenter)) {
            googleMap.setCenter(lngLatCenter)
        }
    }

    // Zooming

    getZoom() {
        const {googleMap} = this
        const zoom = googleMap.getZoom()
        const sanitizedZoom = Math.min(googleMap.maxZoom, Math.max(googleMap.minZoom, zoom))
        if (sanitizedZoom !== zoom) {
            log.debug(`getZoom: zoom adjusted to fall within range [${googleMap.minZoom} - ${googleMap.maxZoom}]: ${zoom} -> ${sanitizedZoom}`)
        }
        return sanitizedZoom
    }

    setZoom(zoom) {
        const {googleMap} = this
        const sanitizedZoom = Math.min(googleMap.maxZoom, Math.max(googleMap.minZoom, zoom))
        if (sanitizedZoom !== zoom) {
            log.debug(`setZoom: zoom adjusted to fall within range [${googleMap.minZoom} - ${googleMap.maxZoom}]: ${zoom} -> ${sanitizedZoom}`)
        }
        if (googleMap.getZoom() !== sanitizedZoom) {
            googleMap.setZoom(sanitizedZoom)
        }
    }

    zoomIn() {
        const {googleMap} = this
        this.setZoom(googleMap.getZoom() + 1)
    }

    zoomOut() {
        const {googleMap} = this
        this.setZoom(googleMap.getZoom() - 1)
    }

    enableZoomArea() {
        const {google, googleMap} = this
        this.enableDrawingMode({
            drawingMode: google.maps.drawing.OverlayType.RECTANGLE,
            drawingControl: false,
            rectangleOptions: this.drawingOptions
        }, ({overlay: rectangle}) => {
            rectangle.setMap(null)
            googleMap.fitBounds(rectangle.bounds)
            this.cancelZoomArea()
            this.zoomArea$.next()
        })
    }

    cancelZoomArea() {
        this.disableDrawingMode()
    }

    getZoomArea$() {
        return this.zoomArea$
    }

    // Bounds

    fromGoogleBounds(bounds) {
        const {google} = this
        if (bounds && bounds instanceof google.maps.LatLngBounds) {
            const sw = bounds.getSouthWest()
            const ne = bounds.getNorthEast()
            return [
                [sw.lng(), sw.lat()],
                [ne.lng(), ne.lat()]
            ]
        } else {
            return bounds
        }
    }

    toGoogleBounds(bounds) {
        const {google} = this
        if (bounds && bounds instanceof google.maps.LatLngBounds) {
            return bounds
        } else {
            return new google.maps.LatLngBounds(
                {lng: bounds[0][0], lat: bounds[0][1]},
                {lng: bounds[1][0], lat: bounds[1][1]}
            )
        }
    }

    fitBounds(bounds) {
        const PADDING = 2 // compensate for attribution masking
        const {googleMap} = this
        const nextBounds = this.toGoogleBounds(bounds)
        const currentBounds = googleMap.getBounds()
        const boundsChanged = !currentBounds || !currentBounds.equals(nextBounds)
        if (boundsChanged) {
            googleMap.fitBounds(nextBounds, PADDING)
        }
    }

    getBounds() {
        const {googleMap} = this
        return this.fromGoogleBounds(googleMap.getBounds())
    }

    // Markers

    setLocationMarker(options, onRemove) {
        const {google, googleMap} = this
        const marker = new google.maps.Marker({
            label: 'X',
            ...options
        })
        const remove = () => marker.setMap(null)
        marker.addListener('click', onRemove || remove)
        marker.setMap(googleMap)
        return {
            remove
        }
    }

    setAreaMarker(options, onRemove) {
        const {google, googleMap} = this
        const rectangle = new google.maps.Rectangle({
            ...this.drawingOptions,
            fillOpacity: 0,
            strokeOpacity: .5,
            ...options
        })
        const closeMarker = new google.maps.Marker({
            position: options.bounds.getNorthEast(),
            icon: {
                path: 'M12 2C6.47 2 2 6.47 2 12s4.47 10 10 10 10-4.47 10-10S17.53 2 12 2zm5 13.59L15.59 17 12 13.41 8.41 17 7 15.59 10.59 12 7 8.41 8.41 7 12 10.59 15.59 7 17 8.41 13.41 12 17 15.59z',
                fillColor: '#c5b397',
                fillOpacity: 1,
                anchor: new google.maps.Point(12, 12),
                scale: 1
            },
            title: options.title
        })
        const remove = () => {
            closeMarker.setMap(null)
            rectangle.setMap(null)
        }
        closeMarker.addListener('click', onRemove || remove)
        rectangle.setMap(googleMap)
        closeMarker.setMap(googleMap)
        return {
            remove
        }
    }

    // Polygon

    drawPolygon(id, callback) {
        this.drawingPolygon = {id, callback}
        this.redrawPolygon()
    }

    redrawPolygon() {
        const {google} = this
        if (this.drawingPolygon) {
            const {callback} = this.drawingPolygon
            this.enableDrawingMode({
                drawingMode: google.maps.drawing.OverlayType.POLYGON,
                drawingControl: false,
                drawingControlOptions: {
                    position: google.maps.ControlPosition.TOP_CENTER,
                    drawingModes: ['polygon']
                },
                circleOptions: this.drawingOptions,
                polygonOptions: this.drawingOptions,
                rectangleOptions: this.drawingOptions
            }, ({overlay: polygon}) => {
                polygon.setMap(null)
                const toPolygonPath = polygon => polygon.getPaths().getArray()[0].getArray().map(latLng =>
                    [latLng.lng(), latLng.lat()]
                )
                callback(toPolygonPath(polygon))
            })
        }
    }

    // Layers

    getLayer(id) {
        return this.layerById[id]
    }

    setLayer({id, layer}) {
        const existingLayer = this.getLayer(id)
        const unchanged = layer === existingLayer || (existingLayer && existingLayer.equals(layer))
        if (unchanged) {
            return false
        }
        this.removeLayer(id)
        if (layer) {
            this.layerById[id] = layer
            layer.initialize()
        }
        return true
    }

    removeLayer(id) {
        const layer = this.getLayer(id)
        if (layer) {
            layer.removeFromMap()
            delete this.layerById[id]
        }
    }

    removeAllLayers() {
        _.forEach(this.layerById, layer => this.removeLayer(layer))
    }

    hideLayer(id, hidden) {
        const layer = this.getLayer(id)
        this.hiddenLayerById[id] = hidden
        if (layer) {
            layer.hide(hidden)
        }
    }

    isHiddenLayer(id) {
        return this.hiddenLayerById[id]
    }

    isLayerInitialized(id) {
        const layer = this.getLayer(id)
        return !!(layer && layer.isInitialized())
    }

    toggleableLayers() {
        return _.orderBy(Object.values(this.layerById).filter(layer => layer.toggleable), ['layerIndex'])
    }

    setVisibility(visible) {
        log.debug(`Visibility ${visible ? 'on' : 'off'}`)
        _.forEach(this.layerById, (layer, id) =>
            layer.hide && layer.hide(visible ? this.isHiddenLayer(id) : true)
        )
    }

    interactive(enabled) {
        const {googleMap} = this
        googleMap.setOptions({gestureHandling: enabled ? 'greedy' : 'none'})
    }
}
<|endoftext|>


<commit_before>openerp.base.data_export = function(openerp) {
openerp.base.DataExport = openerp.base.Dialog.extend({
    template: 'ExportTreeView',
    dialog_title: 'Export Data',
    init: function(parent, dataset) {
        this._super(parent);
        this.records = {};
        this.dataset = dataset;
        this.exports = new openerp.base.DataSetSearch(
            this, 'ir.exports', this.dataset.get_context());
    },
    start: function() {
        var self = this;
        this._super.apply(this, arguments);
        this.open({
            modal: true,
            width: '55%',
            height: 'auto',
            position: 'top',
            buttons : {
                "Close" : function() {
                    self.close();
                  },
                "Export To File" : function() {
                    self.on_click_export_data();
                  }
               },
            close: function(event, ui){ self.close();}
        });
        self.$element.removeClass('ui-dialog-content ui-widget-content');
        self.$element.find('#add_field').click(function() {
            if ($('#field-tree-structure tr.ui-selected')) {
                var fld = self.$element.find('#field-tree-structure tr.ui-selected').find('a');
                for (var i = 0; i < fld.length; i++) {
                    var id = $(fld[i]).attr('id').split('-')[1];
                    var string = $(fld[i]).attr('string');
                    self.add_field(id, string);
                }
                self.$element.find('#field-tree-structure tr').removeClass('ui-selected');
            }
        });
        self.$element.find('#remove_field').click(function() {
            self.$element.find('#fields_list option:selected').remove();
        });
        self.$element.find('#remove_all_field').click(function() {
            self.$element.find('#fields_list').empty();
        });
        this.$element.find('#export_new_list').click(this.on_show_save_list);

        var got_fields = new $.Deferred();
        this.$element.find('#import_compat').change(function() {
            self.$element.find('#fields_list').empty();
            self.$element.find('#field-tree-structure').remove();
            var import_comp = self.$element.find("#import_compat").val();
            self.rpc("/base/export/get_fields", {
                model: self.dataset.model,
                import_compat: Boolean(import_comp)
            }, function (records) {
                got_fields.resolve();
                self.on_show_data(records);
            });
        }).change();

        return $.when(
            got_fields,
            this.rpc('/base/export/formats', {}, this.do_setup_export_formats),
            this.show_exports_list());
    },
    do_setup_export_formats: function (formats) {
        var $fmts = this.$element.find('#export_format');
        _(formats).each(function (format) {
            $fmts.append(new Option(format[1], format[0]));
        });
    },
    show_exports_list: function() {
        var self = this;
        if (self.$element.find('#saved_export_list').is(':hidden')) {
            self.$element.find('#ExistsExportList').show();
            return;
        }
        return this.exports.read_slice(['name'], {
            domain: [['resource', '=', this.dataset.model]]
        }, function (export_list) {
            if (!export_list.length) {
                return;
            }
            self.$element.find('#ExistsExportList').append(QWeb.render('Exists.ExportList', {'existing_exports': export_list}));
            self.$element.find('#saved_export_list').change(function() {
                self.$element.find('#fields_list option').remove();
                var export_id = self.$element.find('#saved_export_list option:selected').val();
                if (export_id) {
                    self.rpc('/base/export/namelist', {'model': self.dataset.model, export_id: parseInt(export_id)}, self.do_load_export_field);
                }
            });
            self.$element.find('#delete_export_list').click(function() {
                var select_exp = self.$element.find('#saved_export_list option:selected');
                if (select_exp.val()) {
                    self.exports.unlink([parseInt(select_exp.val(), 10)]);
                    select_exp.remove();
                    if (self.$element.find('#saved_export_list option').length <= 1) {
                        self.$element.find('#ExistsExportList').hide();
                    }
                }
            });
        });
    },
    do_load_export_field: function(field_list) {
        var export_node = this.$element.find("#fields_list");
        for (var key in field_list) {
            export_node.append(new Option(field_list[key], key));
        }
    },
    on_show_save_list: function() {
        var self = this;
        var current_node = self.$element.find("#savenewlist");
        if (!(current_node.find("label")).length) {
            current_node.append(QWeb.render('ExportNewList'));
            current_node.find("#add_export_list").click(function() {
                var value = current_node.find("#savelist_name").val();
                if (value) {
                    self.do_save_export_list(value);
                } else {
                    alert("Pleae Enter Save Field List Name");
                }
            });
        } else {
            if (current_node.is(':hidden')) {
                current_node.show();
                current_node.find("#savelist_name").val("");
            } else {
               current_node.hide();
            }
        }
    },
    do_save_export_list: function(value) {
        var self = this;
        var fields = self.get_fields();
        if (!fields.length) {
            return;
        }
        this.exports.create({
            name: value,
            resource: this.dataset.model,
            export_fields: _(fields).map(function (field) {
                return [0, 0, {name: field}];
            })
        }, function (export_list_id) {
            if (!export_list_id) {
                return;
            }
            if (self.$element.find("#saved_export_list").length > 0) {
                self.$element.find("#saved_export_list").append(
                        new Option(value, export_list_id));
            } else {
                self.show_exports_list();
            }
            if (self.$element.find("#saved_export_list").is(":hidden")) {
                self.show_exports_list();
            }
        });
        this.on_show_save_list();
        this.$element.find("#fields_list option").remove();
    },
    on_click: function(id, record) {
        var self = this;
        if (!record['children']) {
            return;
        }
        var model = record['params']['model'],
            prefix = record['params']['prefix'],
            name = record['params']['name'];

        if (!record.loaded) {
            var import_comp = self.$element.find("#import_compat").val();
            self.rpc("/base/export/get_fields", {
                model: model,
                prefix: prefix,
                parent_name: name,
                import_compat: Boolean(import_comp),
                parent_field_type : record['field_type']
            }, function(results) {
                record.loaded = true;
                self.on_show_data(results, record.id);
            });
        } else {
            self.showcontent(record.id);
        }
    },
    on_show_data: function(result, after) {
        var self = this;
        var imp_cmpt = Boolean(self.$element.find("#import_compat").val());

        if (after) {
            var current_tr = self.$element.find("tr[id='treerow-" + after + "']");
            current_tr.addClass('open');
            current_tr.find('img').attr('src','/base/static/src/img/collapse.gif');
            current_tr.after(QWeb.render('ExportTreeView-Secondary.children', {'fields': result}));
        } else {
            self.$element.find('#left_field_panel').append(QWeb.render('ExportTreeView-Secondary', {'fields': result}));
        }
        _.each(result, function(record) {
            self.records[record.id] = record.value;
            if (record.required) {
                var required_fld = self.$element.find("tr[id='treerow-" + record.id + "']").find('#tree-column');
                required_fld.addClass("oe_export_requiredfield");
            }
            self.$element.find("img[id='parentimg-" + record.id +"']").click(function() {
                self.on_click(this.id, record);
            });

            self.$element.find("tr[id='treerow-" + record.id + "']").click(function(e) {
                if (e.shiftKey) {
                    var frst_click, scnd_click = '';
                    if (self.row_index == 0) {
                        self.row_index = this.rowIndex;
                        frst_click = self.$element.find("tr[id^='treerow-']")[self.row_index-1];
                        $(frst_click).addClass("ui-selected");
                    } else {
                        if (this.rowIndex >=self.row_index) {
                            for (var i = (self.row_index-1); i < this.rowIndex; i++) {
                                scnd_click = self.$element.find("tr[id^='treerow-']")[i];
                                if (!$(scnd_click).find('#tree-column').hasClass("oe_export_readonlyfield")) {
                                    $(scnd_click).addClass("ui-selected");
                                }
                            }
                        } else {
                            for (var i = (self.row_index-1); i >= (this.rowIndex-1); i--) {
                                scnd_click = self.$element.find("tr[id^='treerow-']")[i];
                                if (!$(scnd_click).find('#tree-column').hasClass("oe_export_readonlyfield")) {
                                    $(scnd_click).addClass("ui-selected");
                                }
                            }
                        }
                    }
                }
                self.row_index = this.rowIndex;

                self.$element.find("tr[id='treerow-" + record.id + "']").keyup(function() {
                    self.row_index = 0;
                });
                var o2m_selection = self.$element.find("tr[id='treerow-" + record.id + "']").find('#tree-column');
                if ($(o2m_selection).hasClass("oe_export_readonlyfield")) {
                    return false;
                }
                if (e.ctrlKey) {
                    if ($(this).hasClass('ui-selected')) {
                        $(this).removeClass('ui-selected').find('a').blur();
                    } else {
                        $(this).addClass('ui-selected').find('a').focus();
                    }
                } else if (!e.shiftKey) {
                    self.$element.find("tr.ui-selected")
                            .removeClass("ui-selected").find('a').blur();
                    $(this).addClass("ui-selected").find('a').focus();
                }
                return false;
            });

            self.$element.find("tr[id='treerow-" + record.id + "']").keydown(function(e) {
                var keyCode = e.keyCode || e.which;
                var arrow = {left: 37, up: 38, right: 39, down: 40 };
                switch (keyCode) {
                    case arrow.left:
                        if ($(this).hasClass('open')) {
                            self.on_click(this.id, record);
                        }
                        break;
                    case arrow.right:
                        if (!$(this).hasClass('open')) {
                            self.on_click(this.id, record);
                        }
                        break;
                    case arrow.up:
                        var elem = this;
                        $(elem).removeClass("ui-selected");
                        while (!$(elem).prev().is(":visible")) {
                            elem = $(elem).prev();
                        }
                        if (!$(elem).prev().find('#tree-column').hasClass("oe_export_readonlyfield")) {
                            $(elem).prev().addClass("ui-selected");
                        }
                        $(elem).prev().find('a').focus();
                        break;
                    case arrow.down:
                        var elem = this;
                        $(elem).removeClass("ui-selected");
                        while(!$(elem).next().is(":visible")) {
                            elem = $(elem).next();
                        }
                        if (!$(elem).next().find('#tree-column').hasClass("oe_export_readonlyfield")) {
                            $(elem).next().addClass("ui-selected");
                        }
                        $(elem).next().find('a').focus();
                        break;
                }
            });
            self.$element.find("tr[id='treerow-" + record.id + "']").dblclick(function() {
                var $o2m_selection = self.$element.find("tr[id^='treerow-" + record.id + "']").find('#tree-column');
                if (!$o2m_selection.hasClass("oe_export_readonlyfield")) {
                   self.add_field(record.id, $(this).find("a").attr("string"));
                }
            });
        });
        self.$element.find('#fields_list').mouseover(function(event) {
            if (event.relatedTarget) {
                if (event.relatedTarget.attributes['id'] && event.relatedTarget.attributes['string']) {
                    var field_id = event.relatedTarget.attributes["id"]["value"];
                    if (field_id && field_id.split("-")[0] === 'export') {
                        if (!self.$element.find("tr[id='treerow-" + field_id.split("-")[1] + "']").find('#tree-column').hasClass("oe_export_readonlyfield")) {
                            self.add_field(field_id.split("-")[1], event.relatedTarget.attributes["string"]["value"]);
                        }
                    }
                }
            }
        });
    },
    showcontent: function(id) {
        // show & hide the contents
        var $this = this.$element.find("tr[id='treerow-" + id + "']");
        var is_open = $this.hasClass('open');
        $this.toggleClass('open');

        var first_child = $this.find('img');
        if (is_open) {
            first_child.attr('src', '/base/static/src/img/expand.gif');
        } else {
            first_child.attr('src', '/base/static/src/img/collapse.gif');
        }
        var child_field = this.$element.find("tr[id^='treerow-" + id +"/']");
        var child_len = (id.split("/")).length + 1;
        for (var i = 0; i < child_field.length; i++) {
            var $child = $(child_field[i]);
            if (is_open) {
                $child.hide();
            } else if (child_len == (child_field[i].id.split("/")).length) {
                if ($child.hasClass('open')) {
                    $child.removeClass('open');
                    $child.find('img').attr('src', '/base/static/src/img/expand.gif');
                }
                $child.show();
            }
        }
    },
    add_field: function(field_id, string) {
        var field_list = this.$element.find('#fields_list');
        if (this.$element.find("#fields_list option[value='" + field_id + "']")
                && !this.$element.find("#fields_list option[value='" + field_id + "']").length) {
            field_list.append(new Option(string, field_id));
        }
    },
    get_fields: function() {
        var export_field = [];
        this.$element.find("#fields_list option").each(function() {
            export_field.push($(this).val());
        });
        if (!export_field.length) {
            alert('Please select fields to save export list...');
        }
        return export_field;
    },
    on_click_export_data: function() {
        $.blockUI(this.$element);
        var exported_fields = [], self = this;
        this.$element.find("#fields_list option").each(function() {
            var fieldname = self.records[$(this).val()];
            exported_fields.push({name: fieldname, label: $(this).text()});
        });
        if (_.isEmpty(exported_fields)) {
            alert('Please select fields to export...');
            return;
        }

        var export_format = this.$element.find("#export_format").val();
        this.session.get_file({
            url: '/base/export/' + export_format,
            data: {data: JSON.stringify({
                model: this.dataset.model,
                fields: exported_fields,
                ids: this.dataset.ids,
                domain: this.dataset.domain,
                import_compat: Boolean(
                        this.$element.find("#import_compat").val())
            })},
            complete: $.unblockUI
        });
    },
    close: function() {
        $(this.$dialog).remove();
        this._super();
    }
});

};
<commit_msg>[IMP] always export the current record's external ID as first field<commit_after>openerp.base.data_export = function(openerp) {
openerp.base.DataExport = openerp.base.Dialog.extend({
    template: 'ExportTreeView',
    dialog_title: 'Export Data',
    init: function(parent, dataset) {
        this._super(parent);
        this.records = {};
        this.dataset = dataset;
        this.exports = new openerp.base.DataSetSearch(
            this, 'ir.exports', this.dataset.get_context());
    },
    start: function() {
        var self = this;
        this._super.apply(this, arguments);
        this.open({
            modal: true,
            width: '55%',
            height: 'auto',
            position: 'top',
            buttons : {
                "Close" : function() {
                    self.close();
                  },
                "Export To File" : function() {
                    self.on_click_export_data();
                  }
               },
            close: function(event, ui){ self.close();}
        });
        self.$element.removeClass('ui-dialog-content ui-widget-content');
        self.$element.find('#add_field').click(function() {
            if ($('#field-tree-structure tr.ui-selected')) {
                var fld = self.$element.find('#field-tree-structure tr.ui-selected').find('a');
                for (var i = 0; i < fld.length; i++) {
                    var id = $(fld[i]).attr('id').split('-')[1];
                    var string = $(fld[i]).attr('string');
                    self.add_field(id, string);
                }
                self.$element.find('#field-tree-structure tr').removeClass('ui-selected');
            }
        });
        self.$element.find('#remove_field').click(function() {
            self.$element.find('#fields_list option:selected').remove();
        });
        self.$element.find('#remove_all_field').click(function() {
            self.$element.find('#fields_list').empty();
        });
        this.$element.find('#export_new_list').click(this.on_show_save_list);

        var got_fields = new $.Deferred();
        this.$element.find('#import_compat').change(function() {
            self.$element.find('#fields_list').empty();
            self.$element.find('#field-tree-structure').remove();
            var import_comp = self.$element.find("#import_compat").val();
            self.rpc("/base/export/get_fields", {
                model: self.dataset.model,
                import_compat: Boolean(import_comp)
            }, function (records) {
                got_fields.resolve();
                self.on_show_data(records);
            });
        }).change();

        return $.when(
            got_fields,
            this.rpc('/base/export/formats', {}, this.do_setup_export_formats),
            this.show_exports_list());
    },
    do_setup_export_formats: function (formats) {
        var $fmts = this.$element.find('#export_format');
        _(formats).each(function (format) {
            $fmts.append(new Option(format[1], format[0]));
        });
    },
    show_exports_list: function() {
        var self = this;
        if (self.$element.find('#saved_export_list').is(':hidden')) {
            self.$element.find('#ExistsExportList').show();
            return;
        }
        return this.exports.read_slice(['name'], {
            domain: [['resource', '=', this.dataset.model]]
        }, function (export_list) {
            if (!export_list.length) {
                return;
            }
            self.$element.find('#ExistsExportList').append(QWeb.render('Exists.ExportList', {'existing_exports': export_list}));
            self.$element.find('#saved_export_list').change(function() {
                self.$element.find('#fields_list option').remove();
                var export_id = self.$element.find('#saved_export_list option:selected').val();
                if (export_id) {
                    self.rpc('/base/export/namelist', {'model': self.dataset.model, export_id: parseInt(export_id)}, self.do_load_export_field);
                }
            });
            self.$element.find('#delete_export_list').click(function() {
                var select_exp = self.$element.find('#saved_export_list option:selected');
                if (select_exp.val()) {
                    self.exports.unlink([parseInt(select_exp.val(), 10)]);
                    select_exp.remove();
                    if (self.$element.find('#saved_export_list option').length <= 1) {
                        self.$element.find('#ExistsExportList').hide();
                    }
                }
            });
        });
    },
    do_load_export_field: function(field_list) {
        var export_node = this.$element.find("#fields_list");
        for (var key in field_list) {
            export_node.append(new Option(field_list[key], key));
        }
    },
    on_show_save_list: function() {
        var self = this;
        var current_node = self.$element.find("#savenewlist");
        if (!(current_node.find("label")).length) {
            current_node.append(QWeb.render('ExportNewList'));
            current_node.find("#add_export_list").click(function() {
                var value = current_node.find("#savelist_name").val();
                if (value) {
                    self.do_save_export_list(value);
                } else {
                    alert("Pleae Enter Save Field List Name");
                }
            });
        } else {
            if (current_node.is(':hidden')) {
                current_node.show();
                current_node.find("#savelist_name").val("");
            } else {
               current_node.hide();
            }
        }
    },
    do_save_export_list: function(value) {
        var self = this;
        var fields = self.get_fields();
        if (!fields.length) {
            return;
        }
        this.exports.create({
            name: value,
            resource: this.dataset.model,
            export_fields: _(fields).map(function (field) {
                return [0, 0, {name: field}];
            })
        }, function (export_list_id) {
            if (!export_list_id) {
                return;
            }
            if (self.$element.find("#saved_export_list").length > 0) {
                self.$element.find("#saved_export_list").append(
                        new Option(value, export_list_id));
            } else {
                self.show_exports_list();
            }
            if (self.$element.find("#saved_export_list").is(":hidden")) {
                self.show_exports_list();
            }
        });
        this.on_show_save_list();
        this.$element.find("#fields_list option").remove();
    },
    on_click: function(id, record) {
        var self = this;
        if (!record['children']) {
            return;
        }
        var model = record['params']['model'],
            prefix = record['params']['prefix'],
            name = record['params']['name'];

        if (!record.loaded) {
            var import_comp = self.$element.find("#import_compat").val();
            self.rpc("/base/export/get_fields", {
                model: model,
                prefix: prefix,
                parent_name: name,
                import_compat: Boolean(import_comp),
                parent_field_type : record['field_type']
            }, function(results) {
                record.loaded = true;
                self.on_show_data(results, record.id);
            });
        } else {
            self.showcontent(record.id);
        }
    },
    on_show_data: function(result, after) {
        var self = this;
        var imp_cmpt = Boolean(self.$element.find("#import_compat").val());

        if (after) {
            var current_tr = self.$element.find("tr[id='treerow-" + after + "']");
            current_tr.addClass('open');
            current_tr.find('img').attr('src','/base/static/src/img/collapse.gif');
            current_tr.after(QWeb.render('ExportTreeView-Secondary.children', {'fields': result}));
        } else {
            self.$element.find('#left_field_panel').append(QWeb.render('ExportTreeView-Secondary', {'fields': result}));
        }
        _.each(result, function(record) {
            self.records[record.id] = record.value;
            if (record.required) {
                var required_fld = self.$element.find("tr[id='treerow-" + record.id + "']").find('#tree-column');
                required_fld.addClass("oe_export_requiredfield");
            }
            self.$element.find("img[id='parentimg-" + record.id +"']").click(function() {
                self.on_click(this.id, record);
            });

            self.$element.find("tr[id='treerow-" + record.id + "']").click(function(e) {
                if (e.shiftKey) {
                    var frst_click, scnd_click = '';
                    if (self.row_index == 0) {
                        self.row_index = this.rowIndex;
                        frst_click = self.$element.find("tr[id^='treerow-']")[self.row_index-1];
                        $(frst_click).addClass("ui-selected");
                    } else {
                        if (this.rowIndex >=self.row_index) {
                            for (var i = (self.row_index-1); i < this.rowIndex; i++) {
                                scnd_click = self.$element.find("tr[id^='treerow-']")[i];
                                if (!$(scnd_click).find('#tree-column').hasClass("oe_export_readonlyfield")) {
                                    $(scnd_click).addClass("ui-selected");
                                }
                            }
                        } else {
                            for (var i = (self.row_index-1); i >= (this.rowIndex-1); i--) {
                                scnd_click = self.$element.find("tr[id^='treerow-']")[i];
                                if (!$(scnd_click).find('#tree-column').hasClass("oe_export_readonlyfield")) {
                                    $(scnd_click).addClass("ui-selected");
                                }
                            }
                        }
                    }
                }
                self.row_index = this.rowIndex;

                self.$element.find("tr[id='treerow-" + record.id + "']").keyup(function() {
                    self.row_index = 0;
                });
                var o2m_selection = self.$element.find("tr[id='treerow-" + record.id + "']").find('#tree-column');
                if ($(o2m_selection).hasClass("oe_export_readonlyfield")) {
                    return false;
                }
                if (e.ctrlKey) {
                    if ($(this).hasClass('ui-selected')) {
                        $(this).removeClass('ui-selected').find('a').blur();
                    } else {
                        $(this).addClass('ui-selected').find('a').focus();
                    }
                } else if (!e.shiftKey) {
                    self.$element.find("tr.ui-selected")
                            .removeClass("ui-selected").find('a').blur();
                    $(this).addClass("ui-selected").find('a').focus();
                }
                return false;
            });

            self.$element.find("tr[id='treerow-" + record.id + "']").keydown(function(e) {
                var keyCode = e.keyCode || e.which;
                var arrow = {left: 37, up: 38, right: 39, down: 40 };
                switch (keyCode) {
                    case arrow.left:
                        if ($(this).hasClass('open')) {
                            self.on_click(this.id, record);
                        }
                        break;
                    case arrow.right:
                        if (!$(this).hasClass('open')) {
                            self.on_click(this.id, record);
                        }
                        break;
                    case arrow.up:
                        var elem = this;
                        $(elem).removeClass("ui-selected");
                        while (!$(elem).prev().is(":visible")) {
                            elem = $(elem).prev();
                        }
                        if (!$(elem).prev().find('#tree-column').hasClass("oe_export_readonlyfield")) {
                            $(elem).prev().addClass("ui-selected");
                        }
                        $(elem).prev().find('a').focus();
                        break;
                    case arrow.down:
                        var elem = this;
                        $(elem).removeClass("ui-selected");
                        while(!$(elem).next().is(":visible")) {
                            elem = $(elem).next();
                        }
                        if (!$(elem).next().find('#tree-column').hasClass("oe_export_readonlyfield")) {
                            $(elem).next().addClass("ui-selected");
                        }
                        $(elem).next().find('a').focus();
                        break;
                }
            });
            self.$element.find("tr[id='treerow-" + record.id + "']").dblclick(function() {
                var $o2m_selection = self.$element.find("tr[id^='treerow-" + record.id + "']").find('#tree-column');
                if (!$o2m_selection.hasClass("oe_export_readonlyfield")) {
                   self.add_field(record.id, $(this).find("a").attr("string"));
                }
            });
        });
        self.$element.find('#fields_list').mouseover(function(event) {
            if (event.relatedTarget) {
                if (event.relatedTarget.attributes['id'] && event.relatedTarget.attributes['string']) {
                    var field_id = event.relatedTarget.attributes["id"]["value"];
                    if (field_id && field_id.split("-")[0] === 'export') {
                        if (!self.$element.find("tr[id='treerow-" + field_id.split("-")[1] + "']").find('#tree-column').hasClass("oe_export_readonlyfield")) {
                            self.add_field(field_id.split("-")[1], event.relatedTarget.attributes["string"]["value"]);
                        }
                    }
                }
            }
        });
    },
    showcontent: function(id) {
        // show & hide the contents
        var $this = this.$element.find("tr[id='treerow-" + id + "']");
        var is_open = $this.hasClass('open');
        $this.toggleClass('open');

        var first_child = $this.find('img');
        if (is_open) {
            first_child.attr('src', '/base/static/src/img/expand.gif');
        } else {
            first_child.attr('src', '/base/static/src/img/collapse.gif');
        }
        var child_field = this.$element.find("tr[id^='treerow-" + id +"/']");
        var child_len = (id.split("/")).length + 1;
        for (var i = 0; i < child_field.length; i++) {
            var $child = $(child_field[i]);
            if (is_open) {
                $child.hide();
            } else if (child_len == (child_field[i].id.split("/")).length) {
                if ($child.hasClass('open')) {
                    $child.removeClass('open');
                    $child.find('img').attr('src', '/base/static/src/img/expand.gif');
                }
                $child.show();
            }
        }
    },
    add_field: function(field_id, string) {
        var field_list = this.$element.find('#fields_list');
        if (this.$element.find("#fields_list option[value='" + field_id + "']")
                && !this.$element.find("#fields_list option[value='" + field_id + "']").length) {
            field_list.append(new Option(string, field_id));
        }
    },
    get_fields: function() {
        var export_field = [];
        this.$element.find("#fields_list option").each(function() {
            export_field.push($(this).val());
        });
        if (!export_field.length) {
            alert('Please select fields to save export list...');
        }
        return export_field;
    },
    on_click_export_data: function() {
        $.blockUI(this.$element);
        var exported_fields = [], self = this;
        this.$element.find("#fields_list option").each(function() {
            var fieldname = self.records[$(this).val()];
            exported_fields.push({name: fieldname, label: $(this).text()});
        });
        if (_.isEmpty(exported_fields)) {
            alert('Please select fields to export...');
            return;
        }

        exported_fields.unshift({name: 'id', label: 'External ID'});
        var export_format = this.$element.find("#export_format").val();
        this.session.get_file({
            url: '/base/export/' + export_format,
            data: {data: JSON.stringify({
                model: this.dataset.model,
                fields: exported_fields,
                ids: this.dataset.ids,
                domain: this.dataset.domain,
                import_compat: Boolean(
                        this.$element.find("#import_compat").val())
            })},
            complete: $.unblockUI
        });
    },
    close: function() {
        $(this.$dialog).remove();
        this._super();
    }
});

};
<|endoftext|>


<commit_before>/**
 * Created by marwen on 28/12/15.
 */

var cccBlog = angular.module('cccBlog');

cccBlog.service('UtilService',['$rootScope', '$cookieStore', 'Notification',
    function($rootScope, $cookieStore, Notification)
    {
        var self = this;

        self.fireInfoEvent = function(msg){
            var message = {type: 'info', 'msg':msg};
            $rootScope.$emit('NotificationEvent', message);
        };

        self.fireErrorEvent = function(msg){
            var message = {type: 'error', 'msg':msg};
            $rootScope.$emit('NotificationEvent', message);
        };

        self.notifyInfo = function(msg){
            Notification.primary(msg);
        };

        self.notifyError = function(msg){
            Notification.error(msg);
        };

    }
]);

cccBlog.service('UserService',['$http', '$state', '$cookieStore',
    function($http, $state, $cookieStore) {

        var self = this;

        self.setUser = function(aUser){
            $cookieStore.put('authenticatedUser', aUser);
        };

        self.getUser = function(){
            return $cookieStore.get('authenticatedUser');
        };

        self.isUserLoggedIn = function() {
            return $cookieStore.get('authenticatedUser') != null;
        };

        self.logout = function() {
            $cookieStore.remove('authenticatedUser');
            $http.post('logout')
                .success(function(response){
                    console.log('Successfully logged out on server');
                });
        };
    }
]);

cccBlog.service('PostsService',['$resource', '$http',
    function($resource, $http){

        this.loadPosts = function(page, pageSize){
            var promise = $http.get('api/posts/?pageNo='+page+'&pageSize='+pageSize)
                .then(function(response){
                    return response.data;
                });
            return promise;
        };

    }
]);
<commit_msg>refactor(CCC-blog)  delete old service file--MS<commit_after><|endoftext|>


<commit_before>(function($) {
    "use strict";
    var style = {
            "menuStyle": {
                "position": "absolute",
                "backgroundColor": "#ffffff",
                "opacity": "1",
                "right": "-70px",
                "top": "0px",
                "width": "100px",
                "zIndex": "500",
                "color": "#000000",
                "padding": "3px",
                "border": "1px solid #000000",
                "font-family": "verdana,sans-serif",
                "font-size": "0.8em"
            }
        },
        menu = $("
Enter Desktop Mode
")
                   .hide()
                   .css(style.menuStyle)
                   .bind("click", function(event) {
                       event.stopPropagation();
                   }),
        timeoutId = undefined;

    function hideMenu() {
        menu.animate({ "right": "-70px" }, 100, function() { menu.hide(); });
    }

    function applyHoverStyle() {
        $(this).css(style.itemHoverStyle);
    }

    function removeHoverStyle() {
        $(this).css(style.itemStyle);
    }

    function showMenu() {
        if (timeoutId) {
            window.clearTimeout(timeoutId);
            timeoutId = undefined;
        }
        if (!menu.is(":visible")) {
            menu.show();
            menu.animate({ "right": "0px" }, 200);
        }
        timeoutId = window.setTimeout(hideMenu, 500);
    }

    $(document).ready(function() {
        menu.appendTo("body");
    });

    $.fn.flyMenu = function() {
        $(this).bind("mousemove", function(event) {
            showMenu();
            return false;
        });
        return this;
    };
})(jQuery);<commit_msg>how about a fly-out menu with a click handler?<commit_after>(function($) {
    "use strict";
    var style = {
            "menuStyle": {
                "position": "absolute",
                "backgroundColor": "#ffffff",
                "opacity": "1",
                "top": "-50px",
                "right": "0px",
                "zIndex": "500",
                "cursor": "pointer",
                "color": "#000000",
                "padding": "3px",
                "border": "1px solid #000000",
                "font-family": "verdana,sans-serif",
                "font-size": "0.8em"
            }
        },
        menuAction = function() { },
        menu = $("
Enter Desktop Mode
")
                   .hide()
                   .css(style.menuStyle)
                   .bind("click", function(event) {
                       event.stopPropagation();
                       menuAction();
                   }),
        timeoutId = undefined;

    function hideMenu() {
        menu.animate({ "top": "-50px" }, 100, function() { menu.hide(); });
    }

    function applyHoverStyle() {
        $(this).css(style.itemHoverStyle);
    }

    function removeHoverStyle() {
        $(this).css(style.itemStyle);
    }

    function showMenu() {
        if (timeoutId) {
            window.clearTimeout(timeoutId);
            timeoutId = undefined;
        }
        if (!menu.is(":visible")) {
            menu.show();
            menu.animate({ "top": "0px" }, 200);
        }
        timeoutId = window.setTimeout(hideMenu, 500);
    }

    $(document).ready(function() {
        menu.appendTo("body");
    });

    $.fn.flyMenu = function(clickHandler) {
        menuAction = function() {};
        if (typeof clickHandler === "function") {
            menuAction = clickHandler;
        }

        $(this).bind("mousemove", function(event) {
            showMenu();
            return false;
        });
        return this;
    };
})(jQuery);<|endoftext|>


<commit_before>/**
 * General-purpose validator for ngModel.
 * angular.js comes with several built-in validation mechanism for input fields (ngRequired, ngPattern etc.) but using
 * an arbitrary validation function requires creation of a custom formatters and / or parsers.
 * The ui-validate directive makes it easy to use any function(s) defined in scope as a validator function(s).
 * A validator function will trigger validation on both model and input changes.
 *
 * @example <input ui-validate=" 'myValidatorFunction($value)' ">
 * @example <input ui-validate="{ foo : '$value > anotherModel', bar : 'validateFoo($value)' }">
 * @example <input ui-validate="{ foo : '$value > anotherModel' }" ui-validate-watch=" 'anotherModel' ">
 * @example <input ui-validate="{ foo : '$value > anotherModel', bar : 'validateFoo($value)' }" ui-validate-watch=" { foo : 'anotherModel' } ">
 *
 * @param ui-validate {string|object literal} If strings is passed it should be a scope's function to be used as a validator.
 * If an object literal is passed a key denotes a validation error key while a value should be a validator function.
 * In both cases validator function should take a value to validate as its argument and should return true/false indicating a validation result.
 */
angular.module('ui.directives').directive('uiValidate', function () {

  return {
    restrict: 'A',
    require: 'ngModel',
    link: function (scope, elm, attrs, ctrl) {
      var validateFn, watch, validators = {},
        validateExpr = scope.$eval(attrs.uiValidate);

      if (!validateExpr) return;

      if (angular.isString(validateExpr)) {
        validateExpr = { validator: validateExpr };
      }

      angular.forEach(validateExpr, function (expression, key) {
        validateFn = function (valueToValidate) {
          if (scope.$eval(expression, { '$value' : valueToValidate })) {
            ctrl.$setValidity(key, true);
            return valueToValidate;
          } else {
            ctrl.$setValidity(key, false);
            return undefined;
          }
        };
        validators[key] = validateFn;
        ctrl.$formatters.push(validateFn);
        ctrl.$parsers.push(validateFn);
      });

      // Support for ui-validate-watch
      if (attrs.uiValidateWatch) {
        watch = scope.$eval(attrs.uiValidateWatch);
        if (angular.isString(watch)) {
          scope.$watch(watch, function(){
            angular.forEach(validators, function(validatorFn, key){
              validatorFn(ctrl.$viewValue);
            });
          });
        } else {
          angular.forEach(watch, function(expression, key){
            scope.$watch(expression, function(){
              validators[key](ctrl.$viewValue);
            });
          });
        }
      }
    }
  };
});<commit_msg>Update modules/directives/validate/validate.js<commit_after>/**
 * General-purpose validator for ngModel.
 * angular.js comes with several built-in validation mechanism for input fields (ngRequired, ngPattern etc.) but using
 * an arbitrary validation function requires creation of a custom formatters and / or parsers.
 * The ui-validate directive makes it easy to use any function(s) defined in scope as a validator function(s).
 * A validator function will trigger validation on both model and input changes.
 *
 * @example <input ui-validate=" 'myValidatorFunction($value)' ">
 * @example <input ui-validate="{ foo : '$value > anotherModel', bar : 'validateFoo($value)' }">
 * @example <input ui-validate="{ foo : '$value > anotherModel' }" ui-validate-watch=" 'anotherModel' ">
 * @example <input ui-validate="{ foo : '$value > anotherModel', bar : 'validateFoo($value)' }" ui-validate-watch=" { foo : 'anotherModel' } ">
 *
 * @param ui-validate {string|object literal} If strings is passed it should be a scope's function to be used as a validator.
 * If an object literal is passed a key denotes a validation error key while a value should be a validator function.
 * In both cases validator function should take a value to validate as its argument and should return true/false indicating a validation result.
 */
angular.module('ui.directives').directive('uiValidate', function () {

  return {
    restrict: 'A',
    require: 'ngModel',
    link: function (scope, elm, attrs, ctrl) {
      var validateFn, watch, validators = {},
        validateExpr = scope.$eval(attrs.uiValidate);

      if (!validateExpr) return;

      if (angular.isString(validateExpr)) {
        validateExpr = { validator: validateExpr };
      }

      angular.forEach(validateExpr, function (expression, key) {
        validateFn = function (valueToValidate) {
          if (scope.$eval(expression, { '$value' : valueToValidate })) {
            ctrl.$setValidity(key, true);
            return valueToValidate;
          } else {
            ctrl.$setValidity(key, false);
            return undefined;
          }
        };
        validators[key] = validateFn;
        ctrl.$formatters.push(validateFn);
        ctrl.$parsers.push(validateFn);
      });

      // Support for ui-validate-watch
      if (attrs.uiValidateWatch) {
        watch = scope.$eval(attrs.uiValidateWatch);
        if (angular.isString(watch)) {
          scope.$watch(watch, function(){
            angular.forEach(validators, function(validatorFn, key){
              validatorFn(ctrl.$modelValue);
            });
          });
        } else {
          angular.forEach(watch, function(expression, key){
            scope.$watch(expression, function(){
              validators[key](ctrl.$modelValue);
            });
          });
        }
      }
    }
  };
});
<|endoftext|>


<commit_before>// A package for running jobs synchronized across multiple processes
SyncedCron = {
  _entries: {},
  running: false,
  options: {
    //Log job run details to console
    log: true,

    logger: null,

    //Name of collection to use for synchronisation and logging
    collectionName: 'cronHistory',

    //Default to using localTime
    utc: false,

    //TTL in seconds for history records in collection to expire
    //NOTE: Unset to remove expiry but ensure you remove the index from
    //mongo by hand
    collectionTTL: 172800
  },
  config: function(opts) {
    this.options = _.extend({}, this.options, opts);
  }
}

Later = Npm.require('later');

/*
  Logger factory function. Takes a prefix string and options object
  and uses an injected `logger` if provided, else falls back to
  Meteor's `Log` package.

  Will send a log object to the injected logger, on the following form:

    message: String
    level: String (info, warn, error, debug)
    tag: 'SyncedCron'
*/
function createLogger(prefix) {
  check(prefix, String);

  // Return noop if logging is disabled.
  if(SyncedCron.options.log === false) {
    return function() {};
  }

  return function(level, message) {
    check(level, Match.OneOf('info', 'error', 'warn', 'debug'));
    check(message, String);

    var logger = SyncedCron.options && SyncedCron.options.logger;

    if(logger && _.isFunction(logger)) {

      logger({
        level: level,
        message: message,
        tag: prefix
      });

    } else {
      Log[level]({ message: prefix + ': ' + message });
    }
  }
}

var log;

Meteor.startup(function() {
  var options = SyncedCron.options;

  log = createLogger('SyncedCron');

  ['info', 'warn', 'error', 'debug'].forEach(function(level) {
    log[level] = _.partial(log, level);
  });

  // Don't allow TTL less than 5 minutes so we don't break synchronization
  var minTTL = 300;

  // Use UTC or localtime for evaluating schedules
  if (options.utc)
    Later.date.UTC();
  else
    Later.date.localTime();

  // collection holding the job history records
  SyncedCron._collection = new Mongo.Collection(options.collectionName);
  SyncedCron._collection._ensureIndex({intendedAt: 1, name: 1}, {unique: true});

  if (options.collectionTTL) {
    if (options.collectionTTL > minTTL)
      SyncedCron._collection._ensureIndex({startedAt: 1 },
        { expireAfterSeconds: options.collectionTTL } );
    else
      log.warn('Not going to use a TTL that is shorter than:' + minTTL);
  }
});

var scheduleEntry = function(entry) {
  var schedule = entry.schedule(Later.parse);
  entry._timer =
    SyncedCron._laterSetInterval(SyncedCron._entryWrapper(entry), schedule);

  log.info('Scheduled "' + entry.name + '" next run @'
    + Later.schedule(schedule).next(1));
}

// add a scheduled job
// SyncedCron.add({
//   name: String, //*required* unique name of the job
//   schedule: function(laterParser) {},//*required* when to run the job
//   job: function() {}, //*required* the code to run
// });
SyncedCron.add = function(entry) {
  check(entry.name, String);
  check(entry.schedule, Function);
  check(entry.job, Function);

  // check
  if (!this._entries[entry.name]) {
    this._entries[entry.name] = entry;

    // If cron is already running, start directly.
    if (this.running) {
      scheduleEntry(entry);
    }
  }
}

// Start processing added jobs
SyncedCron.start = function() {
  var self = this;

  Meteor.startup(function() {
    // Schedule each job with later.js
    _.each(self._entries, function(entry) {
      scheduleEntry(entry);
    });
    self.running = true;
  });
}

// Return the next scheduled date of the first matching entry or undefined
SyncedCron.nextScheduledAtDate = function(jobName) {
  var entry = this._entries[jobName];

  if (entry)
    return Later.schedule(entry.schedule(Later.parse)).next(1);
}

// Remove and stop the entry referenced by jobName
SyncedCron.remove = function(jobName) {
  var entry = this._entries[jobName];

  if (entry) {
    if (entry._timer)
      entry._timer.clear();

    delete this._entries[jobName];
    log.info('Removed "' + entry.name + '"');
  }
}

// Pause processing, but do not remove jobs so that the start method will
// restart existing jobs
SyncedCron.pause = function() {
  if (this.running) {
    _.each(this._entries, function(entry) {
      entry._timer.clear();
    });
    this.running = false;
  }
}

// Stop processing and remove ALL jobs
SyncedCron.stop = function() {
  _.each(this._entries, function(entry, name) {
    SyncedCron.remove(name);
  });
  this.running = false;
}

// The meat of our logic. Checks if the specified has already run. If not,
// records that it's running the job, runs it, and records the output
SyncedCron._entryWrapper = function(entry) {
  var self = this;

  return function(intendedAt) {
    var jobHistory = {
      intendedAt: intendedAt,
      name: entry.name,
      startedAt: new Date()
    };

    // If we have a dup key error, another instance has already tried to run
    // this job.
    try {
      jobHistory._id = self._collection.insert(jobHistory);
    } catch(e) {
      // http://www.mongodb.org/about/contributors/error-codes/
      // 11000 == duplicate key error
      if (e.name === 'MongoError' && e.code === 11000) {
        log.info('Not running "' + entry.name + '" again.');
        return;
      }

      throw e;
    };

    // run and record the job
    try {
      log.info('Starting "' + entry.name + '".');
      var output = entry.job(intendedAt); // <- Run the actual job

      log.info('Finished "' + entry.name + '".');
      self._collection.update({_id: jobHistory._id}, {
        $set: {
          finishedAt: new Date(),
          result: output
        }
      });
    } catch(e) {
      log.info('Exception "' + entry.name +'" ' + e.stack);
      self._collection.update({_id: jobHistory._id}, {
        $set: {
          finishedAt: new Date(),
          error: e.stack
        }
      });
    }
  };
}

// for tests
SyncedCron._reset = function() {
  this._entries = {};
  this._collection.remove({});
  this.running = false;
}

// ---------------------------------------------------------------------------
// The following two functions are lifted from the later.js package, however
// I've made the following changes:
// - Use Meteor.setTimeout and Meteor.clearTimeout
// - Added an 'intendedAt' parameter to the callback fn that specifies the precise
//   time the callback function *should* be run (so we can co-ordinate jobs)
//   between multiple, potentially laggy and unsynced machines

// From: https://github.com/bunkat/later/blob/master/src/core/setinterval.js
SyncedCron._laterSetInterval = function(fn, sched) {

  var t = SyncedCron._laterSetTimeout(scheduleTimeout, sched),
      done = false;

  /**
  * Executes the specified function and then sets the timeout for the next
  * interval.
  */
  function scheduleTimeout(intendedAt) {
    if(!done) {
      fn(intendedAt);
      t = SyncedCron._laterSetTimeout(scheduleTimeout, sched);
    }
  }

  return {

    /**
    * Clears the timeout.
    */
    clear: function() {
      done = true;
      t.clear();
    }

  };

};

// From: https://github.com/bunkat/later/blob/master/src/core/settimeout.js
SyncedCron._laterSetTimeout = function(fn, sched) {

  var s = Later.schedule(sched), t;
  scheduleTimeout();

  /**
  * Schedules the timeout to occur. If the next occurrence is greater than the
  * max supported delay (2147483647 ms) than we delay for that amount before
  * attempting to schedule the timeout again.
  */
  function scheduleTimeout() {
    var now = Date.now(),
        next = s.next(2, now),
        diff = next[0].getTime() - now,
        intendedAt = next[0];

    // minimum time to fire is one second, use next occurrence instead
    if(diff < 1000) {
      diff = next[1].getTime() - now;
      intendedAt = next[1];
    }

    if(diff < 2147483647) {
      t = Meteor.setTimeout(function() { fn(intendedAt); }, diff);
    }
    else {
      t = Meteor.setTimeout(scheduleTimeout, 2147483647);
    }
  }

  return {

    /**
    * Clears the timeout.
    */
    clear: function() {
      Meteor.clearTimeout(t);
    }

  };

};
// ---------------------------------------------------------------------------
<commit_msg>Discard milliseconds from intendedAt<commit_after>// A package for running jobs synchronized across multiple processes
SyncedCron = {
  _entries: {},
  running: false,
  options: {
    //Log job run details to console
    log: true,

    logger: null,

    //Name of collection to use for synchronisation and logging
    collectionName: 'cronHistory',

    //Default to using localTime
    utc: false,

    //TTL in seconds for history records in collection to expire
    //NOTE: Unset to remove expiry but ensure you remove the index from
    //mongo by hand
    collectionTTL: 172800
  },
  config: function(opts) {
    this.options = _.extend({}, this.options, opts);
  }
}

Later = Npm.require('later');

/*
  Logger factory function. Takes a prefix string and options object
  and uses an injected `logger` if provided, else falls back to
  Meteor's `Log` package.

  Will send a log object to the injected logger, on the following form:

    message: String
    level: String (info, warn, error, debug)
    tag: 'SyncedCron'
*/
function createLogger(prefix) {
  check(prefix, String);

  // Return noop if logging is disabled.
  if(SyncedCron.options.log === false) {
    return function() {};
  }

  return function(level, message) {
    check(level, Match.OneOf('info', 'error', 'warn', 'debug'));
    check(message, String);

    var logger = SyncedCron.options && SyncedCron.options.logger;

    if(logger && _.isFunction(logger)) {

      logger({
        level: level,
        message: message,
        tag: prefix
      });

    } else {
      Log[level]({ message: prefix + ': ' + message });
    }
  }
}

var log;

Meteor.startup(function() {
  var options = SyncedCron.options;

  log = createLogger('SyncedCron');

  ['info', 'warn', 'error', 'debug'].forEach(function(level) {
    log[level] = _.partial(log, level);
  });

  // Don't allow TTL less than 5 minutes so we don't break synchronization
  var minTTL = 300;

  // Use UTC or localtime for evaluating schedules
  if (options.utc)
    Later.date.UTC();
  else
    Later.date.localTime();

  // collection holding the job history records
  SyncedCron._collection = new Mongo.Collection(options.collectionName);
  SyncedCron._collection._ensureIndex({intendedAt: 1, name: 1}, {unique: true});

  if (options.collectionTTL) {
    if (options.collectionTTL > minTTL)
      SyncedCron._collection._ensureIndex({startedAt: 1 },
        { expireAfterSeconds: options.collectionTTL } );
    else
      log.warn('Not going to use a TTL that is shorter than:' + minTTL);
  }
});

var scheduleEntry = function(entry) {
  var schedule = entry.schedule(Later.parse);
  entry._timer =
    SyncedCron._laterSetInterval(SyncedCron._entryWrapper(entry), schedule);

  log.info('Scheduled "' + entry.name + '" next run @'
    + Later.schedule(schedule).next(1));
}

// add a scheduled job
// SyncedCron.add({
//   name: String, //*required* unique name of the job
//   schedule: function(laterParser) {},//*required* when to run the job
//   job: function() {}, //*required* the code to run
// });
SyncedCron.add = function(entry) {
  check(entry.name, String);
  check(entry.schedule, Function);
  check(entry.job, Function);

  // check
  if (!this._entries[entry.name]) {
    this._entries[entry.name] = entry;

    // If cron is already running, start directly.
    if (this.running) {
      scheduleEntry(entry);
    }
  }
}

// Start processing added jobs
SyncedCron.start = function() {
  var self = this;

  Meteor.startup(function() {
    // Schedule each job with later.js
    _.each(self._entries, function(entry) {
      scheduleEntry(entry);
    });
    self.running = true;
  });
}

// Return the next scheduled date of the first matching entry or undefined
SyncedCron.nextScheduledAtDate = function(jobName) {
  var entry = this._entries[jobName];

  if (entry)
    return Later.schedule(entry.schedule(Later.parse)).next(1);
}

// Remove and stop the entry referenced by jobName
SyncedCron.remove = function(jobName) {
  var entry = this._entries[jobName];

  if (entry) {
    if (entry._timer)
      entry._timer.clear();

    delete this._entries[jobName];
    log.info('Removed "' + entry.name + '"');
  }
}

// Pause processing, but do not remove jobs so that the start method will
// restart existing jobs
SyncedCron.pause = function() {
  if (this.running) {
    _.each(this._entries, function(entry) {
      entry._timer.clear();
    });
    this.running = false;
  }
}

// Stop processing and remove ALL jobs
SyncedCron.stop = function() {
  _.each(this._entries, function(entry, name) {
    SyncedCron.remove(name);
  });
  this.running = false;
}

// The meat of our logic. Checks if the specified has already run. If not,
// records that it's running the job, runs it, and records the output
SyncedCron._entryWrapper = function(entry) {
  var self = this;

  return function(intendedAt) {
    intendedAt = new Date(intendedAt.getTime());
    intendedAt.setMilliseconds(0);

    var jobHistory = {
      intendedAt: intendedAt,
      name: entry.name,
      startedAt: new Date()
    };

    // If we have a dup key error, another instance has already tried to run
    // this job.
    try {
      jobHistory._id = self._collection.insert(jobHistory);
    } catch(e) {
      // http://www.mongodb.org/about/contributors/error-codes/
      // 11000 == duplicate key error
      if (e.name === 'MongoError' && e.code === 11000) {
        log.info('Not running "' + entry.name + '" again.');
        return;
      }

      throw e;
    };

    // run and record the job
    try {
      log.info('Starting "' + entry.name + '".');
      var output = entry.job(intendedAt); // <- Run the actual job

      log.info('Finished "' + entry.name + '".');
      self._collection.update({_id: jobHistory._id}, {
        $set: {
          finishedAt: new Date(),
          result: output
        }
      });
    } catch(e) {
      log.info('Exception "' + entry.name +'" ' + e.stack);
      self._collection.update({_id: jobHistory._id}, {
        $set: {
          finishedAt: new Date(),
          error: e.stack
        }
      });
    }
  };
}

// for tests
SyncedCron._reset = function() {
  this._entries = {};
  this._collection.remove({});
  this.running = false;
}

// ---------------------------------------------------------------------------
// The following two functions are lifted from the later.js package, however
// I've made the following changes:
// - Use Meteor.setTimeout and Meteor.clearTimeout
// - Added an 'intendedAt' parameter to the callback fn that specifies the precise
//   time the callback function *should* be run (so we can co-ordinate jobs)
//   between multiple, potentially laggy and unsynced machines

// From: https://github.com/bunkat/later/blob/master/src/core/setinterval.js
SyncedCron._laterSetInterval = function(fn, sched) {

  var t = SyncedCron._laterSetTimeout(scheduleTimeout, sched),
      done = false;

  /**
  * Executes the specified function and then sets the timeout for the next
  * interval.
  */
  function scheduleTimeout(intendedAt) {
    if(!done) {
      fn(intendedAt);
      t = SyncedCron._laterSetTimeout(scheduleTimeout, sched);
    }
  }

  return {

    /**
    * Clears the timeout.
    */
    clear: function() {
      done = true;
      t.clear();
    }

  };

};

// From: https://github.com/bunkat/later/blob/master/src/core/settimeout.js
SyncedCron._laterSetTimeout = function(fn, sched) {

  var s = Later.schedule(sched), t;
  scheduleTimeout();

  /**
  * Schedules the timeout to occur. If the next occurrence is greater than the
  * max supported delay (2147483647 ms) than we delay for that amount before
  * attempting to schedule the timeout again.
  */
  function scheduleTimeout() {
    var now = Date.now(),
        next = s.next(2, now),
        diff = next[0].getTime() - now,
        intendedAt = next[0];

    // minimum time to fire is one second, use next occurrence instead
    if(diff < 1000) {
      diff = next[1].getTime() - now;
      intendedAt = next[1];
    }

    if(diff < 2147483647) {
      t = Meteor.setTimeout(function() { fn(intendedAt); }, diff);
    }
    else {
      t = Meteor.setTimeout(scheduleTimeout, 2147483647);
    }
  }

  return {

    /**
    * Clears the timeout.
    */
    clear: function() {
      Meteor.clearTimeout(t);
    }

  };

};
// ---------------------------------------------------------------------------
<|endoftext|>