]> CyberLeo.Net >> Repos - SourceForge/phpwiki.git/blob - lib/plugin/RateIt.php
rename hash for php-5.1
[SourceForge/phpwiki.git] / lib / plugin / RateIt.php
1 <?php // -*-php-*-
2 rcs_id('$Id: RateIt.php,v 1.20 2006-03-04 13:57:28 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  * - add php_prediction
81  */
82
83 require_once("lib/WikiPlugin.php");
84 require_once("lib/wikilens/RatingsDb.php");
85
86 class WikiPlugin_RateIt
87 extends WikiPlugin
88 {
89     function getName() {
90         return _("RateIt");
91     }
92     function getDescription() {
93         return _("Rating system. Store user ratings per page");
94     }
95     function getVersion() {
96         return preg_replace("/[Revision: $]/", '',
97                             "\$Revision: 1.20 $");
98     }
99
100     function RatingWidgetJavascript() {
101         global $WikiTheme;
102         if (!empty($this->imgPrefix))
103             $imgPrefix = $this->imgPrefix;
104         elseif (defined("RATEIT_IMGPREFIX"))
105             $imgPrefix = RATEIT_IMGPREFIX;
106         else $imgPrefix = '';
107         if ($imgPrefix and !$WikiTheme->_findData("images/RateIt".$imgPrefix."Nk0.png",1))
108             $imgPrefix = '';
109         $img   = substr($WikiTheme->_findData("images/RateIt".$imgPrefix."Nk0.png"),0,-7);
110         $urlprefix = WikiURL("",0,1); // TODO: check actions USE_PATH_INFO=false
111         $js = "
112 function displayRating(imgPrefix, ratingvalue, pred) {
113   var cancel = imgPrefix + 'Cancel';
114   for (i=1; i<=10; i++) {
115     var imgName = imgPrefix + i;
116     var imgSrc = '".$img."';   
117     document[imgName].title = '"._("Your rating ")."'+ratingvalue;
118     var imgType = 'N';
119     if (pred) {
120         imgType = 'R';
121     } else if (i<=(ratingvalue*2)) {
122         imgType = 'O';
123     }
124     document[imgName].src = imgSrc + imgType + ((i%2) ? 'k1' : 'k0') + '.png';
125   }
126   //document[cancel].src = imgSrc + 'Cancel.png';
127 }
128 function click(actionImg, pagename, version, imgPrefix, dimension, rating) {
129   if (rating == 'X') {
130     deleteRating(actionImg, pagename, dimension);
131     displayRating(imgPrefix, 0, 0);
132   } else {
133     submitRating(actionImg, pagename, version, dimension, rating);
134     displayRating(imgPrefix, rating, 0);
135   }
136 }
137 function submitRating(actionImg, page, version, dimension, rating) {
138   var myRand = Math.round(Math.random()*(1000000));
139   var imgSrc = '".$urlprefix."' + escape(page) + '?version=' + version + '&action=".urlencode(_("RateIt"))."&mode=add&rating=' + rating + '&dimension=' + dimension + '&nopurge=1&rand=' + myRand"
140         .(!empty($_GET['start_debug']) ? "+'&start_debug=1'" : '').";
141   ".(DEBUG ? '' : '//')."alert('submitRating(\"'+actionImg+'\", \"'+page+'\", '+version+', '+dimension+', '+rating+') => '+imgSrc);
142   document[actionImg].src = imgSrc;
143 }
144 function deleteRating(actionImg, page, dimension) {
145   var myRand = Math.round(Math.random()*(1000000));
146   var imgSrc = '".$urlprefix."' + escape(page) + '?action=".urlencode(_("RateIt"))."&mode=delete&dimension=' + dimension + '&nopurge=1&rand=' + myRand"
147         .(!empty($_GET['start_debug']) ? "+'&start_debug=1'" : '').";
148   ".(DEBUG ? '' : '//')."alert('deleteRating(\"'+actionImg+'\", \"'+page+'\", '+version+', '+dimension+')');
149   document[actionImg].src = imgSrc;
150 }
151 ";
152         return JavaScript($js);
153     }
154
155     function actionImgPath() {
156         global $WikiTheme;
157         return $WikiTheme->_findFile("images/RateItAction.png");
158     }
159
160     /**
161      * Take a string and quote it sufficiently to be passed as a Javascript
162      * string between ''s
163      */
164     function _javascript_quote_string($s) {
165         return str_replace("'", "\'", $s);
166     }
167
168     function getDefaultArguments() {
169         return array( 'pagename'  => '[pagename]',
170                       'version'   => false,
171                       'id'        => 'rateit',
172                       'imgPrefix' => '',      // '' or BStar or Star
173                       'dimension' => false,
174                       'small'     => false,
175                       'show'      => false,
176                       'mode'      => false,
177                       );
178     }
179
180     function head() { // early side-effects (before body)
181         global $WikiTheme;
182         $WikiTheme->addMoreHeaders($this->RatingWidgetJavascript());
183     }
184
185     // todo: only for signed users
186     // todo: set rating dbi for external rating database
187     function run($dbi, $argstr, &$request, $basepage) {
188         global $WikiTheme;
189         //$this->_request = & $request;
190         //$this->_dbi = & $dbi;
191         $user = $request->getUser();
192         //FIXME: fails on test with DumpHtml:RateIt
193         if (!is_object($user)) return HTML();
194         $this->userid = $user->getId();
195         $args = $this->getArgs($argstr, $request);
196         $this->dimension = $args['dimension'];
197         $this->imgPrefix = $args['imgPrefix'];
198         if ($this->dimension == '') {
199             $this->dimension = 0;
200             $args['dimension'] = 0;
201         }
202         if ($args['pagename']) {
203             // Expand relative page names.
204             $page = new WikiPageName($args['pagename'], $basepage);
205             $args['pagename'] = $page->name;
206         }
207         if (empty($args['pagename'])) {
208             return $this->error(_("no page specified"));
209         }
210         $this->pagename = $args['pagename'];
211
212         $rdbi = RatingsDb::getTheRatingsDb();
213         $this->_rdbi =& $rdbi;
214
215         if ($args['mode'] === 'add') {
216             //if (!$user->isSignedIn()) return $this->error(_("You must sign in"));
217             $actionImg = $WikiTheme->_path . $this->actionImgPath();
218             $rdbi->addRating($request->getArg('rating'), $this->userid, $this->pagename, $this->dimension);
219
220             if (!empty($request->_is_buffering_output))
221                 ob_end_clean();  // discard any previous output
222             // delete the cache
223             $page = $request->getPage();
224             //$page->set('_cached_html', false);
225             $request->cacheControl('MUST-REVALIDATE');
226             $dbi->touch();
227             //fake validators without args
228             $request->appendValidators(array('wikiname' => WIKI_NAME,
229                                              'args'     => wikihash('')));
230             header('Content-type: image/png');
231             readfile($actionImg);
232             exit();
233         } elseif ($args['mode'] === 'delete') {
234             //if (!$user->isSignedIn()) return $this->error(_("You must sign in"));
235             $actionImg = $WikiTheme->_path . $this->actionImgPath();
236             $rdbi->deleteRating($this->userid, $this->pagename, $this->dimension);
237             if (!empty($request->_is_buffering_output))
238                 ob_end_clean();  // discard any previous output
239             // delete the cache
240             $page = $request->getPage();
241             //$page->set('_cached_html', false);
242             $request->cacheControl('MUST-REVALIDATE');
243             $dbi->touch();
244             //fake validators without args
245             $request->appendValidators(array('wikiname' => WIKI_NAME,
246                                              'args'     => wikihash('')));
247             header('Content-type: image/png');
248             readfile($actionImg);
249             exit();
250         } elseif (! $args['show'] ) {
251             return $this->RatingWidgetHtml($args['pagename'], $args['version'], $args['imgPrefix'], 
252                                            $args['dimension'], $args['small']);
253         } else {
254             //if (!$user->isSignedIn()) return $this->error(_("You must sign in"));
255             //extract($args);
256             $rating = $rdbi->getRating();
257             $html = HTML::p($this->pagename.": ".
258                             sprintf(_("Rated by %d users | Average rating %.1f stars"),
259                                     $rdbi->getNumUsers($this->pagename, $this->dimension),
260                                     $rdbi->getAvg($this->pagename, $this->dimension)),
261                             HTML::br());
262             if ($rating) {
263                 $html->pushContent(sprintf(_("Your rating was %.1f"),
264                                            $rating));
265             } else {
266                 $pred = $rdbi->getPrediction($this->userid, $this->pagename, $this->dimension);
267                 if (is_string($pred))
268                     $html->pushContent(sprintf(_("%s prediction for you is %s stars"),
269                                                WIKI_NAME, $pred));
270                 elseif ($pred)
271                     $html->pushContent(sprintf(_("%s prediction for you is %.1f stars"),
272                                                WIKI_NAME, $pred));
273             }
274             //$html->pushContent(HTML::p());
275             //$html->pushContent(HTML::em("(Experimental: This might be entirely bogus data)"));
276             return $html;
277         }
278     }
279
280     // box is used to display a fixed-width, narrow version with common header
281     function box($args=false, $request=false, $basepage=false) {
282         if (!$request) $request =& $GLOBALS['request'];
283         if (!$request->_user->isSignedIn()) return;
284         if (!isset($args)) $args = array();
285         $args['small'] = 1;
286         $argstr = '';
287         foreach ($args as $key => $value)
288             $argstr .= $key."=".$value;
289         $widget = $this->run($request->_dbi, $argstr, $request, $basepage);
290
291         return $this->makeBox(WikiLink(_("RateIt"),'',_("Rate It")),
292                               $widget);
293     }
294
295     /**
296      * HTML widget display
297      *
298      * This needs to be put in the <body> section of the page.
299      *
300      * @param pagename    Name of the page to rate
301      * @param version     Version of the page to rate (may be "" for current)
302      * @param imgPrefix   Prefix of the names of the images that display the rating
303      *                    You can have two widgets for the same page displayed at
304      *                    once iff the imgPrefix-s are different.
305      * @param dimension   Id of the dimension to rate
306      * @param small       Makes a smaller ratings widget if non-false
307      *
308      * Limitations: Currently this can only print the current users ratings.
309      *              And only the widget, but no value (for buddies) also.
310      */
311     function RatingWidgetHtml($pagename, $version, $imgPrefix, $dimension, $small = false) {
312         global $WikiTheme, $request;
313
314         $imgId = MangleXmlIdentifier($pagename) . $imgPrefix;
315         $actionImgName = $imgId . 'RateItAction';
316         $dbi =& $GLOBALS['request']->_dbi;
317         $version = $dbi->_backend->get_latest_version($pagename);
318        
319         //$rdbi =& $this->_rdbi;
320         $rdbi = RatingsDb::getTheRatingsDb();
321         
322         // check if the imgPrefix icons exist.
323         if (! $WikiTheme->_findData("images/RateIt".$imgPrefix."Nk0.png", true))
324             $imgPrefix = '';
325         
326         // Protect against 's, though not \r or \n
327         $reImgPrefix     = $this->_javascript_quote_string($imgPrefix);
328         $reActionImgName = $this->_javascript_quote_string($actionImgName);
329         $rePagename      = $this->_javascript_quote_string($pagename);
330         //$dimension = $args['pagename'] . "rat";
331     
332         $html = HTML::span(array("id" => $imgId));
333         for ($i=0; $i < 2; $i++) {
334             $nk[$i]   = $WikiTheme->_findData("images/RateIt".$imgPrefix."Nk".$i.".png");
335             $none[$i] = $WikiTheme->_findData("images/RateIt".$imgPrefix."Rk".$i.".png");
336         }
337
338         $user = $request->getUser();
339         $userid = $user->getId();
340         //if (!isset($args['rating']))
341         $rating = $rdbi->getRating($userid, $pagename, $dimension);
342         if (!$rating) {
343             $pred = $rdbi->getPrediction($userid, $pagename, $dimension);
344         }
345         for ($i = 1; $i <= 10; $i++) {
346             $a1 = HTML::a(array('href' => 'javascript:click(\'' . $reActionImgName . '\',\'' . 
347                                 $rePagename . '\',\'' . $version . '\',\'' . 
348                                 $reImgPrefix . '\',\'' . $dimension . '\',' . ($i/2) . ')'));
349             $img_attr = array();
350             $img_attr['src'] = $nk[$i%2];
351             //if (!$rating and !$pred)
352               //  $img_attr['src'] = $none[$i%2];
353             
354             $img_attr['name'] = $imgPrefix . $i;
355             $img_attr['alt'] = $img_attr['name'];
356             $img_attr['border'] = 0;
357             $a1->pushContent(HTML::img($img_attr));
358             $a1->addToolTip(_("Rate the topic of this page"));
359             $html->pushContent($a1);
360             
361             //This adds a space between the rating smilies:
362             // if (($i%2) == 0) $html->pushContent(' ');
363         }
364         $html->pushContent(HTML::Raw('&nbsp;'));
365        
366         $a0 = HTML::a(array('href' => 'javascript:click(\'' . $reActionImgName . '\',\'' . 
367                             $rePagename . '\',\'' . $version . '\',\'' . $reImgPrefix . 
368                             '\',\'' . $dimension . '\',\'X\')'));
369
370         $msg = _("Cancel rating");
371         $a0->pushContent(HTML::img(array('src' => $WikiTheme->getImageUrl("RateIt".$imgPrefix."Cancel"),
372                                          'name'=> $imgPrefix.'Cancel',
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.19  2004/11/15 16:00:01  rurban
406 // enable RateIt imgPrefix: '' or 'Star' or 'BStar',
407 // enable blue prediction icons,
408 // enable buddy predictions.
409 //
410 // Revision 1.18  2004/11/01 10:43:59  rurban
411 // seperate PassUser methods into seperate dir (memory usage)
412 // fix WikiUser (old) overlarge data session
413 // remove wikidb arg from various page class methods, use global ->_dbi instead
414 // ...
415 //
416 // Revision 1.17  2004/08/05 17:31:52  rurban
417 // more xhtml conformance fixes
418 //
419 // Revision 1.16  2004/08/05 17:23:54  rurban
420 // add alt tag for xhtml conformance
421 //
422 // Revision 1.15  2004/07/09 12:50:50  rurban
423 // references are declared, not enforced
424 //
425 // Revision 1.14  2004/07/08 20:30:07  rurban
426 // plugin->run consistency: request as reference, added basepage.
427 // encountered strange bug in AllPages (and the test) which destroys ->_dbi
428 //
429 // Revision 1.12  2004/06/30 19:59:07  dfrankow
430 // Make changes suitable so that wikilens theme (and wikilens.org) work properly.
431 // + Remove predictions (for now)
432 // + Use new RatingsDb singleton.
433 // + Change RatingWidgetHtml() to use parameters like a normal PHP function
434 //   so we can have PHP check that we're passing the right # of them.
435 // + Change RatingWidgetHtml() to be callable static-ally
436 //   (without a plugin object)
437 // + Remove the "RateIt" button for now, because we don't use it on wikilens.org.
438 //   Maybe if someone wants the button, there can be an arg or flag for it.
439 // + Always show the cancel button, because UI widgets should not hide.
440 // + Remove the "No opinion" button for now, because we don't yet store that.
441 //   This is a useful thing, tho, for the future.
442 //
443 // Revision 1.11  2004/06/19 10:22:41  rurban
444 // outcomment the pear specific methods to let all pages load
445 //
446 // Revision 1.10  2004/06/18 14:42:17  rurban
447 // added wikilens libs (not yet merged good enough, some work for DanFr)
448 //
449 // Revision 1.9  2004/06/14 11:31:39  rurban
450 // renamed global $Theme to $WikiTheme (gforge nameclash)
451 // inherit PageList default options from PageList
452 //   default sortby=pagename
453 // use options in PageList_Selectable (limit, sortby, ...)
454 // added action revert, with button at action=diff
455 // added option regex to WikiAdminSearchReplace
456 //
457 // Revision 1.8  2004/06/01 15:28:01  rurban
458 // AdminUser only ADMIN_USER not member of Administrators
459 // some RateIt improvements by dfrankow
460 // edit_toolbar buttons
461 //
462 // Revision _1.2  2004/04/29 17:55:03  dfrankow
463 // Check in escape() changes to protect against leading spaces in pagename.
464 // This is untested with Reini's _("RateIt") additions to this plugin.
465 //
466 // Revision 1.7  2004/04/21 04:29:50  rurban
467 // write WikiURL consistently (not WikiUrl)
468 //
469 // Revision 1.6  2004/04/12 14:07:12  rurban
470 // more docs
471 //
472 // Revision 1.5  2004/04/11 10:42:02  rurban
473 // pgsrc/CreatePagePlugin
474 //
475 // Revision 1.4  2004/04/06 20:00:11  rurban
476 // Cleanup of special PageList column types
477 // Added support of plugin and theme specific Pagelist Types
478 // Added support for theme specific UserPreferences
479 // Added session support for ip-based throttling
480 //   sql table schema change: ALTER TABLE session ADD sess_ip CHAR(15);
481 // Enhanced postgres schema
482 // Added DB_Session_dba support
483 //
484 // Revision 1.3  2004/04/01 06:29:51  rurban
485 // better wording
486 // RateIt also for ADODB
487 //
488 // Revision 1.2  2004/03/31 06:22:22  rurban
489 // shorter javascript,
490 // added prediction buttons and display logic,
491 // empty HTML if not signed in.
492 // fixed deleting (empty dimension => 0)
493 //
494 // Revision 1.1  2004/03/30 02:38:06  rurban
495 // RateIt support (currently no recommendation engine yet)
496 //
497
498 // For emacs users
499 // Local Variables:
500 // mode: php
501 // tab-width: 8
502 // c-basic-offset: 4
503 // c-hanging-comment-ender-p: nil
504 // indent-tabs-mode: nil
505 // End:
506 ?>