<commit_before>import { Record, List } from 'immutable';

import React from 'react';
import { connect } from 'react-redux';

import page from 'page';

import StackChart from '../../../../shared/js/components/StackChart';
import {makeAmountString} from '../../../../shared/js/components/MoneyAmount';
import PageTitle from '../../../../shared/js/components/gironde.fr/PageTitle';
import SecundaryTitle from '../../../../shared/js/components/gironde.fr/SecundaryTitle';
import PrimaryCallToAction from '../../../../shared/js/components/gironde.fr/PrimaryCallToAction';
import Markdown from '../../../../shared/js/components/Markdown';
import {m52ToAggregated, hierarchicalAggregated} from '../../../../shared/js/finance/memoized';
import {flattenTree} from '../../../../shared/js/finance/visitHierarchical';
import {EXPENDITURES} from '../../../../shared/js/finance/constants';

import {urls, INSERTION_PICTO, ENFANCE_PICTO, HANDICAPES_PICTO, PERSONNES_AGEES_PICTO} from '../../constants/resources';

import FocusDetail from '../FocusDetail';
import FocusDonut from '../FocusDonut';

import colorClassById from '../../colorClassById';

/*

interface FocusSolidarityProps{
    currentYear,
    solidarityByYear: Map<year, YearSolidarityRecord>
}

*/

export function FocusSol({
    currentYear, currentYearSolidarity, solidarityByYear, screenWidth, urls
}) {

    const years = solidarityByYear.keySeq().toJS();

    // TODO current number (May 29th is 0.51 which is different than what was hardcoded (0.52))
    const solidarityProportion = currentYearSolidarity &&currentYearSolidarity.solidarityExpenditures/currentYearSolidarity.totalExpenditures;

    return React.createElement('article', {className: 'focus'},
        React.createElement('section', {},
            React.createElement(PageTitle, {text: 'Solidarités'}),
            React.createElement(Markdown, {},
                `Chaque jour, sur l’ensemble du territoire girondin, le Département poursuit le même objectif: réduire les inégalités géographiques et sociales auprès de toutes les populations. Le Département est un acteur incontournable de la lutte contre l’exclusion et la précarité. Il mène des actions sociales innovantes pour accompagner les personnes fragilisées. Les Girondins peuvent ainsi bénéficier d’allocations, prestations sociales et solidarité dans leur parcours de vie au quotidien.`
            )
        ),
        React.createElement('section', {className: 'top-infos'},
            React.createElement(FocusDonut, {
                proportion: solidarityProportion,
                outerRadius: screenWidth < 400 ? (screenWidth/2 - 20) : 188,
                innerText: [
                    `de dépenses Solidarités`,
                    `dans le total des dépenses`
                ]
            }),
            React.createElement('div', {},
                React.createElement(Markdown, {},
                    `**Avec 120 000 prestations allouées et ${(currentYearSolidarity && currentYearSolidarity.solidarityExpenditures/1000000).toFixed(0)} millions d'euros mobilisés en ${currentYear}, les dépenses de Solidarités pour soutenir les personnes fragilisées évoluent de +4,31% par rapport à 2015.**`),
                React.createElement(PrimaryCallToAction, {href: '#!/finance-details/DF-2', text: `en savoir plus`})
            ),
            React.createElement('div', {className: 'people-fraction'},
                React.createElement('div', {},
                    React.createElement('div', {}, 'Près de'),
                    React.createElement('div', {className: 'number'}, '1/10'),
                    React.createElement('div', {}, `personne accompagnée par le département`)
                )
            )
        ),
        React.createElement('section', {},
            React.createElement(SecundaryTitle, {text: `Les dépenses "Solidarités" augmentent pour tous les publics`}),
            React.createElement(StackChart, {
                WIDTH: screenWidth >= 800 + 80 ?
                    800 :
                    (screenWidth - 85 >= 600 ? screenWidth - 85 : (
                        screenWidth <= 600 ? screenWidth - 10 : 600
                    )),
                portrait: screenWidth <= 600,
                xs: years,
                ysByX: solidarityByYear.map(yearSolidarity => (new List([
                    yearSolidarity['DF-2-1'],
                    yearSolidarity['DF-2-2'],
                    yearSolidarity['DF-2-3'],
                    yearSolidarity['DF-2-4'],
                    yearSolidarity['DF-2-5']
                ]))),
                legendItems: [
                    {
                        id: 'DF-2-1',
                        colorClassName: colorClassById.get('DF-2-1'),
                        text: "Personnes en insertion",
                        url: `#!/finance-details/DF-2-1`
                    },
                    {
                        id: 'DF-2-2',
                        colorClassName: colorClassById.get('DF-2-2'),
                        text: "Personnes en situation de handicap",
                        url: `#!/finance-details/DF-2-2`
                    },
                    {
                        id: 'DF-2-3',
                        colorClassName: colorClassById.get('DF-2-3'),
                        text: "Personnes âgées",
                        url: `#!/finance-details/DF-2-3`
                    },
                    {
                        id: 'DF-2-4',
                        colorClassName: colorClassById.get('DF-2-4'),
                        text: "Enfance",
                        url: `#!/finance-details/DF-2-4`
                    },
                    {
                        id: 'DF-2-5',
                        colorClassName: colorClassById.get('DF-2-5'),
                        text: "Autres",
                        url: `#!/finance-details/DF-2-5`
                    }
                ],
                yValueDisplay: makeAmountString,
                onBrickClicked: (year, id) => { page(`#!/finance-details/${id}`); }
            })
        ),
        React.createElement('section', {},
            React.createElement(SecundaryTitle, {text: `Les actions et les aides varient en fonction des publics`}),
            React.createElement(Markdown, {}, `On distingue quatre catégories de public pouvant être aidé:

            - les personnes en insertion ou en situation de précarité,
            - les personnes en situation de handicap
            - les personnes âgées
            - les enfants
            Le Département définit sa propre politique et les actions qu’il met en œuvre pour chacun de ces publics : hébergements, prestations, subventions, allocations.`),
            React.createElement(FocusDetail, {
                className: 'insertion',
                title: 'Personnes en insertion',
                illustrationUrl: urls[INSERTION_PICTO],
                // (May 29th) different than what was hardcoded ("244 Millions €")
                amount: currentYearSolidarity ? currentYearSolidarity.get('DF-2-1') : undefined,
                proportion: currentYearSolidarity ? currentYearSolidarity.get('DF-2-1')/currentYearSolidarity.solidarityExpenditures : 1,
                text: `Le Revenu de Solidarité Active (RSA) représente près de 94% des aides financières versées aux personnes en insertion. Si vos ressources financières sont faibles ou inexistantes, vous pouvez demander le RSA. Plus d’infos sur: https://www.gironde.fr/insertion-rsa/vous-etes-un-particulier.`,
                highlights: [
                    {
                        strong: "+32%",
                        span: "de dépenses depuis 2012"
                    },
                    {
                        strong: "229M€",
                        span: "dédiés au RSA en 2016"
                    },
                    {
                        strong: "+5.5%",
                        span: "d'allocations RSA par rapport à 2015"
                    }
                ],
                moreUrl: '#!/finance-details/DF-2-1'
            }),
            React.createElement(FocusDetail, {
                className: 'handicap',
                title: 'Personnes en situation de handicap',
                illustrationUrl: urls[HANDICAPES_PICTO],
                // (May 29th) different than what was hardcoded ("218 Millions €",)
                amount: currentYearSolidarity ? currentYearSolidarity.get('DF-2-2') : undefined,
                proportion: currentYearSolidarity ? currentYearSolidarity.get('DF-2-2')/currentYearSolidarity.solidarityExpenditures : 1,
                text: `Les aides du Département versées aux personnes en situation de handicap concernent en 2016:
- la Prestation de Compensation du Handicap (PCH) versée à 9 975 personnes
- la prestation d’hébergement  qui a financé 2 763 places d'hébergement
- l’Allocation Compensation pour Tierce Personne (ACTP) a financé l'emploi d'aides à domicile pour 1 128 personnes

L’objectif de ces aides est de soutenir la vie à domicile, faciliter l’accessibilité au logement, à l’emploi, à la scolarisation et à la vie sociale.`,
                highlights: [
                    {
                        strong: "73M€",
                        span: "pour compenser la perte d'autonomie"
                    },
                    {
                        strong: "122M€",
                        span: "pour des places d’hébergement"
                    },
                    {
                        strong: "8.25M€",
                        span: "pour l'emploi de 757 aides à domicile"
                    }
                ],
                moreUrl: '#!/finance-details/DF-2-2'
            }),
            React.createElement(FocusDetail, {
                className: 'elderly',
                title: 'Personnes âgées',
                illustrationUrl: urls[PERSONNES_AGEES_PICTO],
                amount: currentYearSolidarity ? currentYearSolidarity.get('DF-2-3') : undefined,
                proportion: currentYearSolidarity ? currentYearSolidarity.get('DF-2-3')/currentYearSolidarity.solidarityExpenditures : 1,
                text: `L’Allocation Personnalisée d’Autonomie (APA) est la principale aide financière destinée à favoriser l’autonomie des personnes âgées.  Elle est versée directement à la personne ou à l’établissement en charge de cette personne, selon des critères d’attribution précis. https://www.gironde.fr/handicap-grand-age/aides-et-prestations-apa-pch-et-cmi L’application de la loi d’adaptation de la société au vieillissement (ASV) a entraîné une revalorisation de l’APA.`,
                highlights: [
                    {
                        strong: "141.6M€",
                        span: "versés en 2016 pour l’APA"
                    },
                    {
                        strong: "+ 3.53%",
                        span: "en 2016"
                    },
                    {
                        strong: "34 046",
                        span: "bénéficiaires en 2016"
                    }
                ],
                moreUrl: '#!/finance-details/DF-2-3'
            }),
            React.createElement(FocusDetail, {
                className: 'childhood',
                title: 'Enfance',
                illustrationUrl: urls[ENFANCE_PICTO],
                // (May 29th) different than what was hardcoded ("168 Millions €")
                amount: currentYearSolidarity ? currentYearSolidarity.get('DF-2-4') : undefined,
                proportion: currentYearSolidarity ? currentYearSolidarity.get('DF-2-4')/currentYearSolidarity.solidarityExpenditures : 1,
                text: `Le Département veille à protéger les enfants et les jeunes majeurs quand leur sécurité, leur santé et leur éducation sont menacées. Ce sont les professionnels de l’Aide sociale à l’enfance (ASE) qui assurent un suivi au plus près des familles. Si le danger rend impossible le maintien dans sa famille, l’enfant est pris en charge et est confié à des professionnels (familles d’accueil, maison d’enfants à caractère social, centre départemental de l’enfance et de la famille, foyer de l’enfance).
https://www.gironde.fr/enfance-et-famille/protection-de-lenfance.`,
                highlights: [
                    {
                        strong: "166M€",
                        span: "pour les Maisons d’Enfants à Caractère Sociale"
                    },
                    {
                        strong: "1392",
                        span: "enfants accueillis en 2016"
                    },
                    {
                        strong: "835",
                        span: " assistants familiaux pour l'accueil familial"
                    }
                ],
                moreUrl: '#!/finance-details/DF-2-4'
            })
        )
    );

}

const YearSolidarityRecord = Record({
    totalExpenditures: 0,
    solidarityExpenditures: 0,
    'DF-1-1': 0,
    'DF-1-2': 0,
    'DF-1-3': 0,
    'DF-1-4': 0,
    'DF-1-other': 0,
    'DF-2-1': 0,
    'DF-2-2': 0,
    'DF-2-3': 0,
    'DF-2-4': 0,
    'DF-2-5': 0
})

export default connect(
    state => {
        const { m52InstructionByYear, corrections, currentYear, screenWidth } = state;

        const solidarityByYear = m52InstructionByYear.map( ((instruction, year) => {
            const agg = m52ToAggregated(instruction, corrections);

            const hierAgg = hierarchicalAggregated(agg);

            const hierAggByPrestationList = flattenTree(hierAgg);

            const expenditures = hierAggByPrestationList.find(e => e.id === EXPENDITURES).total;
            let solidarityExpenditures = hierAggByPrestationList.find(e => e.id === 'DF-1').total;
            const ysrData = {};
            ['DF-1-1', 'DF-1-2', 'DF-1-3', 'DF-1-4', 'DF-2-1', 'DF-2-2', 'DF-2-3', 'DF-2-4', 'DF-2-5'].forEach(id => {
                ysrData[id] = hierAggByPrestationList.find(e => e.id === id).total;
            });

            let df1other = solidarityExpenditures - (ysrData['DF-1-1'] + ysrData['DF-1-2'] + ysrData['DF-1-3'] + ysrData['DF-1-4']);

            return new YearSolidarityRecord(Object.assign(
                {
                    totalExpenditures: expenditures,
                    solidarityExpenditures,
                    'DF-1-other': df1other
                },
                ysrData
            ))
        }))

        return {
            currentYear,
            currentYearSolidarity: solidarityByYear.get(currentYear),
            solidarityByYear,
            screenWidth,
            urls
        };
    },
    () => ({})
)(FocusSol);
<commit_msg>robustesse<commit_after>import { Record, List } from 'immutable';

import React from 'react';
import { connect } from 'react-redux';

import page from 'page';

import StackChart from '../../../../shared/js/components/StackChart';
import {makeAmountString} from '../../../../shared/js/components/MoneyAmount';
import PageTitle from '../../../../shared/js/components/gironde.fr/PageTitle';
import SecundaryTitle from '../../../../shared/js/components/gironde.fr/SecundaryTitle';
import PrimaryCallToAction from '../../../../shared/js/components/gironde.fr/PrimaryCallToAction';
import Markdown from '../../../../shared/js/components/Markdown';
import {m52ToAggregated, hierarchicalAggregated} from '../../../../shared/js/finance/memoized';
import {flattenTree} from '../../../../shared/js/finance/visitHierarchical';
import {EXPENDITURES} from '../../../../shared/js/finance/constants';

import {urls, INSERTION_PICTO, ENFANCE_PICTO, HANDICAPES_PICTO, PERSONNES_AGEES_PICTO} from '../../constants/resources';

import FocusDetail from '../FocusDetail';
import FocusDonut from '../FocusDonut';

import colorClassById from '../../colorClassById';

/*

interface FocusSolidarityProps{
    currentYear,
    solidarityByYear: Map<year, YearSolidarityRecord>
}

*/

export function FocusSol({
    currentYear, currentYearSolidarity, solidarityByYear, screenWidth, urls
}) {

    const years = solidarityByYear.keySeq().toJS();

    // TODO current number (May 29th is 0.51 which is different than what was hardcoded (0.52))
    const solidarityProportion = currentYearSolidarity &&currentYearSolidarity.solidarityExpenditures/currentYearSolidarity.totalExpenditures;

    return React.createElement('article', {className: 'focus'},
        React.createElement('section', {},
            React.createElement(PageTitle, {text: 'Solidarités'}),
            React.createElement(Markdown, {},
                `Chaque jour, sur l’ensemble du territoire girondin, le Département poursuit le même objectif: réduire les inégalités géographiques et sociales auprès de toutes les populations. Le Département est un acteur incontournable de la lutte contre l’exclusion et la précarité. Il mène des actions sociales innovantes pour accompagner les personnes fragilisées. Les Girondins peuvent ainsi bénéficier d’allocations, prestations sociales et solidarité dans leur parcours de vie au quotidien.`
            )
        ),
        React.createElement('section', {className: 'top-infos'},
            React.createElement(FocusDonut, {
                proportion: solidarityProportion,
                outerRadius: screenWidth < 400 ? (screenWidth/2 - 20) : 188,
                innerText: [
                    `de dépenses Solidarités`,
                    `dans le total des dépenses`
                ]
            }),
            React.createElement('div', {},
                React.createElement(Markdown, {},
                    `**Avec 120 000 prestations allouées et ${currentYearSolidarity && (currentYearSolidarity.solidarityExpenditures/1000000).toFixed(0)} millions d'euros mobilisés en ${currentYear}, les dépenses de Solidarités pour soutenir les personnes fragilisées évoluent de +4,31% par rapport à 2015.**`),
                React.createElement(PrimaryCallToAction, {href: '#!/finance-details/DF-2', text: `en savoir plus`})
            ),
            React.createElement('div', {className: 'people-fraction'},
                React.createElement('div', {},
                    React.createElement('div', {}, 'Près de'),
                    React.createElement('div', {className: 'number'}, '1/10'),
                    React.createElement('div', {}, `personne accompagnée par le département`)
                )
            )
        ),
        React.createElement('section', {},
            React.createElement(SecundaryTitle, {text: `Les dépenses "Solidarités" augmentent pour tous les publics`}),
            React.createElement(StackChart, {
                WIDTH: screenWidth >= 800 + 80 ?
                    800 :
                    (screenWidth - 85 >= 600 ? screenWidth - 85 : (
                        screenWidth <= 600 ? screenWidth - 10 : 600
                    )),
                portrait: screenWidth <= 600,
                xs: years,
                ysByX: solidarityByYear.map(yearSolidarity => (new List([
                    yearSolidarity['DF-2-1'],
                    yearSolidarity['DF-2-2'],
                    yearSolidarity['DF-2-3'],
                    yearSolidarity['DF-2-4'],
                    yearSolidarity['DF-2-5']
                ]))),
                legendItems: [
                    {
                        id: 'DF-2-1',
                        colorClassName: colorClassById.get('DF-2-1'),
                        text: "Personnes en insertion",
                        url: `#!/finance-details/DF-2-1`
                    },
                    {
                        id: 'DF-2-2',
                        colorClassName: colorClassById.get('DF-2-2'),
                        text: "Personnes en situation de handicap",
                        url: `#!/finance-details/DF-2-2`
                    },
                    {
                        id: 'DF-2-3',
                        colorClassName: colorClassById.get('DF-2-3'),
                        text: "Personnes âgées",
                        url: `#!/finance-details/DF-2-3`
                    },
                    {
                        id: 'DF-2-4',
                        colorClassName: colorClassById.get('DF-2-4'),
                        text: "Enfance",
                        url: `#!/finance-details/DF-2-4`
                    },
                    {
                        id: 'DF-2-5',
                        colorClassName: colorClassById.get('DF-2-5'),
                        text: "Autres",
                        url: `#!/finance-details/DF-2-5`
                    }
                ],
                yValueDisplay: makeAmountString,
                onBrickClicked: (year, id) => { page(`#!/finance-details/${id}`); }
            })
        ),
        React.createElement('section', {},
            React.createElement(SecundaryTitle, {text: `Les actions et les aides varient en fonction des publics`}),
            React.createElement(Markdown, {}, `On distingue quatre catégories de public pouvant être aidé:

            - les personnes en insertion ou en situation de précarité,
            - les personnes en situation de handicap
            - les personnes âgées
            - les enfants
            Le Département définit sa propre politique et les actions qu’il met en œuvre pour chacun de ces publics : hébergements, prestations, subventions, allocations.`),
            React.createElement(FocusDetail, {
                className: 'insertion',
                title: 'Personnes en insertion',
                illustrationUrl: urls[INSERTION_PICTO],
                // (May 29th) different than what was hardcoded ("244 Millions €")
                amount: currentYearSolidarity ? currentYearSolidarity.get('DF-2-1') : undefined,
                proportion: currentYearSolidarity ? currentYearSolidarity.get('DF-2-1')/currentYearSolidarity.solidarityExpenditures : 1,
                text: `Le Revenu de Solidarité Active (RSA) représente près de 94% des aides financières versées aux personnes en insertion. Si vos ressources financières sont faibles ou inexistantes, vous pouvez demander le RSA. Plus d’infos sur: https://www.gironde.fr/insertion-rsa/vous-etes-un-particulier.`,
                highlights: [
                    {
                        strong: "+32%",
                        span: "de dépenses depuis 2012"
                    },
                    {
                        strong: "229M€",
                        span: "dédiés au RSA en 2016"
                    },
                    {
                        strong: "+5.5%",
                        span: "d'allocations RSA par rapport à 2015"
                    }
                ],
                moreUrl: '#!/finance-details/DF-2-1'
            }),
            React.createElement(FocusDetail, {
                className: 'handicap',
                title: 'Personnes en situation de handicap',
                illustrationUrl: urls[HANDICAPES_PICTO],
                // (May 29th) different than what was hardcoded ("218 Millions €",)
                amount: currentYearSolidarity ? currentYearSolidarity.get('DF-2-2') : undefined,
                proportion: currentYearSolidarity ? currentYearSolidarity.get('DF-2-2')/currentYearSolidarity.solidarityExpenditures : 1,
                text: `Les aides du Département versées aux personnes en situation de handicap concernent en 2016:
- la Prestation de Compensation du Handicap (PCH) versée à 9 975 personnes
- la prestation d’hébergement  qui a financé 2 763 places d'hébergement
- l’Allocation Compensation pour Tierce Personne (ACTP) a financé l'emploi d'aides à domicile pour 1 128 personnes

L’objectif de ces aides est de soutenir la vie à domicile, faciliter l’accessibilité au logement, à l’emploi, à la scolarisation et à la vie sociale.`,
                highlights: [
                    {
                        strong: "73M€",
                        span: "pour compenser la perte d'autonomie"
                    },
                    {
                        strong: "122M€",
                        span: "pour des places d’hébergement"
                    },
                    {
                        strong: "8.25M€",
                        span: "pour l'emploi de 757 aides à domicile"
                    }
                ],
                moreUrl: '#!/finance-details/DF-2-2'
            }),
            React.createElement(FocusDetail, {
                className: 'elderly',
                title: 'Personnes âgées',
                illustrationUrl: urls[PERSONNES_AGEES_PICTO],
                amount: currentYearSolidarity ? currentYearSolidarity.get('DF-2-3') : undefined,
                proportion: currentYearSolidarity ? currentYearSolidarity.get('DF-2-3')/currentYearSolidarity.solidarityExpenditures : 1,
                text: `L’Allocation Personnalisée d’Autonomie (APA) est la principale aide financière destinée à favoriser l’autonomie des personnes âgées.  Elle est versée directement à la personne ou à l’établissement en charge de cette personne, selon des critères d’attribution précis. https://www.gironde.fr/handicap-grand-age/aides-et-prestations-apa-pch-et-cmi L’application de la loi d’adaptation de la société au vieillissement (ASV) a entraîné une revalorisation de l’APA.`,
                highlights: [
                    {
                        strong: "141.6M€",
                        span: "versés en 2016 pour l’APA"
                    },
                    {
                        strong: "+ 3.53%",
                        span: "en 2016"
                    },
                    {
                        strong: "34 046",
                        span: "bénéficiaires en 2016"
                    }
                ],
                moreUrl: '#!/finance-details/DF-2-3'
            }),
            React.createElement(FocusDetail, {
                className: 'childhood',
                title: 'Enfance',
                illustrationUrl: urls[ENFANCE_PICTO],
                // (May 29th) different than what was hardcoded ("168 Millions €")
                amount: currentYearSolidarity ? currentYearSolidarity.get('DF-2-4') : undefined,
                proportion: currentYearSolidarity ? currentYearSolidarity.get('DF-2-4')/currentYearSolidarity.solidarityExpenditures : 1,
                text: `Le Département veille à protéger les enfants et les jeunes majeurs quand leur sécurité, leur santé et leur éducation sont menacées. Ce sont les professionnels de l’Aide sociale à l’enfance (ASE) qui assurent un suivi au plus près des familles. Si le danger rend impossible le maintien dans sa famille, l’enfant est pris en charge et est confié à des professionnels (familles d’accueil, maison d’enfants à caractère social, centre départemental de l’enfance et de la famille, foyer de l’enfance).
https://www.gironde.fr/enfance-et-famille/protection-de-lenfance.`,
                highlights: [
                    {
                        strong: "166M€",
                        span: "pour les Maisons d’Enfants à Caractère Sociale"
                    },
                    {
                        strong: "1392",
                        span: "enfants accueillis en 2016"
                    },
                    {
                        strong: "835",
                        span: " assistants familiaux pour l'accueil familial"
                    }
                ],
                moreUrl: '#!/finance-details/DF-2-4'
            })
        )
    );

}

const YearSolidarityRecord = Record({
    totalExpenditures: 0,
    solidarityExpenditures: 0,
    'DF-1-1': 0,
    'DF-1-2': 0,
    'DF-1-3': 0,
    'DF-1-4': 0,
    'DF-1-other': 0,
    'DF-2-1': 0,
    'DF-2-2': 0,
    'DF-2-3': 0,
    'DF-2-4': 0,
    'DF-2-5': 0
})

export default connect(
    state => {
        const { m52InstructionByYear, corrections, currentYear, screenWidth } = state;

        const solidarityByYear = m52InstructionByYear.map( ((instruction, year) => {
            const agg = m52ToAggregated(instruction, corrections);

            const hierAgg = hierarchicalAggregated(agg);

            const hierAggByPrestationList = flattenTree(hierAgg);

            const expenditures = hierAggByPrestationList.find(e => e.id === EXPENDITURES).total;
            let solidarityExpenditures = hierAggByPrestationList.find(e => e.id === 'DF-1').total;
            const ysrData = {};
            ['DF-1-1', 'DF-1-2', 'DF-1-3', 'DF-1-4', 'DF-2-1', 'DF-2-2', 'DF-2-3', 'DF-2-4', 'DF-2-5'].forEach(id => {
                ysrData[id] = hierAggByPrestationList.find(e => e.id === id).total;
            });

            let df1other = solidarityExpenditures - (ysrData['DF-1-1'] + ysrData['DF-1-2'] + ysrData['DF-1-3'] + ysrData['DF-1-4']);

            return new YearSolidarityRecord(Object.assign(
                {
                    totalExpenditures: expenditures,
                    solidarityExpenditures,
                    'DF-1-other': df1other
                },
                ysrData
            ))
        }))

        return {
            currentYear,
            currentYearSolidarity: solidarityByYear.get(currentYear),
            solidarityByYear,
            screenWidth,
            urls
        };
    },
    () => ({})
)(FocusSol);
<|endoftext|>


<commit_before>/* jslint node: true */
'use strict';

//	ENiGMA½
const MenuModule			= require('../core/menu_module.js').MenuModule;
const resetScreen			= require('../core/ansi_term.js').resetScreen;
const Config				= require('./config.js').config;
const Errors				= require('./enig_error.js').Errors;
const Log					= require('./logger.js').log;
const getEnigmaUserAgent	= require('./misc_util.js').getEnigmaUserAgent;

//	deps
const async			= require('async');
const _				= require('lodash');
const joinPath		= require('path').join;
const crypto		= require('crypto');
const moment		= require('moment');
const https			= require('https');
const querystring	= require('querystring');
const fs			= require('fs');
const SSHClient		= require('ssh2').Client;

/*
	Configuration block:

	someDoor: {
		module: exodus
		config: {
			//	defaults
			ticketHost: oddnetwork.org
			ticketPort: 1984
			ticketPath: /exodus
			rejectUnauthorized: false // set to true to allow untrusted CA's (dangerous!)
			sshHost: oddnetwork.org
			sshPort: 22
			sshUser: exodus
			sshKeyPem: /path/to/enigma-bbs/misc/exodus.id_rsa

			//	optional
			caPem: /path/to/cacerts.pem	// see https://curl.haxx.se/docs/caextract.html

			//	required
			board: XXXX
			key: XXXX
			door: some_door
		}
	}
*/

exports.moduleInfo = {
	name	: 'Exodus',
	desc	: 'Exodus Door Server Access Module - https://oddnetwork.org/exodus/',
	author	: 'NuSkooler',
};

exports.getModule = class ExodusModule extends MenuModule {
	constructor(options) {
		super(options);

		this.config						= options.menuConfig.config || {};
		this.config.ticketHost			= this.config.ticketHost || 'oddnetwork.org';
		this.config.ticketPort			= this.config.ticketPort || 1984,
		this.config.ticketPath			= this.config.ticketPath || '/exodus';
		this.config.rejectUnauthorized	= _.get(this.config, 'rejectUnauthorized', true);
		this.config.sshHost				= this.config.sshHost || this.config.ticketHost;
		this.config.sshPort				= this.config.sshPort || 22;
		this.config.sshUser				= this.config.sshUser || 'exodus_server';
		this.config.sshKeyPem			= this.config.sshKeyPem || joinPath(Config.paths.misc, 'exodus.id_rsa');
	}

	initSequence() {

		const self				= this;
		let clientTerminated	= false;

		async.waterfall(
			[
				function validateConfig(callback) {
					//	very basic validation on optionals
					async.each( [ 'board', 'key', 'door' ], (key, next) => {
						return _.isString(self.config[key]) ? next(null) : next(Errors.MissingConfig(`Config requires "${key}"!`));
					}, callback);
				},
				function loadCertAuthorities(callback) {
					if(!_.isString(self.config.caPem)) {
						return callback(null, null);
					}

					fs.readFile(self.config.caPem, (err, certAuthorities) => {
						return callback(err, certAuthorities);
					});
				},
				function getTicket(certAuthorities, callback) {
					const now		= moment.utc().unix();
					const sha256	= crypto.createHash('sha256').update(`${self.config.key}${now}`).digest('hex');
					const token		= `${sha256}|${now}`;

					const postData	= querystring.stringify({
						token		: token,
						board		: self.config.board,
						user		: self.client.user.username,
						door		: self.config.door,
					});

					const reqOptions = {
						hostname			: self.config.ticketHost,
						port				: self.config.ticketPort,
						path				: self.config.ticketPath,
						rejectUnauthorized	: self.config.rejectUnauthorized,
						method				: 'POST',
						headers				: {
							'Content-Type'		: 'application/x-www-form-urlencoded',
							'Content-Length'	: postData.length,
							'User-Agent'		: getEnigmaUserAgent(),
						}
					};

					if(certAuthorities) {
						reqOptions.ca = certAuthorities;
					}

					let ticket = '';
					const req = https.request(reqOptions, res => {
						res.on('data', data => {
							ticket += data;
						});

						res.on('end', () => {
							if(ticket.length !== 36) {
								return callback(Errors.Invalid(`Invalid Exodus ticket: ${ticket}`));
							}

							return callback(null, ticket);
						});
					});

					req.on('error', err => {
						return callback(Errors.General(`Exodus error: ${err.message}`));
					});

					req.write(postData);
					req.end();
				},
				function loadPrivateKey(ticket, callback) {
					fs.readFile(self.config.sshKeyPem, (err, privateKey) => {
						return callback(err, ticket, privateKey);
					});
				},
				function establishSecureConnection(ticket, privateKey, callback) {

					let pipeRestored = false;
					let pipedStream;

					function restorePipe() {
						if(pipedStream && !pipeRestored && !clientTerminated) {
							self.client.term.output.unpipe(pipedStream);
							self.client.term.output.resume();
						}
					}

					self.client.term.write(resetScreen());
					self.client.term.write('Connecting to Exodus server, please wait...\n');

					const sshClient = new SSHClient();

					const shellOptions = {
						env : {
							exodus : ticket,
						}
					};

					sshClient.on('ready', () => {
						self.client.once('end', () => {
							self.client.log.info('Connection ended. Terminating Exodus connection');
							clientTerminated = true;
							return sshClient.end();
						});

						sshClient.shell(shellOptions, (err, stream) => {

							pipedStream = stream;	//	:TODO: ewwwwwwwww hack
							self.client.term.output.pipe(stream);

							stream.on('data', d => {
								return self.client.term.rawWrite(d);
							});

							stream.on('close', () => {
								restorePipe();
								return sshClient.end();
							});
						});
					});

					sshClient.on('close', () => {
						restorePipe();
						return callback(null);
					});

					sshClient.connect({
						host		: self.config.sshHost,
						port		: self.config.sshPort,
						username	: self.config.sshUser,
						privateKey	: privateKey,
					});
				}
			],
			err => {
				if(err) {
					self.client.log.warn( { error : err.message }, 'Exodus error');
				}

				if(!clientTerminated) {
					self.prevMenu();
				}
			}
		);
	}
};
<commit_msg>Update to send proper term size for pty<commit_after>/* jslint node: true */
'use strict';

//	ENiGMA½
const MenuModule			= require('../core/menu_module.js').MenuModule;
const resetScreen			= require('../core/ansi_term.js').resetScreen;
const Config				= require('./config.js').config;
const Errors				= require('./enig_error.js').Errors;
const Log					= require('./logger.js').log;
const getEnigmaUserAgent	= require('./misc_util.js').getEnigmaUserAgent;

//	deps
const async			= require('async');
const _				= require('lodash');
const joinPath		= require('path').join;
const crypto		= require('crypto');
const moment		= require('moment');
const https			= require('https');
const querystring	= require('querystring');
const fs			= require('fs');
const SSHClient		= require('ssh2').Client;

/*
	Configuration block:

	someDoor: {
		module: exodus
		config: {
			//	defaults
			ticketHost: oddnetwork.org
			ticketPort: 1984
			ticketPath: /exodus
			rejectUnauthorized: false // set to true to allow untrusted CA's (dangerous!)
			sshHost: oddnetwork.org
			sshPort: 22
			sshUser: exodus
			sshKeyPem: /path/to/enigma-bbs/misc/exodus.id_rsa

			//	optional
			caPem: /path/to/cacerts.pem	// see https://curl.haxx.se/docs/caextract.html

			//	required
			board: XXXX
			key: XXXX
			door: some_door
		}
	}
*/

exports.moduleInfo = {
	name	: 'Exodus',
	desc	: 'Exodus Door Server Access Module - https://oddnetwork.org/exodus/',
	author	: 'NuSkooler',
};

exports.getModule = class ExodusModule extends MenuModule {
	constructor(options) {
		super(options);

		this.config						= options.menuConfig.config || {};
		this.config.ticketHost			= this.config.ticketHost || 'oddnetwork.org';
		this.config.ticketPort			= this.config.ticketPort || 1984,
		this.config.ticketPath			= this.config.ticketPath || '/exodus';
		this.config.rejectUnauthorized	= _.get(this.config, 'rejectUnauthorized', true);
		this.config.sshHost				= this.config.sshHost || this.config.ticketHost;
		this.config.sshPort				= this.config.sshPort || 22;
		this.config.sshUser				= this.config.sshUser || 'exodus_server';
		this.config.sshKeyPem			= this.config.sshKeyPem || joinPath(Config.paths.misc, 'exodus.id_rsa');
	}

	initSequence() {

		const self				= this;
		let clientTerminated	= false;

		async.waterfall(
			[
				function validateConfig(callback) {
					//	very basic validation on optionals
					async.each( [ 'board', 'key', 'door' ], (key, next) => {
						return _.isString(self.config[key]) ? next(null) : next(Errors.MissingConfig(`Config requires "${key}"!`));
					}, callback);
				},
				function loadCertAuthorities(callback) {
					if(!_.isString(self.config.caPem)) {
						return callback(null, null);
					}

					fs.readFile(self.config.caPem, (err, certAuthorities) => {
						return callback(err, certAuthorities);
					});
				},
				function getTicket(certAuthorities, callback) {
					const now		= moment.utc().unix();
					const sha256	= crypto.createHash('sha256').update(`${self.config.key}${now}`).digest('hex');
					const token		= `${sha256}|${now}`;

					const postData	= querystring.stringify({
						token		: token,
						board		: self.config.board,
						user		: self.client.user.username,
						door		: self.config.door,
					});

					const reqOptions = {
						hostname			: self.config.ticketHost,
						port				: self.config.ticketPort,
						path				: self.config.ticketPath,
						rejectUnauthorized	: self.config.rejectUnauthorized,
						method				: 'POST',
						headers				: {
							'Content-Type'		: 'application/x-www-form-urlencoded',
							'Content-Length'	: postData.length,
							'User-Agent'		: getEnigmaUserAgent(),
						}
					};

					if(certAuthorities) {
						reqOptions.ca = certAuthorities;
					}

					let ticket = '';
					const req = https.request(reqOptions, res => {
						res.on('data', data => {
							ticket += data;
						});

						res.on('end', () => {
							if(ticket.length !== 36) {
								return callback(Errors.Invalid(`Invalid Exodus ticket: ${ticket}`));
							}

							return callback(null, ticket);
						});
					});

					req.on('error', err => {
						return callback(Errors.General(`Exodus error: ${err.message}`));
					});

					req.write(postData);
					req.end();
				},
				function loadPrivateKey(ticket, callback) {
					fs.readFile(self.config.sshKeyPem, (err, privateKey) => {
						return callback(err, ticket, privateKey);
					});
				},
				function establishSecureConnection(ticket, privateKey, callback) {

					let pipeRestored = false;
					let pipedStream;

					function restorePipe() {
						if(pipedStream && !pipeRestored && !clientTerminated) {
							self.client.term.output.unpipe(pipedStream);
							self.client.term.output.resume();
						}
					}

					self.client.term.write(resetScreen());
					self.client.term.write('Connecting to Exodus server, please wait...\n');

					const sshClient = new SSHClient();

					const window = {
						rows		: self.client.term.termHeight,
						cols		: self.client.term.termWidth,
						width		: 0,
						height		: 0,
						term		: 'vt100',	//	Want to pass |self.client.term.termClient| here, but we end up getting hung up on :(
					};

					const options = {
						env : {
							exodus : ticket,
						},
					};

					sshClient.on('ready', () => {
						self.client.once('end', () => {
							self.client.log.info('Connection ended. Terminating Exodus connection');
							clientTerminated = true;
							return sshClient.end();
						});

						sshClient.shell(window, options, (err, stream) => {
							pipedStream = stream;	//	:TODO: ewwwwwwwww hack
							self.client.term.output.pipe(stream);

							stream.on('data', d => {
								return self.client.term.rawWrite(d);
							});

							stream.on('close', () => {
								restorePipe();
								return sshClient.end();
							});

							stream.on('error', err => {
								Log.warn( { error : err.message }, 'Exodus SSH client stream error');
							});
						});
					});

					sshClient.on('close', () => {
						restorePipe();
						return callback(null);
					});

					sshClient.connect({
						host		: self.config.sshHost,
						port		: self.config.sshPort,
						username	: self.config.sshUser,
						privateKey	: privateKey,
					});
				}
			],
			err => {
				if(err) {
					self.client.log.warn( { error : err.message }, 'Exodus error');
				}

				if(!clientTerminated) {
					self.prevMenu();
				}
			}
		);
	}
};
<|endoftext|>


<commit_before>/*
 * Copyright 2020 Google LLC
 *
 * Licensed 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
 *
 *     https://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.
 */

/**
 * WordPress dependencies
 */
import { __ } from '@wordpress/i18n';

/**
 * External dependencies
 */
import { useCallback, useMemo, useReducer } from 'react';
import moment from 'moment';
import queryString from 'query-string';

/**
 * Internal dependencies
 */

import {
  STORY_STATUSES,
  STORY_SORT_OPTIONS,
  ORDER_BY_SORT,
  ITEMS_PER_PAGE,
} from '../../constants';
import storyReducer, {
  defaultStoriesState,
  ACTION_TYPES as STORY_ACTION_TYPES,
} from '../reducer/stories';

export function reshapeStoryObject(editStoryURL) {
  return function (originalStoryData) {
    const {
      id,
      title,
      modified,
      status,
      tags,
      date,
      categories,
      author,
      story_data: storyData,
    } = originalStoryData;
    if (
      !Array.isArray(storyData.pages) ||
      !id ||
      storyData.pages.length === 0
    ) {
      return null;
    }
    return {
      id,
      status,
      title: title.rendered,
      modified: moment(modified),
      created: moment(date),
      pages: storyData.pages,
      tags,
      categories,
      author,
      centerTargetAction: '',
      bottomTargetAction: `${editStoryURL}&post=${id}`,
      originalStoryData,
    };
  };
}

const useStoryApi = (dataAdapter, { editStoryURL, wpApi }) => {
  const [state, dispatch] = useReducer(storyReducer, defaultStoriesState);

  const fetchStories = useCallback(
    async ({
      status = STORY_STATUSES[0].value,
      sortOption = STORY_SORT_OPTIONS.LAST_MODIFIED,
      sortDirection,
      searchTerm,
      page = 1,
      perPage = ITEMS_PER_PAGE,
    }) => {
      dispatch({
        type: STORY_ACTION_TYPES.LOADING_STORIES,
        payload: true,
      });

      if (!wpApi) {
        dispatch({
          type: STORY_ACTION_TYPES.FETCH_STORIES_FAILURE,
          payload: true,
        });
        return;
      }

      const query = {
        context: 'edit',
        search: searchTerm || undefined,
        orderby: sortOption,
        page,
        per_page: perPage,
        order: sortDirection || ORDER_BY_SORT[sortOption],
        status,
      };

      try {
        const path = queryString.stringifyUrl({
          url: wpApi,
          query,
        });

        const response = await dataAdapter.get(path, {
          parse: false,
        });

        // TODO add headers for totals by status and have header reflect search
        // only update totals when data is different
        const totalStories = parseInt(response.headers.get('X-WP-Total'));
        const totalPages = parseInt(response.headers.get('X-WP-TotalPages'));

        const serverStoryResponse = await response.json();

        const reshapedStories = serverStoryResponse
          .map(reshapeStoryObject(editStoryURL))
          .filter(Boolean);

        dispatch({
          type: STORY_ACTION_TYPES.FETCH_STORIES_SUCCESS,
          payload: {
            stories: reshapedStories,
            totalPages,
            totalStories,
            page,
          },
        });
      } catch (err) {
        dispatch({
          type: STORY_ACTION_TYPES.FETCH_STORIES_FAILURE,
          payload: true,
        });
      } finally {
        dispatch({
          type: STORY_ACTION_TYPES.LOADING_STORIES,
          payload: false,
        });
      }
    },
    [wpApi, dataAdapter, editStoryURL]
  );

  const updateStory = useCallback(
    async (story) => {
      try {
        const response = await dataAdapter.post(`${wpApi}/${story.id}`, {
          data: story,
        });
        dispatch({
          type: STORY_ACTION_TYPES.UPDATE_STORY,
          payload: reshapeStoryObject(editStoryURL)(response),
        });
      } catch (e) {
        // eslint-disable-next-line no-console
        console.error(e);
      }
    },
    [wpApi, dataAdapter, editStoryURL]
  );

  const trashStory = useCallback(
    async (story) => {
      try {
        await dataAdapter.deleteRequest(`${wpApi}/${story.id}`, {
          data: story,
        });
        dispatch({
          type: STORY_ACTION_TYPES.TRASH_STORY,
          payload: { id: story.id },
        });
      } catch (e) {
        // eslint-disable-next-line no-console
        console.error(e);
      }
    },
    [wpApi, dataAdapter]
  );

  const duplicateStory = useCallback(
    async (story) => {
      try {
        const {
          content,
          story_data,
          style_presets,
          publisher_logo,
          featured_media,
          title,
        } = story.originalStoryData;
        const response = await dataAdapter.post(wpApi, {
          data: {
            content,
            story_data,
            featured_media,
            style_presets,
            publisher_logo,
            title: {
              raw: `${title.raw} ${__('(Copy)', 'web-stories')}`,
            },
            status: 'draft',
          },
        });
        dispatch({
          type: STORY_ACTION_TYPES.DUPLICATE_STORY,
          payload: reshapeStoryObject(editStoryURL)(response),
        });
      } catch (e) {
        // eslint-disable-next-line no-console
        console.error(e);
      }
    },
    [wpApi, dataAdapter, editStoryURL]
  );

  const api = useMemo(
    () => ({
      updateStory,
      fetchStories,
      trashStory,
      duplicateStory,
    }),
    [duplicateStory, trashStory, updateStory, fetchStories]
  );

  return { stories: state, api };
};

export default useStoryApi;
<commit_msg>swapping to use the decoded html version of the title we have access to to turn html symbols from & to characters &<commit_after>/*
 * Copyright 2020 Google LLC
 *
 * Licensed 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
 *
 *     https://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.
 */

/**
 * WordPress dependencies
 */
import { __ } from '@wordpress/i18n';

/**
 * External dependencies
 */
import { useCallback, useMemo, useReducer } from 'react';
import moment from 'moment';
import queryString from 'query-string';

/**
 * Internal dependencies
 */

import {
  STORY_STATUSES,
  STORY_SORT_OPTIONS,
  ORDER_BY_SORT,
  ITEMS_PER_PAGE,
} from '../../constants';
import storyReducer, {
  defaultStoriesState,
  ACTION_TYPES as STORY_ACTION_TYPES,
} from '../reducer/stories';

export function reshapeStoryObject(editStoryURL) {
  return function (originalStoryData) {
    const {
      id,
      title,
      modified,
      status,
      tags,
      date,
      categories,
      author,
      story_data: storyData,
    } = originalStoryData;
    if (
      !Array.isArray(storyData.pages) ||
      !id ||
      storyData.pages.length === 0
    ) {
      return null;
    }
    return {
      id,
      status,
      title: title.raw,
      modified: moment(modified),
      created: moment(date),
      pages: storyData.pages,
      tags,
      categories,
      author,
      centerTargetAction: '',
      bottomTargetAction: `${editStoryURL}&post=${id}`,
      originalStoryData,
    };
  };
}

const useStoryApi = (dataAdapter, { editStoryURL, wpApi }) => {
  const [state, dispatch] = useReducer(storyReducer, defaultStoriesState);

  const fetchStories = useCallback(
    async ({
      status = STORY_STATUSES[0].value,
      sortOption = STORY_SORT_OPTIONS.LAST_MODIFIED,
      sortDirection,
      searchTerm,
      page = 1,
      perPage = ITEMS_PER_PAGE,
    }) => {
      dispatch({
        type: STORY_ACTION_TYPES.LOADING_STORIES,
        payload: true,
      });

      if (!wpApi) {
        dispatch({
          type: STORY_ACTION_TYPES.FETCH_STORIES_FAILURE,
          payload: true,
        });
        return;
      }

      const query = {
        context: 'edit',
        search: searchTerm || undefined,
        orderby: sortOption,
        page,
        per_page: perPage,
        order: sortDirection || ORDER_BY_SORT[sortOption],
        status,
      };

      try {
        const path = queryString.stringifyUrl({
          url: wpApi,
          query,
        });

        const response = await dataAdapter.get(path, {
          parse: false,
        });

        // TODO add headers for totals by status and have header reflect search
        // only update totals when data is different
        const totalStories = parseInt(response.headers.get('X-WP-Total'));
        const totalPages = parseInt(response.headers.get('X-WP-TotalPages'));

        const serverStoryResponse = await response.json();

        const reshapedStories = serverStoryResponse
          .map(reshapeStoryObject(editStoryURL))
          .filter(Boolean);

        dispatch({
          type: STORY_ACTION_TYPES.FETCH_STORIES_SUCCESS,
          payload: {
            stories: reshapedStories,
            totalPages,
            totalStories,
            page,
          },
        });
      } catch (err) {
        dispatch({
          type: STORY_ACTION_TYPES.FETCH_STORIES_FAILURE,
          payload: true,
        });
      } finally {
        dispatch({
          type: STORY_ACTION_TYPES.LOADING_STORIES,
          payload: false,
        });
      }
    },
    [wpApi, dataAdapter, editStoryURL]
  );

  const updateStory = useCallback(
    async (story) => {
      try {
        const response = await dataAdapter.post(`${wpApi}/${story.id}`, {
          data: story,
        });
        dispatch({
          type: STORY_ACTION_TYPES.UPDATE_STORY,
          payload: reshapeStoryObject(editStoryURL)(response),
        });
      } catch (e) {
        // eslint-disable-next-line no-console
        console.error(e);
      }
    },
    [wpApi, dataAdapter, editStoryURL]
  );

  const trashStory = useCallback(
    async (story) => {
      try {
        await dataAdapter.deleteRequest(`${wpApi}/${story.id}`, {
          data: story,
        });
        dispatch({
          type: STORY_ACTION_TYPES.TRASH_STORY,
          payload: { id: story.id },
        });
      } catch (e) {
        // eslint-disable-next-line no-console
        console.error(e);
      }
    },
    [wpApi, dataAdapter]
  );

  const duplicateStory = useCallback(
    async (story) => {
      try {
        const {
          content,
          story_data,
          style_presets,
          publisher_logo,
          featured_media,
          title,
        } = story.originalStoryData;
        const response = await dataAdapter.post(wpApi, {
          data: {
            content,
            story_data,
            featured_media,
            style_presets,
            publisher_logo,
            title: {
              raw: `${title.raw} ${__('(Copy)', 'web-stories')}`,
            },
            status: 'draft',
          },
        });
        dispatch({
          type: STORY_ACTION_TYPES.DUPLICATE_STORY,
          payload: reshapeStoryObject(editStoryURL)(response),
        });
      } catch (e) {
        // eslint-disable-next-line no-console
        console.error(e);
      }
    },
    [wpApi, dataAdapter, editStoryURL]
  );

  const api = useMemo(
    () => ({
      updateStory,
      fetchStories,
      trashStory,
      duplicateStory,
    }),
    [duplicateStory, trashStory, updateStory, fetchStories]
  );

  return { stories: state, api };
};

export default useStoryApi;
<|endoftext|>


<commit_before>/* global localStorage */
import { getElements } from '../../../../packages/spark-core';

const saveSwitchSetting = (value) => {
  if (localStorage) {
    localStorage.setItem('code-example-switch', value);
  }
};

const hideAllSections = (values) => {
  values.forEach((value) => {
    getElements(`[data-${value}-documentation]`, (section) => {
      section.classList.add('drizzle-u-Display--none');
    });
  });
};

const showSections = (values) => {
  values.forEach((value) => {
    getElements(`[data-${value}-documentation]`, (section) => {
      section.classList.remove('drizzle-u-Display--none');
    });
  });
};

const bindUIEvents = (element) => {
  element.querySelectorAll('label').forEach((label) => {
    label.addEventListener('keydown', (e) => {
      if (e.keyCode === 13) {
        element.querySelectorAll(`input[id=${label.getAttribute('for')}]`)[0].click();
      }
    });
  });

  element.addEventListener('change', (e) => {
    const values = [];
    element.querySelectorAll('input').forEach((rdio) => {
      values.push(rdio.value);
    });
    hideAllSections(values);
    showSections([`${e.target.value}`]);
    saveSwitchSetting(e.target.value);
  });
};

const codeExampleSwitch = () => {
  getElements('[data-code-example-switch]', (item) => {
    bindUIEvents(item);
  });
};

export {
  codeExampleSwitch, bindUIEvents, hideAllSections, showSections, saveSwitchSetting,
};
<commit_msg>fixes #979<commit_after>/* global localStorage */
import { getElements } from '../../../../packages/spark-core';

const saveSwitchSetting = (value) => {
  if (localStorage) {
    localStorage.setItem('code-example-switch', value);
  }
};

const hideAllSections = (values) => {
  values.forEach((value) => {
    getElements(`[data-${value}-documentation]`, (section) => {
      section.classList.add('drizzle-u-Display--none');
    });
  });
};

const showSections = (values) => {
  values.forEach((value) => {
    getElements(`[data-${value}-documentation]`, (section) => {
      section.classList.remove('drizzle-u-Display--none');
    });
  });
};

const bindUIEvents = (element) => {
  element.querySelectorAll('label').forEach((label) => {
    label.addEventListener('keydown', (e) => {
      if (e.keyCode === 13) {
        element.querySelectorAll(`input[id=${label.getAttribute('for')}]`)[0].click();
      }
    });
  });

  element.addEventListener('click', (e) => {
    const values = [];
    element.querySelectorAll('input').forEach((rdio) => {
      values.push(rdio.value);
    });
    hideAllSections(values);
    showSections([`${e.target.value}`]);
    saveSwitchSetting(e.target.value);
  });
};

const codeExampleSwitch = () => {
  getElements('[data-code-example-switch]', (item) => {
    bindUIEvents(item);
  });
};

export {
  codeExampleSwitch, bindUIEvents, hideAllSections, showSections, saveSwitchSetting,
};
<|endoftext|>


<commit_before>/** @jsx React.DOM */

var React = window.React = require('react'),
    $ = require('jquery-browserify'),
    mountNode = document.getElementById("app");

var Event = React.createClass({
    render: function () {
        return (

                    {this.props.title}

                {this.props.children}

        )
    }
});

var EventList = React.createClass({
    render: function () {
        var eventNodes = this.props.data.map(function(event, index) {
            return (
                <Event title={event.title} key={index}>
                    {event.description}
                </Event>
            );
        });
        return (

                {eventNodes}

        );
    }
});

var EventForm = React.createClass({
    handleSubmit: function(e) {
        e.preventDefault()
        var title = this.refs.title.getDOMNode().value.trim();
        var description = this.refs.description.getDOMNode().value.trim();
        var latitude = this.refs.latitude.getDOMNode().value;
        var longitude = this.refs.longitude.getDOMNode().value;
        var coordinates = [parseFloat(latitude), parseFloat(longitude)];

        if (!title || !description) {
            return;
        }
        this.props.onEventSubmit({
            title: title,
            description: description,
            coordinates: coordinates
        });
        this.refs.title.getDOMNode().value = "";
        this.refs.description.getDOMNode().value = "";
        return;
    },
    render: function() {
        return (
            <form className="eventForm" onSubmit={this.handleSubmit}>
                <input type="text" placeholder="Event title" ref="title" />
                <input type="text" placeholder="Event description" ref="description" />
                <input type="number" placeholder="0.0" ref="latitude" />
                <input type="number" placeholder="0.0" ref="longitude" />
                <input type="submit" value="Post" />
            </form>
        )
    }
});

var EventBox = React.createClass({
    loadEventsFromServer: function () {
        $.ajax({
            url: this.props.url,
            dataType: 'json',
            type: 'GET',
            success: function (data) {
                this.setState({data: data});
            }.bind(this),
            error: function (xhr, status, err) {
                console.error(this.props.url, status, err.toString());
            }.bind(this)
        });
    },
    handleEventSubmit: function(event) {
        var events = this.state.data;
        events.push(event);
        console.log(JSON.stringify(event));
        this.setState({data: events}, function() {
            $.ajax({
                url: this.props.url,
                dataType: 'json',
                type: 'POST',
                data: event,
                success: function(data) {
                    this.setState({data: data});
                }.bind(this),
                error: function (xhr, status, err) {
                    console.error(this.props.url, status, err.toString());
                }.bind(this)
            });
        });
    },
    getInitialState: function () {
        return {data: []};
    },
    componentDidMount: function () {
        this.loadEventsFromServer();
        setInterval(this.loadEventsFromServer, this.props.pollInterval);
    },
    render: function () {
        return (

Events

                <EventList data={this.state.data} />
                <EventForm onEventSubmit={this.handleEventSubmit} />

        );
    }
});

React.renderComponent(<EventBox url="http://localhost:3000/events" pollInterval={5000} />, mountNode);
<commit_msg>Fixed POST request to correctly send JSON object<commit_after>/** @jsx React.DOM */

var React = window.React = require('react'),
    $ = require('jquery-browserify'),
    mountNode = document.getElementById("app");

var Event = React.createClass({
    render: function () {
        return (

                    {this.props.title}

                {this.props.children}

        )
    }
});

var EventList = React.createClass({
    render: function () {
        var eventNodes = this.props.data.map(function(event, index) {
            return (
                <Event title={event.title} key={index}>
                    {event.description}
                </Event>
            );
        });
        return (

                {eventNodes}

        );
    }
});

var EventForm = React.createClass({
    handleSubmit: function(e) {
        e.preventDefault()
        var title = this.refs.title.getDOMNode().value.trim();
        var description = this.refs.description.getDOMNode().value.trim();
        var latitude = this.refs.latitude.getDOMNode().value;
        var longitude = this.refs.longitude.getDOMNode().value;
        var coordinates = [parseFloat(latitude), parseFloat(longitude)];

        if (!title || !description) {
            return;
        }
        this.props.onEventSubmit({
            title: title,
            description: description,
            coordinates: coordinates
        });
        this.refs.title.getDOMNode().value = "";
        this.refs.description.getDOMNode().value = "";
        return;
    },
    render: function() {
        return (
            <form className="eventForm" onSubmit={this.handleSubmit}>
                <input type="text" placeholder="Event title" ref="title" />
                <input type="text" placeholder="Event description" ref="description" />
                <input type="number" placeholder="0.0" ref="latitude" />
                <input type="number" placeholder="0.0" ref="longitude" />
                <input type="submit" value="Post" />
            </form>
        )
    }
});

var EventBox = React.createClass({
    loadEventsFromServer: function () {
        $.ajax({
            url: this.props.url,
            dataType: 'json',
            type: 'GET',
            success: function (data) {
                this.setState({data: data});
            }.bind(this),
            error: function (xhr, status, err) {
                console.error(this.props.url, status, err.toString());
            }.bind(this)
        });
    },
    handleEventSubmit: function(event) {
        var events = this.state.data;
        events.push(event);
        console.log(JSON.stringify(event));
        this.setState({data: events}, function() {
            $.ajax({
                url: this.props.url,
                dataType: 'json',
                type: 'POST',
                data: JSON.stringify(event),
                success: function(data) {
                    this.setState({data: data});
                }.bind(this),
                error: function (xhr, status, err) {
                    console.error(this.props.url, status, err.toString());
                }.bind(this)
            });
        });
    },
    getInitialState: function () {
        return {data: []};
    },
    componentDidMount: function () {
        this.loadEventsFromServer();
        setInterval(this.loadEventsFromServer, this.props.pollInterval);
    },
    render: function () {
        return (

Events

                <EventList data={this.state.data} />
                <EventForm onEventSubmit={this.handleEventSubmit} />

        );
    }
});

React.renderComponent(<EventBox url="http://localhost:3000/events" pollInterval={15000} />, mountNode);
<|endoftext|>


<commit_before>#!/usr/bin/env node
var path    = require('path'),
    fs      = require('fs'),
    config  = require('./config.js'),
    spawn   = require('child_process').spawn;

console.log("Generating index file:")
console.log("  %s", config.SEARCH_INDEX),
console.log("from %s, ref:%s...", config.SEARCH_REPO, config.SEARCH_REF);

var tmp = config.SEARCH_INDEX + ".tmp";

var cs = spawn(path.join(__dirname, '..', 'codesearch'),
               ['--git_dir', path.join(config.SEARCH_REPO, ".git"),
                '--dump_index', tmp,
                '--order_root', config.ORDER_DIRS.join(' '),
                config.SEARCH_REF] + config.SEARCH_ARGS,
               {
                 customFds: [-1, 1, 2]
               });
cs.on('exit', function(code) {
        if (code !== 0)
          console.error("Index process exited with error %d", code);
        fs.renameSync(tmp, config.SEARCH_INDEX);
        process.exit(0);
      });
cs.stdin.end();
<commit_msg>Right, Javascript doesn't do + on arrays.<commit_after>#!/usr/bin/env node
var path    = require('path'),
    fs      = require('fs'),
    config  = require('./config.js'),
    spawn   = require('child_process').spawn;

console.log("Generating index file:")
console.log("  %s", config.SEARCH_INDEX),
console.log("from %s, ref:%s...", config.SEARCH_REPO, config.SEARCH_REF);

var tmp = config.SEARCH_INDEX + ".tmp";

var cs = spawn(path.join(__dirname, '..', 'codesearch'),
               ['--git_dir', path.join(config.SEARCH_REPO, ".git"),
                '--dump_index', tmp,
                '--order_root', config.ORDER_DIRS.join(' '),
                config.SEARCH_REF].concat(config.SEARCH_ARGS),
               {
                 customFds: [-1, 1, 2]
               });
cs.on('exit', function(code) {
        if (code !== 0)
          console.error("Index process exited with error %d", code);
        fs.renameSync(tmp, config.SEARCH_INDEX);
        process.exit(0);
      });
cs.stdin.end();
<|endoftext|>


<commit_before>/**
 * @namespace hs.api
 * @memberOf hs
 */
define(['angular', 'ol', 'app', 'map', 'core'],

    function(angular, ol, app, map) {
        angular.module('hs.api', ['hs', 'hs.map', 'hs.core'])
            .service("Api", ['$rootScope', 'hs.map.service', '$controller', 'Core',
                function($rootScope, OlMap, $controller, Core) {
                    var me = {};
                    me.gui = {};
                    $rootScope.$on("scope_loaded", function(event, args) {
                        me.gui[args] = {};
                        for (var key in event.targetScope) {
                            //Dont show angularjs functions in the api
                            if (typeof event.targetScope.__proto__[key] === 'undefined') {
                                me.gui[args][key] = event.targetScope[key];
                            }
                        }
                    });
                    me.listScopes = function() {
                        var scopes = getAllScopes();
                        var tmp = {};
                        for (var i = 0; i < scopes.length; i++) {
                            if (scopes[i].scope_name) {
                                tmp[scopes[i].scope_name] = scopes[i];
                            }
                        }
                        return tmp;
                    }
                    me.gui.setLanguage = function(lang) {
                        Core.setLanguage(lang);
                    }

                    me.getMap = function() {
                        return OlMap.map;
                    }

                    var recurseObject = function(obj, levels) {
                        levels += '\t';
                        if (levels.length > 5) return;
                        if (typeof obj === 'function') {
                            var fnStr = obj.toString().replace(/[\n\t]/g, ' '),
                                argStr = fnStr.match(/\((.+?\))/),
                                len = (argStr ? (argStr.index + argStr[0].length) : 11),
                                fnBodyStr = fnStr.substring(len),
                                metaData = {
                                    func: [],
                                    args: {}
                                };

                            console.log(levels, 'function', key, argStr ? argStr[0].split('{')[0] : "()");
                        }
                        if (typeof obj === 'object') {
                            var has_indexes = !(obj && typeof obj.length === 'undefined');
                            console.log(levels, key + (has_indexes ? '' : '.'));
                            if (!has_indexes) {
                                for (key in obj) {
                                    recurseObject(obj[key], levels);
                                }
                            }
                        }
                    }

                    me.printApiDefinitions = function() {
                        for (key in window.hslayers_api) {
                            recurseObject(window.hslayers_api[key], '');
                        }
                    }

                    window.hslayers_api = me;

                    /**
                    * @ngdoc method
                    * @name Api#getAllScopes
                    * @private
                    * @returns {Array} Array of all active Angular Scopes of app
                    * @description Get all scopes defined in app
                    */
                    function getAllScopes() {
                        var getScopes = function(root) {
                            var scopes = [];

                            function visit(scope) {
                                scopes.push(scope);
                            }

                            function traverse(scope) {
                                visit(scope);
                                if (scope.$$nextSibling)
                                    traverse(scope.$$nextSibling);
                                if (scope.$$childHead)
                                    traverse(scope.$$childHead);
                            }
                            traverse(root);
                            return scopes;
                        }
                        return getScopes($rootScope);
                    }
                }
            ])

        .run(function(Api) {});
    })
<commit_msg>fix function(Api) is not using explicit annotation and<commit_after>/**
 * @namespace hs.api
 * @memberOf hs
 */
define(['angular', 'ol', 'app', 'map', 'core'],

    function(angular, ol, app, map) {
        angular.module('hs.api', ['hs', 'hs.map', 'hs.core'])
            .service("Api", ['$rootScope', 'hs.map.service', '$controller', 'Core',
                function($rootScope, OlMap, $controller, Core) {
                    var me = {};
                    me.gui = {};
                    $rootScope.$on("scope_loaded", function(event, args) {
                        me.gui[args] = {};
                        for (var key in event.targetScope) {
                            //Dont show angularjs functions in the api
                            if (typeof event.targetScope.__proto__[key] === 'undefined') {
                                me.gui[args][key] = event.targetScope[key];
                            }
                        }
                    });
                    me.listScopes = function() {
                        var scopes = getAllScopes();
                        var tmp = {};
                        for (var i = 0; i < scopes.length; i++) {
                            if (scopes[i].scope_name) {
                                tmp[scopes[i].scope_name] = scopes[i];
                            }
                        }
                        return tmp;
                    }
                    me.gui.setLanguage = function(lang) {
                        Core.setLanguage(lang);
                    }

                    me.getMap = function() {
                        return OlMap.map;
                    }

                    var recurseObject = function(obj, levels) {
                        levels += '\t';
                        if (levels.length > 5) return;
                        if (typeof obj === 'function') {
                            var fnStr = obj.toString().replace(/[\n\t]/g, ' '),
                                argStr = fnStr.match(/\((.+?\))/),
                                len = (argStr ? (argStr.index + argStr[0].length) : 11),
                                fnBodyStr = fnStr.substring(len),
                                metaData = {
                                    func: [],
                                    args: {}
                                };

                            console.log(levels, 'function', key, argStr ? argStr[0].split('{')[0] : "()");
                        }
                        if (typeof obj === 'object') {
                            var has_indexes = !(obj && typeof obj.length === 'undefined');
                            console.log(levels, key + (has_indexes ? '' : '.'));
                            if (!has_indexes) {
                                for (key in obj) {
                                    recurseObject(obj[key], levels);
                                }
                            }
                        }
                    }

                    me.printApiDefinitions = function() {
                        for (key in window.hslayers_api) {
                            recurseObject(window.hslayers_api[key], '');
                        }
                    }

                    window.hslayers_api = me;

                    /**
                    * @ngdoc method
                    * @name Api#getAllScopes
                    * @private
                    * @returns {Array} Array of all active Angular Scopes of app
                    * @description Get all scopes defined in app
                    */
                    function getAllScopes() {
                        var getScopes = function(root) {
                            var scopes = [];

                            function visit(scope) {
                                scopes.push(scope);
                            }

                            function traverse(scope) {
                                visit(scope);
                                if (scope.$$nextSibling)
                                    traverse(scope.$$nextSibling);
                                if (scope.$$childHead)
                                    traverse(scope.$$childHead);
                            }
                            traverse(root);
                            return scopes;
                        }
                        return getScopes($rootScope);
                    }
                }
            ])

        .run(['Api', function(Api) {}]);
    })
<|endoftext|>


<commit_before>
var ms = require('./schema');

/**
 * Expose `schema`
 */

module.exports = Model;

/**
 * Create a Model# **object** accessed as `schema(key)`
 *
 * **Models** describe resources comprised of properties and methods
 *
 * @param {String} key The unique identifier for the Model#
 * @param {Adapter} [adapter] Optional adapter to use for this new Model#
 * @param {Object} [options]
 *
 * @class Model
 * @constructor
 */

function Model( key, adapter, options ) {

  /**
   * Unique identifying key to reference schema( key )
   *
   * @type {String}
   * @property key
   */

  this.key = key;

  /**
   * Set the default `idAttribute` to 'id'
   * Modify this if your datasource uses an alternative form ('_id' for Mongo)
   */

  this.idAttribute = 'id';

  /**
   * The `resource` used by an adapter to reference this type of model
   * In effect this is the 'table' or 'document' reference in the datastore
   * Defaults to lowercase `Model.key`
   *
   * eg. "User" -> "user"
   *
   * @type {String}
   */

  this.resource = key.toLowerCase();

  /**
   * Properties
   *
   * @type {Model}
   */

  this.properties = {};

  /**
   * Methods
   *
   * @type {Array}
   */

  this.methods = [];

  /**
   * Placeholders for schema middleware
   * @private
   */

  this._pre = {};
  this._post = {};

  /**
   * Initial config defaults
   */

  this.config = {
    castOnSet: true,
    validateOnSet: false
  };

  // Apply options
  if (options) for (var key in options) this.config[key] = options[key];

  /**
   * Internal pointer to any instanced adapter (Set with `.useAdapter()`)
   *
   * @type {Adapter}
   * @see https://github.com/mekanika/adapter
   */

  this.adapter = adapter;
}

/**
 * Helper method to apply a middleware `fn` to a `stack` on `event`
 *
 * @param {Array} stack The schema middleware reference array to push onto
 * @param {String} event The name of the event to apply middleware on
 * @param {Function} fn The method to run, passed ( Query# )
 *
 * @private
 */

function middleware( stack, event, fn ) {
  var _act = event;
  var _hook = fn;

  // Support passing a pure (fn) applying to `all` events
  if (typeof event === 'function' ) {
    _act = 'all';
    _hook = event;
  }

  if (!stack[ _act ]) stack[ _act ] = [];

  // Fold in reference to `this` Model# in the middleware `fn`
  function _fn() {
    var self = this;
    return function() {
      return _hook.apply( self, arguments );
    };
  }

  stack[ _act ].push( _fn.call( this ) );
  return this;
}

/**
 * Delegation method to setup pre middleware to the Model#query()
 *
 * @param {String|Function} event The name of the event to apply middleware on (assumes `all` if passed a function)
 * @param {Function} fn The method to run, passed ( Query# )
 *
 * @return {this}
 */

Model.prototype.pre = function( event, fn ) {
  middleware.call( this, this._pre, event, fn );
};

/**
 * Delegation method to setup post middleware to the Model#query()
 *
 * @param {String|Function} event The name of the event to apply middleware on (assumes `all` if passed a function)
 * @param {Function} fn The method to run, passed ( err, res )
 *
 * @return {this}
 */

Model.prototype.post = function( event, fn ) {
  middleware.call( this, this._post, event, fn );
};

/**
 * Set an explicit adapter to use with this schema
 *
 * Updates an internal pointer to a given `adapter`, used by `Model.prototype.query()`
 *
 * @param {Adapter} adapter An Adapter# instance
 *
 * @return {Model} This schema
 */

Model.prototype.useAdapter = function( adapter ) {
  // Ensure we have an approximately valid adapter
  var adapterError = new Error( 'Must supply valid adapter');
  if (!adapter || !adapter.exec) throw adapterError;

  this.adapter = adapter;

  return this;
};

/**
 * Returns a property by `key`, or undefined if not found
 *
 * @param {String} key The unique identifier for the Property
 *
 * @return {Property|undefined}
 */

Model.prototype.path = function( key ) {
  return this.properties[key];
};

/**
 * Model properties returned as flat array
 *
 * @return {Array}
 */

Model.prototype.getPaths = function() {
  var keys = [];
  for (var key in this.properties) keys.push( key );
  return keys;
};

/**
 * Model required properties returned as flat array
 *
 * @return {Array}
 */

Model.prototype.getRequiredPaths = function() {
  var p = this.properties,
      rp = [];

  for (var key in p) if (p[key].required) rp.push( key );

  return rp;
};

/**
 * Add a new property to the schema structure
 * Overwrites is property is already defined
 *
 * @param {String} key
 * @param {Model} [schema] Mekanika schema options
 *
 * @return {Model}
 */

Model.prototype.prop = function( key, schema ) {
  schema || (schema = {});
  this.properties[key] = schema;
  return this;
};

Model.prototype.property = Model.prototype.attr = Model.prototype.prop;

/**
 * Shortcut method to validate an arbitrary value against a property's rules
 *
 * @param {String} property The property key
 * @param {Mixed} value
 *
 * @return {Array} of errors (empty if none)
 */

Model.prototype.validate = function (property, value) {
  var scm = this.path( property );
  if (!scm) throw new Error('No such property defined: '+property);

  return ms.test( value, scm );
};

/**
 * Add prototype methods on Records
 *
 * @param {String} methodName An identifier for the method
 * @param {Function} fn The Function defining the method
 *
 * @return {Model}
 */

Model.prototype.method = function( methodName, fn ) {
  if (!methodName || !fn)
    throw new Error('Must define both `methodName` and `fn`');

  if (typeof methodName !== 'string')
    throw new Error('`methodName` must be a String');

  if (typeof fn !== 'function')
    throw new Error('`fn` must be a Function');

  this.methods.push( {key:methodName, fn:fn} );
  return this;
};
<commit_msg>Minor comment update<commit_after>
var ms = require('./schema');

/**
 * Expose `schema`
 */

module.exports = Model;

/**
 * Create a Model# **object** accessed as `schema(key)`
 *
 * **Models** describe resources comprised of properties and methods
 *
 * @param {String} key The unique identifier for the Model#
 * @param {Adapter} [adapter] Optional adapter to use for this new Model#
 * @param {Object} [options] Referenced by instantiators (ie. Record)
 *
 * @class Model
 * @constructor
 */

function Model( key, adapter, options ) {

  /**
   * Unique identifying key to reference schema( key )
   *
   * @type {String}
   * @property key
   */

  this.key = key;

  /**
   * Set the default `idAttribute` to 'id'
   * Modify this if your datasource uses an alternative form ('_id' for Mongo)
   */

  this.idAttribute = 'id';

  /**
   * The `resource` used by an adapter to reference this type of model
   * In effect this is the 'table' or 'document' reference in the datastore
   * Defaults to lowercase `Model.key`
   *
   * eg. "User" -> "user"
   *
   * @type {String}
   */

  this.resource = key.toLowerCase();

  /**
   * Properties
   *
   * @type {Model}
   */

  this.properties = {};

  /**
   * Methods
   *
   * @type {Array}
   */

  this.methods = [];

  /**
   * Placeholders for schema middleware
   * @private
   */

  this._pre = {};
  this._post = {};

  /**
   * Initial config defaults
   */

  this.config = {
    castOnSet: true,
    validateOnSet: false
  };

  // Apply options
  if (options) for (var key in options) this.config[key] = options[key];

  /**
   * Internal pointer to any instanced adapter (Set with `.useAdapter()`)
   *
   * @type {Adapter}
   * @see https://github.com/mekanika/adapter
   */

  this.adapter = adapter;
}

/**
 * Helper method to apply a middleware `fn` to a `stack` on `event`
 *
 * @param {Array} stack The schema middleware reference array to push onto
 * @param {String} event The name of the event to apply middleware on
 * @param {Function} fn The method to run, passed ( Query# )
 *
 * @private
 */

function middleware( stack, event, fn ) {
  var _act = event;
  var _hook = fn;

  // Support passing a pure (fn) applying to `all` events
  if (typeof event === 'function' ) {
    _act = 'all';
    _hook = event;
  }

  if (!stack[ _act ]) stack[ _act ] = [];

  // Fold in reference to `this` Model# in the middleware `fn`
  function _fn() {
    var self = this;
    return function() {
      return _hook.apply( self, arguments );
    };
  }

  stack[ _act ].push( _fn.call( this ) );
  return this;
}

/**
 * Delegation method to setup pre middleware to the Model#query()
 *
 * @param {String|Function} event The name of the event to apply middleware on (assumes `all` if passed a function)
 * @param {Function} fn The method to run, passed ( Query# )
 *
 * @return {this}
 */

Model.prototype.pre = function( event, fn ) {
  middleware.call( this, this._pre, event, fn );
};

/**
 * Delegation method to setup post middleware to the Model#query()
 *
 * @param {String|Function} event The name of the event to apply middleware on (assumes `all` if passed a function)
 * @param {Function} fn The method to run, passed ( err, res )
 *
 * @return {this}
 */

Model.prototype.post = function( event, fn ) {
  middleware.call( this, this._post, event, fn );
};

/**
 * Set an explicit adapter to use with this schema
 *
 * Updates an internal pointer to a given `adapter`, used by `Model.prototype.query()`
 *
 * @param {Adapter} adapter An Adapter# instance
 *
 * @return {Model} This schema
 */

Model.prototype.useAdapter = function( adapter ) {
  // Ensure we have an approximately valid adapter
  var adapterError = new Error( 'Must supply valid adapter');
  if (!adapter || !adapter.exec) throw adapterError;

  this.adapter = adapter;

  return this;
};

/**
 * Returns a property by `key`, or undefined if not found
 *
 * @param {String} key The unique identifier for the Property
 *
 * @return {Property|undefined}
 */

Model.prototype.path = function( key ) {
  return this.properties[key];
};

/**
 * Model properties returned as flat array
 *
 * @return {Array}
 */

Model.prototype.getPaths = function() {
  var keys = [];
  for (var key in this.properties) keys.push( key );
  return keys;
};

/**
 * Model required properties returned as flat array
 *
 * @return {Array}
 */

Model.prototype.getRequiredPaths = function() {
  var p = this.properties,
      rp = [];

  for (var key in p) if (p[key].required) rp.push( key );

  return rp;
};

/**
 * Add a new property to the schema structure
 * Overwrites is property is already defined
 *
 * @param {String} key
 * @param {Model} [schema] Mekanika schema options
 *
 * @return {Model}
 */

Model.prototype.prop = function( key, schema ) {
  schema || (schema = {});
  this.properties[key] = schema;
  return this;
};

Model.prototype.property = Model.prototype.attr = Model.prototype.prop;

/**
 * Shortcut method to validate an arbitrary value against a property's rules
 *
 * @param {String} property The property key
 * @param {Mixed} value
 *
 * @return {Array} of errors (empty if none)
 */

Model.prototype.validate = function (property, value) {
  var scm = this.path( property );
  if (!scm) throw new Error('No such property defined: '+property);

  return ms.test( value, scm );
};

/**
 * Add prototype methods on Records
 *
 * @param {String} methodName An identifier for the method
 * @param {Function} fn The Function defining the method
 *
 * @return {Model}
 */

Model.prototype.method = function( methodName, fn ) {
  if (!methodName || !fn)
    throw new Error('Must define both `methodName` and `fn`');

  if (typeof methodName !== 'string')
    throw new Error('`methodName` must be a String');

  if (typeof fn !== 'function')
    throw new Error('`fn` must be a Function');

  this.methods.push( {key:methodName, fn:fn} );
  return this;
};
<|endoftext|>