Subversion Repositories eFlore/Applications.del

Rev

Rev 1489 | Rev 1494 | Go to most recent revision | Details | Compare with Previous | Last modification | View Log | RSS feed

Rev Author Line No. Line
1375 raphael 1
<?php
2
/**
3
 * Le web service observations récupère toutes les observations et, pour chacune d'elle, les
4
 * images qui lui sont associées.
5
 * Basée sur la classe antérieure dans ListeObservations.php de
6
 * Grégoire Duché et Aurélien Peronnet
7
 * (formaterVote, formaterDeterminations, chargerNombreCommentaire, chargerVotes, chargerDeterminations)
8
 *
9
 * @category	php 5.2
10
 * @package		del
11
 * @author		Raphaël Droz <raphael@tela-botanica.org>
12
 * @copyright	Copyright (c) 2013 Tela Botanica (accueil@tela-botanica.org)
13
 * @license	http://www.cecill.info/licences/Licence_CeCILL_V2-fr.txt Licence CECILL
14
 * @license	http://www.gnu.org/licenses/gpl.html Licence GNU-GPL
15
 * @see http://www.tela-botanica.org/wikini/eflore/wakka.php?wiki=ApiIdentiplante01Observations
16
 *
17
 * TODO:
18
 * PDO::prepare()
19
 * Sphinx pour auteur, genre, ns, commune, tag et masque-général
20
 */
21
 
1490 raphael 22
require_once(dirname(__FILE__) . '/../DelTk.php');
1488 raphael 23
/*
24
  restore_error_handler();
25
  restore_exception_handler();
26
  error_reporting(E_ALL);
27
*/
1375 raphael 28
 
1451 raphael 29
class ListeObservations {
1375 raphael 30
 
31
	private $conteneur;
32
	private $gestionBdd;
33
	private $bdd;
34
	private $parametres = array();
35
	private $ressources = array();
36
 
37
	static $tris_possibles = array('date_observation');
38
	// paramètres autorisés
39
 
40
	static $sql_fields_liaisons = array(
41
		'dob' => array('id_observation', 'nom_sel AS `determination.ns`', 'nt AS `determination.nt`',
1488 raphael 42
		'nom_sel_nn AS `determination.nn`', 'famille AS `determination.famille`',
43
		'nom_referentiel AS `determination.referentiel`',
44
		'ce_zone_geo AS id_zone_geo', 'zone_geo', 'lieudit',
45
		'station', 'milieu', 'date_observation', 'mots_cles_texte', 'date_transmission',
46
		'ce_utilisateur AS `auteur.id`', 'prenom_utilisateur AS `auteur.prenom`',
47
		'nom_utilisateur AS `auteur.nom`', 'courriel_utilisateur AS observateur',
48
		'commentaire'),
1386 raphael 49
		'di' => array('id_image', 'date_prise_de_vue AS `date`', 'hauteur',/* 'largeur','nom_original' // apparemment inutilisés */),
1375 raphael 50
		'du' => array('prenom', 'nom', 'courriel'),
51
		'dc' => array('commentaire')
52
	);
53
 
54
 
55
	public function __construct(Conteneur $conteneur = null) {
56
		$this->conteneur = $conteneur == null ? new Conteneur() : $conteneur;
57
		$this->conteneur->chargerConfiguration('config_departements_bruts.ini');
58
		$this->conteneur->chargerConfiguration('config_observations.ini');
59
		$this->conteneur->chargerConfiguration('config_mapping_votes.ini');
60
		$this->conteneur->chargerConfiguration('config_mapping_commentaires.ini');
61
		$this->navigation = $conteneur->getNavigation();
62
		$this->masque = $conteneur->getMasque();
63
		$this->gestionBdd = $conteneur->getGestionBdd();
64
		$this->bdd = $this->gestionBdd->getBdd();
65
	}
66
 
1383 raphael 67
	static function reformateObservation($obs, $url_pattern = '') {
1375 raphael 68
		$obs = array_map('array_filter', $obs);
69
		$obs_merged = array();
70
		foreach($obs as $o) {
71
			$id = $o['id_observation'];
1386 raphael 72
 
73
			// car auteur.id peut être un email, un hash, ou un annuaire_tela.U_ID
74
			// mais dans les deux premiers cas SELECT courriel AS observateur fait déjà l'affaire
75
			if(!is_numeric($o['auteur.id'])) $o['auteur.id'] = "0";
76
			if(!isset($o['auteur.nom'])) $o['auteur.nom'] = '[inconnu]';
77
 
1375 raphael 78
			$image = array_intersect_key($o, array_flip(array('id_image', 'date', 'hauteur' , 'largeur', 'nom_original')));
1383 raphael 79
			$image['binaire.href'] = sprintf($url_pattern, $image['id_image']);
1375 raphael 80
			unset($o['id_image'], $o['date'], $o['hauteur'], $o['largeur'], $o['nom_original']);
81
			if(!isset($obs_merged['"' . $id . '"'])) $obs_merged['"' . $id . '"'] = $o;
82
			$obs_merged['"' . $id . '"']['images'][] = $image;
83
		}
84
		return $obs_merged;
85
	}
86
 
87
	/**
88
	 * Méthode principale de la classe.
89
	 * Lance la récupération des images dans la base et les place dans un objet ResultatService
90
	 * pour l'afficher.
91
	 * @param array $ressources les ressources situées après l'url de base (ex : http://url/ressource1/ressource2)
92
	 * @param array $parametres les paramètres situés après le ? dans l'url
93
	 **/
94
	public function consulter($ressources, $parametres) {
1416 raphael 95
		// SELECT, à terme, pourrait affecter getInfos(), mais en aucune manière getIdObs()
1375 raphael 96
		$req = array('select' => array(), 'join' => array(), 'where' => array(), 'groupby' => array(), 'having' => array());
1416 raphael 97
 
1380 raphael 98
		// toujours nécessaire puisque nous tapons sur v_del_image qui INNER JOIN cel_images, or nous voulons certes
1375 raphael 99
		// toutes les images, mais nous voulons $limite observations uniques.
1416 raphael 100
		$req['groupby'][] = 'vdi.id_observation';
1375 raphael 101
 
102
		$db = $this->bdd;
103
 
104
		// filtrage de l'INPUT
1490 raphael 105
		$params = DelTk::requestFilterParams($parametres, DelTk::$parametres_autorises, $this->conteneur);
1488 raphael 106
 
1490 raphael 107
		$params['masque.tag'] = DelTk::buildTagsAST(@$parametres['masque.tag'], 'OR', ',');
1488 raphael 108
 
1421 raphael 109
		// ... et paramètres par défaut
1490 raphael 110
		$params = array_merge(DelTk::$default_params, $params);
1421 raphael 111
 
1375 raphael 112
		// création des contraintes (masques)
1490 raphael 113
		DelTk::sqlAddConstraint($params, $db, $req);
1375 raphael 114
		self::sqlAddConstraint($params, $db, $req, $this->conteneur);
1416 raphael 115
		self::sqlAddMasqueConstraint($params, $db, $req, $this->conteneur);
1375 raphael 116
 
117
		// 1) grunt-work: *la* requête de récupération des id valides (+ SQL_CALC_FOUND_ROWS)
118
		$idobs_tab = self::getIdObs($params, $req, $db);
119
		// idobs est une liste (toujours ordonnée) des id d'observations recherchées
120
		$idobs = array_values(array_map(create_function('$a', 'return $a["id_observation"];'), $idobs_tab));
121
 
1379 raphael 122
		if($idobs) {
123
			$total = $db->recuperer('SELECT FOUND_ROWS() AS c'); $total = intval($total['c']);
1375 raphael 124
 
1379 raphael 125
			// 2) récupération des données nécessaires pour ces observations (obs + images)
1416 raphael 126
			// ici les champs récupérés sont issus de self::$sql_fields_liaisons mais sans préfixes
127
			// car tout provient de v_del_image
1379 raphael 128
			$obs_unfmt = self::getInfos($idobs, $db);
1375 raphael 129
 
1379 raphael 130
			// 3) suppression, merge des données en tableau assez représentatif du futur JSON en output
1383 raphael 131
			$observations = self::reformateObservation($obs_unfmt, $this->conteneur->getParametre('url_images'));
1375 raphael 132
 
1379 raphael 133
			// 4) récupération des données nécessaires pour ces observations (commentaires + votes)
134
			// modifie $observations
135
			$this->configurer();
136
			$this->chargerDeterminations($observations);
1375 raphael 137
 
1379 raphael 138
			// 5) restauration de l'ordre souhaité initialement
139
			$observations = self::sortArrayByArray($observations, $idobs);
140
		} else {
141
			$observations = array();
142
			$total = 0;
143
		}
144
 
1375 raphael 145
		// 6) JSON output
146
		$resultat = new ResultatService();
1490 raphael 147
		$resultat->corps = array('entete' => DelTk::makeJSONHeader($total, $params, Config::get('url_service')),
1488 raphael 148
		'resultats' => $observations);
1375 raphael 149
 
150
		return $resultat;
151
	}
152
 
153
	static function sortArrayByArray($array, $orderArray) {
154
		$ordered = array();
155
		foreach($orderArray as $key) {
156
			if(array_key_exists('"' . $key . '"', $array)) {
157
				$ordered['"' . $key . '"'] = $array['"' . $key . '"'];
158
				unset($array['"' . $key . '"']);
159
			}
160
		}
161
		return $ordered + $array;
162
	}
163
 
164
	// SQL helpers
165
	/*
166
	 * Retourne une liste ordonnée d'id d'observation correspondant aux critères
167
	 * passés dans p et aux clauses where/join présentes dans le tableau $req
168
	 *
169
	 * @param p: $params (filtrés sauf escape-string)
170
	 * @param req: le tableau représentant les composants de la requete SQL
171
	 * @param db: l'instance de db
172
	 */
173
	static function getIdObs($p, $req, $db) {
174
		$req_s = sprintf('SELECT SQL_CALC_FOUND_ROWS id_observation' .
1488 raphael 175
		' FROM v_del_image vdi'.
176
		' %s' . // LEFT JOIN if any
177
		' WHERE %s'. // where-clause ou TRUE
178
		' %s'. // group-by
179
		' %s'. // having (si commentaires)
180
		' ORDER BY %s %s %s'.
181
		' LIMIT %d, %d -- %s',
1375 raphael 182
 
1488 raphael 183
		$req['join'] ? implode(' ', $req['join']) : '',
184
		$req['where'] ? implode(' AND ', $req['where']) : 'TRUE',
1375 raphael 185
 
1488 raphael 186
		$req['groupby'] ? ('GROUP BY ' . implode(', ', array_unique($req['groupby']))) : '',
187
		$req['having'] ? ('HAVING ' . implode(' AND ', $req['having'])) : '',
1375 raphael 188
 
1488 raphael 189
		$p['tri'], strtoupper($p['ordre']),
190
		// date_transmission peut-être NULL et nous voulons de la consistence
191
		// (sauf après r1860 de Cel)
192
		$p['tri'] == 'date_transmission' ? ', id_observation' : '',
1375 raphael 193
 
1488 raphael 194
		$p['navigation.depart'], $p['navigation.limite'], __FILE__ . ':' . __LINE__);
1379 raphael 195
 
1375 raphael 196
		$res = $db->recupererTous($req_s);
197
		$err = mysql_error();
198
		if(!$res && $err) {
199
			// http_response_code(400);
200
			// if(defined('DEBUG') && DEBUG) header("X-Debug: $req_s");
201
			throw new Exception('not found', 400);
202
		}
203
		// ordre préservé, à partir d'ici c'est important.
204
		return $res;
205
	}
206
 
207
	/*
208
	  Champs récupérés:
209
	  Pour del_images, la vue retourne déjà ce que nous recherchons de cel_obs et cel_images
1416 raphael 210
	  (cel_obs.* et cel_[obs_]images.{id_observation, id_image, date_prise_de_vue AS date, hauteur, largeur})
1375 raphael 211
	  Pour del_commentaires: nous voulons *
1416 raphael 212
	  Reste ensuite à formatter.
213
	  Note: le préfixe de table utilisé ici (vdi) n'impacte *aucune* autre partie du code car rien
214
	  n'en dépend pour l'heure. (inutilisation de $req['select'])
1375 raphael 215
	*/
216
	static function getInfos($idobs, $db) {
217
		/*$select_fields = implode(',', array_merge(
1488 raphael 218
		  array_map(create_function('$a', 'return "vdi.".$a;'), self::$sql_fields_liaisons['dob']),
219
		  array_map(create_function('$a', 'return "vdi.".$a;'), self::$sql_fields_liaisons['di']),
220
		  array_map(create_function('$a', 'return "du.".$a;'), self::$sql_fields_liaisons['du'])));*/
1375 raphael 221
		$select_fields = array_merge(self::$sql_fields_liaisons['dob'],
1488 raphael 222
		self::$sql_fields_liaisons['di']);
1416 raphael 223
		$req_s = sprintf('SELECT %s FROM v_del_image vdi'.
1488 raphael 224
		// ' LEFT JOIN del_commentaire AS dc ON di.id_observation = dc.ce_observation AND dc.nom_sel IS NOT NULL'.
225
		' WHERE id_observation IN (%s)',
226
		implode(',', $select_fields),
227
		implode(',', $idobs));
1375 raphael 228
		return $db->recupererTous($req_s);
229
	}
230
 
231
 
232
	/**
1490 raphael 233
	 * Complément à DelTk::sqlAddConstraint()
1416 raphael 234
	 *
1490 raphael 235
     * @param $p les paramètres (notamment de masque) passés par l'URL et déjà traités/filtrés (sauf quotes)
1416 raphael 236
	 * @param $req le tableau, passé par référence représentant les composants de la requête à bâtir
1375 raphael 237
	 * @param $c conteneur, utilisé soit pour l'appel récursif à requestFilterParams() en cas de param "masque"
238
	 *								soit pour la définition du type (qui utilise la variable nb_commentaires_discussion)
239
	 */
240
	static function sqlAddConstraint($p, $db, &$req, Conteneur $c = NULL) {
241
		if(!empty($p['masque.type'])) {
242
			self::addTypeConstraints($p['masque.type'], $db, $req, $c);
243
		}
1416 raphael 244
	}
1375 raphael 245
 
1416 raphael 246
	/* Le masque fait une recherche générique parmi de nombreux champs ci-dessus.
247
	   Nous initialisons donc ces paramètres (excepté masque biensur), et nous rappelons
248
	   récursivement. À la seule différence que nous n'utiliserons que $or_req['where']
249
	   imploded par des " OR ". */
250
	static function sqlAddMasqueConstraint($p, $db, &$req, Conteneur $c = NULL) {
1375 raphael 251
		if(!empty($p['masque'])) {
252
			$or_params = array('masque.auteur' => $p['masque'],
1488 raphael 253
			'masque.departement' => $p['masque'],
254
			'masque.id_zone_geo' => $p['masque'],
255
			'masque.tag' => $p['masque'],
256
			'masque.ns' => $p['masque'],
257
			'masque.famille' => $p['masque'],
258
			'masque.date' => $p['masque'],
259
			'masque.genre' => $p['masque'],
260
			/* milieu: TODO ? */ );
1490 raphael 261
			$or_masque = DelTk::requestFilterParams($or_params, array_keys($or_params), $c);
262
			$or_masque['masque.tag'] = DelTk::buildTagsAST($p['masque'], 'OR', ' ');
1375 raphael 263
			// $or_req = array('select' => array(), 'join' => array(), 'where' => array(), 'groupby' => array(), 'having' => array());
264
			$or_req = array('join' => array(), 'where' => array());
1490 raphael 265
			DelTk::sqlAddConstraint($or_masque, $db, $or_req);
1375 raphael 266
			self::sqlAddConstraint($or_masque, $db, $or_req);
267
 
268
			if($or_req['where']) {
269
				$req['where'][] = '(' . implode(' OR ', $or_req['where']) . ')';
270
				// utile au cas ou des jointures seraient rajoutées
271
				$req['join'] = array_unique(array_merge($req['join'], $or_req['join']));
272
			}
273
		}
274
	}
275
 
276
 
277
	private function configurer() {
278
		$this->mappingVotes = $this->conteneur->getParametre('mapping_votes');
279
		$this->mappingCommentaire = $this->conteneur->getParametre('mapping_commentaire');
280
	}
281
 
282
 
283
	/*
284
	 * @param $req: la représentation de la requête MySQL complète, à amender.
285
	 */
286
	static function addTypeConstraints($val, $db, &$req, Conteneur $c) {
287
		if(array_key_exists('adeterminer', $val)) {
288
			//On récupère toutes les observations qui on le tag "aDeterminer" *ou* qui n'ont pas de nom d'espèce
289
			$req['where'][] = '(' . implode(' OR ', array(
1416 raphael 290
				'vdi.certitude = "aDeterminer"',
291
				'vdi.mots_cles_texte LIKE "%aDeterminer%"',
292
				'vdi.nom_sel_nn IS NULL', // TODO: ensure pas d'entrée à 0
1375 raphael 293
			)) . ')';
294
		}
295
		if(array_key_exists('aconfirmer', $val)) {
296
			//On récupère toutes les observations qui ne sont pas "aDeterminer" *et* qui ont un nom d'espèce
297
			$req['where'][] = '(' . implode(' AND ', array(
1416 raphael 298
				'vdi.nom_sel IS NOT NULL',
299
				'vdi.certitude != "aDeterminer"',
300
				'(vdi.mots_cles_texte IS NULL OR vdi.mots_cles_texte NOT LIKE "%aDeterminer%"',
1375 raphael 301
			)) . ')';
302
		}
1444 raphael 303
		if(array_key_exists('validees', $val)) {
304
			//On récupère toutes les observations ayant un commentaire doté de proposition_retenue = 1
305
			$req['join'][] = 'INNER JOIN del_commentaire AS dc ON vdi.id_observation = dc.ce_observation AND dc.proposition_retenue = 1';
306
		}
1375 raphael 307
 
308
		// solution n°1: impraticable
309
		if(false && array_key_exists('endiscussion', $val)) {
310
			//Si on veut les observations en discussion,
311
			// on va récupérer les ids des observations dont le nombre de commentaire est supérieur à N
312
			$req['select'][] = 'COUNT(dc.id_commentaire) AS comm_count';
1416 raphael 313
			$req['join'][] = 'INNER JOIN del_commentaire AS dc ON vdi.id_observation = dc.ce_observation';
314
			$req['groupby'][] = 'vdi.id_observation';
1375 raphael 315
			$req['having'][] = "COUNT(id_commentaire) > " . $c->getParametre('nb_commentaires_discussion');
316
		}
317
 
318
		if(array_key_exists('endiscussion', $val)) {
319
			$req['where'][] = '(SELECT COUNT(id_commentaire) FROM del_commentaire AS dc'.
320
				' WHERE ce_observation = id_observation) > ' . intval($c->getParametre('nb_commentaires_discussion'));
321
		}
322
	}
323
 
324
 
325
	/**
1488 raphael 326
	 * Récupérer toutes les déterminations et le nombre de commentaire au total
327
	 * @param array $observations la liste des observations à mettre à jour
328
	 * */
1375 raphael 329
	private function chargerDeterminations(&$observations) {
330
		$idObs = array_values(array_map(create_function('$a', 'return $a["id_observation"];'),
1488 raphael 331
		$observations));
1375 raphael 332
		$r = sprintf('SELECT * FROM del_commentaire AS dc WHERE dc.nom_sel IS NOT NULL AND ce_observation IN (%s) -- %s',
1488 raphael 333
		implode(',',$idObs),
334
		__FILE__ . ':' . __LINE__);
1375 raphael 335
		$propositions = $this->bdd->recupererTous($r);
336
		if(!$propositions) return;
337
		foreach ($propositions as $proposition) {
338
			$idObs = $proposition['ce_observation'];
1386 raphael 339
			$idComment = $proposition['id_commentaire'];
340
			$comment = $this->formaterDetermination($idComment, $proposition);
341
			if($comment) $observations['"' . $idObs . '"']['commentaires'][$idComment] = $comment;
342
 
1375 raphael 343
		}
344
	}
345
 
1386 raphael 346
	private function formaterDetermination($commentId, $proposition) {
347
		if(!$proposition) return NULL;
348
 
349
		$proposition_formatee = array('nb_commentaires' => '0');
350
		foreach ($this->mappingCommentaire as $nomOriginal => $nomFinal) {
351
			if (isset($proposition[$nomOriginal])) {
352
				$proposition_formatee[$nomFinal] = $proposition[$nomOriginal];
353
			}
354
		}
355
 
356
		// Charger les votes sur les déterminations
357
		$resultatsVotes = $this->bdd->recupererTous(
358
			sprintf('SELECT * FROM del_commentaire_vote WHERE ce_proposition = %d', $commentId));
359
 
1375 raphael 360
		foreach ($resultatsVotes as $vote) {
1386 raphael 361
			$proposition_formatee['votes'][$vote['id_vote']] = $this->formaterVote($vote);
1375 raphael 362
		}
363
 
1386 raphael 364
 
365
		// chargerNombreCommentaire()
366
		// Charger le nombre de commentaires (sans détermination) associé à l'observation
367
		$listeCommentaires = $this->bdd->recupererTous(sprintf(
368
			'SELECT ce_commentaire_parent, ce_proposition, COUNT( id_commentaire ) AS nb '.
369
			'FROM del_commentaire WHERE ce_proposition = %d GROUP BY ce_proposition -- %s',
370
			$commentId, __FILE__ . ':' . __LINE__));
1375 raphael 371
		foreach ($listeCommentaires as $ligneProposition) {
372
			// ce test sert à exclure les proposition de 1er niveau qui sont elles aussi des commentaires
1386 raphael 373
			if($ligneProposition['ce_commentaire_parent']) {
374
				// TODO/debug: id_commentaire_parent != $commentId ??
375
				// reprendre la "logique" du code... moins de boucles, moins de requêtes, ...
376
				if($ligneProposition['ce_commentaire_parent'] != $commentId) {
377
					// restore_error_handler();
378
					error_log(sprintf("possible error: nb_commentaires = %s: comment = %d, parent = %d, %s",
1488 raphael 379
					$ligneProposition['nb'], $commentId, $ligneProposition['ce_commentaire_parent'], __FILE__));
1386 raphael 380
				}
381
				$proposition_formatee['nb_commentaires'] = $ligneProposition['nb'];
1375 raphael 382
			} else {
1386 raphael 383
				$proposition_formatee['observation']['nb_commentaires'] = $ligneProposition['nb'];
1375 raphael 384
			}
385
		}
386
 
1386 raphael 387
		return $proposition_formatee;
1375 raphael 388
	}
389
 
390
	/**
1488 raphael 391
	 *	Formater un vote en fonction du fichier de configuration config_votes.ini
392
	 *	@param $votes array()
393
	 * */
1375 raphael 394
	private function formaterVote($vote) {
395
		$retour = array();
396
		foreach ($vote as $param=>$valeur) {
397
			$retour[$this->mappingVotes[$param]] = $valeur;
398
		}
399
		return $retour;
400
	}
401
}