]> CyberLeo.Net >> Repos - SourceForge/phpwiki.git/blob - lib/plugin/RateIt.php
AdminUser only ADMIN_USER not member of Administrators
[SourceForge/phpwiki.git] / lib / plugin / RateIt.php
1 <?php // -*-php-*-
2 rcs_id('$Id: RateIt.php,v 1.8 2004-06-01 15:28:01 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 //define('RATING_STORAGE','WIKIPAGE');   // not fully supported yet
24 define('RATING_STORAGE','SQL');          // only for mysql yet.
25 // leave undefined for internal, slow php engine.
26 //define('RATING_EXTERNAL',PHPWIKI_DIR . 'suggest.exe');
27
28 /**
29  * RateIt: A recommender system, based on MovieLens and suggest.
30  * Store user ratings per pagename. The wikilens theme displays a navbar image bar
31  * with some nice javascript magic and this plugin shows various recommendations.
32  *
33  * There should be two methods to store ratings:
34  * In a SQL database as in wikilens http://dickens.cs.umn.edu/dfrankow/wikilens
35  *
36  * The most important fact: A page has more than one rating. There can
37  * be (and will be!) many ratings per page (ratee): different raters
38  * (users), in different dimensions. Are those stored per page
39  * (ratee)? Then what if I wish to access the ratings per rater
40  * (user)? 
41  * wikilens plans several user-centered applications like:
42  * a) show my ratings
43  * b) show my buddies' ratings
44  * c) show how my ratings are like my buddies'
45  * d) show where I agree/disagree with my buddy
46  * e) show what this group of people agree/disagree on
47  *
48  * If the ratings are stored in a real DB in a table, we can index the
49  * ratings by rater and ratee, and be confident in
50  * performance. Currently MovieLens has 80,000 users, 7,000 items,
51  * 10,000,000 ratings. This is an average of 1400 ratings/page if each
52  * page were rated equally. However, they're not: the most popular
53  * things have tens of thousands of ratings (e.g., "Pulp Fiction" has
54  * 42,000 ratings). If ratings are stored per page, you would have to
55  * save/read huge page metadata every time someone submits a
56  * rating. Finally, the movie domain has an unusually small number of
57  * items-- I'd expect a lot more in music, for example.
58  *
59  * For a simple rating system one can also store the rating in the page 
60  * metadata (default).
61  *
62  * Recommender Engines:
63  * Recommendation/Prediction is a special field of "Data Mining"
64  * For a list of (also free) software see 
65  *  http://www.the-data-mine.com/bin/view/Software/WebIndex
66  * - movielens: (Java Server) will be gpl'd in summer 2004 (weighted)
67  * - suggest: is free for non-commercial use, available as compiled library
68  *     (non-weighted)
69  * - Autoclass: simple public domain C library
70  * - MLC++: C++ library http://www.sgi.com/tech/mlc/
71  *
72  * Usage:    <?plugin RateIt ?>              to enable rating on this page
73  *   Note: The wikilens theme must be enabled, to enable this plugin!
74  *   Or use a sidebar based theme with the box method.
75  *           <?plugin RateIt show=ratings ?> to show my ratings
76  *           <?plugin RateIt show=buddies ?> to show my buddies
77  *           <?plugin RateIt show=ratings dimension=1 ?>
78  *
79  * @author:  Dan Frankowski (wikilens author), Reini Urban (as plugin)
80  *
81  * TODO: 
82  * - fix RATING_STORAGE = WIKIPAGE
83  * - fix smart caching
84  * - finish mysuggest.c (external engine with data from mysql)
85  * - add php_prediction
86  * - add the various show modes (esp. TopN queries in PHP)
87  */
88 /*
89  CREATE TABLE rating (
90         dimension INT(4) NOT NULL,
91         raterpage INT(11) NOT NULL,
92         rateepage INT(11) NOT NULL,
93         ratingvalue FLOAT NOT NULL,
94         rateeversion INT(11) NOT NULL,
95         tstamp TIMESTAMP(14) NOT NULL,
96         PRIMARY KEY (dimension, raterpage, rateepage)
97  );
98 */
99
100 require_once("lib/WikiPlugin.php");
101
102 class WikiPlugin_RateIt
103 extends WikiPlugin
104 {
105     function getName() {
106         return _("RateIt");
107     }
108     function getDescription() {
109         return _("Rating system. Store user ratings per page");
110     }
111     function getVersion() {
112         return preg_replace("/[Revision: $]/", '',
113                             "\$Revision: 1.8 $");
114     }
115
116     function RatingWidgetJavascript() {
117         global $Theme;
118         $img   = substr($Theme->_findData("images/RateItNk0.png"),0,-7);
119         $urlprefix = WikiURL("",0,1);
120         $js = "
121 function displayRating(imgPrefix, ratingvalue, pred) {
122   var cancel = imgPrefix + 'Cancel';
123   for (i=1; i<=10; i++) {
124     var imgName = imgPrefix + i;
125     var imgSrc = '".$img."';
126     if (pred)
127       document[imgName].title = '"._("Predicted rating ")."'+ratingvalue;
128     else    
129       document[imgName].title = '"._("Your rating ")."'+ratingvalue;
130     if (i<=(ratingvalue*2)) {
131       if (pred)
132         document[imgName].src = imgSrc + ((i%2) ? 'Rk1' : 'Rk0') + '.png';
133       else
134         document[imgName].src = imgSrc + ((i%2) ? 'Ok1' : 'Ok0') + '.png';
135     } else {
136       document[imgName].src = imgSrc + ((i%2) ? 'Nk1' : 'Nk0') + '.png';
137     }
138   }
139   if ((pred == 0) && (ratingvalue > 0))
140     document[cancel].src = imgSrc + 'Cancel.png';
141   else
142     document[cancel].src = imgSrc + 'CancelN.png';
143 }
144 function click(actionImg, pagename, version, imgPrefix, dimension, rating) {
145   if (rating == 'X') {
146     deleteRating(actionImg, pagename, dimension);
147     displayRating(imgPrefix, 0, 0);
148   } else {
149     submitRating(actionImg, pagename, version, dimension, rating);
150     displayRating(imgPrefix, rating, 0);
151   }
152 }
153 function submitRating(actionImg, page, version, dimension, rating) {
154   var myRand = Math.round(Math.random()*(1000000));
155   var imgSrc = escape(page) + '?version=' + version + '&action=".urlencode(_("RateIt"))."&mode=add&rating=' + rating + '&dimension=' + dimension + '&nopurge=cache&rand=' + myRand;
156   //alert('submitRating(' + page + ', ' + version + ', ' + dimension + ', ' + rating + ') => '+imgSrc);
157   document[actionImg].src= imgSrc;
158 }
159 function deleteRating(actionImg, page, dimension) {
160   var myRand = Math.round(Math.random()*(1000000));
161   var imgSrc = '".$urlprefix."' + escape(page) + '?action=".urlencode(_("RateIt"))."&mode=delete&dimension=' + dimension + '&nopurge=cache&rand=' + myRand;
162   //alert('deleteRating(' + page + ', ' + version + ', ' + dimension + ')');
163   document[actionImg].src= imgSrc;
164 }
165 ";
166         return JavaScript($js);
167     }
168
169     function actionImgPath() {
170         global $Theme;
171         return $Theme->_findFile("images/RateItAction.png");
172     }
173
174     /**
175      * Take a string and quote it sufficiently to be passed as a Javascript
176      * string between ''s
177      */
178     function _javascript_quote_string($s) {
179         return str_replace("'", "\'", $s);
180     }
181
182     function getDefaultArguments() {
183         return array( 'pagename'  => '[pagename]',
184                       'version'   => false,
185                       'id'        => 'rateit',
186                       'imgPrefix' => '',
187                       'dimension' => false,
188                       'small'     => false,
189                       'show'      => false,
190                       'mode'      => false,
191                       );
192     }
193
194     function head() { // early side-effects (before body)
195         global $Theme;
196         $Theme->addMoreHeaders($this->RatingWidgetJavascript());
197     }
198
199     // todo: only for signed users
200     // todo: set rating dbi for external rating database
201     function run($dbi, $argstr, $request, $basepage) {
202         global $Theme;
203         $this->_request = & $request;
204         $this->_dbi = & $dbi;
205         $user = & $request->getUser();
206         $this->userid = $user->UserName();
207         $args = $this->getArgs($argstr, $request);
208         $this->dimension = $args['dimension'];
209         if ($this->dimension == '') {
210             $this->dimension = 0;
211             $args['dimension'] = 0;
212         }
213         if ($args['pagename']) {
214             // Expand relative page names.
215             $page = new WikiPageName($args['pagename'], $basepage);
216             $args['pagename'] = $page->name;
217         }
218         if (empty($args['pagename'])) {
219             return $this->error(_("no page specified"));
220         }
221         $this->pagename = $args['pagename'];
222
223         if (RATING_STORAGE == 'SQL') {
224             $dbi = &$this->_dbi->_backend;
225             if (isa($dbi,'WikiDB_backend_PearDB'))
226                 $this->dbtype = "PearDB";
227             else
228                 $this->dbtype = "ADODB";
229             $this->iter_class = "WikiDB_backend_".$this->dbtype."_generic_iter";
230             extract($dbi->_table_names);
231             if (empty($rating_tbl)) {
232                 $rating_tbl = (!empty($GLOBALS['DBParams']['prefix']) 
233                                ? $GLOBALS['DBParams']['prefix'] : '') . 'rating';
234                 $dbi->_table_names['rating_tbl'] = $rating_tbl;
235             }
236         }
237
238         if ($args['mode'] === 'add') {
239             if (!$user->isSignedIn())
240                 return $this->error(_("You must sign in"));
241             global $Theme;
242             $actionImg = $Theme->_path . $this->actionImgPath();
243             $this->addRating($request->getArg('rating'));
244             ob_end_clean();  // discard any previous output
245             // delete the cache
246             $page = $request->getPage();
247             $page->set('_cached_html', false);
248             $request->cacheControl('MUST-REVALIDATE');
249             $dbi->touch();
250             //fake validators without args
251             $request->appendValidators(array('wikiname' => WIKI_NAME,
252                                              'args'     => hash('')));
253             header('Content-type: image/png');
254             readfile($actionImg);
255             exit();
256         } elseif ($args['mode'] === 'delete') {
257             if (!$user->isSignedIn())
258                 return $this->error(_("You must sign in"));
259             global $Theme;
260             $actionImg = $Theme->_path . $this->actionImgPath();
261             $this->deleteRating();
262             ob_end_clean();  // discard any previous output
263             // delete the cache
264             $page = $request->getPage();
265             $page->set('_cached_html', false);
266             $request->cacheControl('MUST-REVALIDATE');
267             $dbi->touch();
268             //fake validators without args
269             $request->appendValidators(array('wikiname' => WIKI_NAME,
270                                              'args'     => hash('')));
271             header('Content-type: image/png');
272             readfile($actionImg);
273             exit();
274         } elseif (! $args['show'] ) {
275             // we must use the head method instead, because <body> is already printed.
276             // $Theme->addMoreHeaders($this->RatingWidgetJavascript()); 
277             // or we change the header in the ob_buffer.
278
279             //Todo: add a validator based on the users last rating mtime
280             $rating = $this->getRating();
281             /*
282                 static $validated = 0;
283                 if (!$validated) {
284                 //$page = $request->getPage();
285                 //$page->set('_cached_html', false);
286                   $request->cacheControl('REVALIDATE');
287                   $validated = 1;
288                 }
289             */
290             $args['rating'] = $rating;
291             return $this->RatingWidgetHtml($args);
292         } else {
293             if (!$user->isSignedIn())
294                 return $this->error(_("You must sign in"));
295             extract($args);
296             $rating = $this->getRating();
297             $html = HTML::p(sprintf(_("Rated by %d users | Average rating %.1f stars"),
298                                     $this->getNumUsers($this->pagename,$this->dimension),
299                                     $this->getAvg($this->pagename,$this->dimension)),
300                             HTML::br());
301             if ($rating !== false)
302                 $html->pushContent(sprintf(_("Your rating was %.1f"),
303                                            $rating));
304             else {
305                 $pred = $this->getPrediction($this->userid,$this->pagename,$this->dimension);
306                 if (is_string($pred))
307                     $html->pushContent(sprintf(_("%s prediction for you is %s stars"),
308                                                WIKI_NAME, $pred));
309                 elseif ($pred)
310                     $html->pushContent(sprintf(_("%s prediction for you is %.1f stars"),
311                                                WIKI_NAME, $pred));
312             }
313             $html->pushContent(HTML::p());
314             $html->pushContent(HTML::em("(Experimental: This is entirely bogus data)"));
315             return $html;
316         }
317     }
318
319     // box is used to display a fixed-width, narrow version with common header
320     function box($args=false, $request=false, $basepage=false) {
321         if (!$request) $request =& $GLOBALS['request'];
322         if (!$request->_user->isSignedIn()) return;
323         if (!isset($args)) $args = array();
324         $args['small'] = 1;
325         $argstr = '';
326         foreach ($args as $key => $value)
327             $argstr .= $key."=".$value;
328         $widget = $this->run($request->_dbi, $argstr, $request, $basepage);
329
330         return $this->makeBox(WikiLink(_("RateIt"),'',_("Rate It")),
331                               $widget);
332     }
333
334     function addRating($rating, $userid=null, $pagename=null, $dimension=null) {
335         if (is_null($dimension)) $dimension = $this->dimension;
336         if (is_null($userid))    $userid = $this->userid; 
337         if (is_null($pagename))  $pagename = $this->pagename;
338         if (RATING_STORAGE == 'SQL') {
339             $page = $this->_dbi->getPage($this->pagename);
340             $current = $page->getCurrentRevision();
341             $rateeversion = $current->getVersion();
342             $this->sql_rate($userid, $pagename, $rateeversion, $dimension, $rating);
343         } else {
344             $this->metadata_set_rating($userid, $pagename, $dimension, $rating);
345         }
346     }
347
348     function deleteRating($userid=null, $pagename=null, $dimension=null) {
349         if (is_null($dimension)) $dimension = $this->dimension;
350         if (is_null($userid))    $userid = $this->userid; 
351         if (is_null($pagename))  $pagename = $this->pagename;
352         if (RATING_STORAGE == 'SQL') {
353             $this->sql_delete_rating($userid, $pagename, $dimension);
354         } else {
355             $this->metadata_set_rating($userid, $pagename, $dimension, -1);
356         }
357     }
358
359     function getRating($userid=null, $pagename=null, $dimension=null) {
360         if (is_null($dimension)) $dimension = $this->dimension;
361         if (is_null($userid))    $userid = $this->userid; 
362         if (is_null($pagename))  $pagename = $this->pagename;
363         if (RATING_STORAGE == 'SQL') {
364             $ratings_iter = $this->sql_get_rating($dimension, $userid, $pagename);
365             if ($rating = $ratings_iter->next()) {
366                 return $rating['ratingvalue'];
367             } else 
368                 return false;
369         } else {
370             return $this->metadata_get_rating($userid, $pagename, $dimension);
371         }
372     }
373
374     function getUsersRated($dimension=null, $orderby = null) {
375         if (is_null($dimension)) $dimension = $this->dimension;
376         if (is_null($userid))    $userid = $this->userid; 
377         if (is_null($pagename))  $pagename = $this->pagename;
378         if (RATING_STORAGE == 'SQL') {
379             $ratings_iter = $this->sql_get_users_rated($dimension, $orderby);
380             if ($rating = $ratings_iter->next()) {
381                 return $rating['ratingvalue'];
382             } else 
383                 return false;
384         } else {
385             return $this->metadata_get_users_rated($dimension, $orderby);
386         }
387     }
388
389     // TODO
390     // Currently we have to call the "suggest" CGI
391     //   http://www-users.cs.umn.edu/~karypis/suggest/
392     // until we implement a simple recommendation engine.
393     // Note that "suggest" is only free for non-profit organizations.
394     // I am currently writing a binary CGI using suggest, which loads 
395     // data from mysql.
396     function getPrediction($userid=null, $pagename=null, $dimension=null) {
397         if (is_null($dimension)) $dimension = $this->dimension;
398         if (is_null($userid))    $userid   = $this->userid; 
399         if (is_null($pagename))  $pagename = $this->pagename;
400         $dbi = &$this->_dbi->_backend;
401         if (isset($pagename))
402             $page = $dbi->_get_pageid($pagename);
403         else return 0;
404         if (isset($userid))
405             $user = $dbi->_get_pageid($userid);
406         else return 0;
407         
408         return 0;
409         
410         if (defined('RATING_EXTERNAL')) {
411             // how call suggest.exe? as CGI or natively
412             //$rating = HTML::Raw("<!--#include virtual=".RATING_ENGINE." -->");
413             $args = "-u$user -p$page -malpha"; // --top 10
414             if (isset($dimension))
415                 $args .= " -d$dimension";
416             $rating = passthru(RATING_EXTERNAL . " $args");
417         } else {
418             $rating = $this->php_prediction($userid, $pagename, $dimension);
419         }
420         return $rating;
421     }
422
423     /**
424      * TODO: slow item-based recommendation engine, similar to suggest RType=2.
425      * Only the SUGGEST_EstimateAlpha part
426      */
427     function php_prediction($userid=null, $pagename=null, $dimension=null) {
428         if (is_null($dimension)) $dimension = $this->dimension;
429         if (is_null($userid))    $userid   = $this->userid; 
430         if (is_null($pagename))  $pagename = $this->pagename;
431         if (RATING_STORAGE == 'SQL') {
432             $rating = 0;
433         } else {
434             $rating = 0;
435         }
436         return $rating;
437     }
438     
439     function getNumUsers($pagename=null, $dimension=null) {
440         if (is_null($dimension)) $dimension = $this->dimension;
441         if (is_null($pagename))  $pagename = $this->pagename;
442         if (RATING_STORAGE == 'SQL') {
443             $ratings_iter = $this->sql_get_rating($dimension, null, $pagename,
444                                                   null, "ratee");
445             return $ratings_iter->count();
446         } else {
447             $page = $this->_dbi->getPage($pagename);
448             $data = $page->get('rating');
449             if (!empty($data[$dimension]))
450                 return count($data[$dimension]);
451             else 
452                 return 0;
453         }
454     }
455     // TODO: metadata method
456     function getAvg($pagename=null, $dimension=null) {
457         if (is_null($dimension)) $dimension = $this->dimension;
458         if (is_null($pagename))  $pagename = $this->pagename;
459         if (RATING_STORAGE == 'SQL') {
460             $dbi = &$this->_dbi->_backend;
461             $where = "WHERE 1";
462             if (isset($pagename)) {
463                 $raterid = $dbi->_get_pageid($pagename, true);
464                 $where .= " AND raterpage=$raterid";
465             }
466             if (isset($dimension)) {
467                 $where .= " AND dimension=$dimension";
468             }
469             //$dbh = &$this->_dbi;
470             extract($dbi->_table_names);
471             $query = "SELECT AVG(ratingvalue) as avg"
472                    . " FROM $rating_tbl r, $page_tbl p "
473                    . $where. " GROUP BY raterpage";
474             $result = $dbi->_dbh->query($query);
475             $iter = new $this->iter_class($this,$result);
476             $row = $iter->next();
477             return $row['avg'];
478         } else {
479             return 2.5;
480         }
481     }
482
483     /**
484      * Get ratings.
485      *
486      * @param dimension  The rating dimension id.
487      *                   Example: 0
488      *                   [optional]
489      *                   If this is null (or left off), the search for ratings
490      *                   is not restricted by dimension.
491      *
492      * @param rater  The page id of the rater, i.e. page doing the rating.
493      *               This is a Wiki page id, often of a user page.
494      *               Example: "DanFr"
495      *               [optional]
496      *               If this is null (or left off), the search for ratings
497      *               is not restricted by rater.
498      *               TODO: Support an array
499      *
500      * @param ratee  The page id of the ratee, i.e. page being rated.
501      *               Example: "DudeWheresMyCar"
502      *               [optional]
503      *               If this is null (or left off), the search for ratings
504      *               is not restricted by ratee.
505      *               TODO: Support an array
506      *
507      * @param orderby An order-by clause with fields and (optionally) ASC
508      *                or DESC.
509      *               Example: "ratingvalue DESC"
510      *               [optional]
511      *               If this is null (or left off), the search for ratings
512      *               has no guaranteed order
513      *
514      * @param pageinfo The type of page that has its info returned (i.e.,
515      *               'pagename', 'hits', and 'pagedata') in the rows.
516      *               Example: "rater"
517      *               [optional]
518      *               If this is null (or left off), the info returned
519      *               is for the 'ratee' page (i.e., thing being rated).
520      *
521      * @return DB iterator with results 
522      */
523     function sql_get_rating($dimension=null, $rater=null, $ratee=null,
524                             $orderby=null, $pageinfo = "ratee") {
525         if (empty($dimension)) $dimension=null;
526         $result = $this->_sql_get_rating_result($dimension, $rater, $ratee, $orderby, $pageinfo);
527         return new $this->iter_class($this, $result);
528     }
529
530     function sql_get_users_rated($dimension=null, $orderby=null) {
531         if (empty($dimension)) $dimension=null;
532         $result = $this->_sql_get_rating_result($dimension, null, null, $orderby, "rater");
533         return new $this->iter_class($this, $result);
534     }
535
536     /**
537      * @access private
538      * @return result ressource, suitable to the iterator
539      */
540     function _sql_get_rating_result($dimension=null, $rater=null, $ratee=null,
541                                     $orderby=null, $pageinfo = "ratee") {
542         // pageinfo must be 'rater' or 'ratee'
543         if (($pageinfo != "ratee") && ($pageinfo != "rater"))
544             return;
545
546         $dbi = &$this->_dbi->_backend;
547         //$dbh = &$this->_dbi;
548         extract($dbi->_table_names);
549         $where = "WHERE r." . $pageinfo . "page = p.id";
550         if (isset($dimension)) {
551             $where .= " AND dimension=$dimension";
552         }
553         if (isset($rater)) {
554             $raterid = $dbi->_get_pageid($rater, true);
555             $where .= " AND raterpage=$raterid";
556         }
557         if (isset($ratee)) {
558             $rateeid = $dbi->_get_pageid($ratee, true);
559             $where .= " AND rateepage=$rateeid";
560         }
561         $orderbyStr = "";
562         if (isset($orderby)) {
563             $orderbyStr = " ORDER BY " . $orderby;
564         }
565         if (isset($rater) or isset($ratee)) $what = '*';
566         // same as _get_users_rated_result()
567         else $what = 'DISTINCT p.pagename';
568
569         $query = "SELECT $what"
570                . " FROM $rating_tbl r, $page_tbl p "
571                . $where
572                . $orderbyStr;
573
574         $result = $dbi->_dbh->query($query);
575         return $result;
576     }
577
578     /**
579      * Delete a rating.
580      *
581      * @param rater  The page id of the rater, i.e. page doing the rating.
582      *               This is a Wiki page id, often of a user page.
583      * @param ratee  The page id of the ratee, i.e. page being rated.
584      * @param dimension  The rating dimension id.
585      *
586      * @access public
587      *
588      * @return true upon success
589      */
590     function sql_delete_rating($rater, $ratee, $dimension) {
591         //$dbh = &$this->_dbi;
592         $dbi = &$this->_dbi->_backend;
593         extract($dbi->_table_names);
594
595         $dbi->lock();
596         $raterid = $dbi->_get_pageid($rater, true);
597         $rateeid = $dbi->_get_pageid($ratee, true);
598         $where = "WHERE raterpage=$raterid and rateepage=$rateeid";
599         if (isset($dimension)) {
600             $where .= " AND dimension=$dimension";
601         }
602         $dbi->_dbh->query("DELETE FROM $rating_tbl $where");
603         $dbi->unlock();
604         return true;
605     }
606
607     /**
608      * Rate a page.
609      *
610      * @param rater  The page id of the rater, i.e. page doing the rating.
611      *               This is a Wiki page id, often of a user page.
612      * @param ratee  The page id of the ratee, i.e. page being rated.
613      * @param rateeversion  The version of the ratee page.
614      * @param dimension  The rating dimension id.
615      * @param rating The rating value (a float).
616      *
617      * @access public
618      *
619      * @return true upon success
620      */
621     //               ($this->userid, $this->pagename, $this->dimension, $rating);
622     function sql_rate($rater, $ratee, $rateeversion, $dimension, $rating) {
623         $dbi = &$this->_dbi->_backend;
624         extract($dbi->_table_names);
625         if (empty($rating_tbl))
626             $rating_tbl = $this->_dbi->getParam('prefix') . 'rating';
627
628         //$dbi->lock();
629         $raterid = $dbi->_get_pageid($rater, true);
630         $rateeid = $dbi->_get_pageid($ratee, true);
631         assert($raterid);
632         assert($rateeid);
633         $where = "WHERE raterpage='$raterid' AND rateepage='$rateeid'";
634         if (isset($dimension)) $where .= " AND dimension='$dimension'";
635         // atomic transaction:
636         $dbi->_dbh->query("UPDATE $rating_tbl SET ratingvalue='$rating', rateeversion='$rateeversion' $where");
637
638         /*
639         $dbi->_dbh->query("DELETE FROM $rating_tbl $where");
640         // NOTE: Leave tstamp off the insert, and MySQL automatically updates it (only if MySQL is used)
641         $dbi->_dbh->query("INSERT INTO $rating_tbl (dimension, raterpage, rateepage, ratingvalue, rateeversion) VALUES ('$dimension', $raterid, $rateeid, '$rating', '$rateeversion')");
642         */
643         //$dbi->unlock();
644         return true;
645     }
646
647     function metadata_get_rating($userid, $pagename, $dimension) {
648         $page = $this->_dbi->getPage($pagename);
649         $data = $page->get('rating');
650         if (!empty($data[$dimension][$userid]))
651             return (float)$data[$dimension][$userid];
652         else 
653             return false;
654     }
655
656     function metadata_set_rating($userid, $pagename, $dimension, $rating = -1) {
657         $page = $this->_dbi->getPage($pagename);
658         $data = $page->get('rating');
659         if ($rating == -1)
660             unset($data[$dimension][$userid]);
661         else {
662             if (empty($data[$dimension][$userid]))
663                 $data[$dimension] = array($userid => (float)$rating);
664             else
665                 $data[$dimension][$userid] = $rating;
666         }
667         $page->set('rating',$data);
668     }
669
670     /**
671      * HTML widget display
672      *
673      * This needs to be put in the <body> section of the page.
674      *
675      * @param pagename    Name of the page to rate
676      * @param version     Version of the page to rate (may be "" for current)
677      * @param imgPrefix   Prefix of the names of the images that display the rating
678      *                    You can have two widgets for the same page displayed at
679      *                    once iff the imgPrefix-s are different.
680      * @param dimension   Id of the dimension to rate
681      * @param small       Makes a smaller ratings widget if non-false
682      *
683      * Limitations: Currently this can only print the current users ratings.
684      *              And only the widget, but no value (for buddies) also.
685      */
686     function RatingWidgetHtml($args) {
687         global $Theme, $request;
688         extract($args);
689         if (!$request->_user->isSignedIn()) return;
690         $imgPrefix = $pagename . $imgPrefix;
691         $actionImgName = $imgPrefix . 'RateItAction';
692         $dbi =& $GLOBALS['request']->getDbh();
693         $version = $dbi->_backend->get_latest_version($pagename);
694
695         // Protect against 's, though not \r or \n
696         $reImgPrefix     = $this->_javascript_quote_string($imgPrefix);
697         $reActionImgName = $this->_javascript_quote_string($actionImgName);
698         $rePagename      = $this->_javascript_quote_string($pagename);
699         //$dimension = $args['pagename'] . "rat";
700     
701         $html = HTML::span(array("id" => $id));
702         for ($i=0; $i < 2; $i++) {
703             $nk[$i]   = $Theme->_findData("images/RateItNk$i.png");
704             $none[$i] = $Theme->_findData("images/RateItRk$i.png");
705         }
706         if (!$small) {
707             $html->pushContent(Button(_("RateIt"),_("RateIt"), $pagename));
708             $html->pushContent(HTML::raw('&nbsp;'));
709         }
710        
711
712         $user = $request->getUser();
713         $userid = $user->getId();
714         if (!isset($args['rating']))
715             $rating = $this->getRating($userid, $pagename, $dimension);
716         if (!$rating) {
717             $pred = $this->getPrediction($userid,$pagename,$dimension);
718         }
719         for ($i = 1; $i <= 10; $i++) {
720             $a1 = HTML::a(array('href' => 'javascript:click(\'' . $reActionImgName . '\',\'' . $rePagename . '\',\'' . $version . '\',\'' . $reImgPrefix . '\',\'' . $dimension . '\',' . ($i/2) . ')'));
721             $img_attr = array();
722             $img_attr['src'] = $nk[$i%2];
723             if (!$rating and !$pred)
724                 $img_attr['src'] = $none[$i%2];
725             $img_attr['name'] = $imgPrefix . $i;
726             $img_attr['border'] = 0;
727             $a1->pushContent(HTML::img($img_attr));
728             $a1->addToolTip(_("Rate the topic of this page"));
729             $html->pushContent($a1);
730             //This adds a space between the rating smilies:
731             // if (($i%2) == 0) $html->pushContent(' ');
732         }
733         $html->pushContent(HTML::Raw('&nbsp;'));
734         $a0 = HTML::a(array('href' => 'javascript:click(\'' . $reActionImgName . '\',\'' . $rePagename . '\',\'' . $version . '\',\'' . $reImgPrefix . '\',\'' . $dimension . '\',\'X\')'));
735         if ($rating) {
736             $msg = _("Cancel rating");
737             $a0->pushContent(HTML::img(array('src' => $Theme->getImageUrl("RateItCancel"),
738                                              'name'=> $imgPrefix.'Cancel',
739                                              'alt' => $msg)));
740             $a0->addToolTip($msg);
741             $html->pushContent($a0);
742         } elseif ($pred) {
743             $msg = _("No opinion");
744             $html->pushContent(HTML::img(array('src' => $Theme->getImageUrl("RateItCancelN"),
745                                                'name'=> $imgPrefix.'Cancel',
746                                                'alt' => $msg)));
747             //$a0->addToolTip($msg);
748             //$html->pushContent($a0);
749         }
750         $img_attr = array();
751         $img_attr['src'] = $Theme->_findData("images/RateItAction.png");
752         $img_attr['name'] = $actionImgName;
753         //$img_attr['class'] = 'k' . $i;
754         $img_attr['border'] = 0;
755         $html->pushContent(HTML::img($img_attr));
756         // Display the current rating if there is one
757         if ($rating) 
758             $html->pushContent(JavaScript('displayRating(\'' . $reImgPrefix . '\','.$rating .',0)'));
759         elseif ($pred)
760             $html->pushContent(JavaScript('displayRating(\'' . $reImgPrefix . '\','.$pred .',1)'));
761         else 
762             $html->pushContent(JavaScript('displayRating(\'' . $reImgPrefix . '\',0,0)'));    
763         return $html;
764     }
765
766 };
767
768
769 // $Log: not supported by cvs2svn $
770 // Revision _1.2  2004/04/29 17:55:03  dfrankow
771 // Check in escape() changes to protect against leading spaces in pagename.
772 // This is untested with Reini's _("RateIt") additions to this plugin.
773 //
774 // Revision 1.7  2004/04/21 04:29:50  rurban
775 // write WikiURL consistently (not WikiUrl)
776 //
777 // Revision 1.6  2004/04/12 14:07:12  rurban
778 // more docs
779 //
780 // Revision 1.5  2004/04/11 10:42:02  rurban
781 // pgsrc/CreatePagePlugin
782 //
783 // Revision 1.4  2004/04/06 20:00:11  rurban
784 // Cleanup of special PageList column types
785 // Added support of plugin and theme specific Pagelist Types
786 // Added support for theme specific UserPreferences
787 // Added session support for ip-based throttling
788 //   sql table schema change: ALTER TABLE session ADD sess_ip CHAR(15);
789 // Enhanced postgres schema
790 // Added DB_Session_dba support
791 //
792 // Revision 1.3  2004/04/01 06:29:51  rurban
793 // better wording
794 // RateIt also for ADODB
795 //
796 // Revision 1.2  2004/03/31 06:22:22  rurban
797 // shorter javascript,
798 // added prediction buttons and display logic,
799 // empty HTML if not signed in.
800 // fixed deleting (empty dimension => 0)
801 //
802 // Revision 1.1  2004/03/30 02:38:06  rurban
803 // RateIt support (currently no recommendation engine yet)
804 //
805
806 // For emacs users
807 // Local Variables:
808 // mode: php
809 // tab-width: 8
810 // c-basic-offset: 4
811 // c-hanging-comment-ender-p: nil
812 // indent-tabs-mode: nil
813 // End:
814 ?>