Nettoyage d'automne; première brique, la recherche de cotisations

pull/1/head
Mael Thomas 2016-11-15 19:46:17 +01:00
parent 2fa97f1516
commit 3075b83963
42 changed files with 113 additions and 737 deletions

View File

@ -1,11 +0,0 @@
Prochaine étape :
- mettre les données dans le state : ce qu'on veut c'est la liste de variables fusionnées.
- en déduire, à partir de la liste des sélections user elle aussi dans le state, la liste des variables à afficher
- Afficher du YAML simplifié
En fonction des valeurs des variables de la simulation, ou des choix de l'utilisateur dans les étiquettes, le parser YAML doit être capable de nous donner une vue simplifiée de la variable.
Ex. en choisissant 'salarié cadre', je dois pouvoir calculer l'AGFF mais surtout obtenir une description du calcul, qui peut être dans un premier temps un bout de YAML "résolu", cad où tout ce qui ne concerne pas les cadre a été enlevé.

View File

@ -1,19 +0,0 @@
/* TAGS */
export let SELECT_TAG = 'SELECT_TAG'
export function selectTag(tagName, tagValue) {
return {type: SELECT_TAG, tagName, tagValue}
}
export let RESET_TAGS = 'RESET_TAGS'
export function resetTags() {
return {type: RESET_TAGS}
}
/* VARIBALES */
export let SELECT_VARIABLE = 'SELECT_VARIABLE'
export function selectVariable(name) {
return {type: SELECT_VARIABLE, name}
}

View File

@ -1,3 +0,0 @@
#top-input-variables {
text-align: center;
}

View File

@ -1,35 +0,0 @@
import React, { Component } from 'react'
import {connect} from 'react-redux'
import {usedVariables} from '../selectors/usedVariables'
import './Analyse.css'
let mapStateToProps = state => (
{
usedVariables: usedVariables(state)
}
)
@connect(mapStateToProps)
export default class Analyse extends Component {
state = {variableStats: null}
render() {
let {variableStats} = this.state
return (
<div>
{variableStats ?
<ul id="top-input-variables">
{variableStats.map(([name, count]) =>
<li key={name}>{name} {count}</li>
)}
</ul>
: <div>En attente...</div>}
</div>
)
}
componentDidMount(){
this.props.usedVariables.then(
variables => this.setState({variableStats: variables})
)
}
}

View File

@ -1,145 +0,0 @@
body {
font-family: Open Sans;
color: #333;
}
#header {
text-align: center;
background: blue;
color: red;
font-weight: 600;
margin-bottom: 2em;
}
h1 {
text-align: center;
font-weight: 200;
font-size: 215%;
}
ul {
list-style: none;
padding: 0;
margin: 0;
}
li {
margin: 0;
}
#tag-navigation {
font-size: 130%;
font-weight: 300;
width: 20%;
padding: 1em;
display: inline-block;
}
#tag-navigation h2 {
font-size: 120%;
font-weight: 300;
margin-top: 0;
opacity: .65;
padding-bottom: .3em;
border-bottom: 1px solid #ccc;
margin-bottom: 0
}
#tag-navigation .content {
padding-top: 1.5em;
border-right: 1px solid #ccc;
}
#to-select > li {
margin-bottom: 1.5em;
}
.choices {
margin-top: .6em;
}
.tag-value {
font-size: 75%;
font-weight: 400;
display: inline-block;
text-align: right;
margin-right: 1em;
color: #2980b9;
border: 1px solid #2980b9;
padding: .05em .6em;
cursor: pointer;
border-radius: .1em
}
#to-select span.nb {
font-size: 60%;
margin-left: 2em;
}
#selected {
border-bottom: 1px solid #ccc;
padding-bottom: 1em;
margin-bottom: 1em;
font-weight: 500;
color: #16a085;
}
.tag-map .tag-value {
margin-left: 1em;
background: #16a085;
border: none;
color: white;
margin-bottom: .5em;
cursor: default;
}
#selected button {
background: none;
border: none;
float: right;
cursor: pointer;
color: #16a085;
}
#variables {
width: 70%;
display: inline-block;
padding: 3%;
position: absolute;
}
.variable {
width: 8em;
min-height: 7em;
display: inline-block;
vertical-align: middle;
margin-bottom: 1em;
margin-right: 1em;
padding: 1em 2em;
text-align: center;
}
.variable h3 {
font-weight: 400;
font-size: 120%;
text-transform: capitalize;
}
.variable ul {
padding-left: .6em
}
#selected-variable {
width: 70%;
display: inline-block;
position: absolute;
}
#selected-variable h1 {
text-transform: capitalize;
font-size: 180%;
}
#selected-variable p {
width: 50%;
margin: 0 auto;
border: 1px solid #ddd;
border-left: 2px solid #ddd;
padding: 1em 2em;
}

View File

@ -1,41 +0,0 @@
import React from 'react'
import {connect} from 'react-redux'
import TagNavigation from '../components/TagNavigation'
import Variables from '../components/Variables'
import * as actions from '../actions'
import {bindActionCreators} from 'redux'
import {tagsToSelectSelector, variablesSelector} from '../selectors/selectors'
class Explorer extends React.Component {
render() {
let {variables, selectedTags, selectedVariable, tagsToSelect, actions: {selectTag, resetTags, selectVariable}} = this.props
return (
<div>
<h1>Les prélèvements sociaux sur les salaires</h1>
<TagNavigation selectedTags={selectedTags} tagsToSelect={tagsToSelect} selectTag={selectTag} resetTags={resetTags}/>
<Variables variables={variables}
selectedTags={selectedTags} selectedVariable={selectedVariable}
selectVariable={selectVariable}/>
</div>
)
}
}
const mapStateToProps = state => (
{
selectedTags: state.selectedTags,
tagsToSelect: tagsToSelectSelector(state),
variables: variablesSelector(state),
selectedVariable: state.selectedVariable
}
)
const actionsToProps = dispatch => ({
actions: bindActionCreators(actions, dispatch)
})
const VariableExplorer = connect(mapStateToProps, actionsToProps)(Explorer)
export default VariableExplorer

View File

@ -1,10 +0,0 @@
import React, { Component } from 'react'
export default class Layout extends Component {
render() {
return (<div>
<div id="header">En-tête</div>
{this.props.children}
</div>)
}
}

View File

@ -4,7 +4,7 @@
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>prel2</title>
<link href='https://fonts.googleapis.com/css?family=Open+Sans:400,700,300' rel='stylesheet' type='text/css'>
<link href='https://fonts.googleapis.com/css?family=Open+Sans:200,300,400,700' rel='stylesheet' type='text/css'>
</head>
<body>

View File

@ -1,83 +0,0 @@
import parameters from './load-parameters'
import deepAssign from 'deep-assign'
import R from 'ramda'
/* Fonctions utiles */
let
hasHistoryProp = R.pipe(JSON.stringify, R.contains('"historique":')),
itemHasHistoricProp = (item, prop) => R.has(prop)(item) && hasHistoryProp(item[prop]),
itemIsCalculable = item => false
|| itemHasHistoricProp(item, 'linear')
|| itemHasHistoricProp(item, 'marginalRateTaxScale')
|| item['+'] || item['-']
|| item['logique'] || item['logique numérique']
,
/*
L'attribut tags est une hash map,
ou une liste
- de clés, qui sont un moyen courant d'exprimer [clef]: oui
- de hash maps.
Cette fonction fusionne tout ceci dans un objet
*/
handleHybridTags = R.when(R.isArrayLike,
R.reduce((final, tag) =>
R.merge(final, R.is(Object, tag) ? tag : {[tag]: 'oui'})
, {})
),
tagsConflict = (tags1, tags2) =>
R.compose(
R.any(R.identity),
R.values,
R.mapObjIndexed((tagValue, tag) => tags2[tag] != undefined && tags2[tag] !== tagValue)
)(tags1)
let
groupedItemsByVariable = R.pipe(
// Desugar tags
R.map(p => R.merge(p, {tags: handleHybridTags(p.tags) || {}})),
R.groupBy(R.prop('variable'))
)(parameters),
mergedItemsByVariable =
R.mapObjIndexed((variableItems, name) => {
/* Les items sont des fragments de variables.
Les premiers fragments vont être fusionnés dans les suivants,
sauf s'ils provoquent l'écrasement d'un tag */
let mergedVariableItems = R.tail(variableItems) // Le premier ne peut être étendu
.reduce((mergedItems, item) =>
[ ...mergedItems,
mergedItems.reduce((final, higherLevelItem) => {
let oups = tagsConflict(higherLevelItem.tags, item.tags)
return oups ? item : deepAssign({}, item, higherLevelItem)
}, item)
], R.of(R.head(variableItems)))
return {
name,
// La variable de haut niveau, contenant la plupart du temps une description, etc.
first: R.head(variableItems),
// Tous les tags qui peuvent être trouvés dans les items de cette variable
tags: R.pipe(
R.pluck('tags'),
R.map(R.map(R.of)),
R.reduce(R.mergeWith(R.union), {})
)(variableItems),
// Gardons seulement les variables ayant une implémentation : capable de faire un calcul
calculable: R.filter(itemIsCalculable)(mergedVariableItems)
}}
)(groupedItemsByVariable),
mergedItems = R.values(mergedItemsByVariable),
calculableItems =
R.pipe(
R.values,
R.pluck('calculable'),
R.unnest
)(mergedItemsByVariable)
export {
groupedItemsByVariable,
calculableItems,
mergedItems
}

View File

@ -55,6 +55,6 @@
"yaml-loader": "^0.2.0"
},
"scripts": {
"start": "node server.js"
"start": "node source/server.js"
}
}

View File

@ -1,35 +0,0 @@
import { combineReducers } from 'redux'
import { SELECT_TAG, SELECT_VARIABLE, RESET_TAGS} from './actions'
function selectedTags(state = [], {type, tagName, tagValue}) {
switch (type) {
case SELECT_TAG:
return [...state, [tagName, tagValue]]
case RESET_TAGS:
return []
default:
return state
}
}
function selectedVariable(state = null, {type, name}) {
switch (type) {
case SELECT_VARIABLE:
return name
default:
return state
}
}
function rootVariables(state = ['cout du travail']) {
return state
}
export default combineReducers({
selectedTags,
selectedVariable,
rootVariables
})

View File

@ -1,57 +0,0 @@
import { createSelector } from 'reselect'
import {calculableItems, higherOrderVariables, mergedItems} from '../model'
import R from 'ramda'
let
// variableHasTagValue = variable => ([tag, value]) => console.log('tv', variable.tags, tag, value),
variableHasTagValue = variable => ([osef, [tag, value]]) => R.pathEq(['tags', tag], value)(variable),
variableHasSelectedTags = variable => R.compose(
R.all(variableHasTagValue(variable)),
R.toPairs
),
filterVariables = variables => tags => R.filter(item => variableHasSelectedTags(item)(tags))(variables)
export const finalVariablesSelector = createSelector(
[state => state.selectedTags],
filterVariables(calculableItems)
)
/* Tag names, values, and number of variables per tag */
const unorderedTagStats = finalVariables =>
finalVariables
.reduce((stats, variable) => {
Object.keys(variable.tags).map(
k => {
stats[k] = stats[k] || {number: 0, choices: new Set()}
stats[k].number = stats[k].number + 1
stats[k].choices.add(variable.tags[k])
}
)
return stats
}, {}),
tagStats = stats =>
Object.keys(stats)
.reduce((acc, n) => ([...acc, {name: n, ...stats[n]}]), [])
.sort((a, b) => b.number - a.number)
let tagStatsSelector = createSelector(
[finalVariablesSelector],
variables => tagStats(unorderedTagStats(variables))
)
export let tagsToSelectSelector = createSelector(
[state => state.selectedTags, tagStatsSelector],
(selectedTags, availableTags) =>
availableTags.filter(t => !selectedTags.find(([name]) => t.name === name))
)
export let variablesSelector = createSelector(
[state => state.selectedTags],
selectedTags => R.filter(
({tags}) =>
R.all(
([tag, value]) => tags[tag] && R.contains(value, tags[tag]),
)(selectedTags)
)(mergedItems)
)

View File

@ -1,111 +0,0 @@
import R from 'ramda'
// used to aseptise all yaml text, to avoid doing regexps on accents...
import removeDiacritics from '../utils/remove-diacritics'
/* When you encounter a variable, it may have a calculable key.
This file provides an object with these mappings :
calculable-key -> extract variables called by the calculation value */
let scalarMult = value => {
value = removeDiacritics(value)
// 4 * assiette cotisations sociales
let match = /(?:([0-9])*\s\*\s)?((?:[a-z0-9]|\s|_)+)/g.exec(value)
if (match) return match[2]
}
let logicalCondition = value => {
value = removeDiacritics(value)
let match, variable
// ! ma variable
if (R.contains('!')(value)) {
match = /!((?:[a-z0-9]|\s|_)+)/g.exec(value)
if (match) return match[1]
}
// département établissement ⊂ [57 67 68]
if (R.contains('⊂')(value)) {
match = /((?:[a-z0-9]|\s|_)+)⊂*/g.exec(value)
if (match) return match[1]
}
// ma cotisation > 20
match = /((?:[a-z0-9]|\s|_)+)([<|>]=?)\s+[0-9]+/g.exec(value)
if (match) return match[1]
// 20 <= ma cotisation
match = /[0-9]+\s+([<|>]=?)((?:[a-z0-9]|\s|_)+)/g.exec(value)
if (match) return match[2]
// ma variable = xxxx z
match = /((?:[a-z0-9]|\s|_)+)=((?:[a-z0-9]|\s|_)+)/g.exec(value)
if (match) return match[1]
// ma variable
match = /((?:[a-z0-9]|\s|_)+)/g.exec(value)
if (match) return match[1]
}
/*
Ce YAML :
- cond1
- cond2:
- cond2.1:
- cond2.1.1
- cond2.1.2
- cond2.2
cond2.2.1
- cond3
se traduit en :
cond1 || (
cond2 && (
(
cond2.1 && (
cond2.1.1 ||
cond2.1.2
)
) || cond2.2
)
) || cond3
(':-D)
*/
let logic = list => { // a list is a ||
return list.reduce((variables, next) =>
typeof next == 'string' ?
[...variables, logicalCondition(next)] :
// it's a single key object -> we're facing a && condition
[...variables, logicalCondition(R.keys(next)[0]), ...logic(R.values(next))]
, [])
}
R.uniq(logic)
let plusOrMinus = R.cond([
[R.is(String), removeDiacritics],
[R.isArrayLike, R.map(plusOrMinus)],
[R.is(Object), R.identity]
])
let traversalGuide = {
linear: {
base: scalarMult,
limit: scalarMult,
historique: null
// VAR/case: logic predicate
},
marginalRateTaxScale: {
base: scalarMult
// VAR/case: logic predicate
},
concerne: logic,
'ne concerne pas': logic,
logique: logic, // predicates leading to a boolean,
'logique numérique': () => null, // predicates leading to a number
'+': plusOrMinus, // It's a string or list of string representing variable calls, that's all we want for now :-)
'-': plusOrMinus // Same :-)
}
export default traversalGuide

View File

@ -1,174 +0,0 @@
import R from 'ramda'
import removeDiacritics from '../utils/remove-diacritics'
import { createSelector } from 'reselect'
import {variablesSelector} from './selectors'
import traversalGuide from './traversalGuide'
import dump from 'json!../adhoc-variable-dump/variables.json'
/* AIM : Parse all the variables and extract their references to other variables.
Rank the results by count. */
// Use http://regexr.com/ to understand and write regexps !!
let fetchVariables = false
let resolveVariable = (variable, name, callback) => {
if (variable == null) return callback('TODO ' + name)
let {formula} = variable
if (formula && formula['input_variables']) {
return callback({calls: formula['input_variables']})
} else {
return callback(name)
}
}
let GETAdHocVariable = name => {
let obscureName = name.trim().replace(/\s/g, '_')
if (!fetchVariables){
return new Promise(
resolve => resolveVariable(
dump.variables.find(v => v.name === obscureName),
name,
resolve)
)
}
return new Promise(resolve =>
window.fetch('https://api.openfisca.fr/api/1/variables/?name=' + obscureName)
.then(res => res.json())
.then(json => {
let {error, variables} = json
if (error && JSON.stringify(error).indexOf('Variable does not exist') + 1){
resolveVariable(null, name, resolve)
}
if (variables) {
resolveVariable(variables[0], name, resolve)
}
})
)
}
async function getAdHocVariables(id) {
let variable = await GETAdHocVariable(id)
if (R.is(String, variable))
return variable
else
return Promise.all(
variable.calls.map(getAdHocVariables)
)
}
// Returns a list of list of used variables
// recursive function
let findUsedVariables = (variables, schema, toAnalyse) =>
/*TODO
If instruction of GET call -> return Promise
1) input var OK return string
2) call to other variables -> list of promises
*/
R.cond([
[R.isNil, () => []],
[R.has('adHoc'), ({id}) => getAdHocVariables(id)],
// The traversal of variables has found a parsable variable attribute.
// Parse it to extract variables
[R.is(Function), extractor =>
R.pipe(
extractor,
R.unless(R.isArrayLike, R.of),
R.map(R.cond([
[R.is(String), name => findVariables(variables, name)],
[R.is(Object), tags => findVariables(variables, null, tags)]
])),
R.unnest,
R.map(
/*TODO
if instruction of GET call findUsedVariables(GET CALL)
*/
R.cond([
[R.has('adHoc'), adHocSpec => findUsedVariables(variables, adHocSpec)],
[R.is(String), R.identity],
[R.T, found => findUsedVariables(variables, traversalGuide, found)]
])
)
)(toAnalyse)
],
// Walk the graph using the guiding object until you find a parsable attribute
[R.is(Object), traversalObject =>
R.toPairs(traversalObject).reduce(
(res, [key, value]) => {
return toAnalyse[key] != null ?
[...res, ...findUsedVariables(variables, value, toAnalyse[key])] :
res
}, []
)],
[R.T, () => []]
])(schema)
let calculableVariables = createSelector(
[variablesSelector],
R.pipe(R.pluck('calculable'), R.unnest)
)
/************************************
Functions to find variables */
let findVariablesByName = name =>
R.filter(variable => removeDiacritics(variable.variable) == name)
let findVariablesByTags = tags =>
R.filter(R.pipe(
R.prop('tags'),
R.whereEq(tags)
))
let findVariables = (variables, name, tags) =>
R.pipe(
variables => R.is(String, name)
? findVariablesByName(name)(variables)
: variables,
variables => R.is(Object, tags)
? findVariablesByTags(tags)(variables)
: variables,
R.cond([
[ R.isEmpty, () => variableNotFound(name)],
[ (variables) => R.is(String, name) && variables.length > 1,
() => variableNameCollision(name, tags)],
[ R.T,
R.identity]
]),
)(variables)
let variableNotFound = name =>
// Should do a query to openfisca web api
({adHoc: true, id: name})
let variableNameCollision = (name, tags) =>
`More than one variable corresponds to this name, tags tuple : ${name}, ${tags}`
/*****************************************/
export let usedVariables = createSelector(
[state => state.rootVariables, calculableVariables],
(roots, variables) => {
// get all variables from these roots, rec !
return R.compose(
promises =>
Promise.all(promises).then(
R.pipe(
R.flatten,
R.countBy(R.identity),
R.toPairs,
R.sortBy(R.last),
R.reverse
)
),
R.flatten,
R.map(rootObject => findUsedVariables(variables, traversalGuide, rootObject)),
R.map(root => findVariables(variables, removeDiacritics(root))[0])
)(roots)
}
)

0
source/actions.js Normal file
View File

View File

@ -5,8 +5,6 @@ import DevTools from '../DevTools'
import routes from '../routes'
import {Router, browserHistory} from 'react-router'
import './App.css'
export default class App extends Component {
render() {
const { store } = this.props

View File

@ -4,9 +4,6 @@ import { Provider } from 'react-redux'
import routes from '../routes'
import Router from 'react-router'
import './App.css'
export default class App extends Component {
render() {
const { store } = this.props

View File

@ -0,0 +1,58 @@
#home {
height: 400px;
/*display:flex;
justify-content: center;
align-items: center;*/
}
#brand {
position: relative;
color: #333350;
width: 160px;
margin: 6em auto;
}
#brand img {
display: block;
width: 160px;
margin-bottom: .6em;
}
#brand #name {
font-size: 250%;
line-height: .9em;
}
#brand #version {
font-style: italic;
font-size: 110%;
position: absolute;
bottom: 0;
right: 0;
}
#warning {
padding: 1em 0;
background: #8a1c1c;
color: white;
font-style: italic;
position: fixed;
bottom: 0;
left: 0;
width: 100%;
text-align: center;
}
#search {
width: 400px;
margin: 4em auto;
}
#search input {
font-weight: 300;
text-align: center;
font-size: 2em;
width: 100%;
border: none;
border-bottom: 1px solid #333350
}

22
source/containers/Home.js Normal file
View File

@ -0,0 +1,22 @@
import React, { Component } from 'react'
import './Home.css'
export default class Home extends Component {
render() {
return (
<div id="home">
<section id="brand">
<img src={require('../images/logo.png')} />
<span id="name">
Système <br/>
Social
</span>
<span id="version">alpha</span>
</section>
<section id="search">
<input placeholder="ex. retraite"/>
</section>
</div>)
}
}

View File

@ -0,0 +1,3 @@
body {
font-family: 'Open Sans';
}

View File

@ -0,0 +1,15 @@
import React, { Component } from 'react'
import './Layout.css'
export default class Layout extends Component {
render() {
return (<div>
<div id="header"></div>
{this.props.children}
<div id="warning">
Attention ! Tout le contenu de ce site est hautement expérimental.
</div>
</div>
)
}
}

View File

@ -29,6 +29,7 @@ render(
anchor
)
// Hot react component reloading. Unstable but helpful.
if (module.hot) {
module.hot.accept('./containers/App', () => {
// If you use Webpack 2 in ES modules mode, you can

BIN
source/images/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

8
source/reducers.js Normal file
View File

@ -0,0 +1,8 @@
import { combineReducers } from 'redux'
import { } from './actions'
export default combineReducers({
})

View File

@ -1,14 +1,11 @@
import React from 'react'
import { Route, IndexRoute } from 'react-router'
import Layout from './containers/Layout'
import Explorer from './containers/Explorer'
import Analyse from './containers/Analyse'
import Home from './containers/Home'
export default (
<Route path="/" component={Layout}>
<Route path="analyse" component={Analyse} />
<Route path="variables" component={Explorer} />
<IndexRoute component={Explorer} />
<IndexRoute component={Home} />
<Route path="*" component={() => <h2>On vous a perdu !</h2>} />
</Route>
)

View File

@ -2,6 +2,7 @@ import { takeEvery} from 'redux-saga'
import { call, put} from 'redux-saga/effects'
import Promise from 'core-js/fn/promise'
// Nothing happening here !
function* handleSubmitStep() {
console.log('salut')

View File

@ -8,7 +8,7 @@ module.exports = {
'webpack/hot/only-dev-server',
'react-hot-loader/patch',
'babel-polyfill',
'./entry.js'
'./source/entry.js'
],
output: {
path: require('path').resolve('./dist/'),