4 * Copyright 2004,2007,2009 $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 along
19 * with PhpWiki; if not, write to the Free Software Foundation, Inc.,
20 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 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: <<RateIt >> just the widget without text
68 * Note: The wikilens theme or any derivate must be enabled, to enable this plugin!
69 * <<RateIt show=top >> text plus widget below
70 * <<RateIt show=ratings >> to show my ratings
71 * TODO: <<RateIt show=buddies >> to show my buddies
72 * <<RateIt show=ratings dimension=1 >>
73 * TODO: <<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
92 function getDescription()
94 return _("Rating system. Store user ratings per page");
97 function RatingWidgetJavascript()
100 if (!empty($this->imgPrefix))
101 $imgPrefix = $this->imgPrefix;
102 elseif (defined("RATEIT_IMGPREFIX"))
103 $imgPrefix = RATEIT_IMGPREFIX; else $imgPrefix = '';
104 if ($imgPrefix and !$WikiTheme->_findData("images/RateIt" . $imgPrefix . "Nk0.png", 1))
106 $img = substr($WikiTheme->_findData("images/RateIt" . $imgPrefix . "Nk0.png"), 0, -7);
107 $urlprefix = WikiURL("", 0, 1); // TODO: check actions USE_PATH_INFO=false
108 $js_globals = "var rateit_imgsrc = '" . $img . "';
109 var rateit_action = '" . urlencode("RateIt") . "';
111 $WikiTheme->addMoreHeaders
113 array('src' => $WikiTheme->_findData('themes/wikilens/wikilens.js'))));
114 return JavaScript($js_globals);
117 function actionImgPath()
120 return $WikiTheme->_findFile("images/RateItAction.png", 1);
124 * Take a string and quote it sufficiently to be passed as a Javascript
127 function _javascript_quote_string($s)
129 return str_replace("'", "\'", $s);
132 function getDefaultArguments()
134 return array('pagename' => '[pagename]',
137 'imgPrefix' => '', // '' or BStar or Star
138 'dimension' => false,
146 { // early side-effects (before body)
149 if (!empty($_already)) return;
151 $WikiTheme->addMoreHeaders(JavaScript(
152 "var prediction = new Array; var rating = new Array;
153 var avg = new Array; var numusers = new Array;
154 var msg_rating_votes = '" . _("Rating: %.1f (%d votes)") . "';
155 var msg_curr_rating = '" . _("Your current rating: ") . "';
156 var msg_curr_prediction = '" . _("Your current prediction: ") . "';
157 var msg_chg_rating = '" . _("Change your rating from ") . "';
158 var msg_to = '" . _(" to ") . "';
159 var msg_add_rating = '" . _("Add your rating: ") . "';
160 var msg_thanks = '" . _("Thanks!") . "';
161 var msg_rating_deleted = '" . _("Rating deleted!") . "';
163 $WikiTheme->addMoreHeaders($this->RatingWidgetJavascript());
166 function displayActionImg($mode)
168 global $WikiTheme, $request;
169 if (!empty($request->_is_buffering_output))
170 ob_end_clean(); // discard any previous output
172 $page = $request->getPage();
173 //$page->set('_cached_html', false);
174 $request->cacheControl('MUST-REVALIDATE');
175 $dbi = $request->getDbh();
177 //fake validators without args
178 $request->appendValidators(array('wikiname' => WIKI_NAME,
179 'args' => wikihash('')));
180 $request->discardOutput();
181 $actionImg = $WikiTheme->_path . $this->actionImgPath();
182 if (file_exists($actionImg)) {
183 header('Content-type: image/png');
184 readfile($actionImg);
186 header('Content-type: image/png');
187 echo base64_decode('iVBORw0KGgoAAAANSUhEUgAAAAIAAAACAQMAAABIeJ9nAAAAA1BMVEX///'
188 . '+nxBvIAAAAAXRSTlMAQObYZgAAABNJREFUeF4NwAEBAAAAgJD+r5YGAAQAAXHhfPAAAAAASUVORK5CYII=');
193 // Only for signed users done in template only yet.
194 function run($dbi, $argstr, &$request, $basepage)
197 //$this->_request = & $request;
198 //$this->_dbi = & $dbi;
199 $user = $request->getUser();
200 //FIXME: fails on test with DumpHtml:RateIt
201 if (!is_object($user)) {
202 return HTML::raw('');
204 $this->userid = $user->getId();
205 if (!$this->userid) {
206 return HTML::raw('');
208 $args = $this->getArgs($argstr, $request);
209 $this->dimension = $args['dimension'];
210 $this->imgPrefix = $args['imgPrefix'];
211 if ($this->dimension == '') {
212 $this->dimension = 0;
213 $args['dimension'] = 0;
215 if ($args['pagename']) {
216 // Expand relative page names.
217 $page = new WikiPageName($args['pagename'], $basepage);
218 $args['pagename'] = $page->name;
220 if (empty($args['pagename'])) {
221 return $this->error(_("no page specified"));
223 $this->pagename = $args['pagename'];
225 $rdbi = RatingsDb::getTheRatingsDb();
226 $this->_rdbi =& $rdbi;
228 if ($args['mode'] === 'add') {
229 //if (!$user->isSignedIn()) return $this->error(_("You must sign in"));
230 $this->rating = $request->getArg('rating');
231 $rdbi->addRating($this->rating, $this->userid, $this->pagename, $this->dimension);
232 $this->displayActionImg('add');
234 } elseif ($args['mode'] === 'delete') {
235 //if (!$user->isSignedIn()) return $this->error(_("You must sign in"));
236 $rdbi->deleteRating($this->userid, $this->pagename, $this->dimension);
237 unset($this->rating);
238 $this->displayActionImg('delete');
239 } elseif (!$args['show']) {
240 return $this->RatingWidgetHtml($args['pagename'], $args['version'], $args['imgPrefix'],
241 $args['dimension'], $args['small']);
243 //if (!$user->isSignedIn()) return $this->error(_("You must sign in"));
245 $this->rating = $rdbi->getRating($this->userid, $this->pagename, $this->dimension);
246 $this->avg = $rdbi->getAvg($this->pagename, $this->dimension);
247 $this->numusers = $rdbi->getNumUsers($this->pagename, $this->dimension);
248 // Update this text on rateit in javascript. needed: NumUsers, Avg
251 HTML::span(array('class' => 'rateit'),
252 sprintf(_("Rating: %.1f (%d votes)"),
253 $this->avg, $this->numusers)));
254 if ($args['show'] == 'top') {
255 if (ENABLE_PAGE_PUBLIC) {
256 $page = $dbi->getPage($this->pagename);
257 if ($page->get('public'))
258 $html->setAttr('class', "public");
260 $html->setAttr('id', "rateit-widget-top");
261 $html->pushContent(HTML::br(),
262 $this->RatingWidgetHtml($args['pagename'], $args['version'],
264 $args['dimension'], $args['small']));
265 } elseif ($args['show'] == 'text') {
266 if (!$WikiTheme->DUMP_MODE)
267 $html->pushContent(HTML::br(),
268 sprintf(_("Your rating was %.1f"),
270 } elseif ($this->rating) {
271 $html->pushContent(HTML::br(),
272 sprintf(_("Your rating was %.1f"),
275 $this->pred = $rdbi->getPrediction($this->userid, $this->pagename, $this->dimension);
276 if (is_string($this->pred))
277 $html->pushContent(HTML::br(),
278 sprintf(_("Prediction: %s"),
281 $html->pushContent(HTML::br(),
282 sprintf(_("Prediction: %.1f"),
285 //$html->pushContent(HTML::p());
286 //$html->pushContent(HTML::em("(Experimental: This might be entirely bogus data)"));
291 // box is used to display a fixed-width, narrow version with common header
292 function box($args = false, $request = false, $basepage = false)
294 if (!$request) $request =& $GLOBALS['request'];
295 if (!$request->_user->isSignedIn()) return;
296 if (!isset($args)) $args = array();
299 foreach ($args as $key => $value)
300 $argstr .= $key . "=" . $value;
301 $widget = $this->run($request->_dbi, $argstr, $request, $basepage);
303 return $this->makeBox(WikiLink(_("RateIt"), '', _("Rate It")),
308 * HTML widget display
310 * This needs to be put in the <body> section of the page.
312 * @param Name $pagename
313 * @param version Version of the page to rate (may be "" for current)
314 * @param Prefix $imgPrefix
315 * @param Id $dimension
316 * @param bool|Makes $small
317 * @return \HtmlElement
318 * @internal param \Name $pagename of the page to rate
319 * @internal param \Prefix $imgPrefix of the names of the images that display the rating
320 * You can have two widgets for the same page displayed at
321 * once iff the imgPrefix-s are different.
322 * @internal param \Id $dimension of the dimension to rate
323 * @internal param \Makes $small a smaller ratings widget if non-false
325 * Limitations: Currently this can only print the current users ratings.
326 * And only the widget, but no value (for buddies) also.
328 function RatingWidgetHtml($pagename, $version, $imgPrefix, $dimension, $small = false)
330 global $WikiTheme, $request;
332 $dbi =& $request->_dbi;
333 $version = $dbi->_backend->get_latest_version($pagename);
334 $pageid = sprintf("%u", crc32($pagename)); // MangleXmlIdentifier($pagename)
335 $imgId = 'RateIt' . $pageid;
336 $actionImgName = 'RateIt' . $pageid . 'Action';
338 //$rdbi =& $this->_rdbi;
339 $rdbi = RatingsDb::getTheRatingsDb();
341 // check if the imgPrefix icons exist.
342 if (!$WikiTheme->_findData("images/RateIt" . $imgPrefix . "Nk0.png", true))
345 // Protect against \'s, though not \r or \n
346 $reImgPrefix = $this->_javascript_quote_string($imgPrefix);
347 $reImgId = $this->_javascript_quote_string($imgId);
348 $reActionImgName = $this->_javascript_quote_string($actionImgName);
349 $rePagename = $this->_javascript_quote_string($pagename);
350 //$dimension = $args['pagename'] . "rat";
352 $html = HTML::span(array("class" => "rateit-widget", "id" => $imgId));
353 for ($i = 0; $i < 2; $i++) {
354 $ok[$i] = $WikiTheme->_findData("images/RateIt" . $imgPrefix . "Ok" . $i . ".png"); // empty
355 $nk[$i] = $WikiTheme->_findData("images/RateIt" . $imgPrefix . "Nk" . $i . ".png"); // rated
356 $rk[$i] = $WikiTheme->_findData("images/RateIt" . $imgPrefix . "Rk" . $i . ".png"); // pred
359 if (empty($this->userid)) {
360 $user = $request->getUser();
361 $this->userid = $user->getId();
363 if (empty($this->rating)) {
364 $this->rating = $rdbi->getRating($this->userid, $pagename, $dimension);
365 if (!$this->rating and empty($this->pred)) {
366 $this->pred = $rdbi->getPrediction($this->userid, $pagename, $dimension);
370 for ($i = 1; $i <= 10; $i++) {
372 $a1 = HTML::a(array('href' => "javascript:clickRating('$reImgPrefix','$rePagename','$version',"
373 . "'$reImgId','$dimension',$j)"));
375 $img_attr['src'] = $nk[$i % 2];
377 $img_attr['src'] = $ok[$i % 2];
378 $img_attr['onmouseover'] = "displayRating('$reImgId','$reImgPrefix',$j,0,1)";
379 $img_attr['onmouseout'] = "displayRating('$reImgId','$reImgPrefix',$this->rating,0,1)";
380 } else if (!$this->rating and $this->pred) {
381 $img_attr['src'] = $rk[$i % 2];
382 $img_attr['onmouseover'] = "displayRating('$reImgId','$reImgPrefix',$j,1,1)";
383 $img_attr['onmouseout'] = "displayRating('$reImgId','$reImgPrefix',$this->pred,1,1)";
385 $img_attr['onmouseover'] = "displayRating('$reImgId','$reImgPrefix',$j,0,1)";
386 $img_attr['onmouseout'] = "displayRating('$reImgId','$reImgPrefix',0,0,1)";
388 //$imgName = 'RateIt'.$reImgId.$i;
389 $img_attr['id'] = $imgId . $i;
390 $img_attr['alt'] = $img_attr['id'];
391 $a1->pushContent(HTML::img($img_attr));
392 //$a1->addToolTip(_("Rate the topic of this page"));
393 $html->pushContent($a1);
395 //This adds a space between the rating smilies:
396 //if (($i%2) == 0) $html->pushContent("\n");
398 $html->pushContent(HTML::Raw(" "));
400 $a0 = HTML::a(array('href' => "javascript:clickRating('$reImgPrefix','$rePagename','$version',"
401 . "'$reImgId','$dimension','X')"));
402 $msg = _("Cancel your rating");
403 $imgprops = array('src' => $WikiTheme->getImageUrl("RateIt" . $imgPrefix . "Cancel"),
404 'id' => $imgId . $imgPrefix . 'Cancel',
408 $imgprops['style'] = 'display:none';
409 $a0->pushContent(HTML::img($imgprops));
410 $a0->addToolTip($msg);
411 $html->pushContent($a0);
414 $msg = _("No opinion");
415 $html->pushContent(HTML::img(array('src' => $WikiTheme->getImageUrl("RateItCancelN"),
416 'id' => $imgPrefix.'Cancel',
418 //$a0->addToolTip($msg);
419 //$html->pushContent($a0);
422 $img_attr['src'] = $WikiTheme->_findData("images/spacer.png");
423 $img_attr['id'] = $actionImgName;
424 $img_attr['alt'] = $img_attr['id'];
425 $img_attr['height'] = 15;
426 $img_attr['width'] = 20;
427 $html->pushContent(HTML::img($img_attr));
429 // Display your current rating if there is one, or the current prediction
430 // or the empty widget.
431 $pred = empty($this->pred) ? 0 : $this->pred;
433 if (!empty($this->avg))
434 $js .= "avg['$reImgId']=$this->avg; numusers['$reImgId']=$this->numusers;\n";
436 $js .= "rating['$reImgId']=$this->rating; prediction['$reImgId']=$pred;\n";
437 $html->pushContent(JavaScript($js
438 . "displayRating('$reImgId','$reImgPrefix',$this->rating,0,1);"));
439 } elseif (!empty($this->pred)) {
440 $js .= "rating['$reImgId']=0; prediction['$reImgId']=$this->pred;\n";
441 $html->pushContent(JavaScript($js
442 . "displayRating('$reImgId','$reImgPrefix',$this->pred,1,1);"));
444 $js .= "rating['$reImgId']=0; prediction['$reImgId']=0;\n";
445 $html->pushContent(JavaScript($js
446 . "displayRating('$reImgId','$reImgPrefix',0,0,1);"));
459 // c-hanging-comment-ender-p: nil
460 // indent-tabs-mode: nil