]> CyberLeo.Net >> Repos - SourceForge/phpwiki.git/blob - lib/plugin/RateIt.php
noborder for the recycle bin
[SourceForge/phpwiki.git] / lib / plugin / RateIt.php
1 <?php // -*-php-*-
2 rcs_id('$Id: RateIt.php,v 1.22 2007-02-17 14:15:14 rurban Exp $');
3 /*
4  Copyright 2004 $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 ?>              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 ?>
73  *
74  * @author:  Dan Frankowski (wikilens author), Reini Urban (as plugin)
75  *
76  * TODO: 
77  * - fix RATING_STORAGE = WIKIPAGE
78  * - fix smart caching
79  * - finish mysuggest.c (external engine with data from mysql)
80  */
81
82 require_once("lib/WikiPlugin.php");
83 require_once("lib/wikilens/RatingsDb.php");
84
85 class WikiPlugin_RateIt
86 extends WikiPlugin
87 {
88     function getName() {
89         return _("RateIt");
90     }
91     function getDescription() {
92         return _("Rating system. Store user ratings per page");
93     }
94     function getVersion() {
95         return preg_replace("/[Revision: $]/", '',
96                             "\$Revision: 1.22 $");
97     }
98
99     function RatingWidgetJavascript() {
100         global $WikiTheme;
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))
107             $imgPrefix = '';
108         $img   = substr($WikiTheme->_findData("images/RateIt".$imgPrefix."Nk0.png"),0,-7);
109         $urlprefix = WikiURL("",0,1); // TODO: check actions USE_PATH_INFO=false
110         $js = "
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;
117     var imgType = 'N';
118     if (pred) {
119         imgType = 'R';
120     } else if (i<=(ratingvalue*2)) {
121         imgType = 'O';
122     }
123     document[imgName].src = imgSrc + imgType + ((i%2) ? 'k1' : 'k0') + '.png';
124   }
125   //document[cancel].src = imgSrc + 'Cancel.png';
126 }
127 function click(actionImg, pagename, version, imgPrefix, dimension, rating) {
128   if (rating == 'X') {
129     deleteRating(actionImg, pagename, dimension);
130     displayRating(imgPrefix, 0, 0);
131   } else {
132     submitRating(actionImg, pagename, version, dimension, rating);
133     displayRating(imgPrefix, rating, 0);
134   }
135 }
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;
142 }
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;
149 }
150 ";
151         return JavaScript($js);
152     }
153
154     function actionImgPath() {
155         global $WikiTheme;
156         return $WikiTheme->_findFile("images/RateItAction.png");
157     }
158
159     /**
160      * Take a string and quote it sufficiently to be passed as a Javascript
161      * string between ''s
162      */
163     function _javascript_quote_string($s) {
164         return str_replace("'", "\'", $s);
165     }
166
167     function getDefaultArguments() {
168         return array( 'pagename'  => '[pagename]',
169                       'version'   => false,
170                       'id'        => 'rateit',
171                       'imgPrefix' => '',      // '' or BStar or Star
172                       'dimension' => false,
173                       'small'     => false,
174                       'show'      => false,
175                       'mode'      => false,
176                       );
177     }
178
179     function head() { // early side-effects (before body)
180         global $WikiTheme;
181         $WikiTheme->addMoreHeaders($this->RatingWidgetJavascript());
182     }
183
184     // Only for signed users done in template only yet.
185     function run($dbi, $argstr, &$request, $basepage) {
186         global $WikiTheme;
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;
200         }
201         if ($args['pagename']) {
202             // Expand relative page names.
203             $page = new WikiPageName($args['pagename'], $basepage);
204             $args['pagename'] = $page->name;
205         }
206         if (empty($args['pagename'])) {
207             return $this->error(_("no page specified"));
208         }
209         $this->pagename = $args['pagename'];
210
211         $rdbi = RatingsDb::getTheRatingsDb();
212         $this->_rdbi =& $rdbi;
213
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);
218
219             if (!empty($request->_is_buffering_output))
220                 ob_end_clean();  // discard any previous output
221             // delete the cache
222             $page = $request->getPage();
223             //$page->set('_cached_html', false);
224             $request->cacheControl('MUST-REVALIDATE');
225             $dbi->touch();
226             //fake validators without args
227             $request->appendValidators(array('wikiname' => WIKI_NAME,
228                                              'args'     => wikihash('')));
229             header('Content-type: image/png');
230             readfile($actionImg);
231             exit();
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
238             // delete the cache
239             $page = $request->getPage();
240             //$page->set('_cached_html', false);
241             $request->cacheControl('MUST-REVALIDATE');
242             $dbi->touch();
243             //fake validators without args
244             $request->appendValidators(array('wikiname' => WIKI_NAME,
245                                              'args'     => wikihash('')));
246             header('Content-type: image/png');
247             readfile($actionImg);
248             exit();
249         } elseif (! $args['show'] ) {
250             return $this->RatingWidgetHtml($args['pagename'], $args['version'], $args['imgPrefix'], 
251                                            $args['dimension'], $args['small']);
252         } else {
253             //if (!$user->isSignedIn()) return $this->error(_("You must sign in"));
254             //extract($args);
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)),
260                             HTML::br());
261             if ($rating) {
262                 $html->pushContent(sprintf(_("Your rating was %.1f"),
263                                            $rating));
264             } else {
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"),
268                                                WIKI_NAME, $pred));
269                 elseif ($pred)
270                     $html->pushContent(sprintf(_("%s prediction for you is %.1f stars"),
271                                                WIKI_NAME, $pred));
272             }
273             //$html->pushContent(HTML::p());
274             //$html->pushContent(HTML::em("(Experimental: This might be entirely bogus data)"));
275             return $html;
276         }
277     }
278
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();
284         $args['small'] = 1;
285         $argstr = '';
286         foreach ($args as $key => $value)
287             $argstr .= $key."=".$value;
288         $widget = $this->run($request->_dbi, $argstr, $request, $basepage);
289
290         return $this->makeBox(WikiLink(_("RateIt"),'',_("Rate It")),
291                               $widget);
292     }
293
294     /**
295      * HTML widget display
296      *
297      * This needs to be put in the <body> section of the page.
298      *
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
306      *
307      * Limitations: Currently this can only print the current users ratings.
308      *              And only the widget, but no value (for buddies) also.
309      */
310     function RatingWidgetHtml($pagename, $version, $imgPrefix, $dimension, $small = false) {
311         global $WikiTheme, $request;
312
313         $imgId = MangleXmlIdentifier($pagename) . $imgPrefix;
314         $actionImgName = $imgId . 'RateItAction';
315         $dbi =& $GLOBALS['request']->_dbi;
316         $version = $dbi->_backend->get_latest_version($pagename);
317        
318         //$rdbi =& $this->_rdbi;
319         $rdbi = RatingsDb::getTheRatingsDb();
320         
321         // check if the imgPrefix icons exist.
322         if (! $WikiTheme->_findData("images/RateIt".$imgPrefix."Nk0.png", true))
323             $imgPrefix = '';
324         
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";
330     
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");
335         }
336
337         $user = $request->getUser();
338         $userid = $user->getId();
339         //if (!isset($args['rating']))
340         $rating = $rdbi->getRating($userid, $pagename, $dimension);
341         if (!$rating) {
342             $pred = $rdbi->getPrediction($userid, $pagename, $dimension);
343         }
344         for ($i = 1; $i <= 10; $i++) {
345             $a1 = HTML::a(array('href' => 'javascript:click(\'' . $reActionImgName . '\',\'' . 
346                                 $rePagename . '\',\'' . $version . '\',\'' . 
347                                 $reImgPrefix . '\',\'' . $dimension . '\',' . ($i/2) . ')'));
348             $img_attr = array();
349             $img_attr['src'] = $nk[$i%2];
350             //if (!$rating and !$pred)
351               //  $img_attr['src'] = $none[$i%2];
352             
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);
359             
360             //This adds a space between the rating smilies:
361             // if (($i%2) == 0) $html->pushContent(' ');
362         }
363         $html->pushContent(HTML::Raw('&nbsp;'));
364        
365         $a0 = HTML::a(array('href' => 'javascript:click(\'' . $reActionImgName . '\',\'' . 
366                             $rePagename . '\',\'' . $version . '\',\'' . $reImgPrefix . 
367                             '\',\'' . $dimension . '\',\'X\')'));
368
369         $msg = _("Cancel rating");
370         $a0->pushContent(HTML::img(array('src' => $WikiTheme->getImageUrl("RateIt".$imgPrefix."Cancel"),
371                                          'name'=> $imgPrefix.'Cancel',
372                                          'border' => 0,
373                                          'alt' => $msg)));
374         $a0->addToolTip($msg);
375         $html->pushContent($a0);
376         /*} elseif ($pred) {
377             $msg = _("No opinion");
378             $html->pushContent(HTML::img(array('src' => $WikiTheme->getImageUrl("RateItCancelN"),
379                                                'name'=> $imgPrefix.'Cancel',
380                                                'alt' => $msg)));
381             //$a0->addToolTip($msg);
382             //$html->pushContent($a0);
383         }*/
384         $img_attr = array();
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
392         if ($rating) 
393             $html->pushContent(JavaScript('displayRating(\'' . $reImgPrefix . '\','.$rating .',0)'));
394         elseif ($pred)
395             $html->pushContent(JavaScript('displayRating(\'' . $reImgPrefix . '\','.$pred .',1)'));
396         else 
397             $html->pushContent(JavaScript('displayRating(\'' . $reImgPrefix . '\',0,0)'));    
398         return $html;
399     }
400
401 };
402
403
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
407 //
408 // Revision 1.20  2006/03/04 13:57:28  rurban
409 // rename hash for php-5.1
410 //
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.
415 //
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
420 // ...
421 //
422 // Revision 1.17  2004/08/05 17:31:52  rurban
423 // more xhtml conformance fixes
424 //
425 // Revision 1.16  2004/08/05 17:23:54  rurban
426 // add alt tag for xhtml conformance
427 //
428 // Revision 1.15  2004/07/09 12:50:50  rurban
429 // references are declared, not enforced
430 //
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
434 //
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.
448 //
449 // Revision 1.11  2004/06/19 10:22:41  rurban
450 // outcomment the pear specific methods to let all pages load
451 //
452 // Revision 1.10  2004/06/18 14:42:17  rurban
453 // added wikilens libs (not yet merged good enough, some work for DanFr)
454 //
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
462 //
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
467 //
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.
471 //
472 // Revision 1.7  2004/04/21 04:29:50  rurban
473 // write WikiURL consistently (not WikiUrl)
474 //
475 // Revision 1.6  2004/04/12 14:07:12  rurban
476 // more docs
477 //
478 // Revision 1.5  2004/04/11 10:42:02  rurban
479 // pgsrc/CreatePagePlugin
480 //
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
489 //
490 // Revision 1.3  2004/04/01 06:29:51  rurban
491 // better wording
492 // RateIt also for ADODB
493 //
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)
499 //
500 // Revision 1.1  2004/03/30 02:38:06  rurban
501 // RateIt support (currently no recommendation engine yet)
502 //
503
504 // For emacs users
505 // Local Variables:
506 // mode: php
507 // tab-width: 8
508 // c-basic-offset: 4
509 // c-hanging-comment-ender-p: nil
510 // indent-tabs-mode: nil
511 // End:
512 ?>