<commit_before>import * as PropTypes from 'prop-types'
import {Activator} from 'widget/activation/activator'
import {Button} from 'widget/button'
import {Panel, PanelButtons, PanelContent, PanelHeader} from './panel'
import {activatable} from 'widget/activation/activatable'
import {isMobile} from 'widget/userAgent'
import Label from './label'
import List from 'widget/list'
import React, {Component} from 'react'
import _ from 'lodash'
import guid from 'guid'
import moment from 'moment'
import styles from './datePicker.module.css'

const DATE_FORMAT = 'YYYY-MM-DD'

export const momentDate = date => _.isString(date) ? moment(date, DATE_FORMAT) : moment(date)

const pickDate = (date1, date2, func) => {
    date1 = momentDate(date1)
    date2 = momentDate(date2)
    if (!date1.isValid())
        return date2
    else if (!date2.isValid())
        return date1
    else
        return func(date1, date2)
}

export const maxDate = (date1, date2) => pickDate(date1, date2, moment.max)
export const minDate = (date1, date2) => pickDate(date1, date2, moment.min)
export const constrainDate = (date, min, max) => maxDate(minDate(date, max), min)

export default class DatePicker extends React.Component {
    id = 'DatePicker-' + guid()
    inputElement = React.createRef()

    render() {
        const {input, startDate, endDate, label, autoFocus} = this.props
        const date = moment(input.value, DATE_FORMAT)
        return (
            <Activator id={this.id}>
                {panel =>

                        <DatePickerPanel
                            id={this.id}
                            title={label}
                            date={date.isValid() ? date : moment(startDate, DATE_FORMAT)}
                            startDate={momentDate(startDate)}
                            endDate={momentDate(endDate)}
                            onSelect={date => {
                                const dateString = date.format(DATE_FORMAT)
                                this.inputElement.current.value = dateString
                                input.set(dateString)
                            }}/>
                        {this.renderLabel()}

                            <input
                                ref={this.inputElement}
                                defaultValue={input.value}
                                maxLength={10}
                                autoFocus={autoFocus && !isMobile()}
                                autoComplete='off'
                                autoCorrect='off'
                                autoCapitalize='off'
                                spellCheck='false'
                                className={styles.input}
                                onChange={e => this.setInput(e.target.value)}
                                onBlur={() => {
                                    this.inputElement.current.value = input.value
                                }}
                            />
                            <Button additionalClassName={styles.panelTrigger}
                                chromeless
                                icon='calendar-alt'
                                size='small'
                                onClick={() => panel.activate()}
                            />

                }
            </Activator>
        )
    }

    renderLabel() {
        const {label, tooltip, tooltipPlacement = 'top'} = this.props
        return label ? (

        ) : null
    }

    setInput(value) {
        const {input, startDate, endDate} = this.props
        const date = momentDate(value)
        const formattedDate = date.isValid()
            ? constrainDate(date, startDate, endDate).format(DATE_FORMAT)
            : momentDate(startDate).format(DATE_FORMAT)
        input.set(formattedDate)
    }
}

DatePicker.propTypes = {
    endDate: PropTypes.any.isRequired,
    input: PropTypes.object.isRequired,
    startDate: PropTypes.any.isRequired,
    autoFocus: PropTypes.any,
    label: PropTypes.string
}

class _DatePickerPanel extends React.Component {
    state = {}

    render() {
        const {title} = this.props
        const {date} = this.state
        if (!date)
            return null
        return (
            <Panel
                className={styles.panel}
                type='modal'>
                <PanelHeader
                    icon='calendar-alt'
                    title={title}/>
                <PanelContent className={styles.panelContent}>
                    {this.renderYears()}
                    {this.renderMonths()}
                    {this.renderDays()}
                </PanelContent>
                <PanelButtons onEnter={() => this.select()} onEscape={() => this.close()}>
                    <PanelButtons.Main>
                        {this.renderButtons()}
                    </PanelButtons.Main>
                </PanelButtons>
            </Panel>
        )
    }

    renderYears() {
        const {startDate, endDate} = this.props
        const {date} = this.state
        const startYear = startDate.year()
        const endYear = endDate.year()
        const selectedYear = date.year()
        const options = _.range(startYear, endYear + 1).map(year => ({label: year, value: year}))
        const selectedOption = _.find(options, ({value}) => value === selectedYear)
        return (

                <List
                    options={options}
                    selectedOption={selectedOption}
                    onSelect={option => this.updateDate('year', option.value)}
                    overScroll={true}
                />

        )
    }

    renderMonths() {
        const {date} = this.state
        const {startDate, endDate} = this.props
        const months = moment.monthsShort()
        const selectedMonth = months[date.month()]
        const firstMonthIndex = date.year() === startDate.year() ? startDate.month() : 0
        const lastMonthIndex = date.year() === endDate.year() ? endDate.month() : 11
        return (

                {months.map((month, i) =>
                    <CalendarButton
                        key={month}
                        label={month}
                        selected={month === selectedMonth}
                        className={styles.month}
                        disabled={i < firstMonthIndex || i > lastMonthIndex}
                        onClick={() => this.updateDate('month', month)}/>
                )}

        )
    }

    renderDays() {
        const {date} = this.state
        const {startDate, endDate} = this.props
        const firstOfMonth = moment(date).startOf('month')
        const firstToRender = moment(firstOfMonth).startOf('week')
        const lastOfMonth = moment(date).endOf('month')
        const firstDay = date.isSame(startDate, 'month') ? startDate.date() : 1
        const lastDate = date.isSame(endDate, 'month') ? endDate : lastOfMonth
        const lastDay = lastDate.date()
        const lastToRender = moment(lastOfMonth).endOf('week')
        const daysToRender = lastToRender.diff(firstToRender, 'days') + 1
        const indexOffset = firstOfMonth.day() - 1
        const firstIndex = firstDay + indexOffset
        const lastIndex = lastDay + indexOffset

        return (

                {moment.weekdaysShort().map(weekday =>

                )}
                {_.times(daysToRender, i => {
                    const buttonDate = moment(firstToRender).add(i, 'day')
                    const dayOfMonth = buttonDate.format('DD')
                    return (
                        <CalendarButton
                            key={i}
                            label={dayOfMonth}
                            selected={buttonDate.isSame(date, 'day')}
                            className={styles.date}
                            disabled={i < firstIndex || i > lastIndex}
                            onClick={() => this.updateDate('date', dayOfMonth)}/>
                    )
                }
                )}

        )
    }

    renderButtons() {
        return this.isDirty()
            ? <React.Fragment>
                <PanelButtons.Cancel onClick={() => this.close()}/>
                <PanelButtons.Select onClick={() => this.select()}/>
            </React.Fragment>
            : <PanelButtons.Close onClick={() => this.close()}/>
    }

    componentDidMount() {
        const {date} = this.props
        this.setState({
            date: this.toValidRange(date)
        })
    }

    updateDate(unit, value) {
        this.setState(prevState => ({
            date: this.toValidRange(prevState.date.set(unit, value))
        }))

    }

    toValidRange(date) {
        const {startDate, endDate} = this.props
        return constrainDate(date, startDate, endDate)
    }

    select() {
        const {onSelect} = this.props
        this.isDirty() && onSelect && onSelect(this.state.date)
        this.close()
    }

    close() {
        const {activatable} = this.props
        activatable.deactivate()
    }

    isDirty() {
        return !this.props.date.isSame(this.state.date, 'day')
    }
}

class CalendarButton extends Component {
    element = React.createRef()

    render() {
        let {label, selected, disabled, className, onClick} = this.props
        return (
            <Button
                chromeless={!selected}
                look={selected ? 'highlight' : 'transparent'}
                disabled={disabled}
                additionalClassName={className}
                onClick={onClick}>
                {label}
            </Button>
        )
    }

    componentDidMount() {
        this.element.current.parentNode.parentNode.scrollTop = this.element.current.parentNode.offsetTop
    }
}

CalendarButton.propTypes = {
    className: PropTypes.any,
    label: PropTypes.any,
    selected: PropTypes.any,
    onClick: PropTypes.any
}

const policy = () => ({_: 'allow'})
const id = ({id}) => id
const DatePickerPanel = activatable({id, policy, alwaysAllow: true})(
    _DatePickerPanel
)
<commit_msg>Replaced DataePicker overscroll with unselectable years before and after.<commit_after>import * as PropTypes from 'prop-types'
import {Activator} from 'widget/activation/activator'
import {Button} from 'widget/button'
import {Panel, PanelButtons, PanelContent, PanelHeader} from './panel'
import {activatable} from 'widget/activation/activatable'
import {isMobile} from 'widget/userAgent'
import Label from './label'
import List from 'widget/list'
import React, {Component} from 'react'
import _ from 'lodash'
import guid from 'guid'
import moment from 'moment'
import styles from './datePicker.module.css'

const DATE_FORMAT = 'YYYY-MM-DD'

export const momentDate = date => _.isString(date) ? moment(date, DATE_FORMAT) : moment(date)

const pickDate = (date1, date2, func) => {
    date1 = momentDate(date1)
    date2 = momentDate(date2)
    if (!date1.isValid())
        return date2
    else if (!date2.isValid())
        return date1
    else
        return func(date1, date2)
}

export const maxDate = (date1, date2) => pickDate(date1, date2, moment.max)
export const minDate = (date1, date2) => pickDate(date1, date2, moment.min)
export const constrainDate = (date, min, max) => maxDate(minDate(date, max), min)

export default class DatePicker extends React.Component {
    id = 'DatePicker-' + guid()
    inputElement = React.createRef()

    render() {
        const {input, startDate, endDate, label, autoFocus} = this.props
        const date = moment(input.value, DATE_FORMAT)
        return (
            <Activator id={this.id}>
                {panel =>

                        <DatePickerPanel
                            id={this.id}
                            title={label}
                            date={date.isValid() ? date : moment(startDate, DATE_FORMAT)}
                            startDate={momentDate(startDate)}
                            endDate={momentDate(endDate)}
                            onSelect={date => {
                                const dateString = date.format(DATE_FORMAT)
                                this.inputElement.current.value = dateString
                                input.set(dateString)
                            }}/>
                        {this.renderLabel()}

                            <input
                                ref={this.inputElement}
                                defaultValue={input.value}
                                maxLength={10}
                                autoFocus={autoFocus && !isMobile()}
                                autoComplete='off'
                                autoCorrect='off'
                                autoCapitalize='off'
                                spellCheck='false'
                                className={styles.input}
                                onChange={e => this.setInput(e.target.value)}
                                onBlur={() => {
                                    this.inputElement.current.value = input.value
                                }}
                            />
                            <Button additionalClassName={styles.panelTrigger}
                                chromeless
                                icon='calendar-alt'
                                size='small'
                                onClick={() => panel.activate()}
                            />

                }
            </Activator>
        )
    }

    renderLabel() {
        const {label, tooltip, tooltipPlacement = 'top'} = this.props
        return label ? (

        ) : null
    }

    setInput(value) {
        const {input, startDate, endDate} = this.props
        const date = momentDate(value)
        const formattedDate = date.isValid()
            ? constrainDate(date, startDate, endDate).format(DATE_FORMAT)
            : momentDate(startDate).format(DATE_FORMAT)
        input.set(formattedDate)
    }
}

DatePicker.propTypes = {
    endDate: PropTypes.any.isRequired,
    input: PropTypes.object.isRequired,
    startDate: PropTypes.any.isRequired,
    autoFocus: PropTypes.any,
    label: PropTypes.string
}

class _DatePickerPanel extends React.Component {
    state = {}

    render() {
        const {title} = this.props
        const {date} = this.state
        if (!date)
            return null
        return (
            <Panel
                className={styles.panel}
                type='modal'>
                <PanelHeader
                    icon='calendar-alt'
                    title={title}/>
                <PanelContent className={styles.panelContent}>
                    {this.renderYears()}
                    {this.renderMonths()}
                    {this.renderDays()}
                </PanelContent>
                <PanelButtons onEnter={() => this.select()} onEscape={() => this.close()}>
                    <PanelButtons.Main>
                        {this.renderButtons()}
                    </PanelButtons.Main>
                </PanelButtons>
            </Panel>
        )
    }

    renderYears() {
        const {startDate, endDate} = this.props
        const {date} = this.state
        const startYear = startDate.year()
        const endYear = endDate.year()
        const selectedYear = date.year()
        const options = _.concat(
            _.range(startYear - 5, startYear).map(year => ({label: year})),
            _.range(startYear, endYear + 1).map(year => ({label: year, value: year})),
            _.range(endYear + 1, endYear + 6).map(year => ({label: year}))
        )
        const selectedOption = _.find(options, ({value}) => value === selectedYear)
        return (

                <List
                    options={options}
                    selectedOption={selectedOption}
                    onSelect={option => this.updateDate('year', option.value)}
                />

        )
    }

    renderMonths() {
        const {date} = this.state
        const {startDate, endDate} = this.props
        const months = moment.monthsShort()
        const selectedMonth = months[date.month()]
        const firstMonthIndex = date.year() === startDate.year() ? startDate.month() : 0
        const lastMonthIndex = date.year() === endDate.year() ? endDate.month() : 11
        return (

                {months.map((month, i) =>
                    <CalendarButton
                        key={month}
                        label={month}
                        selected={month === selectedMonth}
                        className={styles.month}
                        disabled={i < firstMonthIndex || i > lastMonthIndex}
                        onClick={() => this.updateDate('month', month)}/>
                )}

        )
    }

    renderDays() {
        const {date} = this.state
        const {startDate, endDate} = this.props
        const firstOfMonth = moment(date).startOf('month')
        const firstToRender = moment(firstOfMonth).startOf('week')
        const lastOfMonth = moment(date).endOf('month')
        const firstDay = date.isSame(startDate, 'month') ? startDate.date() : 1
        const lastDate = date.isSame(endDate, 'month') ? endDate : lastOfMonth
        const lastDay = lastDate.date()
        const lastToRender = moment(lastOfMonth).endOf('week')
        const daysToRender = lastToRender.diff(firstToRender, 'days') + 1
        const indexOffset = firstOfMonth.day() - 1
        const firstIndex = firstDay + indexOffset
        const lastIndex = lastDay + indexOffset

        return (

                {moment.weekdaysShort().map(weekday =>

                )}
                {_.times(daysToRender, i => {
                    const buttonDate = moment(firstToRender).add(i, 'day')
                    const dayOfMonth = buttonDate.format('DD')
                    return (
                        <CalendarButton
                            key={i}
                            label={dayOfMonth}
                            selected={buttonDate.isSame(date, 'day')}
                            className={styles.date}
                            disabled={i < firstIndex || i > lastIndex}
                            onClick={() => this.updateDate('date', dayOfMonth)}/>
                    )
                }
                )}

        )
    }

    renderButtons() {
        return this.isDirty()
            ? <React.Fragment>
                <PanelButtons.Cancel onClick={() => this.close()}/>
                <PanelButtons.Select onClick={() => this.select()}/>
            </React.Fragment>
            : <PanelButtons.Close onClick={() => this.close()}/>
    }

    componentDidMount() {
        const {date} = this.props
        this.setState({
            date: this.toValidRange(date)
        })
    }

    updateDate(unit, value) {
        this.setState(prevState => ({
            date: this.toValidRange(prevState.date.set(unit, value))
        }))

    }

    toValidRange(date) {
        const {startDate, endDate} = this.props
        return constrainDate(date, startDate, endDate)
    }

    select() {
        const {onSelect} = this.props
        this.isDirty() && onSelect && onSelect(this.state.date)
        this.close()
    }

    close() {
        const {activatable} = this.props
        activatable.deactivate()
    }

    isDirty() {
        return !this.props.date.isSame(this.state.date, 'day')
    }
}

class CalendarButton extends Component {
    element = React.createRef()

    render() {
        let {label, selected, disabled, className, onClick} = this.props
        return (
            <Button
                chromeless={!selected}
                look={selected ? 'highlight' : 'transparent'}
                disabled={disabled}
                additionalClassName={className}
                onClick={onClick}>
                {label}
            </Button>
        )
    }

    componentDidMount() {
        this.element.current.parentNode.parentNode.scrollTop = this.element.current.parentNode.offsetTop
    }
}

CalendarButton.propTypes = {
    className: PropTypes.any,
    label: PropTypes.any,
    selected: PropTypes.any,
    onClick: PropTypes.any
}

const policy = () => ({_: 'allow'})
const id = ({id}) => id
const DatePickerPanel = activatable({id, policy, alwaysAllow: true})(
    _DatePickerPanel
)
<|endoftext|>


<commit_before>Hull.define(['underscore', 'h5f'], function(_, H5F) {
  return {
    type: 'Hull',

    templates: ['registration_form', 'registration_complete'],

    refreshEvents: ['model.hull.me.change'],

    complete: false,

    options:{
      editable:false
    },

    defaultFields: [
      {
        type : 'text',
        name : 'name',
        label : 'Name',
        value : '',
        required: true,
        error : 'Please enter your name',
        placeholder : 'bob'
      },
      {
        type : 'email',
        name : 'email',
        label : 'Email',
        value : '',
        required: true,
        error : 'Invalid Email',
        placeholder : 'you@awesome.com'
      }
    ],

    datasources: {
      fields: function() {
        var extra = this.sandbox.data.api.model('app').get('extra');
        return extra.profile_fields || this.defaultFields;
      }
    },

    initialize : function(options, callback) {
      this.formId = "form_"+(new Date()).getTime();
      _.bindAll(this);
    },

    validate: function() {
      this._ensureFormEl();
      var isValid = this.formEl.checkValidity();
      if(isValid) return isValid;
      this.$el.find('[data-hull-input]').each(function(key, el){
        var $el = $(el),
            id = $el.attr('id');
        var errorMsg = $el.siblings('[data-hull-error]')
        errorMsg.text((el.checkValidity()) ? '' : $el.data('errorMessage'));
      });
      return false;
    },

    register: function(profile) {
      var self  = this;
          me    = this.sandbox.data.api.model('me');
      if (this.loggedIn()) {
        this.api('me/profile', 'put', profile, function(newProfile) {
          me.set('profile', newProfile);
          self.render();
        });
      }
    },

    beforeRender: function(data) {
      data.formId = this.formId;
      var fields = _.map(data.fields, function(f) {
        f.value = this._findFieldValue(f.name);
        return f;
      }, this);

      var profile = data.me.profile || {};

      // Check if user.profile contains all the fields with their respective
      // value. If it's the case we consider the form as complete.
      var isComplete = _.every(fields, function(f) {
        var profileField = profile[f.name];
        if (f.type === 'checkbox') {
          return profileField == f.value;
        } else {
          return !!profileField && profileField === f.value;
        }
      });

      this.template = isComplete ? 'registration_complete' : 'registration_form';

      this.fields = fields;
    },

    afterRender: function() {
      if (this.template === 'registration_form') {
        this._ensureFormEl();
        H5F.setup(this.formEl, {
          validClass: "hull-form__input--valid",
          invalidClass: "hull-form__input--invalid",
          requiredClass: "hull-form__input--required",
          placeholderClass: "hull-form__input--placeholder"
        });
      }
    },

    actions: {
      edit: function(e) {
        e.preventDefault();
        e.stopPropagation();
        this.render("registration_form");
        return false;
      },

      submit: function(e, opts) {
        e && e.preventDefault();

        if (!this.validate()) {
          e && e.stopPropagation();
          e && e.stopImmediatePropagation();
          return false;
        }

        var fields = _.clone(this.fields),
            extra  = {},
            el = this.$el;

        _.each(fields, function(field) {
          if (field.type == 'checkbox') {
            extra[field.name] = el.find('#hull-form-' + field.name).is(':checked');
          } else {
            extra[field.name] = el.find('#hull-form-' + field.name).val();
          }
        });

        this.register(extra);
      }
    },

    _findFieldValue: function(name) {
      var me = this.data.me.toJSON() || {};

      var identities = _.reduce(me.identities, function(memo, i) {
        return _.extend(memo, i);
      }, {});

      me.profile = me.profile || {};
      identities = identities || {};

      return me.profile[name] || me[name] || identities[name];
    },

    _ensureFormEl: function() {
      if (this.formEl == null) {
        this.formEl = document.getElementById(this.formId);
      }
    }
  };
});
<commit_msg>Fixes scopes in the registration widget<commit_after>Hull.define(['underscore', 'h5f'], function(_, H5F) {
  return {
    type: 'Hull',

    templates: ['registration_form', 'registration_complete'],

    refreshEvents: ['model.hull.me.change'],

    complete: false,

    options:{
      editable:false
    },

    defaultFields: [
      {
        type : 'text',
        name : 'name',
        label : 'Name',
        value : '',
        required: true,
        error : 'Please enter your name',
        placeholder : 'bob'
      },
      {
        type : 'email',
        name : 'email',
        label : 'Email',
        value : '',
        required: true,
        error : 'Invalid Email',
        placeholder : 'you@awesome.com'
      }
    ],

    datasources: {
      fields: function() {
        var extra = this.sandbox.data.api.model('app').get('extra');
        return extra.profile_fields || this.defaultFields;
      }
    },

    initialize : function(options, callback) {
      this.formId = "form_"+(new Date()).getTime();
      this.sandbox.util._.bindAll(this);
    },

    validate: function() {
      this._ensureFormEl();
      var isValid = this.formEl.checkValidity();
      if(isValid) return isValid;
      this.$el.find('[data-hull-input]').each(function(key, el){
        var $el = $(el),
            id = $el.attr('id');
        var errorMsg = $el.siblings('[data-hull-error]')
        errorMsg.text((el.checkValidity()) ? '' : $el.data('errorMessage'));
      });
      return false;
    },

    register: function(profile) {
      var self  = this;
          me    = this.sandbox.data.api.model('me');
      if (this.loggedIn()) {
        this.api('me/profile', 'put', profile, function(newProfile) {
          me.set('profile', newProfile);
          self.render();
        });
      }
    },

    beforeRender: function(data) {
      data.formId = this.formId;
      var fields = this.sandbox.util._.map(data.fields, function(f) {
        f.value = this._findFieldValue(f.name);
        return f;
      }, this);

      var profile = data.me.profile || {};

      // Check if user.profile contains all the fields with their respective
      // value. If it's the case we consider the form as complete.
      var isComplete = this.sandbox.util._.every(fields, function(f) {
        var profileField = profile[f.name];
        if (f.type === 'checkbox') {
          return profileField == f.value;
        } else {
          return !!profileField && profileField === f.value;
        }
      });

      this.template = isComplete ? 'registration_complete' : 'registration_form';

      this.fields = fields;
    },

    afterRender: function() {
      if (this.template === 'registration_form') {
        this._ensureFormEl();
        H5F.setup(this.formEl, {
          validClass: "hull-form__input--valid",
          invalidClass: "hull-form__input--invalid",
          requiredClass: "hull-form__input--required",
          placeholderClass: "hull-form__input--placeholder"
        });
      }
    },

    actions: {
      edit: function(e) {
        e.preventDefault();
        e.stopPropagation();
        this.render("registration_form");
        return false;
      },

      submit: function(e, opts) {
        e && e.preventDefault();

        if (!this.validate()) {
          e && e.stopPropagation();
          e && e.stopImmediatePropagation();
          return false;
        }

        var fields = this.sandbox.util._.clone(this.fields),
            extra  = {},
            el = this.$el;

        this.sandbox.util._.each(fields, function(field) {
          if (field.type == 'checkbox') {
            extra[field.name] = el.find('#hull-form-' + field.name).is(':checked');
          } else {
            extra[field.name] = el.find('#hull-form-' + field.name).val();
          }
        });

        this.register(extra);
      }
    },

    _findFieldValue: function(name) {
      var me = this.data.me.toJSON() || {};

      var identities = this.sandbox.util._.reduce(me.identities, this.sandbox.util._.bind(function(memo, i) {
        return this.sandbox.util._.extend(memo, i);
      }, this), {});

      me.profile = me.profile || {};
      identities = identities || {};

      return me.profile[name] || me[name] || identities[name];
    },

    _ensureFormEl: function() {
      if (this.formEl == null) {
        this.formEl = document.getElementById(this.formId);
      }
    }
  };
});
<|endoftext|>


<commit_before>'use strict';

class GameResults {
    /**
     * @param {jQuery} $el
     */
    constructor($el) {
        this.$html      = $el;
        this.apiMgr     = new APIRequestMgr();
        this.pageMgr    = new PageMgr();
        this.pagination = new PaginationMgr(this.$html);
    }

    /**
     * @param {int|string} page
     */
    fetch(page) {
        let self       = this,
            requestUrl = this.$html.attr(GameResults.resources.config.attribute.route) + page,
            onSuccess  = function (response) {
                self.updateHTML(response);
            };

        this.apiMgr.request('GET', requestUrl, undefined, onSuccess);
    }

    /**
     * @param {{meta: {currentPage: {int}, totalPages: {int}}, results: []}} response
     */
    updateHTML(response) {
        let html   = GameResults.resources.html,
            $table = $(html.layout());

        response.results.map(result => $table.append(html.row(result)));

        this.$html.html($table).append(PaginationMgr.resources.html.layout());
        this.pagination.updateHTML(response.meta.currentPage, response.meta.totalPages);
    }
}

GameResults.resources        = {};
GameResults.resources.config = {
    /** @enum {string} */
    attribute: {
        route: 'data-game-results-link'
    }
};
/** @enum {string} */
GameResults.resources.text = {
    id: 'id',
    player: 'winner',
    time: 'finished at'
};
GameResults.resources.html = {
    /**
     * @returns {string}
     */
    layout: function () {
        let text = GameResults.resources.text;

        return '\

 \

 \
                    ' + text.id + '	 \
                    ' + text.time + '	 \
                    ' + text.player + '	 \

 \

';
    },
    /**
     * @param {{id: {int}, player: {id: {int}}, timestamp: {string}}} obj
     *
     * @returns {string}
     */
    row: function (obj) {
        let date = new Date(obj.timestamp);

        return ' \

 \
                ' + obj.id + '	 \
                ' + date.toLocaleString() + '	 \
                ' + obj.player.name + '	 \

';
    }
};
<commit_msg>clean game result model<commit_after>'use strict';

class GameResults {
    /**
     * @param {jQuery} $el
     */
    constructor($el) {
        this.$html      = $el;
        this.apiMgr     = new APIRequestMgr();
        this.pageMgr    = new PageMgr();
        this.pagination = new PaginationMgr(this.$html);
    }

    /**
     * @param {int|string} page
     */
    fetch(page) {
        let self       = this,
            requestUrl = this.$html.attr('data-game-results-link') + page,
            onSuccess  = function (response) {
                self.updateHTML(response);
            };

        this.apiMgr.request('GET', requestUrl, undefined, onSuccess);
    }

    /**
     * @param {{meta: {currentPage: {int}, totalPages: {int}}, results: []}} response
     */
    updateHTML(response) {
        let html   = GameResults.resources.html,
            $table = $(html.layout());

        response.results.map(result => $table.append(html.row(result)));

        this.$html.html($table).append(PaginationMgr.resources.html.layout);
        this.pagination.updateHTML(response.meta.currentPage, response.meta.totalPages);
    }
}

GameResults.resources = {};
/** @enum {string} */
GameResults.resources.text = {
    id: 'id',
    player: 'winner',
    time: 'finished at'
};
GameResults.resources.html = {
    /**
     * @returns {string}
     */
    layout: function () {
        let text = GameResults.resources.text;

        return '\

 \

 \
                    ' + text.id + '	 \
                    ' + text.time + '	 \
                    ' + text.player + '	 \

 \

';
    },
    /**
     * @param {{id: {int}, player: {id: {int}}, timestamp: {string}}} obj
     *
     * @returns {string}
     */
    row: function (obj) {
        let date = new Date(obj.timestamp);

        return ' \

 \
                ' + obj.id + '	 \
                ' + date.toLocaleString() + '	 \
                ' + obj.player.name + '	 \

';
    }
};
<|endoftext|>


<commit_before>/*
 * Licensed to the Sakai Foundation (SF) under one
 * or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. The SF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License. You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied. See the License for the
 * specific language governing permissions and limitations under the License.
 */

/*global $ */

var sakai = sakai || {};

sakai.filerevisions = function(tuid, showSettings){

    ///////////////
    // Variables //
    ///////////////

    var $fileRevisionsDialog = $("#filerevisions_dialog");
    var baseFileData = {};

    // Templates
    var filerevisionsTemplate = "#filerevisions_template";

    // Containers
    var filerevisionsTemplateContainer = "#filerevisions_template_container";

    // Buttons
    var filerevisionsCloseButton = "#filerevisions_close";

    /**
     * Convert given date object to readable date string
     * @param {Object} date Date object
     */
    var getFormattedDate = function(date){
        var months = ["January","February","March","April","May","June","July","August","September","October","November","December"];
        var day = date.getDate();
        var month = months[date.getMonth()];
        var year = date.getFullYear();
        var formattedDate = day + " " + month + " " + year;
        return formattedDate;
    };

    /**
     * Render the template that displays all revision information
     */
    var renderRevisionData = function(){
        var data = [];
        baseFileData.created = getFormattedDate(new Date(baseFileData["jcr:created"]));
        data.data = baseFileData;
        data.linkrevision = $("#content_profile_details_view_revisions").hasClass("link_revision");

        var renderedTemplate = $.TemplateRenderer(filerevisionsTemplate, data);
        $(filerevisionsTemplateContainer).html(renderedTemplate);

        var renderedTemplate = $.TemplateRenderer("#filerevision_header_text_template", data);
        $("#filerevision_header_text").html(renderedTemplate);
    };

    /**
     * Get userprofile with the userid provided
     * @param {Object} userid
     */
    var getUserProfile = function(userid){
        $.ajax({
            url: "/~" + userid + "/public/authprofile.infinity.json",
            success: function(profile){
                baseFileData["sakai:savedByFull"] = sakai.api.User.getDisplayName(profile);
                renderRevisionData();
            },
            error: function(xhr, textStatus, thrownError){
                sakai.api.Util.notification.show("Failed loading profile", "Failed to load file profile information");
            }
        });
    };

    /**
     * Get detailed information on the files in the revision history
     * This data contains path, mimetype, description,...
     */
    var getRevisionInformationDetails = function(){
        var revisionInformationDetails = [];
        for (var i in baseFileData.revisions) {
            if (baseFileData.revisions.hasOwnProperty(i)) {
                var item = {
                    "url": baseFileData.path + ".version.," + i + ",.json",
                    "method": "GET"
                };
                revisionInformationDetails[revisionInformationDetails.length] = item;
            }
        }
        // Do the Batch request
        $.ajax({
            url: sakai.config.URL.BATCH,

            type : "POST",
            dataType: 'json',
            cache: false,
            data: {
                requests: $.toJSON(revisionInformationDetails)
            },
            success: function(data){
                // Get file information out of results
                var revisionFileDetails = [];
                for (var i in data.results){
                    if (data.results.hasOwnProperty(i)) {
                        revisionFileDetails[revisionFileDetails.length] = $.parseJSON(data.results[i].body);
                    }
                }
                baseFileData.revisionFileDetails = revisionFileDetails;
                getUserProfile(baseFileData["sakai:savedBy"]);
            },
            error: function(xhr, textStatus, thrownError){

            }
        });
    };

    /**
     * Get the revision information on the specified file
     */
    var getRevisionInformation = function(){
        $.ajax({
            url: baseFileData.path + ".versions.json",
            type : "GET",
            success: function(data){
                for (var i in data.versions){
                    if (data.versions.hasOwnProperty(i)) {
                        data.versions[i]["jcr:created"] = getFormattedDate(new Date(data.versions[i]["jcr:created"]));
                    }
                }
                baseFileData.revisions = data.versions;
                getRevisionInformationDetails();
            },
            error: function(){
                sakai.api.Util.notification.show("Revision information not retrieved", "The revision information for the file could not be retrieved");
            }
        });
    };

    /**
     * Load the file revisions widget with the appropriate data
     * This function can be called from anywhere within Sakai
     * @param {Object} data Contains data needed to call all revisions for the file
     */
    sakai.filerevisions.initialise = function(data){
        baseFileData = data;
        getRevisionInformation();
        $fileRevisionsDialog.jqmShow();
    };

    $fileRevisionsDialog.jqm({
        modal: true,
        toTop: true
    });

    $(filerevisionsCloseButton).live("click", function(){
        $(filerevisionsTemplateContainer).html("");
        $fileRevisionsDialog.jqmHide();
    });

};
sakai.api.Widgets.widgetLoader.informOnLoad("filerevisions");<commit_msg>SAKIII-986 Parse date in filerevision widget correctly in webkit browsers<commit_after>/*
 * Licensed to the Sakai Foundation (SF) under one
 * or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. The SF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License. You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied. See the License for the
 * specific language governing permissions and limitations under the License.
 */

/*global $ */

var sakai = sakai || {};

sakai.filerevisions = function(tuid, showSettings){

    ///////////////
    // Variables //
    ///////////////

    var $fileRevisionsDialog = $("#filerevisions_dialog");
    var baseFileData = {};

    // Templates
    var filerevisionsTemplate = "#filerevisions_template";

    // Containers
    var filerevisionsTemplateContainer = "#filerevisions_template_container";

    // Buttons
    var filerevisionsCloseButton = "#filerevisions_close";

    /**
     * Convert given date object to readable date string
     * @param {Object} date Date object
     */
    var getFormattedDate = function(date){
        var months = ["January","February","March","April","May","June","July","August","September","October","November","December"];
        var day = date.getDate();
        var month = months[date.getMonth()];
        var year = date.getFullYear();
        var formattedDate = day + " " + month + " " + year;
        return formattedDate;
    };

    /**
     * Render the template that displays all revision information
     */
    var renderRevisionData = function(){
        var data = [];
        baseFileData.created = getFormattedDate(new Date(baseFileData["jcr:created"]));
        data.data = baseFileData;
        data.linkrevision = $("#content_profile_details_view_revisions").hasClass("link_revision");

        var renderedTemplate = $.TemplateRenderer(filerevisionsTemplate, data);
        $(filerevisionsTemplateContainer).html(renderedTemplate);

        var renderedTemplate = $.TemplateRenderer("#filerevision_header_text_template", data);
        $("#filerevision_header_text").html(renderedTemplate);
    };

    /**
     * Get userprofile with the userid provided
     * @param {Object} userid
     */
    var getUserProfile = function(userid){
        $.ajax({
            url: "/~" + userid + "/public/authprofile.infinity.json",
            success: function(profile){
                baseFileData["sakai:savedByFull"] = sakai.api.User.getDisplayName(profile);
                renderRevisionData();
            },
            error: function(xhr, textStatus, thrownError){
                sakai.api.Util.notification.show("Failed loading profile", "Failed to load file profile information");
            }
        });
    };

    /**
     * Get detailed information on the files in the revision history
     * This data contains path, mimetype, description,...
     */
    var getRevisionInformationDetails = function(){
        var revisionInformationDetails = [];
        for (var i in baseFileData.revisions) {
            if (baseFileData.revisions.hasOwnProperty(i)) {
                var item = {
                    "url": baseFileData.path + ".version.," + i + ",.json",
                    "method": "GET"
                };
                revisionInformationDetails[revisionInformationDetails.length] = item;
            }
        }
        // Do the Batch request
        $.ajax({
            url: sakai.config.URL.BATCH,

            type : "POST",
            dataType: 'json',
            cache: false,
            data: {
                requests: $.toJSON(revisionInformationDetails)
            },
            success: function(data){
                // Get file information out of results
                var revisionFileDetails = [];
                for (var i in data.results){
                    if (data.results.hasOwnProperty(i)) {
                        revisionFileDetails[revisionFileDetails.length] = $.parseJSON(data.results[i].body);
                    }
                }
                baseFileData.revisionFileDetails = revisionFileDetails;
                getUserProfile(baseFileData["sakai:savedBy"]);
            },
            error: function(xhr, textStatus, thrownError){

            }
        });
    };

    /**
     * Get the revision information on the specified file
     */
    var getRevisionInformation = function(){
        $.ajax({
            url: baseFileData.path + ".versions.json",
            type : "GET",
            success: function(data){
                for (var i in data.versions){
                    if (data.versions.hasOwnProperty(i)) {
                        if (!$.browser.webkit) {
                            data.versions[i]["jcr:created"] = getFormattedDate(new Date(data.versions[i]["jcr:created"]));
                        }else{
                            data.versions[i]["jcr:created"] = getFormattedDate(new Date(data.versions[i]["jcr:created"].split("T")[0]));
                        }
                    }
                }
                baseFileData.revisions = data.versions;
                getRevisionInformationDetails();
            },
            error: function(){
                sakai.api.Util.notification.show("Revision information not retrieved", "The revision information for the file could not be retrieved");
            }
        });
    };

    /**
     * Load the file revisions widget with the appropriate data
     * This function can be called from anywhere within Sakai
     * @param {Object} data Contains data needed to call all revisions for the file
     */
    sakai.filerevisions.initialise = function(data){
        baseFileData = data;
        getRevisionInformation();
        $fileRevisionsDialog.jqmShow();
    };

    $fileRevisionsDialog.jqm({
        modal: true,
        toTop: true
    });

    $(filerevisionsCloseButton).live("click", function(){
        $(filerevisionsTemplateContainer).html("");
        $fileRevisionsDialog.jqmHide();
    });

};
sakai.api.Widgets.widgetLoader.informOnLoad("filerevisions");<|endoftext|>


<commit_before>if (!MapEntity) var MapEntity = {};

L.Control.Screenshot = L.Control.extend({
    includes: L.Mixin.Events,
    options: {
        position: 'topleft',
        title: 'Screenshot',
    },

    screenshot: function () {
        // Screenshot effect
        $('

')
            .appendTo(document.body)
            .fadeOut();
        this.fire('trigger');
    },

    onAdd: function(map) {
        this.map = map;
        this._container = L.DomUtil.create('div', 'leaflet-control-zoom leaflet-control');
        var link = L.DomUtil.create('a', 'leaflet-control-zoom-out screenshot-control', this._container);
        link.href = '#';
        link.title = this.options.title;

        L.DomEvent
            .addListener(link, 'click', L.DomEvent.stopPropagation)
            .addListener(link, 'click', L.DomEvent.preventDefault)
            .addListener(link, 'click', this.screenshot, this);
        return this._container;
    }
});

/**
 * Get URL parameter in Javascript
 * source: http://stackoverflow.com/questions/1403888/get-url-parameter-with-jquery
 */
function getURLParameter(name) {
    var paramEncoded = (RegExp('[?|&]' + name + '=' + '(.+?)(&|$)').exec(location.search)||[,null])[1],
        paramDecoded = decodeURIComponent(paramEncoded);
    return paramDecoded == 'null' ? null : paramDecoded;
}

/**
 * Uniqify arrays
 * http://www.shamasis.net/2009/09/fast-algorithm-to-find-unique-items-in-javascript-array/
 */
Array.prototype.unique = function() {
    var o = {}, i, l = this.length, r = [];
    for(i=0; i<l;i+=1) o[this[i]] = this[i];
    for(i in o) r.push(o[i]);
    return r;
};

MapEntity.Context = new function() {
    var self = this;

    self.serializeFullContext = function(map, kwargs) {
        var context = {},
            filter = kwargs.filter,
            datatable = kwargs.datatable;

        // Map view
        context['mapview'] = {'lat': map.getCenter().lat, 'lng': map.getCenter().lng, 'zoom': map.getZoom()};

        // layers shown by name
        var layers = [];
        $('form.leaflet-control-layers-list input:checked').each(function () {
            layers.push($.trim($(this).parent().text()));
        });
        context['maplayers'] = layers;

        // Form filters
        if (filter) {
            // exclude bbox field, since it comes from the map view.
            var fields = $($(filter).serializeArray()).filter(function (){ return this.name != 'bbox'});
            context['filter'] = $.param(fields);
        }

        // Sort columns
        if (datatable) {
            context['sortcolumns'] = datatable.fnSettings().aaSorting;
        }

        // Extra-info, not restored so far but can be useful for screenshoting
        context['url'] = window.location.toString();
        context['viewport'] = {'width': $(window).width(), 'height': $(window).height()};
        context['mapsize'] = {'width': $('.map-panel').width(), 'height': $('.map-panel').height()};

        // Mark timestamp
        context['timestamp'] = new Date().getTime();

        return JSON.stringify(context);
    },

    self.saveFullContext = function(map, kwargs) {
        var prefix = kwargs.prefix || '',
            serialized = self.serializeFullContext(map, kwargs);
        localStorage.setItem(prefix + 'map-context', serialized);
    };

    self.__loadFullContext = function(kwargs) {
        if (!kwargs) kwargs = {};
        var prefix = kwargs.prefix || '',
            context = localStorage.getItem(prefix + 'map-context');
        if (context)
            return JSON.parse(context);
        return null;
    };

    self.restoreLatestMapView = function (map, prefixes, kwargs) {
        var latest = null;
        for (var i=0; i<prefixes.length; i++) {
            var prefix = prefixes[i],
                context = self.__loadFullContext($.extend(kwargs, {prefix: prefix}));
            if (!latest || (context && context.timestamp && context.timestamp > latest.timestamp)) {
                latest = context;
                if (window.DEBUG) console.log(JSON.stringify(context));
            }
        }
        return self.restoreMapView(map, latest, kwargs);
    }

    self.restoreMapView = function(map, context, kwargs) {
        if (!context) context = self.__loadFullContext(kwargs);
        if (context && context.mapview) {
            map.setView(L.latLng(context.mapview.lat, context.mapview.lng), context.mapview.zoom);
            return true;
        }
        return false;
    };

    self.restoreFullContext = function(map, kwargs) {
        if (!kwargs) kwargs = {};
        var context = getURLParameter('context'),
            filter = kwargs.filter,
            datatable = kwargs.datatable,
            objectsname = kwargs.objectsname;
        if (context) {
            context = JSON.parse(context);
        }
        else {
            // If not received from URL, load from LocalStorage
            context = self.__loadFullContext(kwargs);
        }
        if (!context) {
            console.warn("No context found.");
            map.fitBounds(map.options.maxBounds);
            return;  // No context, no restore.
        }

        if (context.print) {
            // Hide controls
            $('.leaflet-control').hide();   // Hide all
            $('.leaflet-control-scale').show(); // Show scale
            $(map._container).removeClass('leaflet-fade-anim');
        }

        self.restoreMapView(map, context, kwargs);

        if (filter && context.filter) {
            $(filter).deserialize(context.filter);
        }
        if (datatable && context.sortcolumns) {
            datatable.fnSort(context.sortcolumns);
        }
        // Show layers by their name
        if (context.maplayers) {
            var layers = context.maplayers;
            layers.push(objectsname);
            $('form.leaflet-control-layers-list input').each(function () {
                if ($.trim($(this).parent().text()) != objectsname) {
                    $(this).removeAttr('checked');
                }
            });
            for(var i=0; i<layers.length; i++) {
                var layer = layers[i];
                $('form.leaflet-control-layers-list input').each(function () {
                    if ($.trim($(this).parent().text()) == layer) {
                        $(this).attr('checked', 'checked');
                    }
                });
            }
            map.layerscontrol._onInputClick();
        }
    };
};

MapEntity.makeGeoFieldProxy = function($field, layer) {
    // Proxy to field storing WKT. It also stores the matching layer.
    var _current_layer = layer || null,
        topologyMode = false;

    return {
        setTopologyMode: function(state) {
            topologyMode = state;
        },
        // If topologyMode, store WKT
        // Else store the given parameter using JSON.stringify
        storeLayerGeomInField: function(layer) {
            var old_layer = _current_layer;
            _current_layer = layer;

            var serialized = '';
            if (topologyMode) {
                if (layer instanceof L.Marker) {
                    var p = layer.getLatLng(),
                        serialized = {lat: p.lat, lng: p.lng};
                    // In case the marker is snapped, serialize this information.
                    if (layer.snap) {
                        serialized['snap'] = layer.snap.properties.pk;
                    }
                    serialized = JSON.stringify(serialized);
                }
                else
                    serialized = JSON.stringify(layer);
            }
            else {
                if (layer) serialized = L.Util.getWKT(layer);
            }
            $field.val(serialized);
            return old_layer;
        },
        getLayer: function () {
            return _current_layer;
        },
        getSerialized: function() {
            return $field.val();
        }
    };
};

MapEntity.History = L.Control.extend({

    saveListInfo: function (infos) {
        localStorage.setItem('list-search-results', JSON.stringify(infos));
    },

    remove: function (path) {
        $.post(window.SETTINGS.server + 'history/delete/', {path: path}, function() {
            var entries = $("#historylist > li")
              , entry = $("#historylist li a[href='" + path + "']").parents('li')
              , closeCurrent = String(window.location).indexOf(path, window.location.length - path.length) !== -1;
            if (closeCurrent) {
                // Closing current...
                if (entries.length > 2) {
                    // More left
                    entries.find(' > a').get(1).click();
                    $(entry).remove();
                }
                else {
                    // No more, redirect to list view
                    window.location = '/';
                    $(entry).remove();
                }
            }
            else {
                $(entry).remove();
            }
        });
    },

    render: function () {
        var history = this;

        // Show number of results
        infos = localStorage.getItem('list-search-results') || "{nb: '?', model: null}";
        infos = JSON.parse(infos)
        $('#nbresults').text(infos.nb);
        $('#entitylist-dropdown').parent('li').addClass(infos.model);

        $('#historylist a').tooltip({'placement': 'bottom'});
        $('#historylist button.close').click(function (e) {
            e.preventDefault();
            var path = $(this).parents('a').attr('href');
            history.remove(path);
        });

        $('#historylist a').hoverIntent(
            function (e) {
                $(this).find('.close').removeClass('hidden');
                $(this).data('original-text', $(this).find('.content').text());
                var title = $(this).data('original-title');
                if (title)
                    $(this).find('.content').text(title);
            },
            function (e) {
                $(this).find('.content').text($(this).data('original-text'));
                $(this).find('.close').addClass('hidden');
            }
        );

        // Remove all entries returning 404 :) Useful to remove deleted entries
        $('#historylist li.history > a').each(function () {
            var path = $(this).attr('href');
            $.ajax({
                type: "HEAD",
                url: path,
                statusCode: {
                    404: function() {
                        history.remove(path);
                    }
                }
            });
        });
    },
});

MapEntity.history = new MapEntity.History();<commit_msg>Delay in navigation bar update from page load<commit_after>if (!MapEntity) var MapEntity = {};

L.Control.Screenshot = L.Control.extend({
    includes: L.Mixin.Events,
    options: {
        position: 'topleft',
        title: 'Screenshot',
    },

    screenshot: function () {
        // Screenshot effect
        $('

')
            .appendTo(document.body)
            .fadeOut();
        this.fire('trigger');
    },

    onAdd: function(map) {
        this.map = map;
        this._container = L.DomUtil.create('div', 'leaflet-control-zoom leaflet-control');
        var link = L.DomUtil.create('a', 'leaflet-control-zoom-out screenshot-control', this._container);
        link.href = '#';
        link.title = this.options.title;

        L.DomEvent
            .addListener(link, 'click', L.DomEvent.stopPropagation)
            .addListener(link, 'click', L.DomEvent.preventDefault)
            .addListener(link, 'click', this.screenshot, this);
        return this._container;
    }
});

/**
 * Get URL parameter in Javascript
 * source: http://stackoverflow.com/questions/1403888/get-url-parameter-with-jquery
 */
function getURLParameter(name) {
    var paramEncoded = (RegExp('[?|&]' + name + '=' + '(.+?)(&|$)').exec(location.search)||[,null])[1],
        paramDecoded = decodeURIComponent(paramEncoded);
    return paramDecoded == 'null' ? null : paramDecoded;
}

/**
 * Uniqify arrays
 * http://www.shamasis.net/2009/09/fast-algorithm-to-find-unique-items-in-javascript-array/
 */
Array.prototype.unique = function() {
    var o = {}, i, l = this.length, r = [];
    for(i=0; i<l;i+=1) o[this[i]] = this[i];
    for(i in o) r.push(o[i]);
    return r;
};

MapEntity.Context = new function() {
    var self = this;

    self.serializeFullContext = function(map, kwargs) {
        var context = {},
            filter = kwargs.filter,
            datatable = kwargs.datatable;

        // Map view
        context['mapview'] = {'lat': map.getCenter().lat, 'lng': map.getCenter().lng, 'zoom': map.getZoom()};

        // layers shown by name
        var layers = [];
        $('form.leaflet-control-layers-list input:checked').each(function () {
            layers.push($.trim($(this).parent().text()));
        });
        context['maplayers'] = layers;

        // Form filters
        if (filter) {
            // exclude bbox field, since it comes from the map view.
            var fields = $($(filter).serializeArray()).filter(function (){ return this.name != 'bbox'});
            context['filter'] = $.param(fields);
        }

        // Sort columns
        if (datatable) {
            context['sortcolumns'] = datatable.fnSettings().aaSorting;
        }

        // Extra-info, not restored so far but can be useful for screenshoting
        context['url'] = window.location.toString();
        context['viewport'] = {'width': $(window).width(), 'height': $(window).height()};
        context['mapsize'] = {'width': $('.map-panel').width(), 'height': $('.map-panel').height()};

        // Mark timestamp
        context['timestamp'] = new Date().getTime();

        return JSON.stringify(context);
    },

    self.saveFullContext = function(map, kwargs) {
        var prefix = kwargs.prefix || '',
            serialized = self.serializeFullContext(map, kwargs);
        localStorage.setItem(prefix + 'map-context', serialized);
    };

    self.__loadFullContext = function(kwargs) {
        if (!kwargs) kwargs = {};
        var prefix = kwargs.prefix || '',
            context = localStorage.getItem(prefix + 'map-context');
        if (context)
            return JSON.parse(context);
        return null;
    };

    self.restoreLatestMapView = function (map, prefixes, kwargs) {
        var latest = null;
        for (var i=0; i<prefixes.length; i++) {
            var prefix = prefixes[i],
                context = self.__loadFullContext($.extend(kwargs, {prefix: prefix}));
            if (!latest || (context && context.timestamp && context.timestamp > latest.timestamp)) {
                latest = context;
                if (window.DEBUG) console.log(JSON.stringify(context));
            }
        }
        return self.restoreMapView(map, latest, kwargs);
    }

    self.restoreMapView = function(map, context, kwargs) {
        if (!context) context = self.__loadFullContext(kwargs);
        if (context && context.mapview) {
            map.setView(L.latLng(context.mapview.lat, context.mapview.lng), context.mapview.zoom);
            return true;
        }
        return false;
    };

    self.restoreFullContext = function(map, kwargs) {
        if (!kwargs) kwargs = {};
        var context = getURLParameter('context'),
            filter = kwargs.filter,
            datatable = kwargs.datatable,
            objectsname = kwargs.objectsname;
        if (context) {
            context = JSON.parse(context);
        }
        else {
            // If not received from URL, load from LocalStorage
            context = self.__loadFullContext(kwargs);
        }
        if (!context) {
            console.warn("No context found.");
            map.fitBounds(map.options.maxBounds);
            return;  // No context, no restore.
        }

        if (context.print) {
            // Hide controls
            $('.leaflet-control').hide();   // Hide all
            $('.leaflet-control-scale').show(); // Show scale
            $(map._container).removeClass('leaflet-fade-anim');
        }

        self.restoreMapView(map, context, kwargs);

        if (filter && context.filter) {
            $(filter).deserialize(context.filter);
        }
        if (datatable && context.sortcolumns) {
            datatable.fnSort(context.sortcolumns);
        }
        // Show layers by their name
        if (context.maplayers) {
            var layers = context.maplayers;
            layers.push(objectsname);
            $('form.leaflet-control-layers-list input').each(function () {
                if ($.trim($(this).parent().text()) != objectsname) {
                    $(this).removeAttr('checked');
                }
            });
            for(var i=0; i<layers.length; i++) {
                var layer = layers[i];
                $('form.leaflet-control-layers-list input').each(function () {
                    if ($.trim($(this).parent().text()) == layer) {
                        $(this).attr('checked', 'checked');
                    }
                });
            }
            map.layerscontrol._onInputClick();
        }
    };
};

MapEntity.makeGeoFieldProxy = function($field, layer) {
    // Proxy to field storing WKT. It also stores the matching layer.
    var _current_layer = layer || null,
        topologyMode = false;

    return {
        setTopologyMode: function(state) {
            topologyMode = state;
        },
        // If topologyMode, store WKT
        // Else store the given parameter using JSON.stringify
        storeLayerGeomInField: function(layer) {
            var old_layer = _current_layer;
            _current_layer = layer;

            var serialized = '';
            if (topologyMode) {
                if (layer instanceof L.Marker) {
                    var p = layer.getLatLng(),
                        serialized = {lat: p.lat, lng: p.lng};
                    // In case the marker is snapped, serialize this information.
                    if (layer.snap) {
                        serialized['snap'] = layer.snap.properties.pk;
                    }
                    serialized = JSON.stringify(serialized);
                }
                else
                    serialized = JSON.stringify(layer);
            }
            else {
                if (layer) serialized = L.Util.getWKT(layer);
            }
            $field.val(serialized);
            return old_layer;
        },
        getLayer: function () {
            return _current_layer;
        },
        getSerialized: function() {
            return $field.val();
        }
    };
};

MapEntity.History = L.Control.extend({

    saveListInfo: function (infos) {
        localStorage.setItem('list-search-results', JSON.stringify(infos));
    },

    remove: function (path) {
        $.post(window.SETTINGS.server + 'history/delete/', {path: path}, function() {
            var entries = $("#historylist > li")
              , entry = $("#historylist li a[href='" + path + "']").parents('li')
              , closeCurrent = String(window.location).indexOf(path, window.location.length - path.length) !== -1;
            if (closeCurrent) {
                // Closing current...
                if (entries.length > 2) {
                    // More left
                    entries.find(' > a').get(1).click();
                    $(entry).remove();
                }
                else {
                    // No more, redirect to list view
                    window.location = '/';
                    $(entry).remove();
                }
            }
            else {
                $(entry).remove();
            }
        });
    },

    render: function () {
        var history = this;

        // Show number of results
        infos = localStorage.getItem('list-search-results') || "{nb: '?', model: null}";
        infos = JSON.parse(infos)
        $('#nbresults').text(infos.nb);
        $('#entitylist-dropdown').parent('li').addClass(infos.model);

        $('#historylist a').tooltip({'placement': 'bottom'});
        $('#historylist button.close').click(function (e) {
            e.preventDefault();
            var path = $(this).parents('a').attr('href');
            history.remove(path);
        });

        $('#historylist a').hoverIntent(
            function (e) {
                $(this).find('.close').removeClass('hidden');
                $(this).data('original-text', $(this).find('.content').text());
                var title = $(this).data('original-title');
                if (title)
                    $(this).find('.content').text(title);
            },
            function (e) {
                $(this).find('.content').text($(this).data('original-text'));
                $(this).find('.close').addClass('hidden');
            }
        );

        window.setInterval(function () {
            // Remove all entries returning 404 :) Useful to remove deleted entries
            $('#historylist li.history > a').each(function () {
                var path = $(this).attr('href');
                $.ajax({
                    type: "HEAD",
                    url: path,
                    statusCode: {
                        404: function() {
                            history.remove(path);
                        }
                    }
                });
            });
        }, 5000);  // Don't stress loading.
    },
});

MapEntity.history = new MapEntity.History();<|endoftext|>


<commit_before>import querystring from 'querystring';
import url from 'url';

import externalRequest from 'request';
import Joi from 'joi';
import Rx from 'rxjs';

import {
	applyAuthState,
} from '../util/authUtils';

import {
	querySchema
} from './validation';
import { duotoneRef } from './duotone';
import * as apiConfigCreators from '../apiProxy/apiConfigCreators';

const MOCK_RESPONSE_OK = {  // minimal representation of http.IncomingMessage
	statusCode: 200,
	statusMessage: 'OK',
	headers: {
		'x-meetup-request-id': 'mock request'
	},
	request: {
		uri: {},
	},
};

const parseResponseFlags = flagHeader =>
	(flagHeader || '')
		.split(',')
		.filter(pair => pair)  // ignore empty elements in the array
		.map(pair => pair.split('='))
		.reduce((flags, [key, val]) => {
			flags[key] = val === 'true';
			return flags;
		}, {});

export const parseMetaHeaders = headers => {
	const flags = parseResponseFlags(headers['x-meetup-flags']);
	const requestId = headers['x-meetup-request-id'];
	return {
		flags,
		requestId,
	};
};

/**
 * Accept an Error and return an object that will be used in place of the
 * expected API return value
 */
function formatApiError(err) {
	return {
		error: err.message
	};
}

export const errorResponse$ = requestUrl => err =>
	Rx.Observable.of({
		value: formatApiError(err),
		meta: {
			endpoint: url.parse(requestUrl).pathname,
		},
	});

export const parseApiValue = ([response, body]) => {
	// treat non-success HTTP code as an error
	if (response.statusCode < 200 || response.statusCode > 299) {
		return formatApiError(new Error(response.statusMessage));
	}
	try {
		if (response.statusCode === 204) {  // NoContent response type
			return null;
		}

		const value = JSON.parse(body);
		if (value && value.problem) {
			return formatApiError(new Error(`API problem: ${value.problem}: ${value.details}`));
		}
		return value;
	} catch(err) {
		return formatApiError(err);
	}
};

/**
 *
 * mostly error handling - any case where the API does not satisfy the
 * "api response" formatting requirement: plain object containing the requested
 * values
 *
 * This utility is specific to the response format of the API being consumed
 * @param {Array} the callback args for npm request - [response, body], where
 * `response` is an `Http.IncomingMessage` and `body` is the body text of the
 * response.
 * @return responseObj the JSON-parsed text, possibly with error info
 */
export const parseApiResponse = requestUrl => ([response, body]) => {
	const meta = {
		...parseMetaHeaders(response.headers),
		endpoint: url.parse(requestUrl).pathname,
	};

	return {
		value: parseApiValue([response, body]),
		meta,
	};

};

/**
 * Translate a query into an API `endpoint` + `params`. The translation is based
 * on the Meetup REST API.
 *
 * This function serves as an adapter between the structure of a query and the
 * API-specific config needed to get that data. Note that *each* required
 * endpoint needs to be manually configured
 *
 * {@link http://www.meetup.com/meetup_api/docs/batch/}
 *
 * @param {Object} query a query object from the application
 * @return {Object} the arguments for api request, including endpoint
 */
export function queryToApiConfig({ endpoint, ref, type, params, flags }) {
	if (!endpoint) {
		console.warn(
			'Queries without an explicit `endpoint` key are deprecated.',
			'Please specify the endpoint and params in the query function directly.',
			`Query: ${JSON.stringify({ ref, type })}`
		);
		if (!(type in apiConfigCreators)) {
			throw new ReferenceError(`No API specified for query type ${type} and no endpoint provided`);
		}
		const baseConfig = apiConfigCreators[type](params);
		endpoint = baseConfig.endpoint;
		params = baseConfig.params;
	}
	return {
		endpoint,
		params,
		flags,
	};
}

/**
 * Transform each query into the arguments needed for a `request` call.
 *
 * Some request options are constant for all queries, and these are curried into
 * a function that can be called with a single query as part of the request
 * stream
 *
 * @see {@link https://www.npmjs.com/package/request}
 *
 * @param {Object} externalRequestOpts request options that will be applied to
 *   every query request
 * @param {Object} apiConfig { endpoint, params, flags }
 *   call)
 * @return {Object} externalRequestOptsQuery argument for the call to
 *   `externalRequest` for the query
 */
export const buildRequestArgs = externalRequestOpts =>
	({ endpoint, params, flags }) => {

		// cheap, brute-force object clone, acceptable for serializable object
		const externalRequestOptsQuery = JSON.parse(JSON.stringify(externalRequestOpts));
		externalRequestOptsQuery.url = encodeURI(`/${endpoint}`);

		if (flags) {
			externalRequestOptsQuery.headers['X-Meetup-Request-Flags'] = flags.join(',');
		}

		const dataParams = querystring.stringify(params);

		switch (externalRequestOptsQuery.method) {
		case 'get':
		case 'delete':
			externalRequestOptsQuery.url += `?${dataParams}`;
			externalRequestOptsQuery.headers['X-Meta-Photo-Host'] = 'secure';
			break;
		case 'post':
			externalRequestOptsQuery.body = dataParams;
			externalRequestOptsQuery.headers['content-type'] = 'application/x-www-form-urlencoded';
			break;
		}

		return externalRequestOptsQuery;
	};

/**
 * Format apiResponse to match expected state structure
 *
 * @param {Object} apiResponse JSON-parsed api response data
 */
export const apiResponseToQueryResponse = query => ({ value, meta }) => ({
	[query.ref]: {
		type: query.type,
		value,
		meta
	}
});

/**
 * Parse request for queries and request options
 * @return {Object} { queries, externalRequestOpts }
 */
export function parseRequest(request, baseUrl) {
	const {
		headers,
		method,
		query,
		payload,
		state,
	} = request;

	// Forward the Hapi request headers from the client query
	// except for `host` and `accept-encoding`
	// which should be provided by the external api request
	const externalRequestHeaders = {
		...headers,
		authorization: `Bearer ${state.oauth_token}`,
	};

	delete externalRequestHeaders['host'];
	delete externalRequestHeaders['accept-encoding'];
	delete externalRequestHeaders['content-length'];  // original request content-length is irrelevant

	const externalRequestOpts = {
		baseUrl,
		method,
		headers: externalRequestHeaders,  // make a copy to be immutable
		mode: 'no-cors',
		time: true,
		agentOptions: {
			rejectUnauthorized: baseUrl.indexOf('.dev') === -1
		}
	};

	const queriesJSON = request.method === 'get' ? query.queries : payload.queries;
	const validatedQueries = Joi.validate(
		JSON.parse(queriesJSON),
		Joi.array().items(querySchema)
	);
	if (validatedQueries.error) {
		throw validatedQueries.error;
	}
	const queries = validatedQueries.value;
	return { queries, externalRequestOpts };
}

/**
 * From a provided set of signed duotone URLs, create a function that injects
 * the full duotone URL into a group object with the key `duotoneUrl`.
 *
 * @param {Object} duotoneUrls map of `[duotoneRef]: url template root`
 * @param {Object} group group object from API
 * @return {Object} the mutated group object
 */
export const groupDuotoneSetter = duotoneUrls => group => {
	const photo = group.key_photo || group.group_photo || {};
	const duotoneKey = group.photo_gradient && duotoneRef(
			group.photo_gradient.light_color,
			group.photo_gradient.dark_color
		);
	const duotoneUrlRoot = duotoneKey && duotoneUrls[duotoneKey];
	if (duotoneUrlRoot && photo.id) {
		group.duotoneUrl = `${duotoneUrlRoot}/${photo.id}.jpeg`;
	}
	return group;
};

/**
 * From a provided set of signed duotoneUrls, create a function that injects
 * the full duotone URL into an query response containing objects that support
 * duotoned images (anything containing group or event objects
 *
 * @param {Object} duotoneUrls map of `[duotoneRef]: url template root`
 * @param {Object} queryResponse { type: <type>, value: <API object> }
 * @return {Object} the modified queryResponse
 */
export const apiResponseDuotoneSetter = duotoneUrls => {
	const setGroupDuotone = groupDuotoneSetter(duotoneUrls);
	return queryResponse => {
		// inject duotone URLs into any group query response
		Object.keys(queryResponse)
			.forEach(key => {
				const { type, value } = queryResponse[key];
				if (!value || value.error) {
					return;
				}
				let groups;
				switch (type) {
				case 'group':
					groups = value instanceof Array ? value : [value];
					groups.forEach(setGroupDuotone);
					break;
				case 'home':
					(value.rows || []).map(({ items }) => items)
						.forEach(items => items.filter(({ type }) => type === 'group')
							.forEach(({ group }) => setGroupDuotone(group))
						);
					break;
				}
			});
		return queryResponse;
	};
};

/**
 * Fake an API request and directly return the stringified mockResponse
 */
export const makeMockRequest = mockResponse => requestOpts =>
	Rx.Observable.of([MOCK_RESPONSE_OK, JSON.stringify(mockResponse)])
		.do(() => console.log(`MOCKING response to ${requestOpts.url}`));

const externalRequest$ = Rx.Observable.bindNodeCallback(externalRequest);
/**
 * Make a real external API request, return response body string
 */
export const makeExternalApiRequest = (request, API_TIMEOUT) => requestOpts =>
	externalRequest$(requestOpts)
		.timeout(API_TIMEOUT);

export const logApiResponse = appRequest => ([response, body]) => {
	const {
		uri: {
			query,
			pathname,
		},
		method,
	} = response.request;

	const responseLog = {
		request: {
			query: (query || '').split('&').reduce((acc, keyval) => {
				const [key, val] = keyval.split('=');
				acc[key] = val;
				return acc;
			}, {}),
			pathname,
			method,
		},
		response: {
			elapsedTime: response.elapsedTime,
			body: body.length > 256 ? `${body.substr(0, 256)}...`: body,
		},
	};
	appRequest.log(['api', 'info'], JSON.stringify(responseLog, null, 2));
};

/**
 * Login responses contain oauth info that should be applied to the response.
 * If `request.plugins.requestAuth.reply` exists (supplied by the requestAuthPlugin),
 * the application is able to set cookies on the response. Otherwise, return
 * the login response unchanged
 */
export const parseLoginAuth = (request, query) => response => {
	if (query.type === 'login' && request.plugins.requestAuth) {
		const {
			oauth_token,
			refresh_token,
			expires_in,
			member,
		} = response.value;
		applyAuthState(request, request.plugins.requestAuth.reply)({
			oauth_token,
			refresh_token,
			expires_in
		});
		response.value = { member };
	}
	return response;
};

/**
 * Make an API request and parse the response into the expected `response`
 * object shape
 */
export const makeApiRequest$ = (request, API_TIMEOUT, duotoneUrls) => {
	const setApiResponseDuotones = apiResponseDuotoneSetter(duotoneUrls);
	return ([requestOpts, query]) => {
		const request$ = query.mockResponse ?
			makeMockRequest(query.mockResponse) :
			makeExternalApiRequest(request, API_TIMEOUT);

		return Rx.Observable.defer(() => {
			request.log(['api', 'info'], `REST API request: ${requestOpts.url}`);
			return request$(requestOpts)
				.do(logApiResponse(request))             // this will leak private info in API response
				.map(parseApiResponse(requestOpts.url))  // parse into plain object
				.catch(errorResponse$(requestOpts.url))
				.map(parseLoginAuth(request, query))     // login has oauth secrets - special case
				.map(apiResponseToQueryResponse(query))  // convert apiResponse to app-ready queryResponse
				.map(setApiResponseDuotones);            // special duotone prop
		});
	};
};

<commit_msg>only posts have body<commit_after>import querystring from 'querystring';
import url from 'url';

import externalRequest from 'request';
import Joi from 'joi';
import Rx from 'rxjs';

import {
	applyAuthState,
} from '../util/authUtils';

import {
	querySchema
} from './validation';
import { duotoneRef } from './duotone';
import * as apiConfigCreators from '../apiProxy/apiConfigCreators';

const MOCK_RESPONSE_OK = {  // minimal representation of http.IncomingMessage
	statusCode: 200,
	statusMessage: 'OK',
	headers: {
		'x-meetup-request-id': 'mock request'
	},
	request: {
		uri: {},
	},
};

const parseResponseFlags = flagHeader =>
	(flagHeader || '')
		.split(',')
		.filter(pair => pair)  // ignore empty elements in the array
		.map(pair => pair.split('='))
		.reduce((flags, [key, val]) => {
			flags[key] = val === 'true';
			return flags;
		}, {});

export const parseMetaHeaders = headers => {
	const flags = parseResponseFlags(headers['x-meetup-flags']);
	const requestId = headers['x-meetup-request-id'];
	return {
		flags,
		requestId,
	};
};

/**
 * Accept an Error and return an object that will be used in place of the
 * expected API return value
 */
function formatApiError(err) {
	return {
		error: err.message
	};
}

export const errorResponse$ = requestUrl => err =>
	Rx.Observable.of({
		value: formatApiError(err),
		meta: {
			endpoint: url.parse(requestUrl).pathname,
		},
	});

export const parseApiValue = ([response, body]) => {
	// treat non-success HTTP code as an error
	if (response.statusCode < 200 || response.statusCode > 299) {
		return formatApiError(new Error(response.statusMessage));
	}
	try {
		if (response.statusCode === 204) {  // NoContent response type
			return null;
		}

		const value = JSON.parse(body);
		if (value && value.problem) {
			return formatApiError(new Error(`API problem: ${value.problem}: ${value.details}`));
		}
		return value;
	} catch(err) {
		return formatApiError(err);
	}
};

/**
 *
 * mostly error handling - any case where the API does not satisfy the
 * "api response" formatting requirement: plain object containing the requested
 * values
 *
 * This utility is specific to the response format of the API being consumed
 * @param {Array} the callback args for npm request - [response, body], where
 * `response` is an `Http.IncomingMessage` and `body` is the body text of the
 * response.
 * @return responseObj the JSON-parsed text, possibly with error info
 */
export const parseApiResponse = requestUrl => ([response, body]) => {
	const meta = {
		...parseMetaHeaders(response.headers),
		endpoint: url.parse(requestUrl).pathname,
	};

	return {
		value: parseApiValue([response, body]),
		meta,
	};

};

/**
 * Translate a query into an API `endpoint` + `params`. The translation is based
 * on the Meetup REST API.
 *
 * This function serves as an adapter between the structure of a query and the
 * API-specific config needed to get that data. Note that *each* required
 * endpoint needs to be manually configured
 *
 * {@link http://www.meetup.com/meetup_api/docs/batch/}
 *
 * @param {Object} query a query object from the application
 * @return {Object} the arguments for api request, including endpoint
 */
export function queryToApiConfig({ endpoint, ref, type, params, flags }) {
	if (!endpoint) {
		console.warn(
			'Queries without an explicit `endpoint` key are deprecated.',
			'Please specify the endpoint and params in the query function directly.',
			`Query: ${JSON.stringify({ ref, type })}`
		);
		if (!(type in apiConfigCreators)) {
			throw new ReferenceError(`No API specified for query type ${type} and no endpoint provided`);
		}
		const baseConfig = apiConfigCreators[type](params);
		endpoint = baseConfig.endpoint;
		params = baseConfig.params;
	}
	return {
		endpoint,
		params,
		flags,
	};
}

/**
 * Transform each query into the arguments needed for a `request` call.
 *
 * Some request options are constant for all queries, and these are curried into
 * a function that can be called with a single query as part of the request
 * stream
 *
 * @see {@link https://www.npmjs.com/package/request}
 *
 * @param {Object} externalRequestOpts request options that will be applied to
 *   every query request
 * @param {Object} apiConfig { endpoint, params, flags }
 *   call)
 * @return {Object} externalRequestOptsQuery argument for the call to
 *   `externalRequest` for the query
 */
export const buildRequestArgs = externalRequestOpts =>
	({ endpoint, params, flags }) => {

		// cheap, brute-force object clone, acceptable for serializable object
		const externalRequestOptsQuery = JSON.parse(JSON.stringify(externalRequestOpts));
		externalRequestOptsQuery.url = encodeURI(`/${endpoint}`);

		if (flags) {
			externalRequestOptsQuery.headers['X-Meetup-Request-Flags'] = flags.join(',');
		}

		const dataParams = querystring.stringify(params);

		switch (externalRequestOptsQuery.method) {
		case 'get':
		case 'delete':
			externalRequestOptsQuery.url += `?${dataParams}`;
			externalRequestOptsQuery.headers['X-Meta-Photo-Host'] = 'secure';
			break;
		case 'post':
			externalRequestOptsQuery.body = dataParams;
			externalRequestOptsQuery.headers['content-type'] = 'application/x-www-form-urlencoded';
			break;
		}

		return externalRequestOptsQuery;
	};

/**
 * Format apiResponse to match expected state structure
 *
 * @param {Object} apiResponse JSON-parsed api response data
 */
export const apiResponseToQueryResponse = query => ({ value, meta }) => ({
	[query.ref]: {
		type: query.type,
		value,
		meta
	}
});

/**
 * Parse request for queries and request options
 * @return {Object} { queries, externalRequestOpts }
 */
export function parseRequest(request, baseUrl) {
	const {
		headers,
		method,
		query,
		payload,
		state,
	} = request;

	// Forward the Hapi request headers from the client query
	// except for `host` and `accept-encoding`
	// which should be provided by the external api request
	const externalRequestHeaders = {
		...headers,
		authorization: `Bearer ${state.oauth_token}`,
	};

	delete externalRequestHeaders['host'];
	delete externalRequestHeaders['accept-encoding'];
	delete externalRequestHeaders['content-length'];  // original request content-length is irrelevant

	const externalRequestOpts = {
		baseUrl,
		method,
		headers: externalRequestHeaders,  // make a copy to be immutable
		mode: 'no-cors',
		time: true,
		agentOptions: {
			rejectUnauthorized: baseUrl.indexOf('.dev') === -1
		}
	};

	const queriesJSON = request.method === 'post' ? payload.queries : query.queries;
	const validatedQueries = Joi.validate(
		JSON.parse(queriesJSON),
		Joi.array().items(querySchema)
	);
	if (validatedQueries.error) {
		throw validatedQueries.error;
	}
	const queries = validatedQueries.value;
	return { queries, externalRequestOpts };
}

/**
 * From a provided set of signed duotone URLs, create a function that injects
 * the full duotone URL into a group object with the key `duotoneUrl`.
 *
 * @param {Object} duotoneUrls map of `[duotoneRef]: url template root`
 * @param {Object} group group object from API
 * @return {Object} the mutated group object
 */
export const groupDuotoneSetter = duotoneUrls => group => {
	const photo = group.key_photo || group.group_photo || {};
	const duotoneKey = group.photo_gradient && duotoneRef(
			group.photo_gradient.light_color,
			group.photo_gradient.dark_color
		);
	const duotoneUrlRoot = duotoneKey && duotoneUrls[duotoneKey];
	if (duotoneUrlRoot && photo.id) {
		group.duotoneUrl = `${duotoneUrlRoot}/${photo.id}.jpeg`;
	}
	return group;
};

/**
 * From a provided set of signed duotoneUrls, create a function that injects
 * the full duotone URL into an query response containing objects that support
 * duotoned images (anything containing group or event objects
 *
 * @param {Object} duotoneUrls map of `[duotoneRef]: url template root`
 * @param {Object} queryResponse { type: <type>, value: <API object> }
 * @return {Object} the modified queryResponse
 */
export const apiResponseDuotoneSetter = duotoneUrls => {
	const setGroupDuotone = groupDuotoneSetter(duotoneUrls);
	return queryResponse => {
		// inject duotone URLs into any group query response
		Object.keys(queryResponse)
			.forEach(key => {
				const { type, value } = queryResponse[key];
				if (!value || value.error) {
					return;
				}
				let groups;
				switch (type) {
				case 'group':
					groups = value instanceof Array ? value : [value];
					groups.forEach(setGroupDuotone);
					break;
				case 'home':
					(value.rows || []).map(({ items }) => items)
						.forEach(items => items.filter(({ type }) => type === 'group')
							.forEach(({ group }) => setGroupDuotone(group))
						);
					break;
				}
			});
		return queryResponse;
	};
};

/**
 * Fake an API request and directly return the stringified mockResponse
 */
export const makeMockRequest = mockResponse => requestOpts =>
	Rx.Observable.of([MOCK_RESPONSE_OK, JSON.stringify(mockResponse)])
		.do(() => console.log(`MOCKING response to ${requestOpts.url}`));

const externalRequest$ = Rx.Observable.bindNodeCallback(externalRequest);
/**
 * Make a real external API request, return response body string
 */
export const makeExternalApiRequest = (request, API_TIMEOUT) => requestOpts =>
	externalRequest$(requestOpts)
		.timeout(API_TIMEOUT);

export const logApiResponse = appRequest => ([response, body]) => {
	const {
		uri: {
			query,
			pathname,
		},
		method,
	} = response.request;

	const responseLog = {
		request: {
			query: (query || '').split('&').reduce((acc, keyval) => {
				const [key, val] = keyval.split('=');
				acc[key] = val;
				return acc;
			}, {}),
			pathname,
			method,
		},
		response: {
			elapsedTime: response.elapsedTime,
			body: body.length > 256 ? `${body.substr(0, 256)}...`: body,
		},
	};
	appRequest.log(['api', 'info'], JSON.stringify(responseLog, null, 2));
};

/**
 * Login responses contain oauth info that should be applied to the response.
 * If `request.plugins.requestAuth.reply` exists (supplied by the requestAuthPlugin),
 * the application is able to set cookies on the response. Otherwise, return
 * the login response unchanged
 */
export const parseLoginAuth = (request, query) => response => {
	if (query.type === 'login' && request.plugins.requestAuth) {
		const {
			oauth_token,
			refresh_token,
			expires_in,
			member,
		} = response.value;
		applyAuthState(request, request.plugins.requestAuth.reply)({
			oauth_token,
			refresh_token,
			expires_in
		});
		response.value = { member };
	}
	return response;
};

/**
 * Make an API request and parse the response into the expected `response`
 * object shape
 */
export const makeApiRequest$ = (request, API_TIMEOUT, duotoneUrls) => {
	const setApiResponseDuotones = apiResponseDuotoneSetter(duotoneUrls);
	return ([requestOpts, query]) => {
		const request$ = query.mockResponse ?
			makeMockRequest(query.mockResponse) :
			makeExternalApiRequest(request, API_TIMEOUT);

		return Rx.Observable.defer(() => {
			request.log(['api', 'info'], `REST API request: ${requestOpts.url}`);
			return request$(requestOpts)
				.do(logApiResponse(request))             // this will leak private info in API response
				.map(parseApiResponse(requestOpts.url))  // parse into plain object
				.catch(errorResponse$(requestOpts.url))
				.map(parseLoginAuth(request, query))     // login has oauth secrets - special case
				.map(apiResponseToQueryResponse(query))  // convert apiResponse to app-ready queryResponse
				.map(setApiResponseDuotones);            // special duotone prop
		});
	};
};

<|endoftext|>