4 Copyright 2004,2007 $ThePhpWikiProgrammingTeam
6 This file is part of PhpWiki.
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.
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.
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
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.
28 * There should be two methods to store ratings:
29 * In a SQL database as in wikilens http://dickens.cs.umn.edu/dfrankow/wikilens
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
36 * wikilens plans several user-centered applications like:
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
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.
54 * For a simple rating system one can also store the rating in the page
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
64 * - Autoclass: simple public domain C library
65 * - MLC++: C++ library http://www.sgi.com/tech/mlc/
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
75 * @author: Dan Frankowski (wikilens author), Reini Urban (as plugin)
78 * - finish mysuggest.c (external engine with data from mysql)
81 require_once("lib/WikiPlugin.php");
82 require_once("lib/wikilens/RatingsDb.php");
84 class WikiPlugin_RateIt
90 function getDescription() {
91 return _("Rating system. Store user ratings per page");
93 function getVersion() {
94 return preg_replace("/[Revision: $]/", '',
95 "\$Revision: 1.25 $");
98 function RatingWidgetJavascript() {
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))
107 $img = substr($WikiTheme->_findData("images/RateIt".$imgPrefix."Nk0.png"),0,-7);
108 $urlprefix = WikiURL("",0,1); // TODO: check actions USE_PATH_INFO=false
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];
117 if (init) { // re-initialize titles
118 title = '"._("Your current rating: ")."'+curr_rating+' '+ratings[curr_rating*2];
119 var linebreak = '. '; //
 or 
 within IE only;
121 title = '"._("The current prediction: ")."'+ curr_pred+' '+ratings[curr_pred*2];
124 for (i=1; i<=10; i++) {
125 var imgName = imgId + i;
126 var imgSrc = '".$img."';
128 if (curr_rating) document[cancel].style.display = 'inline';
129 document[imgName].title = title;
131 if (ratingvalue > 0) {
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) };
137 if (curr_rating != ratingvalue) {
138 document[imgName].title = '"._("Change your rating from ").
139 "'+curr_rating+' '+ratings[curr_rating*2]+' "._("to")." '+j+' '+ratings[i];
142 document[imgName].onmouseout = function() { displayRating(imgId,imgPrefix,0,0,0) };
143 document[imgName].title = '"._("Add your rating: ")."'+j+' '+ratings[i];
149 document[imgName].title = title+linebreak+'"._("Add your rating: ")."'+ratings[i];
151 } else if (i<=(ratingvalue*2)) {
154 document[imgName].src = imgSrc + imgPrefix + imgType + ((i%2) ? 'k1' : 'k0') + '.png';
157 function click(imgPrefix,pagename,version,imgId,dimension,newrating) {
158 var actionImg = imgId+'Action';
159 if (newrating == 'X') {
160 deleteRating(actionImg,pagename,dimension);
162 displayRating(imgId,imgPrefix,0,0,1);
164 submitRating(actionImg,pagename,version,dimension,newrating);
165 rating[imgId] = newrating;
166 displayRating(imgId,imgPrefix,newrating,0,1);
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;
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;
186 return JavaScript($js);
189 function actionImgPath() {
191 return $WikiTheme->_findFile("images/RateItAction.png");
195 * Take a string and quote it sufficiently to be passed as a Javascript
198 function _javascript_quote_string($s) {
199 return str_replace("'", "\'", $s);
202 function getDefaultArguments() {
203 return array( 'pagename' => '[pagename]',
206 'imgPrefix' => '', // '' or BStar or Star
207 'dimension' => false,
214 function head() { // early side-effects (before body)
216 $WikiTheme->addMoreHeaders(JavaScript("var prediction = new Array;\nvar rating = new Array;"));
217 $WikiTheme->addMoreHeaders($this->RatingWidgetJavascript());
221 function displayActionImg ($mode) {
222 global $WikiTheme, $request;
223 if (!empty($request->_is_buffering_output))
224 ob_end_clean(); // discard any previous output
226 $page = $request->getPage();
227 //$page->set('_cached_html', false);
228 $request->cacheControl('MUST-REVALIDATE');
229 $dbi = $request->getDbh();
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);
240 header('Content-type: image/png');
244 IHDR
\ 1 \ 1\b\ 6 \1f\15Ä
\89 \14IDATx^
\ 5À
\ 1 Â0Ã
\1fzçX
\ 1\ 2ÿ7-
\12` IEND®B`
\82';
249 // Only for signed users done in template only yet.
250 function run($dbi, $argstr, &$request, $basepage) {
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)) return HTML();
257 $this->userid = $user->getId();
258 if (!$this->userid) return HTML();
259 $args = $this->getArgs($argstr, $request);
260 $this->dimension = $args['dimension'];
261 $this->imgPrefix = $args['imgPrefix'];
262 if ($this->dimension == '') {
263 $this->dimension = 0;
264 $args['dimension'] = 0;
266 if ($args['pagename']) {
267 // Expand relative page names.
268 $page = new WikiPageName($args['pagename'], $basepage);
269 $args['pagename'] = $page->name;
271 if (empty($args['pagename'])) {
272 return $this->error(_("no page specified"));
274 $this->pagename = $args['pagename'];
276 $rdbi = RatingsDb::getTheRatingsDb();
277 $this->_rdbi =& $rdbi;
279 if ($args['mode'] === 'add') {
280 //if (!$user->isSignedIn()) return $this->error(_("You must sign in"));
281 $this->rating = $request->getArg('rating');
282 $rdbi->addRating($this->rating, $this->userid, $this->pagename, $this->dimension);
283 $this->displayActionImg('add');
285 } elseif ($args['mode'] === 'delete') {
286 //if (!$user->isSignedIn()) return $this->error(_("You must sign in"));
287 $rdbi->deleteRating($this->userid, $this->pagename, $this->dimension);
288 unset($this->rating);
289 $this->displayActionImg('delete');
290 } elseif (! $args['show'] ) {
291 return $this->RatingWidgetHtml($args['pagename'], $args['version'], $args['imgPrefix'],
292 $args['dimension'], $args['small']);
294 //if (!$user->isSignedIn()) return $this->error(_("You must sign in"));
296 $this->rating = $rdbi->getRating($this->userid, $this->pagename, $this->dimension);
299 HTML::span(array('class' => 'rateit'),
300 sprintf(_("Rating: %.1f (%d votes)"),
301 $rdbi->getAvg($this->pagename, $this->dimension),
302 $rdbi->getNumUsers($this->pagename, $this->dimension))));
303 if ($args['show'] == 'top') {
304 $html->setAttr('id', "rateit-widget-top");
305 $html->pushContent(HTML::br(),
306 $this->RatingWidgetHtml($args['pagename'], $args['version'],
308 $args['dimension'], $args['small']));
309 } elseif ($args['show'] == 'text') {
310 if (!$WikiTheme->DUMP_MODE)
311 $html->pushContent(HTML::br(),
312 sprintf(_("Your rating was %.1f"),
314 } elseif ($this->rating) {
315 $html->pushContent(HTML::br(),
316 sprintf(_("Your rating was %.1f"),
319 $this->pred = $rdbi->getPrediction($this->userid, $this->pagename, $this->dimension);
320 if (is_string($this->pred))
321 $html->pushContent(HTML::br(),
322 sprintf(_("Prediction: %s"),
325 $html->pushContent(HTML::br(),
326 sprintf(_("Prediction: %.1f"),
329 //$html->pushContent(HTML::p());
330 //$html->pushContent(HTML::em("(Experimental: This might be entirely bogus data)"));
335 // box is used to display a fixed-width, narrow version with common header
336 function box($args=false, $request=false, $basepage=false) {
337 if (!$request) $request =& $GLOBALS['request'];
338 if (!$request->_user->isSignedIn()) return;
339 if (!isset($args)) $args = array();
342 foreach ($args as $key => $value)
343 $argstr .= $key."=".$value;
344 $widget = $this->run($request->_dbi, $argstr, $request, $basepage);
346 return $this->makeBox(WikiLink(_("RateIt"),'',_("Rate It")),
351 * HTML widget display
353 * This needs to be put in the <body> section of the page.
355 * @param pagename Name of the page to rate
356 * @param version Version of the page to rate (may be "" for current)
357 * @param imgPrefix Prefix of the names of the images that display the rating
358 * You can have two widgets for the same page displayed at
359 * once iff the imgPrefix-s are different.
360 * @param dimension Id of the dimension to rate
361 * @param small Makes a smaller ratings widget if non-false
363 * Limitations: Currently this can only print the current users ratings.
364 * And only the widget, but no value (for buddies) also.
366 function RatingWidgetHtml($pagename, $version, $imgPrefix, $dimension, $small = false) {
367 global $WikiTheme, $request;
369 $dbi =& $request->_dbi;
370 $version = $dbi->_backend->get_latest_version($pagename);
371 $pageid = sprintf("%u",crc32($pagename)); // MangleXmlIdentifier($pagename)
372 $imgId = 'RateIt' . $pageid;
373 $actionImgName = 'RateIt'.$pageid.'Action';
375 //$rdbi =& $this->_rdbi;
376 $rdbi = RatingsDb::getTheRatingsDb();
378 // check if the imgPrefix icons exist.
379 if (! $WikiTheme->_findData("images/RateIt".$imgPrefix."Nk0.png", true))
382 // Protect against \'s, though not \r or \n
383 $reImgPrefix = $this->_javascript_quote_string($imgPrefix);
384 $reImgId = $this->_javascript_quote_string($imgId);
385 $reActionImgName = $this->_javascript_quote_string($actionImgName);
386 $rePagename = $this->_javascript_quote_string($pagename);
387 //$dimension = $args['pagename'] . "rat";
389 $html = HTML::span(array("class" => "rateit-widget", "id" => $imgId));
390 for ($i=0; $i < 2; $i++) {
391 $ok[$i] = $WikiTheme->_findData("images/RateIt".$imgPrefix."Ok".$i.".png"); // empty
392 $nk[$i] = $WikiTheme->_findData("images/RateIt".$imgPrefix."Nk".$i.".png"); // rated
393 $rk[$i] = $WikiTheme->_findData("images/RateIt".$imgPrefix."Rk".$i.".png"); // pred
396 if (empty($this->userid)) {
397 $user = $request->getUser();
398 $this->userid = $user->getId();
400 if (empty($this->rating)) {
401 $this->rating = $rdbi->getRating($this->userid, $pagename, $dimension);
402 if (!$this->rating and empty($this->pred)) {
403 $this->pred = $rdbi->getPrediction($this->userid, $pagename, $dimension);
406 for ($i = 1; $i <= 10; $i++) {
408 $a1 = HTML::a(array('href' => "javascript:click('$reImgPrefix','$rePagename','$version',"
409 ."'$reImgId','$dimension','$j')"));
411 $img_attr['src'] = $nk[$i%2];
413 $img_attr['src'] = $ok[$i%2];
414 $img_attr['onMouseOver'] = "displayRating('$reImgId','$reImgPrefix',$j,0,1)";
415 $img_attr['onMouseOut'] = "displayRating('$reImgId','$reImgPrefix',$this->rating,0,1)";
417 else if (!$this->rating and $this->pred) {
418 $img_attr['src'] = $rk[$i%2];
419 $img_attr['onMouseOver'] = "displayRating('$reImgId','$reImgPrefix',$j,1,1)";
420 $img_attr['onMouseOut'] = "displayRating('$reImgId','$reImgPrefix',$this->pred,1,1)";
423 $img_attr['onMouseOver'] = "displayRating('$reImgId','$reImgPrefix',$j,0,1)";
424 $img_attr['onMouseOut'] = "displayRating('$reImgId','$reImgPrefix',0,0,1)";
426 //$imgName = 'RateIt'.$reImgId.$i;
427 $img_attr['name'] = $imgId . $i;
428 $img_attr['alt'] = $img_attr['name'];
429 $img_attr['border'] = 0;
430 $a1->pushContent(HTML::img($img_attr));
431 //$a1->addToolTip(_("Rate the topic of this page"));
432 $html->pushContent($a1);
434 //This adds a space between the rating smilies:
435 //if (($i%2) == 0) $html->pushContent("\n");
437 $html->pushContent(HTML::Raw(" "));
439 $a0 = HTML::a(array('href' => "javascript:click('$reImgPrefix','$rePagename','$version',"
440 ."'$reImgId','$dimension','X')"));
442 $msg = _("Cancel your rating");
443 $imgprops = array('src' => $WikiTheme->getImageUrl("RateIt".$imgPrefix."Cancel"),
444 'name' => $imgId.$imgPrefix.'Cancel',
449 $imgprops['style'] = 'display:none';
450 $a0->pushContent(HTML::img($imgprops));
451 $a0->addToolTip($msg);
452 $html->pushContent($a0);
455 $msg = _("No opinion");
456 $html->pushContent(HTML::img(array('src' => $WikiTheme->getImageUrl("RateItCancelN"),
457 'name'=> $imgPrefix.'Cancel',
459 //$a0->addToolTip($msg);
460 //$html->pushContent($a0);
463 $img_attr['src'] = $WikiTheme->_findData("images/spacer.png");
464 $img_attr['name'] = $actionImgName;
465 $img_attr['alt'] = $img_attr['name'];
466 $img_attr['border'] = 0;
467 $img_attr['height'] = 15;
468 $img_attr['width'] = 20;
469 $html->pushContent(HTML::img($img_attr));
470 // Display your current rating if there is one, or the current prediction or the empty widget
472 $html->pushContent(JavaScript("prediction['$reImgId']=0; rating['$reImgId']=$this->rating;\n"
473 ."displayRating('$reImgId','$reImgPrefix',$this->rating,0,1);"));
474 } elseif (!empty($this->pred)) {
475 $html->pushContent(JavaScript("prediction['$reImgId']=$this->pred; rating['$reImgId']=0;\n"
476 ."displayRating('$reImgId','$reImgPrefix',$this->pred,1,1);"));
478 $html->pushContent(JavaScript("prediction['$reImgId']=0; rating['$reImgId']=0;\n"
479 ."displayRating('$reImgId','$reImgPrefix',0,0,1);"));
487 // $Log: not supported by cvs2svn $
488 // Revision 1.24 2008/03/17 19:14:43 rurban
489 // add mode=text: just text, no widget, for dumps
491 // Revision 1.23 2008/01/24 19:19:35 rurban
492 // support multiple plugins per page (e.g. in plugin lists), added show=top, fixed RATING_STORAGE=WIKIPAGE, fixes smart caching
494 // Revision 1.22 2007/02/17 14:15:14 rurban
495 // noborder for the recycle bin
497 // Revision 1.21 2007/01/22 23:50:48 rurban
498 // Do not display if not signed in
500 // Revision 1.20 2006/03/04 13:57:28 rurban
501 // rename hash for php-5.1
503 // Revision 1.19 2004/11/15 16:00:01 rurban
504 // enable RateIt imgPrefix: '' or 'Star' or 'BStar',
505 // enable blue prediction icons,
506 // enable buddy predictions.
508 // Revision 1.18 2004/11/01 10:43:59 rurban
509 // seperate PassUser methods into seperate dir (memory usage)
510 // fix WikiUser (old) overlarge data session
511 // remove wikidb arg from various page class methods, use global ->_dbi instead
514 // Revision 1.17 2004/08/05 17:31:52 rurban
515 // more xhtml conformance fixes
517 // Revision 1.16 2004/08/05 17:23:54 rurban
518 // add alt tag for xhtml conformance
520 // Revision 1.15 2004/07/09 12:50:50 rurban
521 // references are declared, not enforced
523 // Revision 1.14 2004/07/08 20:30:07 rurban
524 // plugin->run consistency: request as reference, added basepage.
525 // encountered strange bug in AllPages (and the test) which destroys ->_dbi
527 // Revision 1.12 2004/06/30 19:59:07 dfrankow
528 // Make changes suitable so that wikilens theme (and wikilens.org) work properly.
529 // + Remove predictions (for now)
530 // + Use new RatingsDb singleton.
531 // + Change RatingWidgetHtml() to use parameters like a normal PHP function
532 // so we can have PHP check that we are passing the right # of them.
533 // + Change RatingWidgetHtml() to be callable static-ally
534 // (without a plugin object)
535 // + Remove the "RateIt" button for now, because we don't use it on wikilens.org.
536 // Maybe if someone wants the button, there can be an arg or flag for it.
537 // + Always show the cancel button, because UI widgets should not hide.
538 // + Remove the "No opinion" button for now, because we don't yet store that.
539 // This is a useful thing, tho, for the future.
541 // Revision 1.11 2004/06/19 10:22:41 rurban
542 // outcomment the pear specific methods to let all pages load
544 // Revision 1.10 2004/06/18 14:42:17 rurban
545 // added wikilens libs (not yet merged good enough, some work for DanFr)
547 // Revision 1.9 2004/06/14 11:31:39 rurban
548 // renamed global $Theme to $WikiTheme (gforge nameclash)
549 // inherit PageList default options from PageList
550 // default sortby=pagename
551 // use options in PageList_Selectable (limit, sortby, ...)
552 // added action revert, with button at action=diff
553 // added option regex to WikiAdminSearchReplace
555 // Revision 1.8 2004/06/01 15:28:01 rurban
556 // AdminUser only ADMIN_USER not member of Administrators
557 // some RateIt improvements by dfrankow
558 // edit_toolbar buttons
560 // Revision _1.2 2004/04/29 17:55:03 dfrankow
561 // Check in escape() changes to protect against leading spaces in pagename.
562 // This is untested with Reini's _("RateIt") additions to this plugin.
564 // Revision 1.7 2004/04/21 04:29:50 rurban
565 // write WikiURL consistently (not WikiUrl)
567 // Revision 1.6 2004/04/12 14:07:12 rurban
570 // Revision 1.5 2004/04/11 10:42:02 rurban
571 // pgsrc/CreatePagePlugin
573 // Revision 1.4 2004/04/06 20:00:11 rurban
574 // Cleanup of special PageList column types
575 // Added support of plugin and theme specific Pagelist Types
576 // Added support for theme specific UserPreferences
577 // Added session support for ip-based throttling
578 // sql table schema change: ALTER TABLE session ADD sess_ip CHAR(15);
579 // Enhanced postgres schema
580 // Added DB_Session_dba support
582 // Revision 1.3 2004/04/01 06:29:51 rurban
584 // RateIt also for ADODB
586 // Revision 1.2 2004/03/31 06:22:22 rurban
587 // shorter javascript,
588 // added prediction buttons and display logic,
589 // empty HTML if not signed in.
590 // fixed deleting (empty dimension => 0)
592 // Revision 1.1 2004/03/30 02:38:06 rurban
593 // RateIt support (currently no recommendation engine yet)
601 // c-hanging-comment-ender-p: nil
602 // indent-tabs-mode: nil