<commit_before>import {Form, form} from 'widget/form/form'
// import {Menu} from './menu'
import {Menu} from 'widget/menu/menu'
import {activatable} from 'widget/activation/activatable'
import {changeBaseLayer} from './baseLayer'
import {compose} from 'compose'
import {msg} from 'translate'
import {select} from 'store'
import ButtonSelect from 'widget/buttonSelect'
import Labels from './labels'
import PropTypes from 'prop-types'
import React from 'react'
import actionBuilder from '../../../action-builder'
// import moment from 'moment'
import styles from './layersMenu.module.css'

const fields = {
    // year: new Form.Field(),
    // month: new Form.Field(),
    dateRange: new Form.Field(),
    proc: new Form.Field()
}

const mapStateToProps = (state, ownProps) => ({
    labelsShown: select([ownProps.statePath, 'labelsShown']),
    baseLayer: select([ownProps.statePath, 'baseLayer']),
    overlayIndex: select([ownProps.statePath, 'overlayIndex']),
    dateRange: select([ownProps.statePath, 'mapLayer.dateRange']),
    proc: select([ownProps.statePath, 'mapLayer.proc'])
    // year: select([ownProps.statePath, 'options.year']),
    // month: select([ownProps.statePath, 'options.month'])
})

class _LayersMenu extends React.Component {

    getPlanetDateOptions() {
        // const {inputs: {year}} = this.props
        // const startDate = moment('2018-08-01')
        // const endDate = moment().subtract(1, 'months')
        //
        // const yearOptions = sequence(startDate.year(), moment().year())
        //     .map(year => ({value: `${year}`, label: `${year}`}))
        //
        // const allMonthOptions = moment.monthsShort()
        //     .map((label, i) => ({value: `${i + 1}`.padStart(2, '0'), label}))
        //
        // const monthOptions = year.value === `${startDate.year()}`
        //     ? allMonthOptions.slice(startDate.month() + 1)
        //     : year.value === `${endDate.year()}`
        //         ? allMonthOptions.slice(0, endDate.month() + 1)
        //         : allMonthOptions
        // return {yearOptions, monthOptions}

        const procOptions = [
            {value: 'rgb', label: 'RGB'},
            {value: 'cir', label: 'CIR'},
        ]
        const dateRangeOptions = [
            {value: '2015-06_2015-11', label: 'Jun 2015 (6 months)'},
            {value: '2015-12_2016-05', label: 'Dec 2015 (6 months)'},
            {value: '2016-06_2016-11', label: 'Jun 2016 (6 months)'},
            {value: '2016-12_2017-05', label: 'Dec 2016 (6 months)'},
            {value: '2017-06_2017-11', label: 'Jun 2017 (6 months)'},
            {value: '2017-12_2018-05', label: 'Dec 2017 (6 months)'},
            {value: '2018-06_2018-11', label: 'Jun 2018 (6 months)'},
            {value: '2018-12_2019-05', label: 'Dec 2018 (6 months)'},
            {value: '2019-06_2019-11', label: 'Jun 2019 (6 months)'},
            {value: '2019-12_2020-05', label: 'Dec 2019 (6 months)'},
            {value: '2020-06_2020-08', label: 'Jun 2020 (3 months)'},
            {value: '2020-09', label: 'Sep 2020 (1 month)'},
            {value: '2020-10', label: 'Oct 2020 (1 month)'},
            {value: '2020-11', label: 'Nov 2020 (1 month)'},
            {value: '2020-12', label: 'Dec 2020 (1 month)'},
            {value: '2021-01', label: 'Jan 2021 (1 month)'},
        ]
        return {procOptions, dateRangeOptions}
    }

    toggleOverlay = selectedOverlayIndex => {
        const {mapContext: {sepalMap, googleMap}, statePath} = this.props
        const overlays = sepalMap.toggleableLayers()

        const prevOverlayIndex = this.selectedOverlayIndex()
        if (selectedOverlayIndex !== prevOverlayIndex) {
            const prevOverlay = overlays.find(layer => layer.layerIndex === prevOverlayIndex)
            prevOverlay && prevOverlay.hide(googleMap, true)
        }

        const selectedOverlay = overlays.find(layer => layer.layerIndex === selectedOverlayIndex)
        const hide = prevOverlayIndex === selectedOverlayIndex
        selectedOverlay && selectedOverlay.hide(googleMap, hide)
        const nextOverlayIndex = hide ? -1 : selectedOverlayIndex

        actionBuilder('SET_OVERLAY', {nextOverlayIndex})
            .set([statePath, 'overlayIndex'], nextOverlayIndex)
            .build()
            .dispatch()
    }

    // changeBaseLayer(type, year, month, proc) {
    changeBaseLayer(type, dateRange, proc) {
        // console.log('changeBaseLayer', dateRange.value, proc.value)
        const {statePath, mapContext} = this.props
        const planetApiKey = mapContext.norwayPlanetApiKey
        changeBaseLayer({
            type,
            mapContext,
            statePath,
            options: {dateRange: dateRange.value, proc: proc.value, planetApiKey}
            // options: {year: year.value, month: month.value, proc: proc.value, planetApiKey}
        })
    }

    selectedOverlayIndex() {
        const {overlayIndex, mapContext: {sepalMap}} = this.props
        if (overlayIndex === undefined) {
            const overlays = sepalMap.toggleableLayers()
            const firstOverlayIndex = overlays.length
                ? overlays.map(layer => layer.layerIndex)[0]
                : -1
            return firstOverlayIndex
        } else {
            return overlayIndex
        }
    }

    render() {
        const {
            inputs: {dateRange, proc},
            // inputs: {year, month, proc},
            activatable: {deactivate},
            labelsShown, baseLayer, labelLayerIndex, statePath, mapContext
        } = this.props
        const overlays = mapContext.sepalMap.toggleableLayers()
        const toggleableOptions = overlays.map(layer =>
            <Menu.Option
                key={layer.layerIndex}
                id={layer.layerIndex}
                label={layer.label}
                description={layer.description}/>)
        // const {yearOptions, monthOptions, procOptions} = this.getPlanetDateOptions()
        const {dateRangeOptions, procOptions} = this.getPlanetDateOptions()

        const procOption = procOptions.find(({value}) => proc.value === value)
        const dateRangeOption = dateRangeOptions.find(({value}) => dateRange.value === value)
        // const yearOption = yearOptions.find(({value}) => year.value === value)
        // const monthOption = monthOptions.find(({value}) => month.value === value)
        return (
            <Menu
                position='top-right'
                className={styles.panel}
                close={deactivate}>
                <Menu.Select
                    id={'BASE_LAYER'}
                    label={msg('process.mosaic.mapToolbar.layersMenu.baseLayer')}
                    onSelect={type => this.changeBaseLayer(type, dateRange, proc)}
                    // onSelect={type => this.changeBaseLayer(type, year, month, proc)}
                    selected={baseLayer || 'SEPAL'}>
                    <Menu.Option
                        id={'SEPAL'}
                        label={msg('process.mosaic.mapToolbar.layersMenu.SEPAL.label')}
                        description={msg('process.mosaic.mapToolbar.layersMenu.SEPAL.description')}/>
                    <Menu.Option
                        id={'GOOGLE_SATELLITE'}
                        label={msg('process.mosaic.mapToolbar.layersMenu.GOOGLE_SATELLITE.label')}
                        description={msg('process.mosaic.mapToolbar.layersMenu.GOOGLE_SATELLITE.description')}/>
                    <Menu.Option
                        id={'PLANET'}
                        label={msg('process.mosaic.mapToolbar.layersMenu.PLANET.label')}
                        description={msg('process.mosaic.mapToolbar.layersMenu.PLANET.description')}
                        right={
                            <div className={styles.layerOptions}>
                                <ButtonSelect
                                    placement='below'
                                    alignment='left'
                                    input={proc}
                                    tooltipPlacement='bottom'
                                    options={procOptions}
                                    label={procOption && procOption.label}
                                    onSelect={proc => this.changeBaseLayer('PLANET', this.props.inputs.dateRange, proc)}
                                    // onSelect={proc => this.changeBaseLayer('PLANET', this.props.inputs.year, this.props.inputs.month, proc)}
                                />
                                <ButtonSelect
                                    placement='below'
                                    alignment='left'
                                    input={dateRange}
                                    tooltipPlacement='bottom'
                                    options={dateRangeOptions}
                                    label={dateRangeOption && dateRangeOption.label}
                                    onSelect={dateRange => this.changeBaseLayer('PLANET', dateRange, this.props.inputs.proc)}
                                />
                                {/*<ButtonSelect*/}
                                {/*    placement='below'*/}
                                {/*    alignment='left'*/}
                                {/*    input={year}*/}
                                {/*    tooltipPlacement='bottom'*/}
                                {/*    options={yearOptions}*/}
                                {/*    label={yearOption && yearOption.label}*/}
                                {/*    onSelect={year => this.changeBaseLayer('PLANET', year, this.props.inputs.month, this.props.inputs.proc)}*/}
                                {/*/>*/}
                                {/*<ButtonSelect*/}
                                {/*    placement='below'*/}
                                {/*    alignment='left'*/}
                                {/*    input={month}*/}
                                {/*    tooltipPlacement='bottom'*/}
                                {/*    options={monthOptions}*/}
                                {/*    label={monthOption && monthOption.label}*/}
                                {/*    onSelect={month => this.changeBaseLayer('PLANET', this.props.inputs.year, month, this.props.inputs.proc)}*/}
                                {/*/>*/}
                            </div>
                        }/>
                </Menu.Select>
                <Menu.Separator/>
                <Menu.Select
                    id={'OVERLAYS'}
                    label={msg('process.mosaic.mapToolbar.layersMenu.overlay')}
                    onSelect={selectedOverlayIndex => this.toggleOverlay(selectedOverlayIndex)}
                    selected={this.selectedOverlayIndex()}>
                    {toggleableOptions}
                </Menu.Select>
                <Menu.Toggle
                    id={'SHOW_LABELS'}
                    label={msg('process.mosaic.mapToolbar.layersMenu.labels.label')}
                    description={msg('process.mosaic.mapToolbar.layersMenu.labels.description')}
                    selected={labelsShown}
                    onChange={shown =>
                        Labels.showLabelsAction({
                            mapContext,
                            shown,
                            layerIndex: labelLayerIndex,
                            statePath
                        }).dispatch()}/>
            </Menu>
        )
    }

    componentDidUpdate() {
        // const {inputs: {year, month, proc}} = this.props
        // if (!year.value)
        // year.set(this.props.year || moment().format('YYYY'))
        // if (!month.value)
        // month.set(this.props.month || moment().subtract(1, 'months').format('MM'))
        // else {
        // const {monthOptions} = this.getPlanetDateOptions()
        // if (!monthOptions.find(({value}) => month.value === value)) {
        //     const monthOption = monthOptions[0]
        //     month.set(monthOption.value)
        //     this.changeBaseLayer('PLANET', year, monthOption, )
        // }
        // }
        const {dateRange: prevDateRange, proc: prevProc, inputs: {dateRange, proc}} = this.props
        // console.log(dateRange.value, proc.value)
        if (!proc.value)
            proc.set(prevProc || 'rgb')
        if (!dateRange.value)
            dateRange.set(prevDateRange || '2020-09')
    }
}

const policy = () => ({
    _: 'allow'
})

export const LayersMenu = compose(
    _LayersMenu,
    form({fields, mapStateToProps}),
    activatable({id: 'layersMenu', policy})
)

// const sequence = (start, end, step = 1) =>
//     Array.apply(null, {length: Math.floor((end - start) / step) + 1})
//         .map((_, i) => i * step + start)

LayersMenu.propTypes = {
    labelLayerIndex: PropTypes.any.isRequired,
    mapContext: PropTypes.object.isRequired,
    statePath: PropTypes.any.isRequired
}
<commit_msg>Removed Planet date.<commit_after>import {Form, form} from 'widget/form/form'
// import {Menu} from './menu'
import {Menu} from 'widget/menu/menu'
import {activatable} from 'widget/activation/activatable'
import {changeBaseLayer} from './baseLayer'
import {compose} from 'compose'
import {msg} from 'translate'
import {select} from 'store'
import ButtonSelect from 'widget/buttonSelect'
import Labels from './labels'
import PropTypes from 'prop-types'
import React from 'react'
import actionBuilder from '../../../action-builder'
// import moment from 'moment'
import styles from './layersMenu.module.css'

const fields = {
    // year: new Form.Field(),
    // month: new Form.Field(),
    dateRange: new Form.Field(),
    proc: new Form.Field()
}

const mapStateToProps = (state, ownProps) => ({
    labelsShown: select([ownProps.statePath, 'labelsShown']),
    baseLayer: select([ownProps.statePath, 'baseLayer']),
    overlayIndex: select([ownProps.statePath, 'overlayIndex']),
    dateRange: select([ownProps.statePath, 'mapLayer.dateRange']),
    proc: select([ownProps.statePath, 'mapLayer.proc'])
    // year: select([ownProps.statePath, 'options.year']),
    // month: select([ownProps.statePath, 'options.month'])
})

class _LayersMenu extends React.Component {

    getPlanetDateOptions() {
        // const {inputs: {year}} = this.props
        // const startDate = moment('2018-08-01')
        // const endDate = moment().subtract(1, 'months')
        //
        // const yearOptions = sequence(startDate.year(), moment().year())
        //     .map(year => ({value: `${year}`, label: `${year}`}))
        //
        // const allMonthOptions = moment.monthsShort()
        //     .map((label, i) => ({value: `${i + 1}`.padStart(2, '0'), label}))
        //
        // const monthOptions = year.value === `${startDate.year()}`
        //     ? allMonthOptions.slice(startDate.month() + 1)
        //     : year.value === `${endDate.year()}`
        //         ? allMonthOptions.slice(0, endDate.month() + 1)
        //         : allMonthOptions
        // return {yearOptions, monthOptions}

        const procOptions = [
            {value: 'rgb', label: 'RGB'},
            {value: 'cir', label: 'CIR'},
        ]
        const dateRangeOptions = [
            {value: '2015-12_2016-05', label: 'Dec 2015 (6 months)'},
            {value: '2016-06_2016-11', label: 'Jun 2016 (6 months)'},
            {value: '2016-12_2017-05', label: 'Dec 2016 (6 months)'},
            {value: '2017-06_2017-11', label: 'Jun 2017 (6 months)'},
            {value: '2017-12_2018-05', label: 'Dec 2017 (6 months)'},
            {value: '2018-06_2018-11', label: 'Jun 2018 (6 months)'},
            {value: '2018-12_2019-05', label: 'Dec 2018 (6 months)'},
            {value: '2019-06_2019-11', label: 'Jun 2019 (6 months)'},
            {value: '2019-12_2020-05', label: 'Dec 2019 (6 months)'},
            {value: '2020-06_2020-08', label: 'Jun 2020 (3 months)'},
            {value: '2020-09', label: 'Sep 2020 (1 month)'},
            {value: '2020-10', label: 'Oct 2020 (1 month)'},
            {value: '2020-11', label: 'Nov 2020 (1 month)'},
            {value: '2020-12', label: 'Dec 2020 (1 month)'},
            {value: '2021-01', label: 'Jan 2021 (1 month)'},
        ]
        return {procOptions, dateRangeOptions}
    }

    toggleOverlay = selectedOverlayIndex => {
        const {mapContext: {sepalMap, googleMap}, statePath} = this.props
        const overlays = sepalMap.toggleableLayers()

        const prevOverlayIndex = this.selectedOverlayIndex()
        if (selectedOverlayIndex !== prevOverlayIndex) {
            const prevOverlay = overlays.find(layer => layer.layerIndex === prevOverlayIndex)
            prevOverlay && prevOverlay.hide(googleMap, true)
        }

        const selectedOverlay = overlays.find(layer => layer.layerIndex === selectedOverlayIndex)
        const hide = prevOverlayIndex === selectedOverlayIndex
        selectedOverlay && selectedOverlay.hide(googleMap, hide)
        const nextOverlayIndex = hide ? -1 : selectedOverlayIndex

        actionBuilder('SET_OVERLAY', {nextOverlayIndex})
            .set([statePath, 'overlayIndex'], nextOverlayIndex)
            .build()
            .dispatch()
    }

    // changeBaseLayer(type, year, month, proc) {
    changeBaseLayer(type, dateRange, proc) {
        // console.log('changeBaseLayer', dateRange.value, proc.value)
        const {statePath, mapContext} = this.props
        const planetApiKey = mapContext.norwayPlanetApiKey
        changeBaseLayer({
            type,
            mapContext,
            statePath,
            options: {dateRange: dateRange.value, proc: proc.value, planetApiKey}
            // options: {year: year.value, month: month.value, proc: proc.value, planetApiKey}
        })
    }

    selectedOverlayIndex() {
        const {overlayIndex, mapContext: {sepalMap}} = this.props
        if (overlayIndex === undefined) {
            const overlays = sepalMap.toggleableLayers()
            const firstOverlayIndex = overlays.length
                ? overlays.map(layer => layer.layerIndex)[0]
                : -1
            return firstOverlayIndex
        } else {
            return overlayIndex
        }
    }

    render() {
        const {
            inputs: {dateRange, proc},
            // inputs: {year, month, proc},
            activatable: {deactivate},
            labelsShown, baseLayer, labelLayerIndex, statePath, mapContext
        } = this.props
        const overlays = mapContext.sepalMap.toggleableLayers()
        const toggleableOptions = overlays.map(layer =>
            <Menu.Option
                key={layer.layerIndex}
                id={layer.layerIndex}
                label={layer.label}
                description={layer.description}/>)
        // const {yearOptions, monthOptions, procOptions} = this.getPlanetDateOptions()
        const {dateRangeOptions, procOptions} = this.getPlanetDateOptions()

        const procOption = procOptions.find(({value}) => proc.value === value)
        const dateRangeOption = dateRangeOptions.find(({value}) => dateRange.value === value)
        // const yearOption = yearOptions.find(({value}) => year.value === value)
        // const monthOption = monthOptions.find(({value}) => month.value === value)
        return (
            <Menu
                position='top-right'
                className={styles.panel}
                close={deactivate}>
                <Menu.Select
                    id={'BASE_LAYER'}
                    label={msg('process.mosaic.mapToolbar.layersMenu.baseLayer')}
                    onSelect={type => this.changeBaseLayer(type, dateRange, proc)}
                    // onSelect={type => this.changeBaseLayer(type, year, month, proc)}
                    selected={baseLayer || 'SEPAL'}>
                    <Menu.Option
                        id={'SEPAL'}
                        label={msg('process.mosaic.mapToolbar.layersMenu.SEPAL.label')}
                        description={msg('process.mosaic.mapToolbar.layersMenu.SEPAL.description')}/>
                    <Menu.Option
                        id={'GOOGLE_SATELLITE'}
                        label={msg('process.mosaic.mapToolbar.layersMenu.GOOGLE_SATELLITE.label')}
                        description={msg('process.mosaic.mapToolbar.layersMenu.GOOGLE_SATELLITE.description')}/>
                    <Menu.Option
                        id={'PLANET'}
                        label={msg('process.mosaic.mapToolbar.layersMenu.PLANET.label')}
                        description={msg('process.mosaic.mapToolbar.layersMenu.PLANET.description')}
                        right={
                            <div className={styles.layerOptions}>
                                <ButtonSelect
                                    placement='below'
                                    alignment='left'
                                    input={proc}
                                    tooltipPlacement='bottom'
                                    options={procOptions}
                                    label={procOption && procOption.label}
                                    onSelect={proc => this.changeBaseLayer('PLANET', this.props.inputs.dateRange, proc)}
                                    // onSelect={proc => this.changeBaseLayer('PLANET', this.props.inputs.year, this.props.inputs.month, proc)}
                                />
                                <ButtonSelect
                                    placement='below'
                                    alignment='left'
                                    input={dateRange}
                                    tooltipPlacement='bottom'
                                    options={dateRangeOptions}
                                    label={dateRangeOption && dateRangeOption.label}
                                    onSelect={dateRange => this.changeBaseLayer('PLANET', dateRange, this.props.inputs.proc)}
                                />
                                {/*<ButtonSelect*/}
                                {/*    placement='below'*/}
                                {/*    alignment='left'*/}
                                {/*    input={year}*/}
                                {/*    tooltipPlacement='bottom'*/}
                                {/*    options={yearOptions}*/}
                                {/*    label={yearOption && yearOption.label}*/}
                                {/*    onSelect={year => this.changeBaseLayer('PLANET', year, this.props.inputs.month, this.props.inputs.proc)}*/}
                                {/*/>*/}
                                {/*<ButtonSelect*/}
                                {/*    placement='below'*/}
                                {/*    alignment='left'*/}
                                {/*    input={month}*/}
                                {/*    tooltipPlacement='bottom'*/}
                                {/*    options={monthOptions}*/}
                                {/*    label={monthOption && monthOption.label}*/}
                                {/*    onSelect={month => this.changeBaseLayer('PLANET', this.props.inputs.year, month, this.props.inputs.proc)}*/}
                                {/*/>*/}
                            </div>
                        }/>
                </Menu.Select>
                <Menu.Separator/>
                <Menu.Select
                    id={'OVERLAYS'}
                    label={msg('process.mosaic.mapToolbar.layersMenu.overlay')}
                    onSelect={selectedOverlayIndex => this.toggleOverlay(selectedOverlayIndex)}
                    selected={this.selectedOverlayIndex()}>
                    {toggleableOptions}
                </Menu.Select>
                <Menu.Toggle
                    id={'SHOW_LABELS'}
                    label={msg('process.mosaic.mapToolbar.layersMenu.labels.label')}
                    description={msg('process.mosaic.mapToolbar.layersMenu.labels.description')}
                    selected={labelsShown}
                    onChange={shown =>
                        Labels.showLabelsAction({
                            mapContext,
                            shown,
                            layerIndex: labelLayerIndex,
                            statePath
                        }).dispatch()}/>
            </Menu>
        )
    }

    componentDidUpdate() {
        // const {inputs: {year, month, proc}} = this.props
        // if (!year.value)
        // year.set(this.props.year || moment().format('YYYY'))
        // if (!month.value)
        // month.set(this.props.month || moment().subtract(1, 'months').format('MM'))
        // else {
        // const {monthOptions} = this.getPlanetDateOptions()
        // if (!monthOptions.find(({value}) => month.value === value)) {
        //     const monthOption = monthOptions[0]
        //     month.set(monthOption.value)
        //     this.changeBaseLayer('PLANET', year, monthOption, )
        // }
        // }
        const {dateRange: prevDateRange, proc: prevProc, inputs: {dateRange, proc}} = this.props
        // console.log(dateRange.value, proc.value)
        if (!proc.value)
            proc.set(prevProc || 'rgb')
        if (!dateRange.value)
            dateRange.set(prevDateRange || '2020-09')
    }
}

const policy = () => ({
    _: 'allow'
})

export const LayersMenu = compose(
    _LayersMenu,
    form({fields, mapStateToProps}),
    activatable({id: 'layersMenu', policy})
)

// const sequence = (start, end, step = 1) =>
//     Array.apply(null, {length: Math.floor((end - start) / step) + 1})
//         .map((_, i) => i * step + start)

LayersMenu.propTypes = {
    labelLayerIndex: PropTypes.any.isRequired,
    mapContext: PropTypes.object.isRequired,
    statePath: PropTypes.any.isRequired
}
<|endoftext|>


<commit_before>(function( $, undefined ) {
    $.widget("mobile.date",{
      options:{
		defaultDate: null, // Used when field is blank: actual date,
			// +/-number for offset from today, null for today
		appendText: "", // Display text following the input box, e.g. showing the format
		buttonText: "...", // Text for trigger button
		buttonImage: "", // URL for trigger button image
		buttonImageOnly: false, // True if the image appears alone, false if it appears on a button
		hideIfNoPrevNext: false, // True to hide next/previous month links
			// if not applicable, false to just disable them
		navigationAsDateFormat: false, // True if date formatting applied to prev/today/next links
		gotoCurrent: false, // True if today link goes back to current selection instead
		changeMonth: false, // True if month can be selected directly, false if only prev/next
		changeYear: false, // True if year can be selected directly, false if only prev/next
		yearRange: "c-10:c+10", // Range of years to display in drop-down,
			// either relative to today's year (-nn:+nn), relative to currently displayed year
			// (c-nn:c+nn), absolute (nnnn:nnnn), or a combination of the above (nnnn:-n)
		showOtherMonths: false, // True to show dates in other months, false to leave blank
		selectOtherMonths: false, // True to allow selection of dates in other months, false for unselectable
		showWeek: false, // True to show week of the year, false to not show it
		calculateWeek: this.iso8601Week, // How to calculate the week of the year,
			// takes a Date and returns the number of the week for it
		shortYearCutoff: "+10", // Short year values < this are in the current century,
			// > this are in the previous century,
			// string value starting with "+" for current year + value
		minDate: null, // The earliest selectable date, or null for no limit
		maxDate: null, // The latest selectable date, or null for no limit
		beforeShowDay: null, // Function that takes a date and returns an array with
			// [0] = true if selectable, false if not, [1] = custom CSS class name(s) or "",
			// [2] = cell title (optional), e.g. $.datepicker.noWeekends
		onSelect: null, // Define a callback function when a date is selected
		onChangeMonthYear: null, // Define a callback function when the month or year is changed
		beforeShow: null, // Define a callback function when the calendar is shown
		numberOfMonths: 1, // Number of months to show at a time
		showCurrentAtPos: 0, // The position in multipe months at which to show the current month (starting at 0)
		stepMonths: 1, // Number of months to step back/forward
		stepBigMonths: 12, // Number of months to step back/forward for the big links
		altField: "", // Selector for an alternate field to store selected dates into
		altFormat: "", // The date format to use for the alternate field
		constrainInput: true, // The input is constrained by the current date format
		showButtonPanel: false, // True to show button panel, false to not show it
		autoSize: false, // True to size the input for the date format, false to leave as is
		disabled: false, // The initial disabled state
		inline: false, // True to set the calendar always visible
    theme: "a",
    dateFormat: "mm/dd/yy"
      },
      _create:function(){
        var calendar, interval,
          that = this;

        $.each([ 'onSelect', 'onChangeMonthYear', 'beforeShow' ], function(key, val){
			    that.options[ '_'+val ] = that.options[ val ];
    			that.options[ val ] = function( date, inst ){
    				var args = arguments;
            that.element.trigger( "change" );
    				setTimeout(function(){
    					that.addMobileStyle();
    					if (that.options[ '_'+val ]) {
    						that.options[ '_'+val ].apply( null, args );
    					}
    				}, 0);
    			}
        });

        if( this.options.inline ){
	        this.options.altField = this.element;
          calendar = $("<div>").datepicker(this.options);
          this.element.parent().after(calendar);
        } else {
          this.element.datepicker( this.options );
          calendar= this.element.datepicker( "widget" );
        }

        this.calendar = calendar;

        this.baseWidget = ( !this.options.inline )? this.element: this.calendar;

        this._on({
          "change": function() {
            if( this.options.inline ){
              this.calendar.datepicker( "setDate", this.element.val() );
            }
            this._delay( "addMobileStyle", 10 );
          },
          "input": function() {
            interval = window.setInterval( function(){
              if( !that.calendar.hasClass( "mobile-enhanced" ) ){
                that.addMobileStyle();
              } else {
                clearInterval( interval );
              }
            });
          }
        });
        this.addMobileStyle();
      },
      setOption:function( key, value ){
        this.calendar.datepicker("option",key,value);
      },
      getDate: function(){
        console.log( this.baseWidget );
        return this.baseWidget.datepicker("getDate");
      },
      _destroy: function(){
        return this.baseWidget.datepicker("destroy");
      },
      isDisabled: function(){
        return this.baseWidget.datepicker("isDisabled");
      },
      refresh: function(){
        return this.baseWidget.datepicker("refresh");
      },
      setDate: function( date ){
        return this.baseWidget.datepicker("setDate", date );
      },
      widget:function(){
       return this.element;
      },
      theme: 'a',
      addMobileStyle: function(){
          this.calendar.addClass("ui-shadow")
          .find( ".ui-datepicker-calendar" ).addClass( "mobile-enhanced" ).end()
          .find(".ui-datepicker-calendar a,.ui-datepicker-prev,.ui-datepicker-next").addClass("ui-btn").end()
          .find(".ui-datepicker-prev").addClass("ui-btn-icon-notext ui-btn-inline ui-corner-all ui-icon-arrow-l ui-shadow").end()
          .find(".ui-datepicker-next").addClass("ui-btn-icon-notext ui-btn-inline ui-corner-all ui-icon-arrow-r ui-shadow").end()
          .find(".ui-datepicker-header").addClass("ui-body-"+this.options.theme+" ui-corner-top").removeClass("ui-corner-all").end()
          .find(".ui-datepicker-calendar th" ).addClass("ui-bar-"+this.options.theme).end()
          .find(".ui-datepicker-calendar td" ).addClass("ui-body-"+this.options.theme).end()
          .find(".ui-datepicker-calendar a.ui-state-active").addClass("ui-btn-active").end()
          .find(".ui-datepicker-calendar a.ui-state-highlight").addClass("ui-btn-up-"+this.options.theme).end().find(".ui-state-disabled").css("opacity","1");
      }
    });

 })( jQuery );
<commit_msg>Bug fix on callbacks<commit_after>(function( $, undefined ) {
    $.widget("mobile.date",{
      options:{
		defaultDate: null, // Used when field is blank: actual date,
			// +/-number for offset from today, null for today
		appendText: "", // Display text following the input box, e.g. showing the format
		buttonText: "...", // Text for trigger button
		buttonImage: "", // URL for trigger button image
		buttonImageOnly: false, // True if the image appears alone, false if it appears on a button
		hideIfNoPrevNext: false, // True to hide next/previous month links
			// if not applicable, false to just disable them
		navigationAsDateFormat: false, // True if date formatting applied to prev/today/next links
		gotoCurrent: false, // True if today link goes back to current selection instead
		changeMonth: false, // True if month can be selected directly, false if only prev/next
		changeYear: false, // True if year can be selected directly, false if only prev/next
		yearRange: "c-10:c+10", // Range of years to display in drop-down,
			// either relative to today's year (-nn:+nn), relative to currently displayed year
			// (c-nn:c+nn), absolute (nnnn:nnnn), or a combination of the above (nnnn:-n)
		showOtherMonths: false, // True to show dates in other months, false to leave blank
		selectOtherMonths: false, // True to allow selection of dates in other months, false for unselectable
		showWeek: false, // True to show week of the year, false to not show it
		calculateWeek: this.iso8601Week, // How to calculate the week of the year,
			// takes a Date and returns the number of the week for it
		shortYearCutoff: "+10", // Short year values < this are in the current century,
			// > this are in the previous century,
			// string value starting with "+" for current year + value
		minDate: null, // The earliest selectable date, or null for no limit
		maxDate: null, // The latest selectable date, or null for no limit
		beforeShowDay: null, // Function that takes a date and returns an array with
			// [0] = true if selectable, false if not, [1] = custom CSS class name(s) or "",
			// [2] = cell title (optional), e.g. $.datepicker.noWeekends
		onSelect: null, // Define a callback function when a date is selected
		onChangeMonthYear: null, // Define a callback function when the month or year is changed
		beforeShow: null, // Define a callback function when the calendar is shown
		numberOfMonths: 1, // Number of months to show at a time
		showCurrentAtPos: 0, // The position in multipe months at which to show the current month (starting at 0)
		stepMonths: 1, // Number of months to step back/forward
		stepBigMonths: 12, // Number of months to step back/forward for the big links
		altField: "", // Selector for an alternate field to store selected dates into
		altFormat: "", // The date format to use for the alternate field
		constrainInput: true, // The input is constrained by the current date format
		showButtonPanel: false, // True to show button panel, false to not show it
		autoSize: false, // True to size the input for the date format, false to leave as is
		disabled: false, // The initial disabled state
		inline: false, // True to set the calendar always visible
    theme: "a",
    dateFormat: "mm/dd/yy"
      },
      _create:function(){
        var calendar, interval,
          that = this;

        $.each([ 'onSelect', 'onChangeMonthYear', 'beforeShow' ], function(key, val){
        		that.options[ '_'+val ] = that.options[ val ];
        		that.options[ val ] = function(){
    				var args = arguments;
    				if (val == 'onSelect') {
    					that.element.trigger( "change" );
    				}
    				setTimeout(function(){
    					that.addMobileStyle();
    					if (that.options[ '_'+val ]) {
    						that.options[ '_'+val ].apply( null, args );
    					}
    				}, 0);
    			}
        });

        if( this.options.inline ){
	        this.options.altField = this.element;
          calendar = $("<div>").datepicker(this.options);
          this.element.parent().after(calendar);
        } else {
          this.element.datepicker( this.options );
          calendar= this.element.datepicker( "widget" );
        }

        this.calendar = calendar;

        this.baseWidget = ( !this.options.inline )? this.element: this.calendar;

        this._on({
          "change": function() {
            if( this.options.inline ){
              this.calendar.datepicker( "setDate", this.element.val() );
            }
            this._delay( "addMobileStyle", 10 );
          },
          "input": function() {
            interval = window.setInterval( function(){
              if( !that.calendar.hasClass( "mobile-enhanced" ) ){
                that.addMobileStyle();
              } else {
                clearInterval( interval );
              }
            });
          }
        });
        this.addMobileStyle();
      },
      setOption:function( key, value ){
        this.calendar.datepicker("option",key,value);
      },
      getDate: function(){
        console.log( this.baseWidget );
        return this.baseWidget.datepicker("getDate");
      },
      _destroy: function(){
        return this.baseWidget.datepicker("destroy");
      },
      isDisabled: function(){
        return this.baseWidget.datepicker("isDisabled");
      },
      refresh: function(){
        return this.baseWidget.datepicker("refresh");
      },
      setDate: function( date ){
        return this.baseWidget.datepicker("setDate", date );
      },
      widget:function(){
       return this.element;
      },
      theme: 'a',
      addMobileStyle: function(){
          this.calendar.addClass("ui-shadow")
          .find( ".ui-datepicker-calendar" ).addClass( "mobile-enhanced" ).end()
          .find(".ui-datepicker-calendar a,.ui-datepicker-prev,.ui-datepicker-next").addClass("ui-btn").end()
          .find(".ui-datepicker-prev").addClass("ui-btn-icon-notext ui-btn-inline ui-corner-all ui-icon-arrow-l ui-shadow").end()
          .find(".ui-datepicker-next").addClass("ui-btn-icon-notext ui-btn-inline ui-corner-all ui-icon-arrow-r ui-shadow").end()
          .find(".ui-datepicker-header").addClass("ui-body-"+this.options.theme+" ui-corner-top").removeClass("ui-corner-all").end()
          .find(".ui-datepicker-calendar th" ).addClass("ui-bar-"+this.options.theme).end()
          .find(".ui-datepicker-calendar td" ).addClass("ui-body-"+this.options.theme).end()
          .find(".ui-datepicker-calendar a.ui-state-active").addClass("ui-btn-active").end()
          .find(".ui-datepicker-calendar a.ui-state-highlight").addClass("ui-btn-up-"+this.options.theme).end().find(".ui-state-disabled").css("opacity","1");
      }
    });

 })( jQuery );
<|endoftext|>


<commit_before>(function() {
	'use strict';

	var $ = require('zepto-browserify').Zepto,
	    _ = require('underscore')._,
	    Telepathy = require('telepathy'),
	    telepathy = new Telepathy();

	// todo: show dialog prompt for default username and master password

	// google.com: u25YhZz44m
	$('#domain').on('keydown', _.debounce(function() {
		$('#password').text(telepathy.password({
			alphabet: $('#lax').attr('checked') ?
			            Telepathy.alphabet.base62 :
			            Telepathy.alphabet.base94,
			domain: this.value
		}));
	}));

	$('.modal-close').on('click', function() {
		$(this).parents('.modal').removeClass('open');
	});

	$('.modal-bg').on('click', function(event) {
		if ($('.modal.open').length && !$(event.target).parents('.modal').length) {
			event.preventDefault();
			event.stopPropagation();
			$('.modal.open').removeClass('open');
		}
	});

	$('#open-settings').on('click', function(event) {
		if (!$('.modal.open').length) {
			event.preventDefault();
			event.stopPropagation();
			$('#settings').addClass('open');
		}
	});

	// Disable scroll if iOS standalone is happening
	$(document).bind('touchmove', function(event) {
		if (!window.navigator.standalone)
			return;

		event.preventDefault();
		return false;
	});

	var UI = {
		settings: {
		},

		save: function() {
			localStorage.telepathyWeb = JSON.stringify({
				settings: this.settings
			});
		},

		load: function() {
			var data = JSON.parse(localStorage.telepathyWeb),
			    that = this;

			if (!data) return;

			if (typeof data.settings != 'object') {
				Object.keys(data.settings).forEach(function(key) {
					that.settings[key] = data.settings[key];
				});
			}
		}
	};

	UI.load();
})();
<commit_msg>Make touchmove blocking only bind in standalone<commit_after>(function() {
	'use strict';

	var $ = require('zepto-browserify').Zepto,
	    _ = require('underscore')._,
	    Telepathy = require('telepathy'),
	    telepathy = new Telepathy();

	// todo: show dialog prompt for default username and master password

	// google.com: u25YhZz44m
	$('#domain').on('keydown', _.debounce(function() {
		$('#password').text(telepathy.password({
			alphabet: $('#lax').attr('checked') ?
			            Telepathy.alphabet.base62 :
			            Telepathy.alphabet.base94,
			domain: this.value
		}));
	}));

	$('.modal-close').on('click', function() {
		$(this).parents('.modal').removeClass('open');
	});

	$('.modal-bg').on('click', function(event) {
		if ($('.modal.open').length && !$(event.target).parents('.modal').length) {
			event.preventDefault();
			event.stopPropagation();
			$('.modal.open').removeClass('open');
		}
	});

	$('#open-settings').on('click', function(event) {
		if (!$('.modal.open').length) {
			event.preventDefault();
			event.stopPropagation();
			$('#settings').addClass('open');
		}
	});

	// Disable scroll if iOS standalone is happening
	if (!window.navigator.standalone) {
		$(document).bind('touchmove', function(event) {
			event.preventDefault();
			return false;
		});
	}

	var UI = {
		settings: {
		},

		save: function() {
			localStorage.telepathyWeb = JSON.stringify({
				settings: this.settings
			});
		},

		load: function() {
			var data = JSON.parse(localStorage.telepathyWeb),
			    that = this;

			if (!data) return;

			if (typeof data.settings != 'object') {
				Object.keys(data.settings).forEach(function(key) {
					that.settings[key] = data.settings[key];
				});
			}
		}
	};

	UI.load();
})();
<|endoftext|>


<commit_before>'use strict'

var _ = require('lodash');
var Promise = require('bluebird');
var Sequelize = require('sequelize');
var crypto = require('crypto');

var commonFields = {
  //Common form fields
  latitude: {
    type: 'num',
    required: true,
    uniqueHash: true,
  },
  longitude: {
    type: 'num',
    required: true,
    uniqueHash: true,
  },
  observationDateTime: {
    type: 'timestamp',
    required: true,
    uniqueHash: true,
  },
  monitoringCode: {
    type: 'text',
    required: true
  },
  endDateTime: {
    type: 'timestamp',
    required: true
  },
  startDateTime: {
    type: 'timestamp',
    required: true
  },
  observers: {
    type: 'text'
  },
  rain: {
    type: 'choice',
    relation: {
      model: 'nomenclature',
      filter: {type: 'main_rain'}
    }
  },
  temperature: 'num',
  windDirection: {
    type: 'choice',
    relation: {
      model: 'nomenclature',
      filter: {type: 'main_wind_direction'}
    }
  },
  windSpeed: {
    type: 'choice',
    relation: {
      model: 'nomenclature',
      filter: {type: 'main_wind_force'}
    }
  },
  cloudiness: {
    type: 'choice',
    relation: {
      model: 'nomenclature',
      filter: {type: 'main_cloud_level'}
    }
  },
  cloudsType: 'text',
  visibility: '+num',
  mto: 'text',
  notes: 'text',
  threats: {
    type: 'multi',
    relation: {
      model: 'nomenclature',
      filter: {type: 'main_threats'}
    }
  },
  pictures: 'json',
  track: 'text',

  //Internal fields
  user: {
    type: 'choice',
    required: true,
    uniqueHash: true,
    relation: {
      model: 'user'
    }
  },
  createdAt: {
    type: 'timestamp',
    required: true
  },
  updatedAt: {
    type: 'timestamp',
    required: true
  },
  imported: 'int'

};

module.exports = Model;

function Model(modelName_, fields_, foreignKeyDefs) {
  if (!(this instanceof Model)) return new Model(modelName_, fields_, foreignKeyDefs);
  var fields = _.extend(fields_, commonFields);
  var schema = generateSchema(fields);
  var modelName = modelName_;
  var modelLoweredName = modelName.substr(0, 1).toLowerCase() + modelName.substr(1);
  var foreignKeys = foreignKeyDefs;

  this.getFields = function () {
    return fields;
  };

  this.getSchema = function () {
    return schema;
  };

  this.getEditInputs = function () {
    var editInputs = {id: {required: true}};
    for (var prop in fields) {
      if (prop == 'createdAt' || prop == 'updatedAt' || prop == 'imported')
        continue;
      editInputs[prop] = {};
    }

    return editInputs;
  };

  this.getInsertInputs = function () {
    var insertInputs = {};
    for (var prop in fields) {
      if (prop == 'createdAt' || prop == 'updatedAt' || prop == 'imported')
        continue;
      insertInputs[prop] = {required: fields[prop].required && prop != 'user'};
    }

    return insertInputs;
  };

  this.getModelDefinition = function (sequelize, DataTypes) {
    var modelFieldDef = _.cloneDeep(schema);
    delete modelFieldDef.createdAt;
    delete modelFieldDef.updatedAt;

    var model = sequelize.define(modelName, modelFieldDef, {
      freezeTableName: true,
      indexes: [
        {fields: ['species']},
        {fields: ['userId']}
      ],
      classMethods: {
        associate: function (models) {
          if (!foreignKeys || foreignKeys.length <= 0)
            return;
          for (var i = 0; i < foreignKeys.length; i += 1) {
            var foreignKey = foreignKeys[i];
            models[modelLoweredName].belongsTo(models[foreignKey.targetModelName], {
              as: foreignKey.as,
              foreignKey: foreignKey.foreignKey,
              targetKey: foreignKey.targetKey
            });
          }
        }
      },
      instanceMethods: {
        calculateHash: Model.generateCalcHash(fields),
        apiData: function (api) {
          var data = {};
          var self = this;
          return Promise.props(_.mapValues(fields, function (field, name) {
            if (_.isString(field)) field = {type: field};
            switch (field.type) {
              case 'multi':
              {
                switch (field.relation.model) {
                  case 'nomenclature':
                  {
                    var res = [];
                    var bg = self[name + 'Bg'] && self[name + 'Bg'].split('|').map(function (val) {
                        return val.trim();
                      }) || [];
                    var en = self[name + 'En'] && self[name + 'En'].split('|').map(function (val) {
                        return val.trim();
                      }) || [];
                    while (bg.length && en.length) {
                      res.push({
                        label: {
                          bg: bg.shift(),
                          en: en.shift()
                        }
                      });
                    }
                    return res;
                  }
                  default:
                    return Promise.reject(new Error('[' + name + '] Unhandled relation model ' + field.relation.model));
                }
              }
              case 'choice':
              {
                switch (field.relation.model) {
                  case 'nomenclature':
                  {
                    return (self[name + 'Bg'] || self[name + 'En']) && {
                        label: {
                          bg: self[name + 'Bg'],
                          en: self[name + 'En']
                        }
                      } || null;
                  }
                  case 'species':
                  {
                    return self[name];
                  }
                  case 'user':
                  {
                    return self[name + 'Id'];
                  }
                  default:
                    return Promise.reject(new Error('[' + name + '] Unhandled relation model ' + field.relation.model));
                }
              }
              case 'json':
              {
                return self[name] && JSON.parse(self[name]);
              }
              default:
                return self[name];
            }
          })).then(function (data) {
            data.id = self.id;
            data.createdAt = self.createdAt;
            data.updatedAt = self.updatedAt;
            return data;
          });
        },

        apiUpdate: function (data) {
          var self = this;

          _.forEach(fields, function (field, name) {
            if (_.isString(field)) field = {type: field};
            switch (field.type) {
              case 'multi':
              {
                switch (field.relation.model) {
                  case 'nomenclature':
                  {
                    if (!_.has(data, name)) return;

                    var val = data[name];

                    if (!val) {
                      self[name + 'Bg'] = null;
                      self[name + 'En'] = null;
                    }
                    if (!_.isArray(val)) val = [val];
                    self[name + 'Bg'] = _.reduce(val, function (sum, v) {
                      return sum + (sum && ' | ' || '') + v.label.bg;
                    }, '');
                    self[name + 'En'] = _.reduce(val, function (sum, v) {
                      return sum + (sum && ' | ' || '') + v.label.en;
                    }, '');

                    break;
                  }
                  default:
                    throw new Error('[' + name + '] Unsupported relation model ' + field.relation.model);
                }
                break;
              }
              case 'choice':
              {
                switch (field.relation.model) {
                  case 'nomenclature':
                  {
                    if (!_.has(data, name) || !data[name]) return;

                    console.log('saving nomenclature ' + name);
                    self[name + 'Bg'] = data[name].label.bg;
                    self[name + 'En'] = data[name].label.en;
                    break;
                  }
                  case 'species':
                  {
                    if (!_.has(data, name)) return;

                    self[name] = data[name];
                    break;
                  }
                  case 'user':
                  {
                    if (!_.has(data, name)) return;

                    self[name + 'Id'] = data[name];
                    break;
                  }
                  default:
                  {
                    throw new Error('[' + name + '] Unsupported relation model ' + field.relation.model);
                  }
                }
                break;
              }
              case 'json':
                if (!_.has(data, name)) return;

                self[name] = JSON.stringify(data[name]);
                break;
              default:
                if (!_.has(data, name)) return;

                self[name] = data[name];
                break;
            }
          });

          return this;
        }
      }
    });

    ['beforeCreate', 'beforeUpdate', 'beforeSync', 'beforeSave'].forEach(function(hook){
      model.hook(hook, function(instance) {
        console.log('hook', hook, typeof this, typeof instance);
      });
      model.hook(hook, updateHash);
    });

    return model;

    function updateHash(instance) {
      instance.hash = instance.calculateHash();
    }
  };

  return this;
}

function generateSchema(fields, resultObj) {
  var fieldsDef = resultObj || {};

  if (!('hash' in fieldsDef)) {
    fieldsDef.hash = Sequelize.TEXT;
  }

  _.forEach(fields, function (field, name) {
    if (_.isString(field)) {
      field = {type: field};
    }

    var fd = {
      allowNull: !field.required
    };

    switch (field.type) {
      case 'multi':
      case 'choice':
      {
        switch (field.relation.model) {
          case 'nomenclature':
          {
            fieldsDef[name + 'Bg'] = _.extend({
              type: Sequelize.TEXT
            }, fd);
            fieldsDef[name + 'En'] = _.extend({
              type: Sequelize.TEXT
            }, fd);
            break;
          }
          case 'species':
          {
            fieldsDef[name] = _.extend({
              type: Sequelize.TEXT
            }, fd);
            break;
          }
          case 'zone':
          {
            fieldsDef[name + 'Id'] = _.extend({
              type: field.type === 'multi'
                ? Sequelize.TEXT
                : Sequelize.STRING(10)
            }, fd);
            break;
          }
          case 'user':
          {
            fieldsDef[name + 'Id'] = _.extend({
              type: field.type === 'multi'
                ? Sequelize.TEXT
                : Sequelize.INTEGER
            });
            break;
          }
          default:
            throw new Error('[' + name + '] Unknown relation model ' + field.relation.model);
        }
        break;
      }
      case 'timestamp':
      {
        fieldsDef[name] = _.extend({
          type: Sequelize.DATE
        }, fd);
        break;
      }
      case 'float':
      case '+num':
      case 'num':
      {
        fieldsDef[name] = _.extend({
          type: Sequelize.FLOAT
        }, fd);
        break;
      }
      case '+int':
      case 'int':
      {
        fieldsDef[name] = _.extend({
          type: Sequelize.INTEGER
        }, fd);
        break;
      }
      case 'text':
      {
        fieldsDef[name] = _.extend({
          type: Sequelize.TEXT
        }, fd);
        break;
      }
      case 'boolean':
      {
        fieldsDef[name] = _.extend({
          type: Sequelize.BOOLEAN
        }, fd);
        break;
      }
      case 'json':
      {
        fieldsDef[name] = _.extend({
          type: Sequelize.TEXT
        }, fd);
        break;
      }
      default:
        throw new Error('[' + name + '] Unknown field type ' + field.type);

    }
  });
  return fieldsDef;
}

function generateCalcHash(fields) {
  var hashFields = [];

  var schemaFields = Model.generateSchema(_.pickBy(fields, function(field){
    return field.uniqueHash;
  }));
  _.forOwn(schemaFields, function(def, key) {
    if (key !== 'hash') hashFields.push(key);
  });
  schemaFields = null;
  if (!hashFields.length) throw new Error('no hash fields defined!');

  return function() {
    var hash = crypto.createHash('sha256');
    hash.update(JSON.stringify(_.pick(this, hashFields)));
    return hash.digest('hex');
  }

}

Model.generateSchema = generateSchema;
Model.generateCalcHash = generateCalcHash;
<commit_msg>allow clearing of single-choice fields<commit_after>'use strict'

var _ = require('lodash');
var Promise = require('bluebird');
var Sequelize = require('sequelize');
var crypto = require('crypto');

var commonFields = {
  //Common form fields
  latitude: {
    type: 'num',
    required: true,
    uniqueHash: true,
  },
  longitude: {
    type: 'num',
    required: true,
    uniqueHash: true,
  },
  observationDateTime: {
    type: 'timestamp',
    required: true,
    uniqueHash: true,
  },
  monitoringCode: {
    type: 'text',
    required: true
  },
  endDateTime: {
    type: 'timestamp',
    required: true
  },
  startDateTime: {
    type: 'timestamp',
    required: true
  },
  observers: {
    type: 'text'
  },
  rain: {
    type: 'choice',
    relation: {
      model: 'nomenclature',
      filter: {type: 'main_rain'}
    }
  },
  temperature: 'num',
  windDirection: {
    type: 'choice',
    relation: {
      model: 'nomenclature',
      filter: {type: 'main_wind_direction'}
    }
  },
  windSpeed: {
    type: 'choice',
    relation: {
      model: 'nomenclature',
      filter: {type: 'main_wind_force'}
    }
  },
  cloudiness: {
    type: 'choice',
    relation: {
      model: 'nomenclature',
      filter: {type: 'main_cloud_level'}
    }
  },
  cloudsType: 'text',
  visibility: '+num',
  mto: 'text',
  notes: 'text',
  threats: {
    type: 'multi',
    relation: {
      model: 'nomenclature',
      filter: {type: 'main_threats'}
    }
  },
  pictures: 'json',
  track: 'text',

  //Internal fields
  user: {
    type: 'choice',
    required: true,
    uniqueHash: true,
    relation: {
      model: 'user'
    }
  },
  createdAt: {
    type: 'timestamp',
    required: true
  },
  updatedAt: {
    type: 'timestamp',
    required: true
  },
  imported: 'int'

};

module.exports = Model;

function Model(modelName_, fields_, foreignKeyDefs) {
  if (!(this instanceof Model)) return new Model(modelName_, fields_, foreignKeyDefs);
  var fields = _.extend(fields_, commonFields);
  var schema = generateSchema(fields);
  var modelName = modelName_;
  var modelLoweredName = modelName.substr(0, 1).toLowerCase() + modelName.substr(1);
  var foreignKeys = foreignKeyDefs;

  this.getFields = function () {
    return fields;
  };

  this.getSchema = function () {
    return schema;
  };

  this.getEditInputs = function () {
    var editInputs = {id: {required: true}};
    for (var prop in fields) {
      if (prop == 'createdAt' || prop == 'updatedAt' || prop == 'imported')
        continue;
      editInputs[prop] = {};
    }

    return editInputs;
  };

  this.getInsertInputs = function () {
    var insertInputs = {};
    for (var prop in fields) {
      if (prop == 'createdAt' || prop == 'updatedAt' || prop == 'imported')
        continue;
      insertInputs[prop] = {required: fields[prop].required && prop != 'user'};
    }

    return insertInputs;
  };

  this.getModelDefinition = function (sequelize, DataTypes) {
    var modelFieldDef = _.cloneDeep(schema);
    delete modelFieldDef.createdAt;
    delete modelFieldDef.updatedAt;

    var model = sequelize.define(modelName, modelFieldDef, {
      freezeTableName: true,
      indexes: [
        {fields: ['species']},
        {fields: ['userId']}
      ],
      classMethods: {
        associate: function (models) {
          if (!foreignKeys || foreignKeys.length <= 0)
            return;
          for (var i = 0; i < foreignKeys.length; i += 1) {
            var foreignKey = foreignKeys[i];
            models[modelLoweredName].belongsTo(models[foreignKey.targetModelName], {
              as: foreignKey.as,
              foreignKey: foreignKey.foreignKey,
              targetKey: foreignKey.targetKey
            });
          }
        }
      },
      instanceMethods: {
        calculateHash: Model.generateCalcHash(fields),
        apiData: function (api) {
          var data = {};
          var self = this;
          return Promise.props(_.mapValues(fields, function (field, name) {
            if (_.isString(field)) field = {type: field};
            switch (field.type) {
              case 'multi':
              {
                switch (field.relation.model) {
                  case 'nomenclature':
                  {
                    var res = [];
                    var bg = self[name + 'Bg'] && self[name + 'Bg'].split('|').map(function (val) {
                        return val.trim();
                      }) || [];
                    var en = self[name + 'En'] && self[name + 'En'].split('|').map(function (val) {
                        return val.trim();
                      }) || [];
                    while (bg.length && en.length) {
                      res.push({
                        label: {
                          bg: bg.shift(),
                          en: en.shift()
                        }
                      });
                    }
                    return res;
                  }
                  default:
                    return Promise.reject(new Error('[' + name + '] Unhandled relation model ' + field.relation.model));
                }
              }
              case 'choice':
              {
                switch (field.relation.model) {
                  case 'nomenclature':
                  {
                    return (self[name + 'Bg'] || self[name + 'En']) && {
                        label: {
                          bg: self[name + 'Bg'],
                          en: self[name + 'En']
                        }
                      } || null;
                  }
                  case 'species':
                  {
                    return self[name];
                  }
                  case 'user':
                  {
                    return self[name + 'Id'];
                  }
                  default:
                    return Promise.reject(new Error('[' + name + '] Unhandled relation model ' + field.relation.model));
                }
              }
              case 'json':
              {
                return self[name] && JSON.parse(self[name]);
              }
              default:
                return self[name];
            }
          })).then(function (data) {
            data.id = self.id;
            data.createdAt = self.createdAt;
            data.updatedAt = self.updatedAt;
            return data;
          });
        },

        apiUpdate: function (data) {
          var self = this;

          _.forEach(fields, function (field, name) {
            if (_.isString(field)) field = {type: field};
            switch (field.type) {
              case 'multi':
              {
                switch (field.relation.model) {
                  case 'nomenclature':
                  {
                    if (!_.has(data, name)) return;

                    var val = data[name];

                    if (!val) {
                      self[name + 'Bg'] = null;
                      self[name + 'En'] = null;
                    }
                    if (!_.isArray(val)) val = [val];
                    self[name + 'Bg'] = _.reduce(val, function (sum, v) {
                      return sum + (sum && ' | ' || '') + v.label.bg;
                    }, '');
                    self[name + 'En'] = _.reduce(val, function (sum, v) {
                      return sum + (sum && ' | ' || '') + v.label.en;
                    }, '');

                    break;
                  }
                  default:
                    throw new Error('[' + name + '] Unsupported relation model ' + field.relation.model);
                }
                break;
              }
              case 'choice':
              {
                switch (field.relation.model) {
                  case 'nomenclature':
                  {
                    if (!_.has(data, name)) return;

                    console.log('saving nomenclature ' + name);
                    self[name + 'Bg'] = data[name] && data[name].label.bg;
                    self[name + 'En'] = data[name] && data[name].label.en;
                    break;
                  }
                  case 'species':
                  {
                    if (!_.has(data, name)) return;

                    self[name] = data[name];
                    break;
                  }
                  case 'user':
                  {
                    if (!_.has(data, name)) return;

                    self[name + 'Id'] = data[name];
                    break;
                  }
                  default:
                  {
                    throw new Error('[' + name + '] Unsupported relation model ' + field.relation.model);
                  }
                }
                break;
              }
              case 'json':
                if (!_.has(data, name)) return;

                self[name] = JSON.stringify(data[name]);
                break;
              default:
                if (!_.has(data, name)) return;

                self[name] = data[name];
                break;
            }
          });

          return this;
        }
      }
    });

    ['beforeCreate', 'beforeUpdate', 'beforeSync', 'beforeSave'].forEach(function(hook){
      model.hook(hook, function(instance) {
        console.log('hook', hook, typeof this, typeof instance);
      });
      model.hook(hook, updateHash);
    });

    return model;

    function updateHash(instance) {
      instance.hash = instance.calculateHash();
    }
  };

  return this;
}

function generateSchema(fields, resultObj) {
  var fieldsDef = resultObj || {};

  if (!('hash' in fieldsDef)) {
    fieldsDef.hash = Sequelize.TEXT;
  }

  _.forEach(fields, function (field, name) {
    if (_.isString(field)) {
      field = {type: field};
    }

    var fd = {
      allowNull: !field.required
    };

    switch (field.type) {
      case 'multi':
      case 'choice':
      {
        switch (field.relation.model) {
          case 'nomenclature':
          {
            fieldsDef[name + 'Bg'] = _.extend({
              type: Sequelize.TEXT
            }, fd);
            fieldsDef[name + 'En'] = _.extend({
              type: Sequelize.TEXT
            }, fd);
            break;
          }
          case 'species':
          {
            fieldsDef[name] = _.extend({
              type: Sequelize.TEXT
            }, fd);
            break;
          }
          case 'zone':
          {
            fieldsDef[name + 'Id'] = _.extend({
              type: field.type === 'multi'
                ? Sequelize.TEXT
                : Sequelize.STRING(10)
            }, fd);
            break;
          }
          case 'user':
          {
            fieldsDef[name + 'Id'] = _.extend({
              type: field.type === 'multi'
                ? Sequelize.TEXT
                : Sequelize.INTEGER
            });
            break;
          }
          default:
            throw new Error('[' + name + '] Unknown relation model ' + field.relation.model);
        }
        break;
      }
      case 'timestamp':
      {
        fieldsDef[name] = _.extend({
          type: Sequelize.DATE
        }, fd);
        break;
      }
      case 'float':
      case '+num':
      case 'num':
      {
        fieldsDef[name] = _.extend({
          type: Sequelize.FLOAT
        }, fd);
        break;
      }
      case '+int':
      case 'int':
      {
        fieldsDef[name] = _.extend({
          type: Sequelize.INTEGER
        }, fd);
        break;
      }
      case 'text':
      {
        fieldsDef[name] = _.extend({
          type: Sequelize.TEXT
        }, fd);
        break;
      }
      case 'boolean':
      {
        fieldsDef[name] = _.extend({
          type: Sequelize.BOOLEAN
        }, fd);
        break;
      }
      case 'json':
      {
        fieldsDef[name] = _.extend({
          type: Sequelize.TEXT
        }, fd);
        break;
      }
      default:
        throw new Error('[' + name + '] Unknown field type ' + field.type);

    }
  });
  return fieldsDef;
}

function generateCalcHash(fields) {
  var hashFields = [];

  var schemaFields = Model.generateSchema(_.pickBy(fields, function(field){
    return field.uniqueHash;
  }));
  _.forOwn(schemaFields, function(def, key) {
    if (key !== 'hash') hashFields.push(key);
  });
  schemaFields = null;
  if (!hashFields.length) throw new Error('no hash fields defined!');

  return function() {
    var hash = crypto.createHash('sha256');
    hash.update(JSON.stringify(_.pick(this, hashFields)));
    return hash.digest('hex');
  }

}

Model.generateSchema = generateSchema;
Model.generateCalcHash = generateCalcHash;
<|endoftext|>


<commit_before>function pageViewModel(gvm) {
    // projecttitle
    gvm.projecttitle = ko.observable("");
    gvm.projectId = $("#projectHeader").data('value');

    // Page specific i18n bindings
    gvm.title = ko.computed(function (){i18n.setLocale(gvm.lang()); return gvm.app() + ' - ' + i18n.__("ProjectTitle") + ": " + gvm.projecttitle();}, gvm);
    gvm.pageHeader = ko.observable("Project");
    gvm.projectname = ko.computed(function(){i18n.setLocale(gvm.lang()); return i18n.__("ProjectName");}, gvm);

    gvm.getProjectInfo = function() {
        $.getJSON('/api/project/' + gvm.projectId, function(data) {
            gvm.pageHeader(data[0].code + ' - ' + data[0].name);
        });
    };

    gvm.getAllData = function() {
         $.getJSON('/api/project/getAllData/' + gvm.projectId, function(data) {
            console.log(data);
         });
    }
}

/*function fetchProjectStructure() {
    viewModel.clearStructure();

    $.getJSON("/api/projectstructure/" + projectid, function(data){
        $.each(data, function(i, item){
            var competence = viewModel.updateCompetence(item.id, item.code, item.description, item.max, item.weight);

            $.each(item.subcompetences, function(i, subcomp){
                var subcompetence = new SubCompetence(competence, subcomp.id, subcomp.code, subcomp.description, subcomp.weight);
                competence.subcompetences.push(subcompetence);

                $.each(subcomp.indicators, function(i, indic){
                    subcompetence.indicators.push(new Indicator(subcompetence, indic.id, indic.name, indic.description));
                });
            });
        })
    });
}*/

<commit_msg>assess score<commit_after>function pageViewModel(gvm) {
    // projecttitle
    gvm.projecttitle = ko.observable("");
    gvm.projectId = $("#projectHeader").data('value');

    // Page specific i18n bindings
    gvm.title = ko.computed(function (){i18n.setLocale(gvm.lang()); return gvm.app() + ' - ' + i18n.__("ProjectTitle") + ": " + gvm.projecttitle();}, gvm);
    gvm.pageHeader = ko.observable("Project");
    gvm.projectname = ko.computed(function(){i18n.setLocale(gvm.lang()); return i18n.__("ProjectName");}, gvm);

    gvm.getProjectInfo = function() {
        $.getJSON('/api/project/' + gvm.projectId, function(data) {
            gvm.pageHeader(data[0].code + ' - ' + data[0].name);
        });
    };

    gvm.getAllData = function() {
         $.getJSON('/api/project/getAllData/' + gvm.projectId, function(data) {
            console.log(data);
         });
    }
}

function initPage() {
    viewModel.getProjectInfo();
    viewModel.getAllData();
}

<|endoftext|>


<commit_before>define(['../random/randInt'], function (randInt) {

    /**
     * Shuffle array items.
     */
    function shuffle(arr) {
        var results = [],
            rnd;
        if (arr == null) {
            return results;
        }

        var i = -1, len = arr.length, value;
        while (++i < len) {
            if (!i) {
                results[0] = arr[0];
            } else {
                rnd = randInt(0, i);
                results[i] = results[rnd];
                results[rnd] = arr[i];
            }
        }

        return results;
    }

    return shuffle;
});
<commit_msg>remove unused variable<commit_after>define(['../random/randInt'], function (randInt) {

    /**
     * Shuffle array items.
     */
    function shuffle(arr) {
        var results = [],
            rnd;
        if (arr == null) {
            return results;
        }

        var i = -1, len = arr.length;
        while (++i < len) {
            if (!i) {
                results[0] = arr[0];
            } else {
                rnd = randInt(0, i);
                results[i] = results[rnd];
                results[rnd] = arr[i];
            }
        }

        return results;
    }

    return shuffle;
});
<|endoftext|>


<commit_before>// ==========================================================================
// Project:   The M-Project - Mobile HTML5 Application Framework
// Copyright: (c) 2010 M-Way Solutions GmbH. All rights reserved.
// Creator:   Sebastian
// Date:      28.10.2010
// License:   Dual licensed under the MIT or GPL Version 2 licenses.
//            http://github.com/mwaylabs/The-M-Project/blob/master/MIT-LICENSE
//            http://github.com/mwaylabs/The-M-Project/blob/master/GPL-LICENSE
// ==========================================================================

M.STATE_UNDEFINED = 'state_undefined';
M.STATE_NEW = 'state_new';
M.STATE_INSYNCPOS = 'state_insyncpos';
M.STATE_INSYNCNEG = 'state_insyncneg';
M.STATE_LOCALCHANGED = 'state_localchange';
M.STATE_VALID = 'state_valid';
M.STATE_INVALID = 'state_invalid';
M.STATE_DELETED = 'state_deleted';

m_require('core/foundation/model_registry.js');

/**
 * @class
 *
 * M.Model is the prototype for every model and for every model record (a model itself is the blueprint for a model record).
 * Models hold the business data of an application respectively the application's state. It's usually the part of an application that is persisted to storage.
 * M.Model acts as the gatekeeper to storage. It uses data provider for persistence and validators to validate its records.
 *
 * @extends M.Object
 */
M.Model = M.Object.extend(
/** @scope M.Model.prototype */ {
    /**
     * The type of this object.
     *
     * @type String
     */
    type: 'M.Model',

    /**
     * The name of the model.
     *
     * @type String
     */
    name: '',

    /**
     * Unique identifier for the model record.
     *
     * Note: Unique doesn't mean that this id is a global unique ID, it is just unique
     * for records of this type of model.
     *
     * @type Number
     */
    id: null,

    /**
     * The model's record defines the properties that are semantically bound to this model:
     * e.g. a person's record is in simplest case: firstname, lastname, age.
     *
     * @type Object record
     */
    record: null,

    /**
     * Object containing all meta information for the object's properties
     * @type Object
     */
    __meta: {},

    /**
     * Manages records of this model
     * @type Object
     */
    recordManager: null,

    /**
     *
     * @param obj
     */
    records: null,

    /**
     * A constant defining the model's state. Important e.g. for syncing storage
     * @type String
     */
    state: M.STATE_UNDEFINED,

    /**
     * determines whether model shall be validated before saving to storage or not.
     * @type Boolean
     */
    usesValidation: YES,

    /**
     * The model's data provider.
     *
     * Needs a refactoring, also in connection with storageEngine.
     *
     * @type Object
     */
    dataProvider: null,

    /**
     * Creates a new record of the model, means an instance of the model based on the blueprint.
     * You pass the object's specific attributes to it as an object.
     *
     * @param {Object} obj The specific attributes as an object, e.g. {firstname: 'peter', lastname ='fox'}
     * @returns {Object} The model record with the passed properties set. State depends on newly creation or fetch from storage: if
     * from storage then state is M.STATE_NEW or 'state_new', if fetched from database then it is M.STATE_VALID or 'state_valid'
     */
    createRecord: function(obj) {
        var modelRecord = this.extend({
            id: obj.id ? obj.id : M.Application.modelRegistry.getNextId(this.name),
            record: obj
        });
        modelRecord.state = obj.state ? obj.state : M.STATE_NEW;
        delete obj.state;
        delete modelRecord.record.id;
        return modelRecord;
    },

    /**
     * Create defines a new model blueprint. It is passed an object with the model's attributes and the model's business logic
     * and after it the type of data provider to use.
     *
     * @param {Object} obj An object defining the model's
     * @param {Object} dp The data provider to use, e. g. M.LocalStorageProvider
     * @returns {Object} The model blueprint: acts as blueprint to all records created with @link M.Model#createRecord
     */
    create: function(obj, dp) {
        var model = M.Model.extend({
            __meta: {},
            name: obj.__name__,
            dataProvider: dp,
            usesValidation: obj.usesValidation === null || obj.usesValidation === undefined ? this.usesValidation : obj.usesValidation
        });
        delete obj.__name__;
        delete obj.usesValidation;

        for(var prop in obj) {
            if(typeof(obj[prop]) === 'function') {
                model[prop] = obj[prop];
            } else if(obj[prop].type === 'M.ModelAttribute') {
                model.__meta[prop] = obj[prop];
            }
        }

        model.recordManager = M.RecordManager.extend({});
        model.records = model.recordManager.records;

        M.Application.modelRegistry.register(model.name);

        /* Re-set the just registered model's id, if there is a value stored */
        /* Model Registry stores the current id of a model type into localStorage */
        var id = localStorage.getItem(model.name);
        if(id) {
            M.Application.modelRegistry.setId(model.name, parseInt(id));
        }
        return model;
    },

    /**
     * Returns a M.ModelAttribute object to map an attribute in our record.
     *
     * @param {String} type type of the attribute
     * @param {Object} opts options for the attribute, like required flag and validators array
     * @returns {Object} An M.ModelAttribute object configured with the type and options passed to the function.
     */
    attr: function(type, opts) {
        return M.ModelAttribute.attr(type, opts);
    },

    /*
     * get and set methods for encapsulated attribute access
     */

    /**
     * Get attribute propName from model
     * @param {String} propName the name of the property whose value shall be returned
     * @returns {Object|String} value of property
     */
    get: function(propName) {
        return this.record[propName];
    },

    /**
     * Set attribute propName of model with value val
     * @param {String} propName the name of the property whose value shall be set
     * @param {String|Object} val the new value
     */
    set: function(propName, val) {
        this.record[propName] = val;
    },

    /**
     * Validates the model, means calling validate for each property.
     * @returns {Boolean} Indicating whether this record is valid (YES|true) or not (NO|false).
     */
    validate: function() {
        var isValid = YES;
        var validationErrorOccured = NO;
        /* clear validation error buffer before validation */
        M.Validator.clearErrorBuffer();

        /*
        * validationBasis depends on the state of the model: if the model is in state NEW, all properties (__meta includes all)
        * shall be considered for validation. if model is in another state, the model's record is used. example: the model is loaded from
        * a database with only two properties included (select name, age FROM...). record now only contains these two properties but __meta
        * still has all properties listed. models are valid if loaded from database so when saved again only the loaded properties need to get
        * validated because all others have not been touched. that's why then record is used.
        * */
        var validationBasis = this.state === M.STATE_NEW ? this.__meta : this.record;

        for (var i in validationBasis) {
            var prop = this.__meta[i];
            var obj = {
                value: this.record[i],
                modelId: this.name + '_' + this.id,
                property: i
            };
            if (!prop.validate(obj)) {
                isValid = NO;
            }
        }
        /* set state of model */
        /*if(!isValid) {
            this.state = M.STATE_INVALID;
        } else {
            this.state = M.STATE_VALID;
        }*/
        return isValid;
    },

    /* CRUD Methods below */
    /**
     * Calls the corresponding find() of the data provider to fetch data based on the passed query or key.
     *
     * @param {Object} obj The param object with query or key and callbacks.
     * @returns {Boolean|Object} Depends on data provider used. When WebSQL used, a boolean is returned, the find result is returned asynchronously,
     * because the call itself is asynchronous. If LocalStorage is used, the result of the query is returned.
     */
    find: function(obj){
        /* extends the given obj with self as model property in obj */
        return this.dataProvider.find( $.extend(obj, {model: this}) );
    },

    /**
     * Create or update a record in storage if it is valid (first check this).
     *
     * @param {Object} obj The param object with query and callbacks.
     * @returns {Boolean} The result of the data provider function call. Is a boolean. With LocalStorage used, it indicates if the save operation was successful.
     * When WebSQL is used, the result of the save operation returns asynchronously. The result then is just the standard result returned by the web sql provider's save method
     * which does not necessarily indicate whether the operation was successful, because the operation is asynchronous, means the operation's end is not predictable.
     */
    save: function(obj) {
        obj = obj ? obj: {};
        if(!this.id) {
            return NO;
        }
        var isValid = YES;

        if(this.usesValidation) {
            isValid = this.validate();
        }

        if(isValid) {
            return this.dataProvider.save($.extend(obj, {model: this}));
        }
    },

    /**
     * Delete a record in storage.
     * @returns {Boolean} Indicating whether deletion was successful or not (only with synchronous data providers, e.g. LocalStorage). When asynchronous data providers
     * are used, e.g. WebSQL provider the real result comes asynchronous and here just the result of the del() function call of the @link M.WebSqlProvider is used.
     */
    del: function() {
        if(!this.id) {
            return NO;
        }

       var isDel = this.dataProvider.del({model: this});
        if(isDel) {
            this.state = M.STATE_DELETED;
            return YES
        }

    }

});<commit_msg>createRecord now deletes passed properties to record object that are not part of __meta<commit_after>// ==========================================================================
// Project:   The M-Project - Mobile HTML5 Application Framework
// Copyright: (c) 2010 M-Way Solutions GmbH. All rights reserved.
// Creator:   Sebastian
// Date:      28.10.2010
// License:   Dual licensed under the MIT or GPL Version 2 licenses.
//            http://github.com/mwaylabs/The-M-Project/blob/master/MIT-LICENSE
//            http://github.com/mwaylabs/The-M-Project/blob/master/GPL-LICENSE
// ==========================================================================

M.STATE_UNDEFINED = 'state_undefined';
M.STATE_NEW = 'state_new';
M.STATE_INSYNCPOS = 'state_insyncpos';
M.STATE_INSYNCNEG = 'state_insyncneg';
M.STATE_LOCALCHANGED = 'state_localchange';
M.STATE_VALID = 'state_valid';
M.STATE_INVALID = 'state_invalid';
M.STATE_DELETED = 'state_deleted';

m_require('core/foundation/model_registry.js');

/**
 * @class
 *
 * M.Model is the prototype for every model and for every model record (a model itself is the blueprint for a model record).
 * Models hold the business data of an application respectively the application's state. It's usually the part of an application that is persisted to storage.
 * M.Model acts as the gatekeeper to storage. It uses data provider for persistence and validators to validate its records.
 *
 * @extends M.Object
 */
M.Model = M.Object.extend(
/** @scope M.Model.prototype */ {
    /**
     * The type of this object.
     *
     * @type String
     */
    type: 'M.Model',

    /**
     * The name of the model.
     *
     * @type String
     */
    name: '',

    /**
     * Unique identifier for the model record.
     *
     * Note: Unique doesn't mean that this id is a global unique ID, it is just unique
     * for records of this type of model.
     *
     * @type Number
     */
    id: null,

    /**
     * The model's record defines the properties that are semantically bound to this model:
     * e.g. a person's record is in simplest case: firstname, lastname, age.
     *
     * @type Object record
     */
    record: null,

    /**
     * Object containing all meta information for the object's properties
     * @type Object
     */
    __meta: {},

    /**
     * Manages records of this model
     * @type Object
     */
    recordManager: null,

    /**
     *
     * @param obj
     */
    records: null,

    /**
     * A constant defining the model's state. Important e.g. for syncing storage
     * @type String
     */
    state: M.STATE_UNDEFINED,

    /**
     * determines whether model shall be validated before saving to storage or not.
     * @type Boolean
     */
    usesValidation: YES,

    /**
     * The model's data provider.
     *
     * Needs a refactoring, also in connection with storageEngine.
     *
     * @type Object
     */
    dataProvider: null,

    /**
     * Creates a new record of the model, means an instance of the model based on the blueprint.
     * You pass the object's specific attributes to it as an object.
     *
     * @param {Object} obj The specific attributes as an object, e.g. {firstname: 'peter', lastname ='fox'}
     * @returns {Object} The model record with the passed properties set. State depends on newly creation or fetch from storage: if
     * from storage then state is M.STATE_NEW or 'state_new', if fetched from database then it is M.STATE_VALID or 'state_valid'
     */
    createRecord: function(obj) {
        var modelRecord = this.extend({
            id: obj.id ? obj.id : M.Application.modelRegistry.getNextId(this.name),
            record: obj /* properties that are here added to record but are not part of __meta are deleted later (see below) */
        });

        modelRecord.state = obj.state ? obj.state : M.STATE_NEW;
        delete obj.state;
        delete modelRecord.record.id;

        /* if record contains properties that are not part of __meta (means that are not defined in the model blueprint) delete them */
        for(var i in modelRecord.record) {
            if(!modelRecord.__meta.hasOwnProperty(i)) {
                delete modelRecord.record[i];
            }
        }

        this.recordManager.add(modelRecord);
        return modelRecord;
    },

    /**
     * Create defines a new model blueprint. It is passed an object with the model's attributes and the model's business logic
     * and after it the type of data provider to use.
     *
     * @param {Object} obj An object defining the model's
     * @param {Object} dp The data provider to use, e. g. M.LocalStorageProvider
     * @returns {Object} The model blueprint: acts as blueprint to all records created with @link M.Model#createRecord
     */
    create: function(obj, dp) {
        var model = M.Model.extend({
            __meta: {},
            name: obj.__name__,
            dataProvider: dp,
            usesValidation: obj.usesValidation === null || obj.usesValidation === undefined ? this.usesValidation : obj.usesValidation
        });
        delete obj.__name__;
        delete obj.usesValidation;

        for(var prop in obj) {
            if(typeof(obj[prop]) === 'function') {
                model[prop] = obj[prop];
            } else if(obj[prop].type === 'M.ModelAttribute') {
                model.__meta[prop] = obj[prop];
            }
        }

        model.recordManager = M.RecordManager.extend({});
        model.records = model.recordManager.records;

        M.Application.modelRegistry.register(model.name);

        /* Re-set the just registered model's id, if there is a value stored */
        /* Model Registry stores the current id of a model type into localStorage */
        var id = localStorage.getItem(model.name);
        if(id) {
            M.Application.modelRegistry.setId(model.name, parseInt(id));
        }
        return model;
    },

    /**
     * Returns a M.ModelAttribute object to map an attribute in our record.
     *
     * @param {String} type type of the attribute
     * @param {Object} opts options for the attribute, like required flag and validators array
     * @returns {Object} An M.ModelAttribute object configured with the type and options passed to the function.
     */
    attr: function(type, opts) {
        return M.ModelAttribute.attr(type, opts);
    },

    /*
     * get and set methods for encapsulated attribute access
     */

    /**
     * Get attribute propName from model
     * @param {String} propName the name of the property whose value shall be returned
     * @returns {Object|String} value of property
     */
    get: function(propName) {
        return this.record[propName];
    },

    /**
     * Set attribute propName of model with value val
     * @param {String} propName the name of the property whose value shall be set
     * @param {String|Object} val the new value
     */
    set: function(propName, val) {
        this.record[propName] = val;
        this.__meta[propName].isUpdated = YES;
    },

    /**
     * Validates the model, means calling validate for each property.
     * @returns {Boolean} Indicating whether this record is valid (YES|true) or not (NO|false).
     */
    validate: function() {
        var isValid = YES;
        var validationErrorOccured = NO;
        /* clear validation error buffer before validation */
        M.Validator.clearErrorBuffer();

        /*
        * validationBasis depends on the state of the model: if the model is in state NEW, all properties (__meta includes all)
        * shall be considered for validation. if model is in another state, the model's record is used. example: the model is loaded from
        * a database with only two properties included (select name, age FROM...). record now only contains these two properties but __meta
        * still has all properties listed. models are valid if loaded from database so when saved again only the loaded properties need to get
        * validated because all others have not been touched. that's why then record is used.
        * */
        var validationBasis = this.state === M.STATE_NEW ? this.__meta : this.record;

        for (var i in validationBasis) {
            var prop = this.__meta[i];
            var obj = {
                value: this.record[i],
                modelId: this.name + '_' + this.id,
                property: i
            };
            if (!prop.validate(obj)) {
                isValid = NO;
            }
        }
        /* set state of model */
        /*if(!isValid) {
            this.state = M.STATE_INVALID;
        } else {
            this.state = M.STATE_VALID;
        }*/
        return isValid;
    },

    /* CRUD Methods below */
    /**
     * Calls the corresponding find() of the data provider to fetch data based on the passed query or key.
     *
     * @param {Object} obj The param object with query or key and callbacks.
     * @returns {Boolean|Object} Depends on data provider used. When WebSQL used, a boolean is returned, the find result is returned asynchronously,
     * because the call itself is asynchronous. If LocalStorage is used, the result of the query is returned.
     */
    find: function(obj){
        /* check if the record list shall be cleared (default) before new found model records are appended to the record list */
        /* TODO: needs to be placed in callback */
        obj['deleteRecordList'] = obj['deleteRecordList'] ? obj.deleteRecordList : YES;
        if(obj.deleteRecordList) {
            this.recordManager.removeAll();
        }

        /* extends the given obj with self as model property in obj */
        return this.dataProvider.find( $.extend(obj, {model: this}) );
    },

    /**
     * Create or update a record in storage if it is valid (first check this).
     *
     * @param {Object} obj The param object with query and callbacks.
     * @returns {Boolean} The result of the data provider function call. Is a boolean. With LocalStorage used, it indicates if the save operation was successful.
     * When WebSQL is used, the result of the save operation returns asynchronously. The result then is just the standard result returned by the web sql provider's save method
     * which does not necessarily indicate whether the operation was successful, because the operation is asynchronous, means the operation's end is not predictable.
     */
    save: function(obj) {
        obj = obj ? obj: {};
        if(!this.id) {
            return NO;
        }
        var isValid = YES;

        if(this.usesValidation) {
            isValid = this.validate();
        }

        if(isValid) {
            return this.dataProvider.save($.extend(obj, {model: this}));
        }
    },

    /**
     * Delete a record in storage.
     * @returns {Boolean} Indicating whether deletion was successful or not (only with synchronous data providers, e.g. LocalStorage). When asynchronous data providers
     * are used, e.g. WebSQL provider the real result comes asynchronous and here just the result of the del() function call of the @link M.WebSqlProvider is used.
     */
    del: function(obj) {
        obj = obj ? obj : {};
        if(!this.id) {
            return NO;
        }

       var isDel = this.dataProvider.del($.extend(obj, {model: this}));
        if(isDel) {
            this.state = M.STATE_DELETED;
            return YES
        }

    }

});<|endoftext|>