...
 
Commits (4)
...@@ -63,7 +63,12 @@ ...@@ -63,7 +63,12 @@
/** /**
* @callback entityPersonneCallback * @callback entityPersonneCallback
* @param {Error} [error] Erreur éventuelle * @param {Error} [error] Erreur éventuelle
* @param {EntityPersonne} [personne] Une Entity Personne * @param {EntityPersonne} [personne] Une entity Personne
*/
/**
* @callback entityPersonnesCallback
* @param {Error} [error] Erreur éventuelle
* @param {EntityPersonne[]} [personnes] Une liste d'entity Personne
*/ */
/** /**
......
...@@ -18,9 +18,10 @@ import ResourceCreate from './components/ResourceCreate' ...@@ -18,9 +18,10 @@ import ResourceCreate from './components/ResourceCreate'
import ResourceForm from './components/ResourceForm' import ResourceForm from './components/ResourceForm'
import ResourceSearch from './components/ResourceSearch' import ResourceSearch from './components/ResourceSearch'
import Notifications from './components/Notifications' import Notifications from './components/Notifications'
import GroupesPerso from './components/groupes/GroupesPerso'
import GroupeEdition from './components/groupes/GroupeEdition' import GroupeEdition from './components/groupes/GroupeEdition'
import GroupeListerMembres from './components/groupes/GroupeListerMembres'
import GroupesOuverts from './components/groupes/GroupesOuverts' import GroupesOuverts from './components/groupes/GroupesOuverts'
import GroupesPerso from './components/groupes/GroupesPerso'
import GroupesPublics from './components/groupes/GroupesPublics' import GroupesPublics from './components/groupes/GroupesPublics'
import NotFound from './components/NotFound' import NotFound from './components/NotFound'
import ErrorBoundary from './components/ErrorBoundary' import ErrorBoundary from './components/ErrorBoundary'
...@@ -36,8 +37,15 @@ const beforeSend = (report) => { ...@@ -36,8 +37,15 @@ const beforeSend = (report) => {
report.metaData.state = store.getState() report.metaData.state = store.getState()
} }
// ATTENTION à garder la liste des routes synchrones dans app/server/main/controllerMain.js // Route fournit à son component les props
// - location: {pathname, search, hash}
// - match: {params, path, url}
// Par ex une route /foo/:bar appellée via /foo/baz va fournir
// match.path: '/foo/:bar'
// match.url: '/foo/baz'
// match.params.bar: 'baz'
// ATTENTION à garder la liste des routes synchrones dans app/server/main/controllerMain.js
const App = () => ( const App = () => (
<ErrorBoundary beforeSend={beforeSend}> <ErrorBoundary beforeSend={beforeSend}>
<Provider store={store}> <Provider store={store}>
...@@ -59,6 +67,7 @@ const App = () => ( ...@@ -59,6 +67,7 @@ const App = () => (
<Route exact path="/ressource/rechercher" component={ResourceSearch} /> <Route exact path="/ressource/rechercher" component={ResourceSearch} />
<Route exact path="/groupe/ajouter" component={GroupeEdition} /> <Route exact path="/groupe/ajouter" component={GroupeEdition} />
<Route exact path="/groupe/editer/:groupe" component={GroupeEdition} /> <Route exact path="/groupe/editer/:groupe" component={GroupeEdition} />
<Route exact path="/groupe/listerMembres/:groupe" component={GroupeListerMembres} />
<Route exact path="/groupes/perso" component={GroupesPerso} /> <Route exact path="/groupes/perso" component={GroupesPerso} />
<Route exact path="/groupes/ouverts" component={GroupesOuverts} /> <Route exact path="/groupes/ouverts" component={GroupesOuverts} />
<Route exact path="/groupes/publics" component={GroupesPublics} /> <Route exact path="/groupes/publics" component={GroupesPublics} />
......
import {GET, POST, DELETE} from '../utils/httpMethods' import {GET, POST, DELETE} from '../utils/httpMethods'
import {addNotification} from './notifications' import {addNotification} from './notifications'
import { import {
getGroupesPersoUrl,
getSaveGroupeUrl,
getGroupeUrl, getGroupeUrl,
getGroupeJoinUrl,
getGroupeFollowUrl, getGroupeFollowUrl,
getGroupeIgnoreUrl,
getGroupeJoinUrl,
getGroupeLeaveUrl, getGroupeLeaveUrl,
getGroupeIgnoreUrl getGroupesPersoUrl,
getSaveGroupeUrl
} from '../apiRoutes' } from '../apiRoutes'
const dummyCb = () => {}
export const loadGroupes = () => dispatch => { export const loadGroupes = () => dispatch => {
const requestSuccess = ({ const requestSuccess = ({
groupes, groupes,
...@@ -67,7 +69,7 @@ const deleteGroupeAction = (nom) => ({ ...@@ -67,7 +69,7 @@ const deleteGroupeAction = (nom) => ({
payload: { nom } payload: { nom }
}) })
export const deleteGroupe = (nom, success = () => {}) => dispatch => { export const deleteGroupe = (nom, success = dummyCb) => dispatch => {
const requestSuccess = () => { const requestSuccess = () => {
dispatch( dispatch(
addNotification({ addNotification({
...@@ -96,7 +98,7 @@ const leaveGroupeAction = (nom) => ({ ...@@ -96,7 +98,7 @@ const leaveGroupeAction = (nom) => ({
export const leaveGroupe = ( export const leaveGroupe = (
nom, nom,
success = () => {} success = dummyCb
) => dispatch => { ) => dispatch => {
const requestSuccess = () => { const requestSuccess = () => {
dispatch( dispatch(
...@@ -126,7 +128,7 @@ const ignoreGroupeAction = (nom) => ({ ...@@ -126,7 +128,7 @@ const ignoreGroupeAction = (nom) => ({
export const ignoreGroupe = ( export const ignoreGroupe = (
nom, nom,
success = () => {} success = dummyCb
) => dispatch => { ) => dispatch => {
const requestSuccess = () => { const requestSuccess = () => {
dispatch( dispatch(
...@@ -157,7 +159,7 @@ const joinGroupeAction = groupe => ({ ...@@ -157,7 +159,7 @@ const joinGroupeAction = groupe => ({
export const joinGroupe = ( export const joinGroupe = (
nom, nom,
success = () => {} success = dummyCb
) => dispatch => { ) => dispatch => {
const requestSuccess = (groupe) => { const requestSuccess = (groupe) => {
dispatch( dispatch(
...@@ -187,7 +189,7 @@ const followGroupeAction = groupe => ({ ...@@ -187,7 +189,7 @@ const followGroupeAction = groupe => ({
export const followGroupe = ( export const followGroupe = (
nom, nom,
success = () => {} success = dummyCb
) => dispatch => { ) => dispatch => {
const requestSuccess = (groupe) => { const requestSuccess = (groupe) => {
dispatch( dispatch(
......
import {GET, POST} from '../utils/httpMethods'
import {addNotification} from './notifications'
import {
getGroupeMembresUrl,
getGroupeRetirerMembreUrl
} from '../apiRoutes'
// la limite par défaut est à 100 dans le contrôleur, on le gère pas coté front
export const loadMembres = ({nom, skip}) => (dispatch) => GET(getGroupeMembresUrl({nom, skip}))
.then(({membres, nbMembres, nbAbonnes, skip, limit}) => dispatch({
type: 'LOAD_MEMBRES',
payload: {membres, nbMembres, nbAbonnes, skip, limit}
})).catch((error) => dispatch(
addNotification({
level: 'error',
message: `Le chargement des membres a échoué : ${error.message}`
})
))
export const retirerMembre = ({groupeNom, personneOid}) => (dispatch) => {
const url = getGroupeRetirerMembreUrl()
const body = {
groupeNom,
personneOid
}
return POST(url, {body}).then(({membre}) => {
dispatch(
addNotification({
level: 'info',
message: `Membre retiré : ${membre.prenom} ${membre.nom} (${membre.pid})`
})
)
dispatch({
type: 'RETIRER_MEMBRE',
payload: {membre}
})
}).catch(error => {
console.log(error)
dispatch(
addNotification({
level: 'error',
message: error.message
})
)
})
}
...@@ -76,6 +76,38 @@ const personneCurrent = () => `personne/current` ...@@ -76,6 +76,38 @@ const personneCurrent = () => `personne/current`
*/ */
export const getCurrentPersonneUrl = makeGetter(personneCurrent) export const getCurrentPersonneUrl = makeGetter(personneCurrent)
const membres = ({nom, skip, limit}) => {
let url = `groupe/listerMembres/${encodeURIComponent(nom)}`
const hasSkipOption = typeof skip === 'number' && skip
if (hasSkipOption) url += `?skip=${skip}`
if (typeof limit === 'number' && limit) url += `${hasSkipOption ? '&' : '?'}limit=${limit}`
return url
}
/**
* Liste des membres du groupe
* @type {routeGetter}
* @param {object} urlParams
* @param {string} urlParams.nom Le nom du groupe
* @param {number} [urlParams.skip] offset (si la liste précédente excédait la limite)
* @param {number} [urlParams.limit] limit max du nb de personnes qui seront retournées
* @param {string} [baseUrl]
*/
export const getGroupeMembresUrl = makeGetter(membres, ['nom'])
const groupeAjouterMembre = () => 'groupe/ajouterMembre'
/**
* Ajouter un membre à un groupe (cf doc du controleur pour les params à passer en post)
* @type {routeGetter}
*/
export const getGroupeAjouterMembreUrl = makeGetter(groupeAjouterMembre)
const groupeRetirerMembre = () => 'groupe/retirerMembre'
/**
* Retirer un membre d'un groupe (cf doc du controleur pour les params à passer en post)
* @type {routeGetter}
*/
export const getGroupeRetirerMembreUrl = makeGetter(groupeRetirerMembre)
const clone = ({oid}) => `clone/${oid}` const clone = ({oid}) => `clone/${oid}`
/** /**
* Cloner une ressource * Cloner une ressource
......
...@@ -5,6 +5,10 @@ import {NavLink} from 'react-router-dom' ...@@ -5,6 +5,10 @@ import {NavLink} from 'react-router-dom'
import {Fields} from 'redux-form' import {Fields} from 'redux-form'
import GroupesSelector from './GroupesSelector' import GroupesSelector from './GroupesSelector'
/**
* Composant pour sélectionner un groupe dans la liste (sur l'édition de ressource)
* @param {string[]} groupesList
*/
const GroupContainer = ({groupesList}) => { const GroupContainer = ({groupesList}) => {
if (groupesList.length) { if (groupesList.length) {
return ( return (
......
...@@ -111,7 +111,6 @@ const getInitialValues = ({ ...@@ -111,7 +111,6 @@ const getInitialValues = ({
match: {params: {groupe: groupeNom}}, match: {params: {groupe: groupeNom}},
// vient de la session (via ensureLogged appelé par groupesLoader) // vient de la session (via ensureLogged appelé par groupesLoader)
personne: { personne: {
oid,
nom, nom,
prenom prenom
} }
......
import PropTypes from 'prop-types'
import React, {Fragment} from 'react'
import {NavLink} from 'react-router-dom'
import groupeMembresLoader from './hoc/groupeMembresLoader'
import {saveGroupe} from 'client-react/actions/groupes'
import validate, {asyncBlurFields, asyncValidate} from 'client-react/utils/groupeValidate'
import onSubmitFail from 'client-react/utils/onSubmitFail'
import groupesLoader from 'client-react/components/groupes/hoc/groupesLoader'
const getHeader = ({nbMembres, skip, limit, length}) => {
if (nbMembres === 0) return 'Aucun membre'
if (length === nbMembres) {
return `${nbMembres} membre${nbMembres > 1 ? 's' : ''}`
}
return `${length} membre${length > 1 ? 's' : ''} (de ${skip + 1} à ${Math.min(skip + limit, nbMembres)} sur ${nbMembres})`
}
/**
* La liste des groupes publics (auth)
* @type PureComponent
* @param {object} props
* @param {Groupe[]} props.groupes
* @param {function} props.followGroupe
* @param {function} props.ignoreGroupe
* @param {function} props.groupesSuivis
*/
const GroupeListerMembres = ({
nom,
membres,
nbMembres,
nbAbonnes,
skip,
limit,
previous,
next,
ajouterMembre,
retirerMembre
}) => (
<Fragment>
<div className="fr">
<NavLink to={`/groupe/editer/${encodeURIComponent(nom)}`} className="btn--info"><i className="fa fa-edit"></i>Modifier</NavLink>
</div>
<h1 className="col-3">Groupe « {nom} » <i>({nbAbonnes} abonné{nbAbonnes > 1 ? 's' : ''})</i></h1>
<ul className="clear">
{previous ? (<a onClick={previous}>&lt;&lt;précédents</a>) : null}
<span style={{margin: '0 1rem'}}>{getHeader({nbMembres, skip, limit, length: (membres && membres.length) || 0})}</span>
{next ? (<a onClick={next}>suivants &gt;&gt;</a>) : null}
{membres.map(({nom, prenom, oid}, index) => (<li key={index}>{prenom} {nom} <button onClick={retirerMembre({oid})}>Retirer du groupe</button></li>))}
</ul>
<label className="field">Ajouter un membre <i>(en le faisant vous vous engagez à lui avoir demandé son avis avant)</i></label>
<form className="grid-6" onSubmit={(event, values, ...rest) => {
event.preventDefault()
console.log('onSubmit', Object.keys(event), event.target, event)
const {nom, pid} = event.data || {}
ajouterMembre({nom, pid})
}}>
<div className="field col-2">
<label htmlFor="form-membres-pid">Identifiant</label>
<input id="form-membres-pid" type="text" name="pid" placeholder="cf. « mes informations personnelles »"/>
</div>
<div className="field col-3">
<label htmlFor="form-membres-nom">Son nom</label>
<input id="form-membres-nom" name="nom" placeholder="Entrez également le nom (insensible à la casse)" />
</div>
<div style={{position: 'relative'}}>
<button type="submit" className="btn--primary" style={{position: 'absolute', bottom: '12px', left: '1rem'}}>Ajouter</button>
</div>
</form>
</Fragment>
)
GroupeListerMembres.propTypes = {
nom: PropTypes.string,
membres: PropTypes.array,
nbMembres: PropTypes.number,
nbAbonnes: PropTypes.number,
skip: PropTypes.number,
limit: PropTypes.number,
previous: PropTypes.func,
next: PropTypes.func,
ajouterMembre: PropTypes.func,
retirerMembre: PropTypes.func
}
// groupesListeLoader contient ensureLogged
export default groupeMembresLoader(GroupeListerMembres)
...@@ -59,7 +59,9 @@ const AdminLinksInner = ({ ...@@ -59,7 +59,9 @@ const AdminLinksInner = ({
}) => ( }) => (
<span className="links"> <span className="links">
<a href="#" onClick={() => deleteGroupe(nom)} className="btn--info"><i className="fa fa-trash"></i>Supprimer</a> <a href="#" onClick={() => deleteGroupe(nom)} className="btn--info"><i className="fa fa-trash"></i>Supprimer</a>
<NavLink to={`/groupe/editer/${encodeURIComponent(nom)}`} className="btn--info"><i className="fa fa-edit"></i>Modifier</NavLink> <NavLink to={`/groupe/editer/${encodeURIComponent(nom)}`} className="btn--info"><i className="fa fa-edit"></i>Modifier</NavLink>
<NavLink <NavLink
className="btn--info" className="btn--info"
to={{ to={{
...@@ -67,6 +69,12 @@ const AdminLinksInner = ({ ...@@ -67,6 +69,12 @@ const AdminLinksInner = ({
hash: 'results', hash: 'results',
search: `groupes=${encodeURIComponent(nom)}` search: `groupes=${encodeURIComponent(nom)}`
}}><i className="fa fa-bookmark"></i> Voir les ressources du groupe</NavLink> }}><i className="fa fa-bookmark"></i> Voir les ressources du groupe</NavLink>
<NavLink
className="btn--info"
to={{pathname: `/groupe/listerMembres/${encodeURIComponent(nom)}`}}
><i className="fa fa-users" /> Membres du groupe</NavLink>
{groupesMembre.includes(nom) {groupesMembre.includes(nom)
? null // le lien quitter sera dans la liste des groupesMembre ? null // le lien quitter sera dans la liste des groupesMembre
: (<button className="btn--info" onClick={() => joinGroupe(nom)}><i className="fa fa-sign-in-alt"></i>Rejoindre</button>) : (<button className="btn--info" onClick={() => joinGroupe(nom)}><i className="fa fa-sign-in-alt"></i>Rejoindre</button>)
......
import React, {Component} from 'react'
import PropTypes from 'prop-types'
import {GET, POST} from 'client-react/utils/httpMethods'
import ensureLogged from 'client-react/hoc/ensureLogged'
import addNotify from 'client-react/hoc/addNotify'
import getDisplayName from 'client-react/utils/getDisplayName'
import {getGroupeAjouterMembreUrl, getGroupeMembresUrl, getGroupeRetirerMembreUrl} from 'client-react/apiRoutes'
/**
* Higher Order Component qui charge les membres d'un groupe (passera au component wrappé les props membres, nbMembres, nbAbonnes)
* @param {Component} WrappedComponent
* @return {Component} WrappedComponent enrichi
*/
const groupeMembresLoader = WrappedComponent => {
class GroupeMembresLoader extends Component {
constructor (props) {
console.log('constructeur GroupeMembresLoader avec les props', props)
super(props)
const {match: {params: {groupe}}} = props
this.state = {nom: groupe, membres: null, nbMembres: undefined, nbAbonnes: undefined, skip: undefined, limit: undefined}
}
refresh (skip) {
const urlOptions = {nom: this.state.nom}
if (skip) urlOptions.skip = skip
// pour debug suivant / précédent fixer ici une limite basse, genre urlOptions.limit = 3
const url = getGroupeMembresUrl(urlOptions)
GET(url)
.then(({membres, nbMembres, nbAbonnes, skip, limit}) => this.setState({membres, nbMembres, nbAbonnes, skip, limit}))
.catch(error => {
if (error.status === 403) {
// On affiche le message d'erreur sur la page, plus clair et évite une page vide
this.setState({errorMessage: error.message})
} else {
console.error(error)
this.props.notify({level: 'error', message: error.message})
}
})
}
ajouterMembre ({nom, pid}) {
const url = getGroupeAjouterMembreUrl()
const body = {
groupeNom: this.state.nom,
personneNom: nom,
pid
}
POST(url, {body}).then(({membre}) => {
this.props.notify(`Membre ajouté ${membre.prenom} ${membre.nom} (${membre.pid})`)
const membres = []
let toAdd = true
this.state.membres.forEach((m) => {
// on l'insère dès que c'est sa place dans l'ordre alpha
if (toAdd && membre.nom < m.nom) {
membres.push(membre)
toAdd = false
}
membres.push(m)
})
// pour éviter de refaire une requête et conserver un affichage cohérent on incrémente la limite
const newStateProps = {
membres,
nbMembres: this.state.nbMembres + 1,
limit: this.state.limit + 1
}
this.setState(newStateProps)
}).catch(error => {
console.log(error)
this.props.notify({level: 'error', message: error.message})
})
}
retirerMembre ({oid}) {
const url = getGroupeRetirerMembreUrl()
const body = {
groupeNom: this.state.nom,
personneOid: oid
}
POST(url, {body}).then(({membre}) => {
this.props.notify(`Membre supprimé : ${membre.prenom} ${membre.nom} (${membre.pid})`)
const newStateProps = {
membres: this.state.membres.filter(m => m.oid !== membre.oid),
nbMembres: this.state.nbMembres - 1,
limit: this.state.limit - 1
}
this.setState(newStateProps)
}).catch(error => {
console.log(error)
this.props.notify({level: 'error', message: error.message})
})
}
componentDidMount () {
this.refresh()
}
render () {
if (this.state.errorMessage) return (<p className="alert--warning">{this.state.errorMessage}</p>)
if (this.state.membres === null) return null
const childProps = {...this.props, ...this.state}
if (this.state.skip > 0) {
childProps.previous = () => this.refresh(Math.max(0, this.state.skip - this.state.limit))
}
if (this.state.nbMembres > this.state.skip + this.state.limit) {
childProps.next = () => this.refresh(this.state.skip + this.state.limit)
}
childProps.ajouterMembre = this.ajouterMembre.bind(this)
childProps.retirerMembre = this.retirerMembre.bind(this)
return (
<WrappedComponent {...childProps} />
)
}
}
GroupeMembresLoader.displayName = `groupesListeLoader(${getDisplayName(WrappedComponent)})`
GroupeMembresLoader.propTypes = {
match: PropTypes.shape({
params: PropTypes.shape({
groupe: PropTypes.string
})
}),
notify: PropTypes.func
}
return ensureLogged(addNotify(GroupeMembresLoader))
}
export default groupeMembresLoader
import {connect} from 'react-redux'
import {addNotification} from 'client-react/actions/notifications'
function _notify (dispatch, notification) {
if (!notification) return console.error(Error('notify appelé sans rien à notifier'))
if (notification instanceof Error) {
console.error(notification)
return dispatch(addNotification({message: notification.message, level: 'error'}))
}
// on accepte les string
const message = typeof notification === 'string' ? notification : notification.message
if (typeof message !== 'string') return console.error(Error('appel incorrect de notify'), notification)
if (!message) return console.error(Error('notify appelé avec un message vide'))
const level = notification.level || 'info'
dispatch(addNotification({message, level}))
}
const mapDispatchToProps = (dispatch) => ({
/**
* Affiche une notification
* @param {string|object|Error} notification Si c'est une Error, elle sera affichée en console et le message en notification, sinon il faut une string ou un objet avec au moins une propriété message
* @param {string} notification.message
* @param {string} [notification.level=info]
* @return {*}
*/
notify: _notify.bind(null, dispatch)
})
/**
* High Order Component qui fournit une méthode notify aux props de WrappedComponent
* @param {Component} WrappedComponent
* @return {Component} Le composant enrichi
*/
const addNotify = (WrappedComponent) => connect(null, mapDispatchToProps)(WrappedComponent)
export default addNotify
...@@ -81,6 +81,7 @@ module.exports = function (mainComponent) { ...@@ -81,6 +81,7 @@ module.exports = function (mainComponent) {
'/ressource/rechercher', '/ressource/rechercher',
'/groupe/ajouter', '/groupe/ajouter',
'/groupe/editer/:groupe', '/groupe/editer/:groupe',
'/groupe/listerMembres/:groupe',
'/groupes/perso', '/groupes/perso',
'/groupes/ouverts', '/groupes/ouverts',
'/groupes/publics' '/groupes/publics'
......
...@@ -71,9 +71,9 @@ module.exports = function (component) { ...@@ -71,9 +71,9 @@ module.exports = function (component) {
if (!data.oid && !$accessControl.hasGenericPermission('createGroupe', context)) return $json.denied(context, 'Vous n’avez pas les droits suffisants pour créer un groupe') if (!data.oid && !$accessControl.hasGenericPermission('createGroupe', context)) return $json.denied(context, 'Vous n’avez pas les droits suffisants pour créer un groupe')
flow().seq(function () { flow().seq(function () {
if (data.oid) return EntityGroupe.match('oid').equals(data.oid).grabOne(this) if (data.oid) return $groupeRepository.load(data.oid)
// à la création on cherche d'après le nom pour vérifier qu'il n'existe pas // à la création on cherche d'après le nom pour vérifier qu'il n'existe pas
EntityGroupe.match('nom').equals(data.nom).grabOne(this) $groupeRepository.loadByNom(data.nom, this)
}).seq(function (groupeBdd) { }).seq(function (groupeBdd) {
const nextStep = this const nextStep = this
if (data.oid) { if (data.oid) {
...@@ -148,8 +148,67 @@ module.exports = function (component) { ...@@ -148,8 +148,67 @@ module.exports = function (component) {
controller.options('', optionsOk) controller.options('', optionsOk)
/** /**
* Récupère un groupe (ce serait plus logique sur GET /api/groupe/:oid mais on a déjà plein de route /api/groupe/actionQcq) * Ajoute un membre à un groupe. Il faut les propriétés groupeNom, personneNom, pid
* @route GET /api/groupe/byId/:oid * @route POST /api/groupe/ajouterMembre
*/
controller.post('ajouterMembre', function (context) {
const {groupeNom, personneNom, pid} = context.post
const myOid = $accessControl.getCurrentUserOid(context)
// checks de base
if (!myOid) return $json.denied(context, 'Vous devez être authentifié pour ajouter un membre à un groupe')
if (!groupeNom) return $json.sendKo(context, 'Impossible de modifier un groupe sans nom')
if (!personneNom || !pid || typeof personneNom !== 'string' || typeof pid !== 'string') return $json.sendKo(context, 'Paramètres invalides')
// on charge le groupe pour vérifier qu'on en est gestionnaire
let groupe
flow().seq(function () {
$groupeRepository.loadByNom(groupeNom, this)
}).seq(function (_groupe) {
if (!isManaged(context, _groupe)) return $json.denied(context, 'Vous n’êtes pas gestionnaire de ce groupe et ne pouvez pas modifier ses membres')
groupe = _groupe
$personneRepository.loadByPidAndNom(pid, personneNom, this)
}).seq(function (personne) {
$personneRepository.ajouterGroupesMembre(personne, groupe, this)
}).seq(function (personne) {
// on renvoie pas tout
const {nom, oid, pid, prenom} = personne
$json.sendOk(context, {membre: {nom, oid, pid, prenom}})
}).catch((error) => $json.sendKo(context, error))
})
/**
* Retire un membre d'un groupe. Il faut les propriétés groupeNom, personneOid
* @route POST /api/groupe/retirerMembre
*/
controller.post('retirerMembre', function (context) {
const {groupeNom, personneOid} = context.post
const myOid = $accessControl.getCurrentUserOid(context)
// checks de base
if (!myOid) return $json.denied(context, 'Vous devez être authentifié pour ajouter un membre à un groupe')
if (!groupeNom) return $json.sendKo(context, 'Impossible de modifier un groupe sans nom')
if (!groupeNom || !personneOid || typeof groupeNom !== 'string' || typeof personneOid !== 'string') return $json.sendKo(context, 'Paramètres invalides')
// on charge le groupe pour vérifier qu'on en est gestionnaire
let groupe
flow().seq(function () {
$groupeRepository.loadByNom(groupeNom, this)
}).seq(function (_groupe) {
if (!isManaged(context, _groupe)) return $json.denied(context, 'Vous n’êtes pas gestionnaire de ce groupe et ne pouvez pas modifier ses membres')
groupe = _groupe
$personneRepository.load(personneOid, this)
}).seq(function (personne) {
if (!personne) return $json.sendKo(context, `La personne ${personneOid} n’existe pas`)
$personneRepository.retirerGroupesMembre(personne, groupe, this)
}).seq(function (personne) {
// on renvoie pas tout
const {nom, oid, pid, prenom} = personne
$json.sendOk(context, {membre: {nom, oid, pid, prenom}})
}).catch((error) => $json.sendKo(context, error))
})
/**
* Récupère un groupe (d'après son oid)
* @route GET /api/groupe/:oid
*/ */
controller.get(':oid', function (context) { controller.get(':oid', function (context) {
const {oid} = context.arguments const {oid} = context.arguments
...@@ -168,7 +227,7 @@ module.exports = function (component) { ...@@ -168,7 +227,7 @@ module.exports = function (component) {
}) })
/** /**
* Récupère les détails d'un groupe * Récupère un groupe d'après son nom
* @route GET /api/groupe/byNom/:nom * @route GET /api/groupe/byNom/:nom
*/ */
controller.get('byNom/:nom', function (context) { controller.get('byNom/:nom', function (context) {
...@@ -234,6 +293,38 @@ module.exports = function (component) { ...@@ -234,6 +293,38 @@ module.exports = function (component) {
}) })
}) })
controller.get('listerMembres/:nom', function (context) {
const myOid = $accessControl.getCurrentUserOid(context)
if (!myOid) return $json.denied(context, 'Vous devez être authentifié pour lister les membres du groupe')
const nom = context.arguments.nom.trim()
const options = {
skip: Number(context.get.skip) || 0,
limit: Number(context.get.limit) || 100
}
const data = options
flow().seq(function () {
$groupeRepository.loadByNom(nom, this)
}).seq(function (groupe) {
if (!groupe) return $json.notFound(context, `Le groupe « ${nom} » n’existe pas`)
if (!isManaged(context, groupe)) return $json.denied(context, 'Vous n’êtes pas gestionnaire de ce groupe et ne pouvez pas en lister les membres')
$personneRepository.countMembres(nom, this)
}).seq(function (nb) {
data.nbMembres = nb
if (!nb) return this(null, [])
$personneRepository.loadMembres(nom, options, this)
}).seq(function (personnes) {
data.membres = personnes.map(({nom, oid, pid, prenom}) => ({nom, oid, pid, prenom}))
$personneRepository.countAbonnes(nom, this)
}).seq(function (nbAbonnes) {
data.nbAbonnes = nbAbonnes
$json.sendOk(context, data)
}).catch(function (error) {
$json.sendKo(context, error)
})
})
/** /**
* Retire le groupe au user courant * Retire le groupe au user courant
* @route GET /groupe/quitter/:nom * @route GET /groupe/quitter/:nom
...@@ -334,7 +425,7 @@ module.exports = function (component) { ...@@ -334,7 +425,7 @@ module.exports = function (component) {
/** /**
* Delete groupe par nom, retourne {@link reponseDeleted} * Delete groupe par nom, retourne {@link reponseDeleted}
* @route DELETE /api/groupe/: * @route DELETE /api/groupe/:nom
* @param {String} nom * @param {String} nom
*/ */
controller.delete(':nom', function (context) { controller.delete(':nom', function (context) {
......
...@@ -49,7 +49,7 @@ module.exports = function (component) { ...@@ -49,7 +49,7 @@ module.exports = function (component) {
* @param {string[]} [options.whiteList] Une liste de noms de groupes que l'on peut ajouter sans vérifier si on est membre ou pas * @param {string[]} [options.whiteList] Une liste de noms de groupes que l'on peut ajouter sans vérifier si on est membre ou pas
* @param {errorCallback} next * @param {errorCallback} next
*/ */
function addGroupe (context, ressource, groupeNom, options, next) { function addGroupeToRess (context, ressource, groupeNom, options, next) {
if (!ressource) throw new Error('pas de ressource') if (!ressource) throw new Error('pas de ressource')
if (!groupeNom) throw new Error('pas de nom de groupe') if (!groupeNom) throw new Error('pas de nom de groupe')
if (arguments.length === 4) { if (arguments.length === 4) {
...@@ -171,14 +171,14 @@ module.exports = function (component) { ...@@ -171,14 +171,14 @@ module.exports = function (component) {
const options = { const options = {
whiteList: (ressourceOriginale && ressourceOriginale.groupes) || [] whiteList: (ressourceOriginale && ressourceOriginale.groupes) || []
} }
addGroupe(context, ressourceNew, groupeNom, options, this) addGroupeToRess(context, ressourceNew, groupeNom, options, this)
// @todo faudrait vérifier que l'on a pas viré de groupe auxquel on appartient pas, et si c'était le cas cloner ? // @todo faudrait vérifier que l'on a pas viré de groupe auxquel on appartient pas, et si c'était le cas cloner ?
}).set(groupesAuteursVoulus).seqEach(function (groupeNom) { }).set(groupesAuteursVoulus).seqEach(function (groupeNom) {
const options = { const options = {
whiteList: (ressourceOriginale && ressourceOriginale.groupesAuteurs) || [], whiteList: (ressourceOriginale && ressourceOriginale.groupesAuteurs) || [],
isGroupeAuteur: true isGroupeAuteur: true
} }
addGroupe(context, ressourceNew, groupeNom, options, this) addGroupeToRess(context, ressourceNew, groupeNom, options, this)
}).seq(function () { }).seq(function () {
next(null, ressourceNew) next(null, ressourceNew)
}).catch(addErrorAndNext) }).catch(addErrorAndNext)
......
...@@ -40,40 +40,46 @@ const {looksLikePid} = require('../../utils/validators') ...@@ -40,40 +40,46 @@ const {looksLikePid} = require('../../utils/validators')
module.exports = function (component) { module.exports = function (component) {
component.service('$personneRepository', function (EntityPersonne, EntityGroupe, $cachePersonne, $groupeRepository) { component.service('$personneRepository', function (EntityPersonne, EntityGroupe, $cachePersonne, $groupeRepository) {
/** /**
* Ajoute un groupe à la personne, comme membre (en le créant s'il n'existait pas), * Ajoute un groupe à la personne, comme membre
* @param {Personne|EntityPersonne} personne (sa prop groupesMembre sera modifiée) * @param {EntityPersonne} personne
* @param {string} groupeNom * @param {EntityGroupe} groupe
* @param {errorCallback} next * @param {personneCallback} next appelé avec personne sauvegardé
* @memberOf $personneRepository * @memberOf $personneRepository
*/ */
function addGroupe (personne, groupeNom, next) { function ajouterGroupesMembre (personne, groupe, next) {
const oid = personne.oid if (!personne || !personne.oid || !groupe || !groupe.oid) return next(new Error('paramètres invalides'))
if (!oid) return next(new Error('Impossible d’ajouter un groupe à une personne sans oid')) const {oid} = personne
const {nom} = groupe
if (!Array.isArray(personne.groupesMembre)) personne.groupesMembre = [] if (!Array.isArray(personne.groupesMembre)) personne.groupesMembre = []
if (personne.groupesMembre.includes(groupeNom)) return next(Error(`La personne ${oid} était déjà membre du groupe ${groupeNom}`)) if (personne.groupesMembre.includes(nom)) {
console.error(Error(`La personne ${oid} était déjà membre du groupe ${nom}`))
return next(null, personne)
}
personne.groupesMembre.push(nom)
personne.store(next)
}
// on veut une EntityPersonne si on en a pas déjà une (faudra la sauvegarder) /**
const entityPersonne = personne.store ? personne : EntityPersonne.create(personne) * @callback nbCallback
* @param {Error} [error]
* @param {number} nombre entier positif ou nul
*/
/**
* Retourne le nb de followers d'un groupe
* @param {string} groupeNom
* @param {nbCallback} next
*/
function countAbonnes (groupeNom, next) {
EntityPersonne.match('groupesSuivis').equals(groupeNom).count(next)
}
// faut regarder si le groupe existe /**
flow().seq(function () { * Retourne le nb de followers d'un groupe
$groupeRepository.loadByNom(groupeNom, this) * @param {string} groupeNom
}).seq(function (groupe) { * @param {nbCallback} next
if (groupe) return this(null, groupe) */
// sinon on le crée function countMembres (groupeNom, next) {
const newGroupe = { EntityPersonne.match('groupesMembre').equals(groupeNom).count(next)
nom: groupeNom,
gestionnaires: [oid]
}
EntityGroupe.create(newGroupe).store(this)
}).seq(function (groupe) {
entityPersonne.groupesMembre.push(groupe.nom)
entityPersonne.store(this)
}).seq(function (personneBdd) {
// on mute le groupesMembre du personne passé en argument
personne.groupesMembre = personneBdd.groupesMembre
next()
}).catch(next)
} }
/** /**
...@@ -180,6 +186,24 @@ module.exports = function (component) { ...@@ -180,6 +186,24 @@ module.exports = function (component) {
}).catch(next) }).catch(next)
} }
/**
* Retourne la liste des membres d'un groupe
* @param {string} groupeNom
* @param {object} [options]
* @param {object} [options.skip] offset
* @param {object} [options.limit=500] le nb max de personnes à retourner
* @param {entityPersonnesCallback} next
*/
function loadMembres (groupeNom, options, next) {
if (typeof options === 'function') {
next = options
options = {}
}
if (!options.limit) options.limit = 100
if (typeof options.skip !== 'number') options.skip = 0
EntityPersonne.match('groupesMembre').equals(groupeNom).sort('nom').grab(options, next)
}
/** /**
* Efface un groupe chez toutes les personnes qui en sont membre ou qui le suivent * Efface un groupe chez toutes les personnes qui en sont membre ou qui le suivent
* @param {string} groupName Nom du groupe * @param {string} groupName Nom du groupe
...@@ -258,6 +282,25 @@ module.exports = function (component) { ...@@ -258,6 +282,25 @@ module.exports = function (component) {
updateMembres(0) updateMembres(0)
} }
/**
* Retire un groupe à la personne, comme membre,
* @param {EntityPersonne} personne
* @param {EntityGroupe} groupe
* @param {personneCallback} next appelé avec personne sauvegardé
* @memberOf $personneRepository
*/
function retirerGroupesMembre (personne, groupe, next) {
if (!personne || !personne.oid || !groupe || !groupe.oid) return next(new Error('paramètres invalides'))
const {oid} = personne
const {nom} = groupe
if (!Array.isArray(personne.groupesMembre) || !personne.groupesMembre.includes(nom)) {
log.error(Error(`La personne ${oid} n’était pas membre du groupe ${nom}`))
return next(null, personne)
}
personne.groupesMembre = personne.groupesMembre.filter(groupeNom => groupeNom !== nom)
personne.store(next)
}
/** /**
* Enregistre une personne en bdd (et met à jour le cache) * Enregistre une personne en bdd (et met à jour le cache)
* @param {EntityPersonne} personne * @param {EntityPersonne} personne
...@@ -315,14 +358,18 @@ module.exports = function (component) { ...@@ -315,14 +358,18 @@ module.exports = function (component) {
* @service $personneRepository * @service $personneRepository
*/ */
const $personneRepository = { const $personneRepository = {
addGroupe, ajouterGroupesMembre,
countAbonnes,
countMembres,
delete: deletePersonne, delete: deletePersonne,
load, load,
loadByOids, loadByOids,
loadByPidAndNom, loadByPidAndNom,
loadByPids, loadByPids,
loadMembres,
removeGroup, removeGroup,
renameGroup, renameGroup,
retirerGroupesMembre,
save, save,
updateOrCreate updateOrCreate
} }
......