Action "Boing Boing" version 0.3
Description
Le but de ce script est de donner un aperçu rapide des derniers changements ayant eu lieu sur le Wiki. En cela il est similaire à l'action
DerniersChangements. Cependant il diffère en plusieurs points:
- Il tente d'afficher le plan du site sous forme hiérarchique, à partir de la page d'accueil, en suivant récursivement les liens. En cela il ressemble a PlanDuSite.
- La date de dernière modification n'est pas le seul critère pris en compte. La taille de la page concernée, ainsi que le volume qui a été modifié sont pris en compte.
- Les modifications pour chaque page ont une importance décroissante avec leur âge. Les changements datant de moins de 3 jours sont les plus significatifs, ceux de 2 semaines un peu moins, ceux de 2 mois beaucoup moins, et ceux de plus de 3 mois ne sont pas pris en compte.
- Le rendu est plus visuel, car une barre de couleur bleue s'affiche à côté du nom de chaque page, d'autant plus grosse que la page est grande, et d'autant plus verte que la page a subi des modifications récentes.
- Les pages ne sont affichées qu'une seule fois.
- Seules les pages auxquelles l'utilisateur a accès en lecture apparaissent.
- Les pages ayant un sommaire référencé par l'action 'trail' apparaissent bien rangées sous leur page de sommaire.
Attention: cette action est gourmande en temps machine. Optimisation en cours.
Utilisation
Insérez un appel de ce type dans une page vide
Effet
A coté de chaque page, un barre de couleur apparait. Une petite barre correspond a une petite page, et une grosse barre a une grosse. La partie verte veut dire que la page a reçu récemment des modifications; et une barre entièrement bleue représente une page où il ne se passe rien de neuf.
Installation
Démonstration
http://valentin.deleplace.free.fr/wikini/wakka.php?wiki=BoingBoing03
Code source
<?php
/*
Version 0.3 de l'action "Boing Boing" pour WikiNi
Copyright 2005 Valentin Deleplace
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
3. The name of the author may not be used to endorse or promote products
derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
/*
Explications
Le but de ce script est de donner un aperçu rapide des derniers
changements ayant eu lieu sur le Wiki. En cela il est similaire
à l'action DerniersChangements. Cependant il diffère en plusieurs
points:
-Il tente d'afficher le plan du site sous forme hiérarchique,
à partir de la page d'accueil, en suivant récursivement les
liens. En cela il ressemble a PlanDuSite. Attention, ce n'est
là qu'un "arbre couvrant", en vrai le wiki est un bon gros
graphe cyclique bien touffu.
-La date de dernière modification n'est pas le seul critère
pris en compte. La taille de la page concernée, ainsi que
le volume qui a été modifié sont pris en compte.
-Les modifications pour chaque page ont une importance
décroissante avec leur age. Les changements datant de moins
de 3 jours sont les plus significatifs, ceux de 2 semaines
un peu moins, ceux de 2 mois beaucoup moins, et ceux de plus
de 3 mois ne sont pas pris en compte.
-Le rendu est plus visuel, car un graphisme s'affiche à côté du
nom de chaque page, d'autant plus gros que la page est grande,
et d'autant plus voyant que la page a subi des modifications
recentes. Ce graphisme peut être un gif animé, ou bien une
barre horizontale, etc.
-Les pages ne sont affichées qu'une seule fois
-Les pages ayant un sommaire référencé par l'action 'trail'
apparaissent bien rangées sous leur page de sommaire.
*/
//vérification de sécurité
if (!eregi("wakka.php", $_SERVER['PHP_SELF'])) {
die ("accès direct interdit");
}
$wiki = $this;
/*
Comme toutes les pages du site sont censées être affichées, je charge
en une seule requête toutes les informations "légères" qui peuvent être
utiles, mais pas le contenu textuel de la page.
Cela suppose qu'en MySQL, la requête LENGTH(body) se fait en temps O(1)
et non en temps proportionnel à la longueur de la page.
*/
$req = "SELECT id, tag, time, owner, LENGTH(body) as taille
FROM ".$wiki->config["table_prefix"]."pages
WHERE latest='Y'" ;
$resul = $wiki->LoadAll( $req );
$pages = array();
foreach( $resul as $page ){
$tag = $page["tag"];
$pages[ $tag ] = array( "id" => $page["id"],
"time" => $page["time"],
"owner" => $page["owner"],
"taille" => $page["taille"] );
}
// Chargement de tous les droits en lecture
$username = "";
if( $user = $this->GetUser() ){
$username = $user["name"];
}
$req = "SELECT page_tag, list FROM ".$wiki->config["table_prefix"]."acls WHERE privilege='read'";
$resul = $wiki->LoadAll( $req );
foreach( $resul as $page ){
$tag = $page["page_tag"];
if( $pages[$tag]["owner"] == $username ){
$pages[ $tag ][ "lectureOK" ] = 1;
continue;
}
$liste = $page["list"];
$autorises = explode("\n", $liste);
if( in_array($username, $autorises)
|| in_array("*", $autorises)
|| ($username && in_array("+",$autorises)) ){
$pages[ $tag ][ "lectureOK" ] = 1;
}
}
// Pourquoi la page d'accueil est pas dans la table acls?? mystère
$pages[ $this->config["root_page"] ]["lectureOK"] = 1;
/*
Renvoie de facon brute les lignes de différence entre les deux textes.
Cette fonction sert plutot à comparer 2 versions d'un même texte...
Le résultat n'est pas vraiment destiné à être formaté, mais plutôt
analysé (en dessous il n'est utilisé que pour sa longueur).
*/
function pbdiff($texteA,$texteB,$wiki){
$bodyA = explode("\n", $texteA);
$bodyB = explode("\n", $texteB);
$added = array_diff($bodyA, $bodyB);
$deleted = array_diff($bodyB, $bodyA);
$output = "";
if ($added){
$output .= implode("\n", $added);
}
$output .= "\n";
if ($deleted){
$output .= implode("\n", $deleted);
}
$output .= "\n";
return $output;
}
/*
Renvoie un score répondant à la question: quantifier le volume
de changements que cette page a subi récemment. Pour cela, 4
versions de la même page sont comparées:
- La page dans son état actuel
- La page telle qu'elle était il y a 3 jours
- La page telle qu'elle était il y a 3 semaines
- La page telle qu'elle était il y a 3 mois
Les changements mesurés sont pondérés de façon décroissante
avec l'âge.
*/
function volumeMouvement( $tag ){
global $wiki;
$req0 = "SELECT body FROM ".$wiki->config["table_prefix"]."pages WHERE tag = '".mysql_escape_string($tag)."' AND latest='Y'";
$etat0 = $wiki->LoadAll( $req0 );
$body0 = $etat0[0]["body"];
// ok meme si la requete echoue, car
// $body3jours == ""
// <=> la page n'existait pas encore
// <=> la page etait vide
// <=> la creation est vue comme un ajout de lignes
$req3jours = "SELECT body FROM ".$wiki->config["table_prefix"]."pages WHERE tag = '".mysql_escape_string($tag)."' AND DATE_SUB(CURDATE(),INTERVAL 3 DAY) >= time ORDER BY time DESC LIMIT 1";
$etat3jours = $wiki->LoadAll( $req3jours );
$body3jours = $etat3jours[0]["body"];
// idem que pour $body3jours
$req3semaines = "SELECT body FROM ".$wiki->config["table_prefix"]."pages WHERE tag = '".mysql_escape_string($tag)."' AND DATE_SUB(CURDATE(),INTERVAL 21 DAY) >= time ORDER BY time DESC LIMIT 1";
$etat3semaines = $wiki->LoadAll( $req3semaines );
$body3semaines = $etat3semaines[0]["body"];
// idem que pour $body3jours
$req3mois = "SELECT body FROM ".$wiki->config["table_prefix"]."pages WHERE tag = '".mysql_escape_string($tag)."' AND DATE_SUB(CURDATE(),INTERVAL 91 DAY) >= time ORDER BY time DESC LIMIT 1";
$etat3mois = $wiki->LoadAll( $req3mois );
$body3mois = $etat3mois[0]["body"];
$dif3jours = pbdiff( $body0, $body3jours, $wiki );
$dif3semaines = pbdiff( $body3jours, $body3semaines, $wiki ); // no overlap with $dif3jours
$dif3mois = pbdiff( $body3semaines, $body3mois, $wiki ); // no overlap with $dif3semaines
return strlen($dif3jours)
+ (strlen($dif3semaines) / 4)
+ (strlen($dif3mois) / 16);
}
/*
Renvoie un score "normalise" a partir d'une
grandeur $score quelconque. Cette fonction est
croissante en $score.
Le $coefEtirement permet de ne pas grimper tout
de suite au score 9, il divise simplement le $score.
Pour la taille en octets d'une page wiki par
exemple, un $coefEtirement de 3000 est judicieux.
Si l'argument est positif, le score sera dans [0..9].
Si l'argument est negatif, le score sera dans [-9..0].
L'arctangente permet de borner le score.
*/
function normalisation($score,$coefEtirement){
$atanscore = 20 * atan((1.0*$score)/$coefEtirement) / M_PI;
return intval( $atanscore );
}
// On commence par rattacher toutes les pages qui ont un sommaire, a leur sommaire
$req = "SELECT tag, body
FROM ".$this->config["table_prefix"]."pages
WHERE latest='Y'
AND (MATCH(tag,body) AGAINST('{{trail')
OR MATCH(tag,body) AGAINST('{{pbtrail'))";
$liste = $this->LoadAll( $req );
$sommaires = array();
$itemsSommaires = array();
foreach( $liste as $p ){
if( ereg( "trail +toc=\"([A-Za-z0-9]+)\"", $p["body"], $match )
&& $pages[ $p["tag"] ]["lectureOK"]
&& $pages[ $match[1] ]["lectureOK"] ){
$pages[ $p["tag"] ]["sommaire"] = $match[1];
$itemsSommaire[] = $p["tag"];
if( ! in_array($match[1],$sommaires) ){
$sommaires[] = $match[1];
}
}
}
//print_r( $sommaires );print "<hr/>\n";
//print_r( $itemsSommaire );print "<hr/>\n";
// Cette fonction pourrait afficher une image, ou une barre de couleur, etc.
// Arguments: des scores entre 0 et 9 (cf la fonction normalisation)
// ici elle dessinera une barre de couleur
function dessinerTagVivacite($tag,$scoreTaille, $ratioMvt){
global $wiki;
$pxLargeurBleue = intval( 50 * ($scoreTaille * (1- $ratioMvt) ));
$pxLargeurVerte = intval( 50 * $scoreTaille * $ratioMvt );
echo '<table cellpadding="0" cellspacing="0"><tr>';
echo '<td>' . $wiki->Link($tag) . ' </td>';
echo '<td bgcolor="#0000CC" width="' . (10+$pxLargeurBleue) . '"> </td>';
if( $pxLargeurVerte != 0 ){
echo '<td bgcolor="#00FF00" width="' . $pxLargeurVerte . '"> </td>';
}
echo '</tr></table>';
}
// Ensuite on parcourt recursivement la table wikini_links...
// Attention, c'est un parcours en profondeur d'abord; moins joli qu'en largeur d'abord
function getlinks($from){
global $wiki;
$req = "SELECT to_tag FROM ".$wiki->config["table_prefix"]."links WHERE from_tag='" . mysql_escape_string($from) . "'";
$resultat = $wiki->LoadAll( $req );
$links = array();
foreach( $resultat as $r ){
$links[] = $r["to_tag"];
}
return $links;
}
// NB: on pourrait se passer de l'argument $t (nombre de tabulations)
// mais la on s'en sert pour la couleur
function parcours_liens($tag, $exclus, $en_attente, $pages, $t){
array_push( $exclus, $tag );
global $wiki;
if( $pages[ $tag ]["lectureOK"] ){
$cibles = array_diff( getlinks($tag), $exclus );
$exclus = array_merge( $exclus, $cibles );
// utilisation du ratio mvt/taille
$taille = $pages[ $tag ][ "taille" ];
$scoreTaille = normalisation( $taille, 3000 );
$ratioMvt = (1.0*volumeMouvement( $tag )) / $taille;
dessinerTagVivacite($tag, $scoreTaille, $ratioMvt);
print( '<div style=\'margin-left:20px;\'>' );
foreach( $cibles as $c ){
if( in_array( $c, $en_attente ) ){
$rec = parcours_sommaire($c,$exclus,$en_attente,$pages, $t+1);
}else{
$rec = parcours_liens($c,$exclus,$en_attente,$pages, $t+1);
}
$exclus = $rec["exclus"];
$en_attente = $rec["en_attente"];
}
print( "</div>\n" );
}
return array("exclus"=>$exclus,"en_attente"=>$en_attente);
}
function parcours_sommaire($tagSommaire, $exclus, $en_attente,$pages, $t){
unset( $en_attente[$tagSommaire] );
global $wiki;
$jaune = "#FFFF". dechex(max(0,15-$t)) ."0";
print( '<br /><div style=\'margin-left:3px;background-color:' . $jaune . ';\'>' );
print( "<b>" . $wiki->Link($tagSommaire) . "</b><br/>\n" );
print( '<div style=\'margin-left:20px;\'>' );
foreach( $pages as $c => $page ){
if( $page["sommaire"] == $tagSommaire ){
if( in_array( $c, $en_attente ) ){
$rec = parcours_sommaire($c,$exclus, $en_attente,$pages,$t+1);
}else{
$rec = parcours_liens($c,$exclus,$en_attente,$pages, $t+1);
}
$exclus = $rec["exclus"];
$en_attente = $rec["en_attente"];
}
}
print( "</div></div><br/>\n" );
return array("exclus"=>$exclus,"en_attente"=>$en_attente);
}
parcours_liens( $this->config["root_page"],
$itemsSommaire,
$sommaires,
$pages,
0 );
?>
Ameliorations a apporter
- Ameliorer le rendu graphique
- Trouver un nom moins ridicule
- Rendre utilisable sur des wikinis de plusieurs centaines de pages, sans faire s'ecrouler le processeur. Pour cela, on peut borner le nombre de pages "analysees" en se limitant aux n dernieres pages modifiees, par exemple n=25. Le plan du site resterait visuellement le meme, mais seules ces pages auraient une barre de couleur.
- On peut modifier le code pour que s'affiche des images (éventuellement des gif animés) à la place des barres de couleur.
- Pour une page de sommaire (action trail), on peut faire la somme de la taille des pages qui s'y rattachent. Cela donne la "taille d'une section", pas forcément facile à dessiner ergonomiquement.
- On peut envisager un traitement spécifique pour l'action include. Pour l'instant, elle compte comme une vingtaine de caractères (l'appel de l'action), alors qu'une page entière s'affiche à la place. On pourrait donc compter le nombre de caractères réellement affichés, et non le nombre de caractères du "code wikini".
Si vous pensez que l'action est interessante, laissez votre nom ici, ca me motivera pour travailler dessus :)
- Cette action a l'air intéressante, surtout justement pour de gros wikis et pour les utilisateurs qui ne passent qu'une fois par semaine ou moins (l'ActionRecentChanges ne listant en effet que les x derniers changements, datant souvent de moins d'une semaine comme c'est le cas sur wikini.net). Cependant elle a l'air extrêmement lourde. Sur votre wiki la page met presque 30 secondes à s'afficher [réponse: non c'était la faute aux gifs animés, pas aux requêtes. voir la version 0.2, sans les images, avec les mêmes requêtes non optimisées -- RipouneT]. Sur un serveur où cette MAX_EXECUTION_TIME serait inférieur à 30s votre action ne fonctionnerait pas. Par ailleurs il est totalement inadmissible de générer des pages en autant de temps (avec, j'imagine, la consommation de CPU à 100%...) sur un hébergement mututalisé (cas le plus fréquent pour l'emploi de wikini, voulu assez léger), surtout si ce sont des pages qui sont souvent consultées (comme c'est le cas des DerniersChangements). Je n'ai fait que survoler la source mais je pense que votre problème doit venir du trop grand nombre de requêtes MySQL exécutées et dont le résultat n'est pas mis en cache. Cette action doit donc absolument être optimisée !
- PS.: évitez s'il vous plait d'éditer plusieurs fois la même page en quelques minutes: non seulement cela crée un grand nombre de versions inutilement (ce qui prend de la place) mais en plus cela rend difficile l'utilisation de l'historique pour comparer les versions. Utilisez donc de préférence la prévisualisation avant de sauvegarder votre page ;-) -- LordFarquaad [Un Wikini idéal passerait en mode aperçu automatiquement lors de mise à jour trop rapprochées d'une même page :-D -- DavidDelon - le problème c'est qu'il faudrait que l'aperçu se fasse sur la version déjà sauvée... ce qui serait peut-être bien c'est que si c'est la même personne qui édite, alors ça ne crée pas une nouvelle version mais ça remplace la dernière... -- LordFarquaad]
- L'action est très interessante, mais comme le dit LordFarquaad, à l'air consommatrice de ressource, il y aurait peut-etre moyen d'optimiser les choses non ? Encore bravo en tout cas, c'est une idée originale, ou est-ce inspiré de quelque chose d'existant ? -- DavidDelon
- Je pars seulement du constat qu'on lit beaucoup plus volontiers une page "vivante" qu'une page "morte", surtout sur les sites que l'on connait déjà. Je ne me suis pas inspiré d'un système existant. --RipouneT
Passé le problème de la consommation (que l'on doit en effet pouvoir améliorer), l'idée est en effet très intéressante et innovante. En revanche, je trouve le système des boules sautantes un peu pénible et pas forcément très lisible. Je verrai plutôt une barre horizontale en face de chaque page, de longueur proportionnelle à la taille des modifs et de densité de rouge proportionnelle à la distance des modifications dans le temps. --
CharlesNepote
- Ah oui ça serait pas mal en effet, surtout qu'on devrait pouvoir faire ça avec un simple bloc html à couleurs variable (ce qui laisse beaucoup plus de nuances possibles) -- LordFarquaad
Bon, puisque vous m'y encouragez... c'est parti j'optimise tout ça :)
Au fait, vous aurez bien un avis sur la question: qu'est-ce qui est plus rapide entre 1 requête qui renvoie 100 lignes, ou bien 3 requêtes qui renvoient 10 lignes chacune? plus généralement, comment faites-vous pour optimiser une action trop gourmande? Comment feriez-vous à ma place pour mettre en cache toutes les informations recueillies? --
RipouneT
- Je dirais que le plus rapide est une requête pour 100 lignes. En fait temps d'exécution d'une requête ne dépend que très peu du nombre de résultats trouvés. Cela dépend principalement du temps de communication entre php et MySQL (ou tout autre SGBD) ainsi que de la complexité de la requête (et donc son optimisation). En général je préfère faire une requête complexe que plusieurs simples (et un principe de base: ne jamais faire de requêtes à l'intérieur d'une boucle ou d'une fonction récursive) -- LordFarquaad
- Bon, la version 0.3 est déjà beaucoup plus rapide, déjà parce qu'elle ne charge pas d'images, ensuite parce que certaines requêtes ont été factorisées. Il reste bien encore 4 requêtes par page affichée (appels dans la fonction récursive de parcours d'arbre), cette factorisation-là sera pour la version 0.4, mais il me faudra me casser la tête avec des LEFT JOIN ou des OUTER JOIN ou je ne sais quoi pour que ce soit possible... -- RipouneT
Je croix que l'action
BoingBoing est utile pour vérifier où ça bouge dans le Wiki.
L'idée serait d'attribuer:
Les XXX pages les plus actives: {{boingboing afficher="20"}}
Les pages à forte activitée: {{boingboing activité="8"}} sur une échelle de niveau de 1 à 10
ça donne {{boingboing afficher="20" activité="8"}} pour afficher les 20 premieres page ayant un niveau d'activité superieur ou égal à 8.
L'action sera trés pratique. --
SloYvY
Je viens de mettre en place cette action sur
Mon site. C'est une idée qui me parait vraiment intérressante J'ai eu quelques petits soucis pour la mise en place :
- J'ai du rajouter des "if (!function_exists("nom de la fonction "))" devant chaque fonction pour ne pas avoir d'erreur fonction déjà déclarée.
- Il y a une faute de frappe dans la déclaration de la variable "itemsSommaires"
- rajouté un test division par zero avant la ligne "$ratioMvt = (1.0*volumeMouvement( $tag )) / $taille;"
En plus des paramètrages sur le nombres d'actions à afficher le plus gros travail semble être au niveau des requettes SQL (le nombre est impréssionant) et de l'affichage graphique qui n'est pas top !
En tout cas bravo pour le travail. C'est tres prometteur --
GoubS