2 rcs_id('$Id: RateIt.php,v 1.22 2007-02-17 14:15:14 rurban Exp $');
4 Copyright 2004 $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 ?> to enable rating on this page
68 * Note: The wikilens theme must be enabled, to enable this plugin!
69 * Or use a sidebar based theme with the box method.
70 * <?plugin RateIt show=ratings ?> to show my ratings
71 * <?plugin RateIt show=buddies ?> to show my buddies
72 * <?plugin RateIt show=ratings dimension=1 ?>
74 * @author: Dan Frankowski (wikilens author), Reini Urban (as plugin)
77 * - fix RATING_STORAGE = WIKIPAGE
79 * - finish mysuggest.c (external engine with data from mysql)
82 require_once("lib/WikiPlugin.php");
83 require_once("lib/wikilens/RatingsDb.php");
85 class WikiPlugin_RateIt
91 function getDescription() {
92 return _("Rating system. Store user ratings per page");
94 function getVersion() {
95 return preg_replace("/[Revision: $]/", '',
96 "\$Revision: 1.22 $");
99 function RatingWidgetJavascript() {
101 if (!empty($this->imgPrefix))
102 $imgPrefix = $this->imgPrefix;
103 elseif (defined("RATEIT_IMGPREFIX"))
104 $imgPrefix = RATEIT_IMGPREFIX;
105 else $imgPrefix = '';
106 if ($imgPrefix and !$WikiTheme->_findData("images/RateIt".$imgPrefix."Nk0.png",1))
108 $img = substr($WikiTheme->_findData("images/RateIt".$imgPrefix."Nk0.png"),0,-7);
109 $urlprefix = WikiURL("",0,1); // TODO: check actions USE_PATH_INFO=false
111 function displayRating(imgPrefix, ratingvalue, pred) {
112 var cancel = imgPrefix + 'Cancel';
113 for (i=1; i<=10; i++) {
114 var imgName = imgPrefix + i;
115 var imgSrc = '".$img."';
116 document[imgName].title = '"._("Your rating ")."'+ratingvalue;
120 } else if (i<=(ratingvalue*2)) {
123 document[imgName].src = imgSrc + imgType + ((i%2) ? 'k1' : 'k0') + '.png';
125 //document[cancel].src = imgSrc + 'Cancel.png';
127 function click(actionImg, pagename, version, imgPrefix, dimension, rating) {
129 deleteRating(actionImg, pagename, dimension);
130 displayRating(imgPrefix, 0, 0);
132 submitRating(actionImg, pagename, version, dimension, rating);
133 displayRating(imgPrefix, rating, 0);
136 function submitRating(actionImg, page, version, dimension, rating) {
137 var myRand = Math.round(Math.random()*(1000000));
138 var imgSrc = '".$urlprefix."' + escape(page) + '?version=' + version + '&action=".urlencode(_("RateIt"))."&mode=add&rating=' + rating + '&dimension=' + dimension + '&nopurge=1&rand=' + myRand"
139 .(!empty($_GET['start_debug']) ? "+'&start_debug=1'" : '').";
140 ".(DEBUG & _DEBUG_REMOTE ? '' : '//')."alert('submitRating(\"'+actionImg+'\", \"'+page+'\", '+version+', '+dimension+', '+rating+') => '+imgSrc);
141 document[actionImg].src = imgSrc;
143 function deleteRating(actionImg, page, dimension) {
144 var myRand = Math.round(Math.random()*(1000000));
145 var imgSrc = '".$urlprefix."' + escape(page) + '?action=".urlencode(_("RateIt"))."&mode=delete&dimension=' + dimension + '&nopurge=1&rand=' + myRand"
146 .(!empty($_GET['start_debug']) ? "+'&start_debug=1'" : '').";
147 ".(DEBUG & _DEBUG_REMOTE ? '' : '//')."alert('deleteRating(\"'+actionImg+'\", \"'+page+'\", '+version+', '+dimension+')');
148 document[actionImg].src = imgSrc;
151 return JavaScript($js);
154 function actionImgPath() {
156 return $WikiTheme->_findFile("images/RateItAction.png");
160 * Take a string and quote it sufficiently to be passed as a Javascript
163 function _javascript_quote_string($s) {
164 return str_replace("'", "\'", $s);
167 function getDefaultArguments() {
168 return array( 'pagename' => '[pagename]',
171 'imgPrefix' => '', // '' or BStar or Star
172 'dimension' => false,
179 function head() { // early side-effects (before body)
181 $WikiTheme->addMoreHeaders($this->RatingWidgetJavascript());
184 // Only for signed users done in template only yet.
185 function run($dbi, $argstr, &$request, $basepage) {
187 //$this->_request = & $request;
188 //$this->_dbi = & $dbi;
189 $user = $request->getUser();
190 //FIXME: fails on test with DumpHtml:RateIt
191 if (!is_object($user)) return HTML();
192 $this->userid = $user->getId();
193 if (!$this->userid) return HTML();
194 $args = $this->getArgs($argstr, $request);
195 $this->dimension = $args['dimension'];
196 $this->imgPrefix = $args['imgPrefix'];
197 if ($this->dimension == '') {
198 $this->dimension = 0;
199 $args['dimension'] = 0;
201 if ($args['pagename']) {
202 // Expand relative page names.
203 $page = new WikiPageName($args['pagename'], $basepage);
204 $args['pagename'] = $page->name;
206 if (empty($args['pagename'])) {
207 return $this->error(_("no page specified"));
209 $this->pagename = $args['pagename'];
211 $rdbi = RatingsDb::getTheRatingsDb();
212 $this->_rdbi =& $rdbi;
214 if ($args['mode'] === 'add') {
215 //if (!$user->isSignedIn()) return $this->error(_("You must sign in"));
216 $actionImg = $WikiTheme->_path . $this->actionImgPath();
217 $rdbi->addRating($request->getArg('rating'), $this->userid, $this->pagename, $this->dimension);
219 if (!empty($request->_is_buffering_output))
220 ob_end_clean(); // discard any previous output
222 $page = $request->getPage();
223 //$page->set('_cached_html', false);
224 $request->cacheControl('MUST-REVALIDATE');
226 //fake validators without args
227 $request->appendValidators(array('wikiname' => WIKI_NAME,
228 'args' => wikihash('')));
229 header('Content-type: image/png');
230 readfile($actionImg);
232 } elseif ($args['mode'] === 'delete') {
233 //if (!$user->isSignedIn()) return $this->error(_("You must sign in"));
234 $actionImg = $WikiTheme->_path . $this->actionImgPath();
235 $rdbi->deleteRating($this->userid, $this->pagename, $this->dimension);
236 if (!empty($request->_is_buffering_output))
237 ob_end_clean(); // discard any previous output
239 $page = $request->getPage();
240 //$page->set('_cached_html', false);
241 $request->cacheControl('MUST-REVALIDATE');
243 //fake validators without args
244 $request->appendValidators(array('wikiname' => WIKI_NAME,
245 'args' => wikihash('')));
246 header('Content-type: image/png');
247 readfile($actionImg);
249 } elseif (! $args['show'] ) {
250 return $this->RatingWidgetHtml($args['pagename'], $args['version'], $args['imgPrefix'],
251 $args['dimension'], $args['small']);
253 //if (!$user->isSignedIn()) return $this->error(_("You must sign in"));
255 $rating = $rdbi->getRating();
256 $html = HTML::p($this->pagename.": ".
257 sprintf(_("Rated by %d users | Average rating %.1f stars"),
258 $rdbi->getNumUsers($this->pagename, $this->dimension),
259 $rdbi->getAvg($this->pagename, $this->dimension)),
262 $html->pushContent(sprintf(_("Your rating was %.1f"),
265 $pred = $rdbi->getPrediction($this->userid, $this->pagename, $this->dimension);
266 if (is_string($pred))
267 $html->pushContent(sprintf(_("%s prediction for you is %s stars"),
270 $html->pushContent(sprintf(_("%s prediction for you is %.1f stars"),
273 //$html->pushContent(HTML::p());
274 //$html->pushContent(HTML::em("(Experimental: This might be entirely bogus data)"));
279 // box is used to display a fixed-width, narrow version with common header
280 function box($args=false, $request=false, $basepage=false) {
281 if (!$request) $request =& $GLOBALS['request'];
282 if (!$request->_user->isSignedIn()) return;
283 if (!isset($args)) $args = array();
286 foreach ($args as $key => $value)
287 $argstr .= $key."=".$value;
288 $widget = $this->run($request->_dbi, $argstr, $request, $basepage);
290 return $this->makeBox(WikiLink(_("RateIt"),'',_("Rate It")),
295 * HTML widget display
297 * This needs to be put in the <body> section of the page.
299 * @param pagename Name of the page to rate
300 * @param version Version of the page to rate (may be "" for current)
301 * @param imgPrefix Prefix of the names of the images that display the rating
302 * You can have two widgets for the same page displayed at
303 * once iff the imgPrefix-s are different.
304 * @param dimension Id of the dimension to rate
305 * @param small Makes a smaller ratings widget if non-false
307 * Limitations: Currently this can only print the current users ratings.
308 * And only the widget, but no value (for buddies) also.
310 function RatingWidgetHtml($pagename, $version, $imgPrefix, $dimension, $small = false) {
311 global $WikiTheme, $request;
313 $imgId = MangleXmlIdentifier($pagename) . $imgPrefix;
314 $actionImgName = $imgId . 'RateItAction';
315 $dbi =& $GLOBALS['request']->_dbi;
316 $version = $dbi->_backend->get_latest_version($pagename);
318 //$rdbi =& $this->_rdbi;
319 $rdbi = RatingsDb::getTheRatingsDb();
321 // check if the imgPrefix icons exist.
322 if (! $WikiTheme->_findData("images/RateIt".$imgPrefix."Nk0.png", true))
325 // Protect against 's, though not \r or \n
326 $reImgPrefix = $this->_javascript_quote_string($imgPrefix);
327 $reActionImgName = $this->_javascript_quote_string($actionImgName);
328 $rePagename = $this->_javascript_quote_string($pagename);
329 //$dimension = $args['pagename'] . "rat";
331 $html = HTML::span(array("id" => $imgId));
332 for ($i=0; $i < 2; $i++) {
333 $nk[$i] = $WikiTheme->_findData("images/RateIt".$imgPrefix."Nk".$i.".png");
334 $none[$i] = $WikiTheme->_findData("images/RateIt".$imgPrefix."Rk".$i.".png");
337 $user = $request->getUser();
338 $userid = $user->getId();
339 //if (!isset($args['rating']))
340 $rating = $rdbi->getRating($userid, $pagename, $dimension);
342 $pred = $rdbi->getPrediction($userid, $pagename, $dimension);
344 for ($i = 1; $i <= 10; $i++) {
345 $a1 = HTML::a(array('href' => 'javascript:click(\'' . $reActionImgName . '\',\'' .
346 $rePagename . '\',\'' . $version . '\',\'' .
347 $reImgPrefix . '\',\'' . $dimension . '\',' . ($i/2) . ')'));
349 $img_attr['src'] = $nk[$i%2];
350 //if (!$rating and !$pred)
351 // $img_attr['src'] = $none[$i%2];
353 $img_attr['name'] = $imgPrefix . $i;
354 $img_attr['alt'] = $img_attr['name'];
355 $img_attr['border'] = 0;
356 $a1->pushContent(HTML::img($img_attr));
357 $a1->addToolTip(_("Rate the topic of this page"));
358 $html->pushContent($a1);
360 //This adds a space between the rating smilies:
361 // if (($i%2) == 0) $html->pushContent(' ');
363 $html->pushContent(HTML::Raw(' '));
365 $a0 = HTML::a(array('href' => 'javascript:click(\'' . $reActionImgName . '\',\'' .
366 $rePagename . '\',\'' . $version . '\',\'' . $reImgPrefix .
367 '\',\'' . $dimension . '\',\'X\')'));
369 $msg = _("Cancel rating");
370 $a0->pushContent(HTML::img(array('src' => $WikiTheme->getImageUrl("RateIt".$imgPrefix."Cancel"),
371 'name'=> $imgPrefix.'Cancel',
374 $a0->addToolTip($msg);
375 $html->pushContent($a0);
377 $msg = _("No opinion");
378 $html->pushContent(HTML::img(array('src' => $WikiTheme->getImageUrl("RateItCancelN"),
379 'name'=> $imgPrefix.'Cancel',
381 //$a0->addToolTip($msg);
382 //$html->pushContent($a0);
385 $img_attr['src'] = $WikiTheme->_findData("images/RateItAction.png");
386 $img_attr['name'] = $actionImgName;
387 $img_attr['alt'] = $img_attr['name'];
388 //$img_attr['class'] = 'k' . $i;
389 $img_attr['border'] = 0;
390 $html->pushContent(HTML::img($img_attr));
391 // Display the current rating if there is one
393 $html->pushContent(JavaScript('displayRating(\'' . $reImgPrefix . '\','.$rating .',0)'));
395 $html->pushContent(JavaScript('displayRating(\'' . $reImgPrefix . '\','.$pred .',1)'));
397 $html->pushContent(JavaScript('displayRating(\'' . $reImgPrefix . '\',0,0)'));
404 // $Log: not supported by cvs2svn $
405 // Revision 1.21 2007/01/22 23:50:48 rurban
406 // Do not diplay if not signed in
408 // Revision 1.20 2006/03/04 13:57:28 rurban
409 // rename hash for php-5.1
411 // Revision 1.19 2004/11/15 16:00:01 rurban
412 // enable RateIt imgPrefix: '' or 'Star' or 'BStar',
413 // enable blue prediction icons,
414 // enable buddy predictions.
416 // Revision 1.18 2004/11/01 10:43:59 rurban
417 // seperate PassUser methods into seperate dir (memory usage)
418 // fix WikiUser (old) overlarge data session
419 // remove wikidb arg from various page class methods, use global ->_dbi instead
422 // Revision 1.17 2004/08/05 17:31:52 rurban
423 // more xhtml conformance fixes
425 // Revision 1.16 2004/08/05 17:23:54 rurban
426 // add alt tag for xhtml conformance
428 // Revision 1.15 2004/07/09 12:50:50 rurban
429 // references are declared, not enforced
431 // Revision 1.14 2004/07/08 20:30:07 rurban
432 // plugin->run consistency: request as reference, added basepage.
433 // encountered strange bug in AllPages (and the test) which destroys ->_dbi
435 // Revision 1.12 2004/06/30 19:59:07 dfrankow
436 // Make changes suitable so that wikilens theme (and wikilens.org) work properly.
437 // + Remove predictions (for now)
438 // + Use new RatingsDb singleton.
439 // + Change RatingWidgetHtml() to use parameters like a normal PHP function
440 // so we can have PHP check that we're passing the right # of them.
441 // + Change RatingWidgetHtml() to be callable static-ally
442 // (without a plugin object)
443 // + Remove the "RateIt" button for now, because we don't use it on wikilens.org.
444 // Maybe if someone wants the button, there can be an arg or flag for it.
445 // + Always show the cancel button, because UI widgets should not hide.
446 // + Remove the "No opinion" button for now, because we don't yet store that.
447 // This is a useful thing, tho, for the future.
449 // Revision 1.11 2004/06/19 10:22:41 rurban
450 // outcomment the pear specific methods to let all pages load
452 // Revision 1.10 2004/06/18 14:42:17 rurban
453 // added wikilens libs (not yet merged good enough, some work for DanFr)
455 // Revision 1.9 2004/06/14 11:31:39 rurban
456 // renamed global $Theme to $WikiTheme (gforge nameclash)
457 // inherit PageList default options from PageList
458 // default sortby=pagename
459 // use options in PageList_Selectable (limit, sortby, ...)
460 // added action revert, with button at action=diff
461 // added option regex to WikiAdminSearchReplace
463 // Revision 1.8 2004/06/01 15:28:01 rurban
464 // AdminUser only ADMIN_USER not member of Administrators
465 // some RateIt improvements by dfrankow
466 // edit_toolbar buttons
468 // Revision _1.2 2004/04/29 17:55:03 dfrankow
469 // Check in escape() changes to protect against leading spaces in pagename.
470 // This is untested with Reini's _("RateIt") additions to this plugin.
472 // Revision 1.7 2004/04/21 04:29:50 rurban
473 // write WikiURL consistently (not WikiUrl)
475 // Revision 1.6 2004/04/12 14:07:12 rurban
478 // Revision 1.5 2004/04/11 10:42:02 rurban
479 // pgsrc/CreatePagePlugin
481 // Revision 1.4 2004/04/06 20:00:11 rurban
482 // Cleanup of special PageList column types
483 // Added support of plugin and theme specific Pagelist Types
484 // Added support for theme specific UserPreferences
485 // Added session support for ip-based throttling
486 // sql table schema change: ALTER TABLE session ADD sess_ip CHAR(15);
487 // Enhanced postgres schema
488 // Added DB_Session_dba support
490 // Revision 1.3 2004/04/01 06:29:51 rurban
492 // RateIt also for ADODB
494 // Revision 1.2 2004/03/31 06:22:22 rurban
495 // shorter javascript,
496 // added prediction buttons and display logic,
497 // empty HTML if not signed in.
498 // fixed deleting (empty dimension => 0)
500 // Revision 1.1 2004/03/30 02:38:06 rurban
501 // RateIt support (currently no recommendation engine yet)
509 // c-hanging-comment-ender-p: nil
510 // indent-tabs-mode: nil