* @author Jean-Pascal MILCENT * @copyright Copyright (c) 2013, Tela Botanica (accueil@tela-botanica.org) * @license GPL v3 * @license CECILL v2 * @see http://www.tela-botanica.org/wikini/eflore/wakka.php?wiki=ApiIdentiplante01Observations * * @config-depend: "url_image" (dans configurations/config_observations.ini) ex: http://www.tela-botanica.org/appli:cel-img:%09dXL.jpg */ // http://localhost/del/services/0.1/observations/#id => une observation donnée et ses images, SANS LES propositions & nombre de commentaire require_once(dirname(__FILE__) . '/../DelTk.php'); class Observation { private $conteneur; private $gestionBdd; private $bdd; /* Map les champs MySQL vers les champs utilisés dans le JSON pour les clients pour chacune des différentes tables utilisées pour le chargement de résultats ci-dessous. - chargerObservation() (v_del_image) - chargerVotesImage() (del_image_vote, del_image_protocole) - chargerCommentaires() (del_commentaire_vote, del_commentaire). Si la valeur vaut 1, aucun alias ne sera défini (nom du champ d'origine), cf consulter() */ static $mappings = array( 'observations' => array( // v_del_image "id_observation" => 1, "date_observation" => 1, "date_transmission" => 1, "famille" => "determination.famille", "nom_sel" => "determination.ns", "nom_sel_nn" => "determination.nn", "nom_referentiel" => "determination.referentiel", "nt" => "determination.nt", "ce_zone_geo" => "id_zone_geo", "zone_geo" => 1, "lieudit" => 1, "station" => 1, "milieu" => 1, "ce_utilisateur" => "auteur.id", "mots_cles_texte" => "mots_cles_texte", "commentaire" => 1), /* exclus car issus de la jointure annuaire: "nom" => "auteur.nom", // XXX: jointure annuaire "prenom" => "auteur.prenom", // XXX: jointure annuaire "courriel" => "observateur", // XXX: jointure annuaire */ /* présents dans cel_obs mais exclus: ordre, nom_ret, nom_ret_nn, latitude, longitude, altitude, geodatum transmission, date_creation, date_modificationabondance, certitude, phenologie, code_insee_calcule */ 'images' => array( // v_del_image "id_image" => 1, "hauteur" => 1, "nom_original" => 1, "date_prise_de_vue" => "date"), /* présents dans cel_images mais exclus: i_commentaire, nom_original, publiable_eflore, i_mots_cles_texte, i_ordre, i_ce_utilisateur, i_prenom_utilisateur, i_nom_utilisateur, i_courriel_utilisateur */ 'protocoles' => array( // del_image_protocole "ce_protocole" => "protocole.id", "id_protocole" => "protocole.id", "intitule" => "protocole.intitule", "descriptif" => "protocole.descriptif", "tag" => "protocole.tag"), /* See desc del_commentaire_vote & desc del_image_vote; Les deux schémas sont similaires, à l'exception de ce_protocole spécifique à del_image_vote et ce_proposition => ce_image */ 'votes' => array( // del_image_vote et del_commentaire_vote "id_vote" => "vote.id", "ce_proposition" => "proposition.id", "ce_image" => "image.id", "ce_utilisateur" => "auteur.id", // attention, conflit avec commentaire, cf ci-dessous "valeur" => "vote", "date" => 1, // attention, conflit avec commentaire, cf ci-dessous // absents du JSON, et pourtant présents dans services/configurations/config_mapping_votes.ini // (nécessiterait une propre jointure sur del_utilisateur) /* "nom" => "auteur.nom", "prenom" => "auteur.prenom", "courriel" => "auteur.courriel" */), 'commentaires' => array( // del_commentaire "id_commentaire" => 1, "ce_observation" => "observation", "ce_proposition" => "proposition", "ce_commentaire_parent" => "id_parent", // les deux alias suivants sont particuliers afin d'éviter un conflit d'alias // lors des jointures avec del_commentaire_vote ci-dessus // (cf cas particulier dans la boucle de chargerCommentaires()) "ce_utilisateur" => "__auteur_com", "date" => "__date_com", "texte" => 1, "utilisateur_nom" => "auteur.nom", "utilisateur_prenom" => "auteur.prenom", "utilisateur_courriel" => "auteur.courriel", "nom_sel" => 1, "nom_sel_nn" => 1, "nom_ret_nn" => 1, "nom_referentiel" => 1, "proposition_initiale" => 1), ); public function __construct(Conteneur $conteneur = null) { $this->conteneur = $conteneur == null ? new Conteneur() : $conteneur; $this->conteneur->chargerConfiguration('config_votes.ini'); $this->conteneur->chargerConfiguration('config_mapping_votes.ini'); $this->conteneur->chargerConfiguration('config_mapping_commentaires.ini'); $this->gestionBdd = $conteneur->getGestionBdd(); $this->bdd = $this->gestionBdd->getBdd(); } /** * Méthode principale de la classe. * Lance la récupération des images dans la base et les place dans un objet ResultatService * pour l'afficher. * @param array $ressources les ressources situées après l'url de base (ex : http://url/ressource1/ressource2) * @param array $parametres les paramètres situés après le ? dans l'url * */ public function consulter($ressources, $parametres) { if (!$ressources || count($ressources) != 1 ) { throw new Exception("Le service observation accepte un unique identifiant d'observation", RestServeur::HTTP_CODE_ERREUR); } // initialise les mappings: // substitue les valeurs à 1 par le nom de la clef (pas d'alias de champ) array_walk(self::$mappings['observations'], create_function('&$val, $k', 'if($val==1) $val = $k;')); array_walk(self::$mappings['images'], create_function('&$val, $k', 'if($val==1) $val = $k;')); array_walk(self::$mappings['protocoles'], create_function('&$val, $k', 'if($val==1) $val = $k;')); array_walk(self::$mappings['votes'], create_function('&$val, $k', 'if($val==1) $val = $k;')); array_walk(self::$mappings['commentaires'], create_function('&$val, $k', 'if($val==1) $val = $k;')); // Gestion des configuration du script $idobs = $ressources[0]; $protocole = isset($parametres['protocole']) && is_numeric($parametres['protocole']) ?intval($parametres['protocole']) : NULL; // 1) récupération de l'observation (et de ses images (v_del_image est une vue utilisant des INNER JOIN)) $liaisons = self::chargerObservation($this->bdd, $idobs); if(!$liaisons) { header('HTTP/1.0 404 Not Found'); // don't die (phpunit) throw(new Exception()); } // 2) réassocie les images "à plat" à leur observation (merge) // TODO: appliquer le formattage dépendant de la configuration en fin de processus $observations = self::reformateObservationSimpleIndex($liaisons, $this->conteneur->getParametre('url_images')); // bien que dans notre cas il n'y ait qu'une seule observation, issue de plusieurs images // dans $liaisons, $observation est un tableau (cf reformateObservation). // Considérons la chose comme telle au cas où le webservice doivent demain demander une paire // d'observations (... convergence avec ListeObservations & DelTk) $observation = array_pop($observations); // 3) charge les données de votes et protocoles associés aux images if ($observation['images']) { $votes = self::chargerVotesImage($this->bdd, $observation['images'], $protocole); // 3") merge/reformate les données retournées self::mapVotesToImages($votes, $observation['images']); } // 4) charge les commentaires et les votes associés // modifie/créé $observation['commentaires'] self::chargerCommentaires($this->bdd, $observation); // désindexe le tableau (tel qu'apparement attendu par les applis), c'est une exception // corriger l'appli cliente pour utiliser les index puis supprimer cette ligne $observation['images'] = array_values($observation['images']); // autre élément de post-processing: le ce_utilisateur de l'observation non-numeric... if (!is_numeric($observation['auteur.id'])) { $observation['auteur.id'] = "0"; } if (!isset($observation['auteur.nom'])) { $observation['auteur.nom'] = '[inconnu]'; } if (isset($parametres['justthrow'])) { return $observation; } // Mettre en forme le résultat et l'envoyer pour affichage $resultat = new ResultatService(); $resultat->corps = $observation; return $resultat; } static function chargerObservation($db, $idobs) { // prenom_utilisateur, nom_utilisateur, courriel_utilisateur sont exclus du mapping // car nous utilisons une construction SQL élaborée pour eux (cf IFNULL ci-dessous) $obs_fields = DelTk::sqlFieldsToAlias(self::$mappings['observations'], NULL, 'dob'); $image_fields = DelTk::sqlFieldsToAlias(self::$mappings['images'], NULL, 'dob'); // champs de l'annuaire (del_utilisateur): id_utilisateur prenom, nom, courriel $annuaire_fields = implode(', ', array("IFNULL(du.prenom, prenom_utilisateur) AS `auteur.prenom`", "IFNULL(du.nom, nom_utilisateur) AS `auteur.nom`", "IFNULL(du.courriel, courriel_utilisateur) AS `auteur.courriel`")); return $db->recupererTous(sprintf( 'SELECT %s, %s, %s FROM v_del_image as dob'. ' LEFT JOIN del_utilisateur du ON CAST(du.id_utilisateur AS CHAR) = CAST(dob.ce_utilisateur AS CHAR)'. ' WHERE dob.id_observation = %d -- %s', $obs_fields, $image_fields, $annuaire_fields, $idobs, __FILE__ . ':' . __LINE__)); } // Charger les images et leurs votes associés static function chargerVotesImage($db, $images, $protocole = NULL) { if (!$images) return NULL; $select = array('votes' => array('id_vote', 'ce_image', 'ce_protocole', 'ce_utilisateur', 'valeur', 'date', /* del_image_vote */), 'protocole' => array('id_protocole', 'intitule', 'descriptif', 'tag' /* del_image_protocole */ )); $vote_fields = DelTk::sqlFieldsToAlias(self::$mappings['votes'], $select['votes'], 'v'); // "v": cf alias dans la requête $proto_fields = DelTk::sqlFieldsToAlias(self::$mappings['protocoles'], $select['protocole'], 'p'); $where = array(); $idsImages = array_values(array_map(create_function('$a', 'return $a["id_image"];'), $images)); $where[] = sprintf('v.ce_image IN (%s)', implode(',', $idsImages)); if ($protocole) { $where[] = "v.ce_protocole = $protocole"; } // pour une fois, on conserve l'ordre en ne luttant pas contre les IDs reçus $ordreDesIdsRecus = sprintf('FIELD(v.ce_image, %s)', implode(',', $idsImages)); $req = sprintf( 'SELECT %s, %s FROM del_image_vote AS v'. ' INNER JOIN del_image_protocole p ON v.ce_protocole = p.id_protocole'. ' WHERE %s'. ' ORDER BY %s -- %s', $vote_fields, $proto_fields, ($where ? implode(' AND ', $where) : 1), $ordreDesIdsRecus, __FILE__ . ':' . __LINE__); //echo "REQUETE: $req"; exit; return $db->recupererTous($req); } /** * Formater une observation depuis une ligne liaison * @param $liaison liaison issue de la recherche * @return $observation l'observation mise en forme * Exemple: vote, au sortir de MySQL contient: * 'xxx' => 'blah', 'descriptif' => 'foo', 'valeur' => 3 * et le tableau de mapping contient: * descriptif = protocole.descriptif, valeur = vote * Alors $retour[ contient: * * */ static function mapVotesToImages($votes, &$images) { if (!$votes) return; // pour chaque vote foreach ($votes as $vote) { $imgid = $vote['image.id']; $protoid = $vote['protocole.id']; // un vote sans image associée ? est-ce possible ? // if(!isset($images[$imgid])) continue; if(!array_key_exists('protocoles_votes', $images[$imgid]) || !array_key_exists($protoid, $images[$imgid]['protocoles_votes'])) { // extrait les champs spécifique au protocole (le LEFT JOIN de chargerVotesImage les ramène en doublons $protocole = array_intersect_key($vote, array_flip(self::$mappings['protocoles'])); $images[$imgid]['protocoles_votes'][$protoid] = $protocole; } $vote = array_intersect_key($vote, array_flip(self::$mappings['votes'])); $images[$imgid]['protocoles_votes'][$protoid]['votes'][$vote['vote.id']] = $vote; } } // Charger les commentaires et leurs votes associés static function chargerCommentaires($db, &$observation) { $select = array('votes' => array('id_vote', 'ce_proposition', 'ce_utilisateur', 'valeur', 'date' /* del_commentaire_vote */), 'commentaires' => array('id_commentaire', 'ce_observation', 'ce_proposition', 'ce_commentaire_parent', 'texte', 'ce_utilisateur', 'utilisateur_prenom', 'utilisateur_nom', 'utilisateur_courriel', 'nom_sel', 'nom_sel_nn', 'nom_ret', 'nom_ret_nn', 'nt', 'famille', 'nom_referentiel', 'date', 'proposition_initiale')); $vote_fields = DelTk::sqlFieldsToAlias(self::$mappings['votes'], $select['votes'], 'cv'); // "v": cf alias dans la requête $comment_fields = DelTk::sqlFieldsToAlias(self::$mappings['commentaires'], $select['commentaires'], 'dc'); $commentaires = $db->recupererTous(sprintf( "SELECT %s, %s FROM del_commentaire as dc". // LEFT JOIN optionnel, mais explicatif: // on ne récupère des infos de vote que pour les commentaires comportant un // nom_sel "valide" " LEFT JOIN del_commentaire_vote cv". " ON cv.ce_proposition = dc.id_commentaire AND dc.nom_sel != '' AND dc.nom_sel IS NOT NULL". " WHERE ce_observation = %d -- %s", $comment_fields, $vote_fields, $observation['id_observation'], __FILE__ . ':' . __LINE__)); if (!$commentaires) return; // les commentaires réunifiées et dont les votes sont mergés $ret = array(); foreach ($commentaires as $comment) { $commentid = $comment['id_commentaire']; $voteid = $comment['vote.id']; if (!array_key_exists($commentid, $ret)) { $comment_extract = array_intersect_key($comment, array_flip(self::$mappings['commentaires'])); // cas particulier: conflit d'aliases avec del_commentaire_vote $comment_extract['auteur.id'] = $comment_extract['__auteur_com']; $comment_extract['date'] = $comment_extract['__date_com']; unset($comment_extract['__auteur_com'], $comment_extract['__date_com']); // toujours un éléments "votes", quand bien même il n'y en aurait pas $comment_extract['votes'] = array(); $ret[$commentid] = $comment_extract; } if (!$comment['nom_sel'] || ! $voteid) continue; $vote = array_intersect_key($comment, array_flip(self::$mappings['votes'])); $ret[$commentid]['votes'][$voteid] = $vote; } $observation['commentaires'] = $ret; } // cf ListeObservation::reformateObservation() et ListeImages2::reformateImagesDoubleIndex() // (trop de variétés de formatage, à unifier côté client pour unifier côté backend ...) static function reformateObservationSimpleIndex($obs, $url_pattern = '') { // XXX: cf Observation.php::consulter(), nous pourriouns ici // conserver les valeurs vides (pour les phptests notamment, ou non) $obs = array_map('array_filter', $obs); $obs_merged = array(); foreach ($obs as $o) { $id = $o['id_observation']; $image = array_intersect_key($o, array_flip(array('id_image', 'date', 'hauteur' , 'largeur', 'nom_original'))); $image['binaire.href'] = sprintf($url_pattern, $image['id_image']); unset($o['id_image'], $o['date'], $o['hauteur'], $o['largeur'], $o['nom_original']); if(!isset($obs_merged[$id])) $obs_merged[$id] = $o; $obs_merged[$id]['images'][$image['id_image']] = $image; } return $obs_merged; } /** * Modifie une observation directement dans le CEL en faisant un appel à un web service du CEL. * Utilisé uniquement par les admins. * Permet de dépublier une observation. * * @param array $ressources tableau des informations contenues dans l'url après le nom du service * @param array $parametres contenu du post * @return mixed Chaine "OK" (en majuscule) en cas de succès, booléen "false" en cas d'échec */ public function modifier($ressources, $parametres) { $controlAcces = $this->conteneur->getControleAcces(); $controlAcces->etreUtilisateurAvecDroitAdmin(); $retour = false; if (isset($parametres['transmission'])) { $idObs = $ressources[0]; $clientRest = $this->conteneur->getRestClient(); $urlTpl = $this->conteneur->getParametre('urlServiceCelObs'); $url = $urlTpl.$idObs; $retourCel = $clientRest->modifier($url, $parametres); $retour = preg_match('/^OK$/i', $retourCel) ? 'OK' : false; if ($retour === false) { $message = "Erreur du web service CEL : ".$retourCel; $code = RestServeur::HTTP_CODE_MAUVAISE_REQUETE; throw new Exception($message, $code); } } else { $message = "Ce web service doit contenir un paramètre 'transmission'."; $code = RestServeur::HTTP_CODE_MAUVAISE_REQUETE; throw new Exception($message, $code); } return $retour; } }