]> CyberLeo.Net >> Repos - SourceForge/phpwiki.git/blob - lib/plugin/RateIt.php
Remove also empty span tags.
[SourceForge/phpwiki.git] / lib / plugin / RateIt.php
1 <?php // -*-php-*-
2 rcs_id('$Id$');
3 /*
4  Copyright 2004,2007 $ThePhpWikiProgrammingTeam
5
6  This file is part of PhpWiki.
7
8  PhpWiki is free software; you can redistribute it and/or modify
9  it under the terms of the GNU General Public License as published by
10  the Free Software Foundation; either version 2 of the License, or
11  (at your option) any later version.
12
13  PhpWiki is distributed in the hope that it will be useful,
14  but WITHOUT ANY WARRANTY; without even the implied warranty of
15  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16  GNU General Public License for more details.
17
18  You should have received a copy of the GNU General Public License
19  along with PhpWiki; if not, write to the Free Software
20  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
21  */
22
23 /**
24  * RateIt: A recommender system, based on MovieLens and suggest.
25  * Store user ratings per pagename. The wikilens theme displays a navbar image bar
26  * with some nice javascript magic and this plugin shows various recommendations.
27  *
28  * There should be two methods to store ratings:
29  * In a SQL database as in wikilens http://dickens.cs.umn.edu/dfrankow/wikilens
30  *
31  * The most important fact: A page has more than one rating. There can
32  * be (and will be!) many ratings per page (ratee): different raters
33  * (users), in different dimensions. Are those stored per page
34  * (ratee)? Then what if I wish to access the ratings per rater
35  * (user)? 
36  * wikilens plans several user-centered applications like:
37  * a) show my ratings
38  * b) show my buddies' ratings
39  * c) show how my ratings are like my buddies'
40  * d) show where I agree/disagree with my buddy
41  * e) show what this group of people agree/disagree on
42  *
43  * If the ratings are stored in a real DB in a table, we can index the
44  * ratings by rater and ratee, and be confident in
45  * performance. Currently MovieLens has 80,000 users, 7,000 items,
46  * 10,000,000 ratings. This is an average of 1400 ratings/page if each
47  * page were rated equally. However, they're not: the most popular
48  * things have tens of thousands of ratings (e.g., "Pulp Fiction" has
49  * 42,000 ratings). If ratings are stored per page, you would have to
50  * save/read huge page metadata every time someone submits a
51  * rating. Finally, the movie domain has an unusually small number of
52  * items-- I'd expect a lot more in music, for example.
53  *
54  * For a simple rating system one can also store the rating in the page 
55  * metadata (default).
56  *
57  * Recommender Engines:
58  * Recommendation/Prediction is a special field of "Data Mining"
59  * For a list of (also free) software see 
60  *  http://www.the-data-mine.com/bin/view/Software/WebIndex
61  * - movielens: (Java Server) will be gpl'd in summer 2004 (weighted)
62  * - suggest: is free for non-commercial use, available as compiled library
63  *     (non-weighted)
64  * - Autoclass: simple public domain C library
65  * - MLC++: C++ library http://www.sgi.com/tech/mlc/
66  *
67  * Usage:    <?plugin RateIt ?>          just the widget without text
68  *   Note: The wikilens theme or any derivate must be enabled, to enable this plugin!
69  *           <?plugin RateIt show=top ?> text plus widget below
70  *           <?plugin RateIt show=ratings ?> to show my ratings
71  *   TODO:   <?plugin RateIt show=buddies ?> to show my buddies
72  *           <?plugin RateIt show=ratings dimension=1 ?>
73  *   TODO:   <?plugin RateIt show=text ?> just text, no widget, for dumps
74  *
75  * @author:  Dan Frankowski (wikilens author), Reini Urban (as plugin)
76  *
77  * TODO: 
78  * - finish mysuggest.c (external engine with data from mysql)
79  */
80
81 require_once("lib/WikiPlugin.php");
82 require_once("lib/wikilens/RatingsDb.php");
83
84 class WikiPlugin_RateIt
85 extends WikiPlugin
86 {
87     function getName() {
88         return _("RateIt");
89     }
90     function getDescription() {
91         return _("Rating system. Store user ratings per page");
92     }
93     function getVersion() {
94         return preg_replace("/[Revision: $]/", '',
95                             "\$Revision$");
96     }
97
98     function RatingWidgetJavascript() {
99         global $WikiTheme;
100         if (!empty($this->imgPrefix))
101             $imgPrefix = $this->imgPrefix;
102         elseif (defined("RATEIT_IMGPREFIX"))
103             $imgPrefix = RATEIT_IMGPREFIX;
104         else $imgPrefix = '';
105         if ($imgPrefix and !$WikiTheme->_findData("images/RateIt".$imgPrefix."Nk0.png",1))
106             $imgPrefix = '';
107         $img   = substr($WikiTheme->_findData("images/RateIt".$imgPrefix."Nk0.png"),0,-7);
108         $urlprefix = WikiURL("",0,1); // TODO: check actions USE_PATH_INFO=false
109         $js = "
110 function displayRating(imgId, imgPrefix, ratingvalue, pred, init) {
111   var ratings = new Array('Not Rated','Awful','Very Poor','Poor','Below Average',
112                           'Average','Above Average','Good','Very Good','Excellent','Outstanding');
113   var cancel = imgId + imgPrefix + 'Cancel';
114   var curr_rating = rating[imgId];
115   var curr_pred = prediction[imgId];
116   var title;
117   if (init) { // re-initialize titles
118     title = '"._("Your current rating: ")."'+curr_rating+' '+ratings[curr_rating*2];
119     var linebreak = '. '; //&#xD or &#13 within IE only;
120     if (pred) {
121       title = '"._("The current prediction: ")."'+ curr_pred+' '+ratings[curr_pred*2];
122     }
123   }
124   for (i=1; i<=10; i++) {
125     var imgName = imgId + i;
126     var imgSrc = '".$img."';
127     if (init) {
128       if (curr_rating) document[cancel].style.display = 'inline';
129       document[imgName].title = title;
130       var j = i/2;
131       if (ratingvalue > 0) {
132         if (curr_rating) {
133           document[imgName].onmouseout = function() { displayRating(imgId,imgPrefix,curr_rating,0,0) };
134         } else if (curr_pred) {
135           document[imgName].onmouseout = function() { displayRating(imgId,imgPrefix,curr_pred,1,0) };
136         }
137         if (curr_rating != ratingvalue) {
138           document[imgName].title = '"._("Change your rating from ").
139             "'+curr_rating+' '+ratings[curr_rating*2]+' "._("to")." '+j+' '+ratings[i];
140         } 
141       } else {
142         document[imgName].onmouseout = function() { displayRating(imgId,imgPrefix,0,0,0) };
143         document[imgName].title = '"._("Add your rating: ")."'+j+' '+ratings[i];
144       }
145     }
146     var imgType = 'N';
147     if (pred) {
148       if (init)
149         document[imgName].title = title+linebreak+'"._("Add your rating: ")."'+ratings[i];
150       imgType = 'R';
151     } else if (i<=(ratingvalue*2)) {
152       imgType = 'O';
153     }
154     document[imgName].src = imgSrc + imgPrefix + imgType + ((i%2) ? 'k1' : 'k0') + '.png';
155   }
156 }
157 function click(imgPrefix,pagename,version,imgId,dimension,newrating) {
158   var actionImg = imgId+'Action';
159   if (newrating == 'X') {
160     deleteRating(actionImg,pagename,dimension);
161     rating[imgId] = 0;
162     displayRating(imgId,imgPrefix,0,0,1);
163   } else {
164     submitRating(actionImg,pagename,version,dimension,newrating);
165     rating[imgId] = newrating;
166     displayRating(imgId,imgPrefix,newrating,0,1);
167   }
168 }
169 function submitRating(actionImg,page,version,dimension,newrating) {
170   var myRand = Math.round(Math.random()*(1000000));
171   var imgSrc = '".$urlprefix."' + escape(page) + '?version=' + version + '&action=".urlencode(_("RateIt"))."&mode=add&rating=' + newrating + '&dimension=' + dimension + '&nocache=1&nopurge=1&rand=' + myRand"
172         .(!empty($_GET['start_debug']) ? "+'&start_debug=1'" : '').";
173   ".(DEBUG & _DEBUG_REMOTE ? '' : '//')."alert('submitRating(\"'+actionImg+'\", \"'+page+'\", '+version+', '+dimension+', '+newrating+') => '+imgSrc);
174   document[actionImg].title = 'Thanks!';
175   document[actionImg].src = imgSrc;
176 }
177 function deleteRating(actionImg, page, dimension) {
178   var myRand = Math.round(Math.random()*(1000000));
179   var imgSrc = '".$urlprefix."' + escape(page) + '?action=".urlencode(_("RateIt"))."&mode=delete&dimension=' + dimension + '&nocache=1&nopurge=1&rand=' + myRand"
180         .(!empty($_GET['start_debug']) ? "+'&start_debug=1'" : '').";
181   ".(DEBUG & _DEBUG_REMOTE ? '' : '//')."alert('deleteRating(\"'+actionImg+'\", \"'+page+'\", '+version+', '+dimension+')');
182   document[actionImg].title = 'Rating deleted!';
183   document[actionImg].src = imgSrc;
184 }
185 ";
186         return JavaScript($js);
187     }
188
189     function actionImgPath() {
190         global $WikiTheme;
191         return $WikiTheme->_findFile("images/RateItAction.png");
192     }
193
194     /**
195      * Take a string and quote it sufficiently to be passed as a Javascript
196      * string between ''s
197      */
198     function _javascript_quote_string($s) {
199         return str_replace("'", "\'", $s);
200     }
201
202     function getDefaultArguments() {
203         return array( 'pagename'  => '[pagename]',
204                       'version'   => false,
205                       'id'        => 'rateit',
206                       'imgPrefix' => '',      // '' or BStar or Star
207                       'dimension' => false,
208                       'small'     => false,
209                       'show'      => false,
210                       'mode'      => false,
211                       );
212     }
213
214     function head() { // early side-effects (before body)
215         global $WikiTheme;
216         $WikiTheme->addMoreHeaders(JavaScript("var prediction = new Array;\nvar rating = new Array;"));
217         $WikiTheme->addMoreHeaders($this->RatingWidgetJavascript());
218         
219     }
220
221     function displayActionImg ($mode) {
222         global $WikiTheme, $request;
223         if (!empty($request->_is_buffering_output))
224             ob_end_clean();  // discard any previous output
225         // delete the cache
226         $page = $request->getPage();
227         //$page->set('_cached_html', false);
228         $request->cacheControl('MUST-REVALIDATE');
229         $dbi = $request->getDbh();
230         $dbi->touch();
231         //fake validators without args
232         $request->appendValidators(array('wikiname' => WIKI_NAME,
233                                              'args'     => wikihash('')));
234         $request->discardOutput();
235         $actionImg = $WikiTheme->_path . $this->actionImgPath();
236         if (file_exists($actionImg)) {
237             header('Content-type: image/png');
238             readfile($actionImg);
239         } else {
240             header('Content-type: image/png');
241             echo '\89PNG
242 \1a
243  
244 IHDR   \ 1   \ 1\b\ 6   \1f\15Ä\89   \14IDATx^\ 5À\ 1         Ã‚0í\1fzçX      \ 1\ 2ÿ7-\12`    IEND®B`\82';
245         }
246         exit;
247     }
248
249     // Only for signed users done in template only yet.
250     function run($dbi, $argstr, &$request, $basepage) {
251         global $WikiTheme;
252         //$this->_request = & $request;
253         //$this->_dbi = & $dbi;
254         $user = $request->getUser();
255         //FIXME: fails on test with DumpHtml:RateIt
256         if (!is_object($user)) {
257             return HTML::raw('');
258         }
259         $this->userid = $user->getId();
260         if (!$this->userid) {
261             return HTML::raw('');
262         }
263         $args = $this->getArgs($argstr, $request);
264         $this->dimension = $args['dimension'];
265         $this->imgPrefix = $args['imgPrefix'];
266         if ($this->dimension == '') {
267             $this->dimension = 0;
268             $args['dimension'] = 0;
269         }
270         if ($args['pagename']) {
271             // Expand relative page names.
272             $page = new WikiPageName($args['pagename'], $basepage);
273             $args['pagename'] = $page->name;
274         }
275         if (empty($args['pagename'])) {
276             return $this->error(_("no page specified"));
277         }
278         $this->pagename = $args['pagename'];
279
280         $rdbi = RatingsDb::getTheRatingsDb();
281         $this->_rdbi =& $rdbi;
282
283         if ($args['mode'] === 'add') {
284             //if (!$user->isSignedIn()) return $this->error(_("You must sign in"));
285             $this->rating = $request->getArg('rating');
286             $rdbi->addRating($this->rating, $this->userid, $this->pagename, $this->dimension);
287             $this->displayActionImg('add');
288
289         } elseif ($args['mode'] === 'delete') {
290             //if (!$user->isSignedIn()) return $this->error(_("You must sign in"));
291             $rdbi->deleteRating($this->userid, $this->pagename, $this->dimension);
292             unset($this->rating);
293             $this->displayActionImg('delete');
294         } elseif (! $args['show'] ) {
295             return $this->RatingWidgetHtml($args['pagename'], $args['version'], $args['imgPrefix'], 
296                                            $args['dimension'], $args['small']);
297         } else {
298             //if (!$user->isSignedIn()) return $this->error(_("You must sign in"));
299             //extract($args);
300             $this->rating = $rdbi->getRating($this->userid, $this->pagename, $this->dimension);
301             $html = HTML::div
302                 (
303                  HTML::span(array('class' => 'rateit'),
304                             sprintf(_("Rating: %.1f (%d votes)"),
305                                     $rdbi->getAvg($this->pagename, $this->dimension),
306                                     $rdbi->getNumUsers($this->pagename, $this->dimension))));
307             if ($args['show'] == 'top') {
308                 $html->setAttr('id', "rateit-widget-top");
309                 $html->pushContent(HTML::br(),
310                                    $this->RatingWidgetHtml($args['pagename'], $args['version'], 
311                                                            $args['imgPrefix'], 
312                                                            $args['dimension'], $args['small']));
313             } elseif ($args['show'] == 'text') {
314                 if (!$WikiTheme->DUMP_MODE)
315                     $html->pushContent(HTML::br(),
316                                        sprintf(_("Your rating was %.1f"),
317                                                $this->rating));
318             } elseif ($this->rating) {
319                 $html->pushContent(HTML::br(),
320                                    sprintf(_("Your rating was %.1f"),
321                                            $this->rating));
322             } else {
323                 $this->pred = $rdbi->getPrediction($this->userid, $this->pagename, $this->dimension);
324                 if (is_string($this->pred))
325                     $html->pushContent(HTML::br(),
326                                        sprintf(_("Prediction: %s"),
327                                                $this->pred));
328                 elseif ($this->pred)
329                     $html->pushContent(HTML::br(),
330                                        sprintf(_("Prediction: %.1f"),
331                                                $this->pred));
332             }
333             //$html->pushContent(HTML::p());
334             //$html->pushContent(HTML::em("(Experimental: This might be entirely bogus data)"));
335             return $html;
336         }
337     }
338
339     // box is used to display a fixed-width, narrow version with common header
340     function box($args=false, $request=false, $basepage=false) {
341         if (!$request) $request =& $GLOBALS['request'];
342         if (!$request->_user->isSignedIn()) return;
343         if (!isset($args)) $args = array();
344         $args['small'] = 1;
345         $argstr = '';
346         foreach ($args as $key => $value)
347             $argstr .= $key."=".$value;
348         $widget = $this->run($request->_dbi, $argstr, $request, $basepage);
349
350         return $this->makeBox(WikiLink(_("RateIt"),'',_("Rate It")),
351                               $widget);
352     }
353
354     /**
355      * HTML widget display
356      *
357      * This needs to be put in the <body> section of the page.
358      *
359      * @param pagename    Name of the page to rate
360      * @param version     Version of the page to rate (may be "" for current)
361      * @param imgPrefix   Prefix of the names of the images that display the rating
362      *                    You can have two widgets for the same page displayed at
363      *                    once iff the imgPrefix-s are different.
364      * @param dimension   Id of the dimension to rate
365      * @param small       Makes a smaller ratings widget if non-false
366      *
367      * Limitations: Currently this can only print the current users ratings.
368      *              And only the widget, but no value (for buddies) also.
369      */
370     function RatingWidgetHtml($pagename, $version, $imgPrefix, $dimension, $small = false) {
371         global $WikiTheme, $request;
372
373         $dbi =& $request->_dbi;
374         $version = $dbi->_backend->get_latest_version($pagename);
375         $pageid = sprintf("%u",crc32($pagename)); // MangleXmlIdentifier($pagename)
376         $imgId = 'RateIt' . $pageid;
377         $actionImgName = 'RateIt'.$pageid.'Action';
378        
379         //$rdbi =& $this->_rdbi;
380         $rdbi = RatingsDb::getTheRatingsDb();
381         
382         // check if the imgPrefix icons exist.
383         if (! $WikiTheme->_findData("images/RateIt".$imgPrefix."Nk0.png", true))
384             $imgPrefix = '';
385         
386         // Protect against \'s, though not \r or \n
387         $reImgPrefix = $this->_javascript_quote_string($imgPrefix);
388         $reImgId     = $this->_javascript_quote_string($imgId);
389         $reActionImgName = $this->_javascript_quote_string($actionImgName);
390         $rePagename      = $this->_javascript_quote_string($pagename);
391         //$dimension = $args['pagename'] . "rat";
392     
393         $html = HTML::span(array("class" => "rateit-widget", "id" => $imgId));
394         for ($i=0; $i < 2; $i++) {
395             $ok[$i] = $WikiTheme->_findData("images/RateIt".$imgPrefix."Ok".$i.".png"); // empty
396             $nk[$i] = $WikiTheme->_findData("images/RateIt".$imgPrefix."Nk".$i.".png"); // rated
397             $rk[$i] = $WikiTheme->_findData("images/RateIt".$imgPrefix."Rk".$i.".png"); // pred
398         }
399
400         if (empty($this->userid)) {
401             $user = $request->getUser();
402             $this->userid = $user->getId();
403         }
404         if (empty($this->rating)) {
405             $this->rating = $rdbi->getRating($this->userid, $pagename, $dimension);
406             if (!$this->rating and empty($this->pred)) {
407                 $this->pred = $rdbi->getPrediction($this->userid, $pagename, $dimension);
408             }
409         }
410         for ($i = 1; $i <= 10; $i++) {
411             $j = $i / 2;
412             $a1 = HTML::a(array('href' => "javascript:click('$reImgPrefix','$rePagename','$version',"
413                                 ."'$reImgId','$dimension','$j')"));
414             $img_attr = array();
415             $img_attr['src'] = $nk[$i%2];
416             if ($this->rating) {
417                 $img_attr['src'] = $ok[$i%2];
418                 $img_attr['onMouseOver'] = "displayRating('$reImgId','$reImgPrefix',$j,0,1)";
419                 $img_attr['onMouseOut']  = "displayRating('$reImgId','$reImgPrefix',$this->rating,0,1)";
420             }
421             else if (!$this->rating and $this->pred) {
422                 $img_attr['src'] = $rk[$i%2];
423                 $img_attr['onMouseOver'] = "displayRating('$reImgId','$reImgPrefix',$j,1,1)";
424                 $img_attr['onMouseOut']  = "displayRating('$reImgId','$reImgPrefix',$this->pred,1,1)";
425             }
426             else {
427                 $img_attr['onMouseOver'] = "displayRating('$reImgId','$reImgPrefix',$j,0,1)";
428                 $img_attr['onMouseOut']  = "displayRating('$reImgId','$reImgPrefix',0,0,1)";
429             }
430             //$imgName = 'RateIt'.$reImgId.$i;
431             $img_attr['name'] = $imgId . $i;
432             $img_attr['alt'] = $img_attr['name'];
433             $img_attr['border'] = 0;
434             $a1->pushContent(HTML::img($img_attr));
435             //$a1->addToolTip(_("Rate the topic of this page"));
436             $html->pushContent($a1);
437             
438             //This adds a space between the rating smilies:
439             //if (($i%2) == 0) $html->pushContent("\n");
440         }
441         $html->pushContent(HTML::Raw("&nbsp;"));
442        
443         $a0 = HTML::a(array('href' => "javascript:click('$reImgPrefix','$rePagename','$version',"
444                             ."'$reImgId','$dimension','X')"));
445
446         $msg = _("Cancel your rating");
447         $imgprops = array('src'   => $WikiTheme->getImageUrl("RateIt".$imgPrefix."Cancel"),
448                           'name'  => $imgId.$imgPrefix.'Cancel',
449                           'border'=> 0,
450                           'alt'   => $msg,
451                           'title' => $msg);
452         if (!$this->rating)
453             $imgprops['style'] = 'display:none';
454         $a0->pushContent(HTML::img($imgprops));
455         $a0->addToolTip($msg);
456         $html->pushContent($a0);
457         
458         /*} elseif ($pred) {
459             $msg = _("No opinion");
460             $html->pushContent(HTML::img(array('src' => $WikiTheme->getImageUrl("RateItCancelN"),
461                                                'name'=> $imgPrefix.'Cancel',
462                                                'alt' => $msg)));
463             //$a0->addToolTip($msg);
464             //$html->pushContent($a0);
465         }*/
466         $img_attr = array();
467         $img_attr['src'] = $WikiTheme->_findData("images/spacer.png");
468         $img_attr['name'] = $actionImgName;
469         $img_attr['alt'] = $img_attr['name'];
470         $img_attr['border'] = 0;
471         $img_attr['height'] = 15;
472         $img_attr['width'] = 20;
473         $html->pushContent(HTML::img($img_attr));
474         // Display your current rating if there is one, or the current prediction or the empty widget
475         if ($this->rating) {
476             $html->pushContent(JavaScript("prediction['$reImgId']=0; rating['$reImgId']=$this->rating;\n"
477                 ."displayRating('$reImgId','$reImgPrefix',$this->rating,0,1);"));
478         } elseif (!empty($this->pred)) {
479             $html->pushContent(JavaScript("prediction['$reImgId']=$this->pred; rating['$reImgId']=0;\n"
480                 ."displayRating('$reImgId','$reImgPrefix',$this->pred,1,1);"));
481         } else {
482             $html->pushContent(JavaScript("prediction['$reImgId']=0; rating['$reImgId']=0;\n"
483                 ."displayRating('$reImgId','$reImgPrefix',0,0,1);"));
484         }
485         return $html;
486     }
487
488 };
489
490 // For emacs users
491 // Local Variables:
492 // mode: php
493 // tab-width: 8
494 // c-basic-offset: 4
495 // c-hanging-comment-ender-p: nil
496 // indent-tabs-mode: nil
497 // End:
498 ?>