]> CyberLeo.Net >> Repos - SourceForge/phpwiki.git/blob - lib/plugin/RateIt.php
seperate PassUser methods into seperate dir (memory usage)
[SourceForge/phpwiki.git] / lib / plugin / RateIt.php
1 <?php // -*-php-*-
2 rcs_id('$Id: RateIt.php,v 1.18 2004-11-01 10:43:59 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.18 $");
98     }
99
100     function RatingWidgetJavascript() {
101         global $WikiTheme;
102         // FIXME: use prefix
103         $img   = substr($WikiTheme->_findData("images/RateItNk0.png"),0,-7);
104         $urlprefix = WikiURL("",0,1);
105         $js = "
106 function displayRating(imgPrefix, ratingvalue, pred) {
107   var cancel = imgPrefix + 'Cancel';
108   for (i=1; i<=10; i++) {
109     var imgName = imgPrefix + i;
110     var imgSrc = '".$img."';   
111       document[imgName].title = '"._("Your rating ")."'+ratingvalue;
112     if (i<=(ratingvalue*2)) {
113         document[imgName].src = imgSrc + ((i%2) ? 'Ok1' : 'Ok0') + '.png';
114     } else {
115       document[imgName].src = imgSrc + ((i%2) ? 'Nk1' : 'Nk0') + '.png';
116     }
117   }
118   
119   //document[cancel].src = imgSrc + 'Cancel.png';
120
121 }
122 function click(actionImg, pagename, version, imgPrefix, dimension, rating) {
123   if (rating == 'X') {
124     deleteRating(actionImg, pagename, dimension);
125     displayRating(imgPrefix, 0, 0);
126   } else {
127     submitRating(actionImg, pagename, version, dimension, rating);
128     displayRating(imgPrefix, rating, 0);
129   }
130 }
131 function submitRating(actionImg, page, version, dimension, rating) {
132   var myRand = Math.round(Math.random()*(1000000));
133   var imgSrc = escape(page) + '?version=' + version + '&action=".urlencode(_("RateIt"))."&mode=add&rating=' + rating + '&dimension=' + dimension + '&nopurge=cache&rand=' + myRand;
134   //alert('submitRating(' + page + ', ' + version + ', ' + dimension + ', ' + rating + ') => '+imgSrc);
135   document[actionImg].src= imgSrc;
136 }
137 function deleteRating(actionImg, page, dimension) {
138   var myRand = Math.round(Math.random()*(1000000));
139   var imgSrc = '".$urlprefix."' + escape(page) + '?action=".urlencode(_("RateIt"))."&mode=delete&dimension=' + dimension + '&nopurge=cache&rand=' + myRand;
140   //alert('deleteRating(' + page + ', ' + version + ', ' + dimension + ')');
141   document[actionImg].src= imgSrc;
142 }
143 ";
144         return JavaScript($js);
145     }
146
147     function actionImgPath() {
148         global $WikiTheme;
149         return $WikiTheme->_findFile("images/RateItAction.png");
150     }
151
152     /**
153      * Take a string and quote it sufficiently to be passed as a Javascript
154      * string between ''s
155      */
156     function _javascript_quote_string($s) {
157         return str_replace("'", "\'", $s);
158     }
159
160     function getDefaultArguments() {
161         return array( 'pagename'  => '[pagename]',
162                       'version'   => false,
163                       'id'        => 'rateit',
164                       'imgPrefix' => '',
165                       'dimension' => false,
166                       'small'     => false,
167                       'show'      => false,
168                       'mode'      => false,
169                       );
170     }
171
172     function head() { // early side-effects (before body)
173         global $WikiTheme;
174         $WikiTheme->addMoreHeaders($this->RatingWidgetJavascript());
175     }
176
177     // todo: only for signed users
178     // todo: set rating dbi for external rating database
179     function run($dbi, $argstr, &$request, $basepage) {
180         global $WikiTheme;
181         //$this->_request = & $request;
182         //$this->_dbi = & $dbi;
183         $user = $request->getUser();
184         //FIXME: fails on test with DumpHtml:RateIt
185         if (!is_object($user)) return HTML();
186         $this->userid = $user->UserName();
187         $args = $this->getArgs($argstr, $request);
188         $this->dimension = $args['dimension'];
189         if ($this->dimension == '') {
190             $this->dimension = 0;
191             $args['dimension'] = 0;
192         }
193         if ($args['pagename']) {
194             // Expand relative page names.
195             $page = new WikiPageName($args['pagename'], $basepage);
196             $args['pagename'] = $page->name;
197         }
198         if (empty($args['pagename'])) {
199             return $this->error(_("no page specified"));
200         }
201         $this->pagename = $args['pagename'];
202
203         $rdbi = RatingsDb::getTheRatingsDb();
204         $this->_rdbi =& $rdbi;
205
206         if ($args['mode'] === 'add') {
207             if (!$user->isSignedIn())
208                 return $this->error(_("You must sign in"));
209             global $WikiTheme;
210             $actionImg = $WikiTheme->_path . $this->actionImgPath();
211             $rdbi->addRating($request->getArg('rating'), $user->getId(), $this->pagename, $this->dimension);
212
213             if (!empty($request->_is_buffering_output))
214                 ob_end_clean();  // discard any previous output
215             // delete the cache
216             $page = $request->getPage();
217             $page->set('_cached_html', false);
218             $request->cacheControl('MUST-REVALIDATE');
219             $dbi->touch();
220             //fake validators without args
221             $request->appendValidators(array('wikiname' => WIKI_NAME,
222                                              'args'     => hash('')));
223             header('Content-type: image/png');
224             readfile($actionImg);
225             exit();
226         } elseif ($args['mode'] === 'delete') {
227             if (!$user->isSignedIn())
228                 return $this->error(_("You must sign in"));
229             global $WikiTheme;
230             $actionImg = $WikiTheme->_path . $this->actionImgPath();
231             $rdbi->deleteRating($user->getId(), $this->pagename, $this->dimension);
232             if (!empty($request->_is_buffering_output))
233                 ob_end_clean();  // discard any previous output
234             // delete the cache
235             $page = $request->getPage();
236             $page->set('_cached_html', false);
237             $request->cacheControl('MUST-REVALIDATE');
238             $dbi->touch();
239             //fake validators without args
240             $request->appendValidators(array('wikiname' => WIKI_NAME,
241                                              'args'     => hash('')));
242             header('Content-type: image/png');
243             readfile($actionImg);
244             exit();
245         } elseif (! $args['show'] ) {
246             // we must use the head method instead, because <body> is already printed.
247             // $WikiTheme->addMoreHeaders($this->RatingWidgetJavascript()); 
248             // or we change the header in the ob_buffer.
249             //Todo: add a validator based on the users last rating mtime
250             //$rating = $rdbi->getRating();
251             /*
252                 static $validated = 0;
253                 if (!$validated) {
254                 //$page = $request->getPage();
255                 //$page->set('_cached_html', false);
256                   $request->cacheControl('REVALIDATE');
257                   $validated = 1;
258                 }
259             */
260             //$args['rating'] = $rating;
261             return $this->RatingWidgetHtml($args['pagename'], $args['version'], $args['imgPrefix'], 
262                                            $args['dimension'], $args['small']);
263         } else {
264             if (!$user->isSignedIn())
265                 return $this->error(_("You must sign in"));
266             extract($args);
267             $rating = $rdbi->getRating();
268             $html = HTML::p($this->pagename.": ".
269                             sprintf(_("Rated by %d users | Average rating %.1f stars"),
270                                     $rdbi->getNumUsers($this->pagename, $this->dimension),
271                                     $rdbi->getAvg($this->pagename, $this->dimension)),
272                             HTML::br());
273             if ($rating) {
274                 $html->pushContent(sprintf(_("Your rating was %.1f"),
275                                            $rating));
276             } else {
277                 $pred = $rdbi->getPrediction($this->userid, $this->pagename, $this->dimension);
278                 if (is_string($pred))
279                     $html->pushContent(sprintf(_("%s prediction for you is %s stars"),
280                                                WIKI_NAME, $pred));
281                 elseif ($pred)
282                     $html->pushContent(sprintf(_("%s prediction for you is %.1f stars"),
283                                                WIKI_NAME, $pred));
284             }
285             $html->pushContent(HTML::p());
286             $html->pushContent(HTML::em("(Experimental: This might be entirely bogus data)"));
287             return $html;
288         }
289     }
290
291     // box is used to display a fixed-width, narrow version with common header
292     function box($args=false, $request=false, $basepage=false) {
293         if (!$request) $request =& $GLOBALS['request'];
294         if (!$request->_user->isSignedIn()) return;
295         if (!isset($args)) $args = array();
296         $args['small'] = 1;
297         $argstr = '';
298         foreach ($args as $key => $value)
299             $argstr .= $key."=".$value;
300         $widget = $this->run($request->_dbi, $argstr, $request, $basepage);
301
302         return $this->makeBox(WikiLink(_("RateIt"),'',_("Rate It")),
303                               $widget);
304     }
305
306     /**
307      * HTML widget display
308      *
309      * This needs to be put in the <body> section of the page.
310      *
311      * @param pagename    Name of the page to rate
312      * @param version     Version of the page to rate (may be "" for current)
313      * @param imgPrefix   Prefix of the names of the images that display the rating
314      *                    You can have two widgets for the same page displayed at
315      *                    once iff the imgPrefix-s are different.
316      * @param dimension   Id of the dimension to rate
317      * @param small       Makes a smaller ratings widget if non-false
318      *
319      * Limitations: Currently this can only print the current users ratings.
320      *              And only the widget, but no value (for buddies) also.
321      */
322     function RatingWidgetHtml($pagename, $version, $imgPrefix, $dimension, $small = false) {
323         global $WikiTheme, $request;
324
325         $imgPrefix = MangleXmlIdentifier($pagename) . $imgPrefix;
326         // FIXME: use prefix
327         $actionImgName = $imgPrefix . 'RateItAction';
328         $dbi =& $GLOBALS['request']->_dbi;
329         $version = $dbi->_backend->get_latest_version($pagename);
330        
331         //$rdbi =& $this->_rdbi;
332         $rdbi = RatingsDb::getTheRatingsDb();
333         $id = 'rateit';
334         // Protect against 's, though not \r or \n
335
336         $reImgPrefix     = $this->_javascript_quote_string($imgPrefix);
337         $reActionImgName = $this->_javascript_quote_string($actionImgName);
338         $rePagename      = $this->_javascript_quote_string($pagename);
339         //$dimension = $args['pagename'] . "rat";
340     
341         $html = HTML::span(array("id" => $id));
342         for ($i=0; $i < 2; $i++) {
343             // FIXME: use prefix
344             $nk[$i]   = $WikiTheme->_findData("images/RateItNk$i.png");
345             $none[$i] = $WikiTheme->_findData("images/RateItRk$i.png");
346         }
347       
348
349         $user = $request->getUser();
350         $userid = $user->getId();
351         //if (!isset($args['rating']))
352         $rating = $rdbi->getRating($userid, $pagename, $dimension);
353         if (!$rating) {
354             $pred = $rdbi->getPrediction($userid,$pagename,$dimension);
355         }
356         for ($i = 1; $i <= 10; $i++) {
357             $a1 = HTML::a(array('href' => 'javascript:click(\'' . $reActionImgName . '\',\'' . 
358                                 $rePagename . '\',\'' . $version . '\',\'' . 
359                                 $reImgPrefix . '\',\'' . $dimension . '\',' . ($i/2) . ')'));
360             $img_attr = array();
361             $img_attr['src'] = $nk[$i%2];
362             //if (!$rating and !$pred)
363               //  $img_attr['src'] = $none[$i%2];
364             
365             $img_attr['name'] = $imgPrefix . $i;
366             $img_attr['alt'] = $img_attr['name'];
367             $img_attr['border'] = 0;
368             $a1->pushContent(HTML::img($img_attr));
369             $a1->addToolTip(_("Rate the topic of this page"));
370             $html->pushContent($a1);
371             
372             //This adds a space between the rating smilies:
373             // if (($i%2) == 0) $html->pushContent(' ');
374         }
375         $html->pushContent(HTML::Raw('&nbsp;'));
376        
377         $a0 = HTML::a(array('href' => 'javascript:click(\'' . $reActionImgName . '\',\'' . 
378                             $rePagename . '\',\'' . $version . '\',\'' . $reImgPrefix . 
379                             '\',\'' . $dimension . '\',\'X\')'));
380
381         $msg = _("Cancel rating");
382         $a0->pushContent(HTML::img(array('src' => $WikiTheme->getImageUrl("RateItCancel"),
383                                          'name'=> $imgPrefix.'Cancel',
384                                          'alt' => $msg)));
385         $a0->addToolTip($msg);
386         $html->pushContent($a0);
387         /*} elseif ($pred) {
388             $msg = _("No opinion");
389             $html->pushContent(HTML::img(array('src' => $WikiTheme->getImageUrl("RateItCancelN"),
390                                                'name'=> $imgPrefix.'Cancel',
391                                                'alt' => $msg)));
392             //$a0->addToolTip($msg);
393             //$html->pushContent($a0);
394         }*/
395         $img_attr = array();
396         $img_attr['src'] = $WikiTheme->_findData("images/RateItAction.png");
397         $img_attr['name'] = $actionImgName;
398         $img_attr['alt'] = $img_attr['name'];
399         //$img_attr['class'] = 'k' . $i;
400         $img_attr['border'] = 0;
401         $html->pushContent(HTML::img($img_attr));
402         // Display the current rating if there is one
403         if ($rating) 
404             $html->pushContent(JavaScript('displayRating(\'' . $reImgPrefix . '\','.$rating .',0)'));
405         elseif ($pred)
406             $html->pushContent(JavaScript('displayRating(\'' . $reImgPrefix . '\','.$pred .',1)'));
407         else 
408             $html->pushContent(JavaScript('displayRating(\'' . $reImgPrefix . '\',0,0)'));    
409         return $html;
410     }
411
412 };
413
414
415 // $Log: not supported by cvs2svn $
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 ?>