]> CyberLeo.Net >> Repos - SourceForge/phpwiki.git/blob - lib/plugin/RateIt.php
function run: @return mixed
[SourceForge/phpwiki.git] / lib / plugin / RateIt.php
1 <?php
2
3 /*
4  * Copyright 2004,2007,2009 $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 along
19  * with PhpWiki; if not, write to the Free Software Foundation, Inc.,
20  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 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:    <<RateIt >>          just the widget without text
68  *   Note: The wikilens theme or any derivate must be enabled, to enable this plugin!
69  *           <<RateIt show=top >> text plus widget below
70  *           <<RateIt show=ratings >> to show my ratings
71  *   TODO:   <<RateIt show=buddies >> to show my buddies
72  *           <<RateIt show=ratings dimension=1 >>
73  *   TODO:   <<RateIt show=text >> just text, no widget, for dumps
74  *
75  * @author:  Dan Frankowski (wikilens author), Reini Urban (as plugin)
76  *
77  * TODO:
78  * - finish mysuggest.c (external engine with data from mysql)
79  */
80
81 require_once 'lib/WikiPlugin.php';
82 require_once 'lib/wikilens/RatingsDb.php';
83
84 class WikiPlugin_RateIt
85     extends WikiPlugin
86 {
87     static $toBeUniq = 1;
88     public $idTop = '';
89
90     function getDescription()
91     {
92         return _("Rating system. Store user ratings per page.");
93     }
94
95     function RatingWidgetJavascript()
96     {
97         global $WikiTheme;
98         if (!empty($this->imgPrefix))
99             $imgPrefix = $this->imgPrefix;
100         elseif (defined("RATEIT_IMGPREFIX"))
101             $imgPrefix = RATEIT_IMGPREFIX; else $imgPrefix = '';
102         if ($imgPrefix and !$WikiTheme->_findData("images/RateIt" . $imgPrefix . "Nk0.png", 1))
103             $imgPrefix = '';
104         $img = substr($WikiTheme->_findData("images/RateIt" . $imgPrefix . "Nk0.png"), 0, -7);
105         $urlprefix = WikiURL("", 0, 1); // TODO: check actions USE_PATH_INFO=false
106         $js_globals = "var rateit_imgsrc = '" . $img . "';
107 var rateit_action = '" . urlencode("RateIt") . "';
108 ";
109         $WikiTheme->addMoreHeaders
110         (JavaScript('',
111             array('src' => $WikiTheme->_findData('themes/wikilens/wikilens.js'))));
112         return JavaScript($js_globals);
113     }
114
115     function actionImgPath()
116     {
117         global $WikiTheme;
118         return $WikiTheme->_findFile("images/RateItAction.png", 1);
119     }
120
121     /**
122      * Take a string and quote it sufficiently to be passed as a Javascript
123      * string between ''s
124      */
125     private function _javascript_quote_string($s)
126     {
127         return str_replace("'", "\'", $s);
128     }
129
130     function getDefaultArguments()
131     {
132         return array('pagename' => '[pagename]',
133             'version' => false,
134             'id' => 'rateit',
135             'imgPrefix' => '', // '' or BStar or Star
136             'dimension' => false,
137             'small' => false,
138             'show' => false,
139             'mode' => false,
140         );
141     }
142
143     function head()
144     { // early side-effects (before body)
145         global $WikiTheme;
146         static $_already;
147         if (!empty($_already)) return;
148         $_already = 1;
149         $WikiTheme->addMoreHeaders(JavaScript(
150             "var prediction = new Array; var rating = new Array;
151 var avg = new Array; var numusers = new Array;
152 var canRate = new Array;
153 var msg_rating_votes = '" . _("Rating: %.1f (%d votes)") . "';
154 var msg_curr_rating = '" . _("Your current rating: ") . "';
155 var msg_curr_prediction = '" . _("Your current prediction: ") . "';
156 var msg_chg_rating = '" . _("Change your rating from ") . "';
157 var msg_to = '" . _(" to ") . "';
158 var msg_add_rating = '" . _("Add your rating: ") . "';
159 var msg_thanks = '" . _("Thanks!") . "';
160 var msg_rating_deleted = '" . _("Rating deleted!") . "';
161 "));
162         $WikiTheme->addMoreHeaders($this->RatingWidgetJavascript());
163     }
164
165     function displayActionImg($mode)
166     {
167         global $WikiTheme, $request;
168         if (!empty($request->_is_buffering_output))
169             ob_end_clean(); // discard any previous output
170         // delete the cache
171         $page = $request->getPage();
172         //$page->set('_cached_html', false);
173         $request->cacheControl('MUST-REVALIDATE');
174         $dbi = $request->getDbh();
175         $dbi->touch();
176         //fake validators without args
177         $request->appendValidators(array('wikiname' => WIKI_NAME,
178             'args' => wikihash('')));
179         $request->discardOutput();
180         $actionImg = $WikiTheme->_path . $this->actionImgPath();
181         if (file_exists($actionImg)) {
182             header('Content-type: image/png');
183             readfile($actionImg);
184         } else {
185             header('Content-type: image/png');
186             echo base64_decode('iVBORw0KGgoAAAANSUhEUgAAAAIAAAACAQMAAABIeJ9nAAAAA1BMVEX///'
187                 . '+nxBvIAAAAAXRSTlMAQObYZgAAABNJREFUeF4NwAEBAAAAgJD+r5YGAAQAAXHhfPAAAAAASUVORK5CYII=');
188         }
189         exit;
190     }
191
192     // Only for signed users done in template only yet.
193     /**
194      * @param WikiDB $dbi
195      * @param string $argstr
196      * @param WikiRequest $request
197      * @param string $basepage
198      * @return mixed
199      */
200     function run($dbi, $argstr, &$request, $basepage)
201     {
202         global $WikiTheme;
203         //$this->_request = & $request;
204         //$this->_dbi = & $dbi;
205         $user = $request->getUser();
206         //FIXME: fails on test with DumpHtml:RateIt
207         if (!is_object($user)) {
208             return HTML::raw('');
209         }
210         $this->userid = $user->getId();
211         if (!$this->userid) {
212             return HTML::raw('');
213         }
214         $args = $this->getArgs($argstr, $request);
215         $this->dimension = $args['dimension'];
216         $this->imgPrefix = $args['imgPrefix'];
217         if ($this->dimension == '') {
218             $this->dimension = 0;
219             $args['dimension'] = 0;
220         }
221         if ($args['pagename']) {
222             // Expand relative page names.
223             $page = new WikiPageName($args['pagename'], $basepage);
224             $args['pagename'] = $page->name;
225         }
226         if (empty($args['pagename'])) {
227             return $this->error(_("no page specified"));
228         }
229         $this->pagename = $args['pagename'];
230
231         $rdbi = RatingsDb::getTheRatingsDb();
232         $this->_rdbi =& $rdbi;
233
234         if ($args['mode'] === 'add') {
235             //if (!$user->isSignedIn()) return $this->error(_("You must sign in"));
236             $this->rating = $request->getArg('rating');
237             $rdbi->addRating($this->rating, $this->userid, $this->pagename, $this->dimension);
238             $this->displayActionImg('add');
239
240         } elseif ($args['mode'] === 'delete') {
241             //if (!$user->isSignedIn()) return $this->error(_("You must sign in"));
242             $rdbi->deleteRating($this->userid, $this->pagename, $this->dimension);
243             unset($this->rating);
244             $this->displayActionImg('delete');
245         } elseif (!$args['show']) {
246             return $this->RatingWidgetHtml($args['pagename'], $args['version'], $args['imgPrefix'],
247                 $args['dimension'], $args['small']);
248         } else {
249             //if (!$user->isSignedIn()) return $this->error(_("You must sign in"));
250             //extract($args);
251             $this->rating = $rdbi->getRating($this->userid, $this->pagename, $this->dimension);
252             $this->avg = $rdbi->getAvg($this->pagename, $this->dimension);
253             $this->numusers = $rdbi->getNumUsers($this->pagename, $this->dimension);
254             // Update this text on rateit in javascript. needed: NumUsers, Avg
255             $html = HTML::div
256             (
257                 HTML::span(array('class' => 'rateit'),
258                     sprintf(_("Rating: %.1f (%d votes)"),
259                         $this->avg, $this->numusers)));
260             if ($args['show'] == 'top') {
261                 if (ENABLE_PAGE_PUBLIC) {
262                     $page = $dbi->getPage($this->pagename);
263                     if ($page->get('public'))
264                         $html->setAttr('class', "public");
265                 }
266                 $html->setAttr('id', "rateit-widget-top");
267                 $html->pushContent(HTML::br(),
268                     $this->RatingWidgetHtml($args['pagename'], $args['version'],
269                         $args['imgPrefix'],
270                         $args['dimension'], $args['small']));
271             } elseif ($args['show'] == 'text') {
272                 if (!$WikiTheme->DUMP_MODE)
273                     $html->pushContent(HTML::br(),
274                         sprintf(_("Your rating was %.1f"),
275                             $this->rating));
276             } elseif ($this->rating) {
277                 $html->pushContent(HTML::br(),
278                     sprintf(_("Your rating was %.1f"),
279                         $this->rating));
280             } else {
281                 $this->pred = $rdbi->getPrediction($this->userid, $this->pagename, $this->dimension);
282                 if (is_string($this->pred))
283                     $html->pushContent(HTML::br(),
284                         sprintf(_("Prediction: %s"),
285                             $this->pred));
286                 elseif ($this->pred)
287                     $html->pushContent(HTML::br(),
288                         sprintf(_("Prediction: %.1f"),
289                             $this->pred));
290             }
291             return $html;
292         }
293         return HTML::raw('');
294     }
295
296     // box is used to display a fixed-width, narrow version with common header
297     function box($args = false, $request = false, $basepage = false)
298     {
299         if (!$request) $request =& $GLOBALS['request'];
300         if (!$request->_user->isSignedIn()) {
301             return HTML::raw('');
302         }
303         if (!isset($args)) $args = array();
304         $args['small'] = 1;
305         $argstr = '';
306         foreach ($args as $key => $value)
307             $argstr .= $key . "=" . $value;
308         $widget = $this->run($request->_dbi, $argstr, $request, $basepage);
309
310         return $this->makeBox(WikiLink(_("RateIt"), '', _("Rate It")),
311             $widget);
312     }
313
314     /**
315      * HTML widget display
316      *
317      * This needs to be put in the <body> section of the page.
318      *
319      * @param string $pagename    Name of the page to rate
320      * @param int $version     Version of the page to rate (may be "" for current)
321      * @param string $imgPrefix   Prefix of the names of the images that display the rating
322      *                    You can have two widgets for the same page displayed at
323      *                    once iff the imgPrefix-s are different.
324      * @param int $dimension   Id of the dimension to rate
325      * @param bool $small       Makes a smaller ratings widget if non-false
326      *
327      * Limitations: Currently this can only print the current users ratings.
328      *              And only the widget, but no value (for buddies) also.
329      * @return $this
330      */
331     function RatingWidgetHtml($pagename, $version, $imgPrefix, $dimension, $small = false)
332     {
333         global $WikiTheme, $request;
334
335         $dbi =& $request->_dbi;
336         $version = $dbi->_backend->get_latest_version($pagename);
337         $pageid = sprintf("%u", crc32($pagename)); // MangleXmlIdentifier($pagename)
338         $imgId = 'RateIt' . $pageid;
339         $actionImgName = 'RateIt' . $pageid . 'Action';
340
341         //$rdbi =& $this->_rdbi;
342         $rdbi = RatingsDb::getTheRatingsDb();
343
344         // check if the imgPrefix icons exist.
345         if (!$WikiTheme->_findData("images/RateIt" . $imgPrefix . "Nk0.png", true))
346             $imgPrefix = '';
347
348         // Protect against \'s, though not \r or \n
349         $reImgPrefix = $this->_javascript_quote_string($imgPrefix);
350         $reImgId = $this->_javascript_quote_string($imgId);
351         $reActionImgName = $this->_javascript_quote_string($actionImgName);
352         $rePagename = $this->_javascript_quote_string($pagename);
353         //$dimension = $args['pagename'] . "rat";
354
355         $html = HTML::span(array("class" => "rateit-widget", "id" => $imgId));
356         for ($i = 0; $i < 2; $i++) {
357             $ok[$i] = $WikiTheme->_findData("images/RateIt" . $imgPrefix . "Ok" . $i . ".png"); // empty
358             $nk[$i] = $WikiTheme->_findData("images/RateIt" . $imgPrefix . "Nk" . $i . ".png"); // rated
359             $rk[$i] = $WikiTheme->_findData("images/RateIt" . $imgPrefix . "Rk" . $i . ".png"); // pred
360         }
361
362         if (empty($this->userid)) {
363             $user = $request->getUser();
364             $this->userid = $user->getId();
365         }
366         if (empty($this->rating)) {
367             $this->rating = $rdbi->getRating($this->userid, $pagename, $dimension);
368             if (!$this->rating and empty($this->pred)) {
369                 $this->pred = $rdbi->getPrediction($this->userid, $pagename, $dimension);
370             }
371         }
372
373         for ($i = 1; $i <= 10; $i++) {
374             $j = $i / 2;
375             $a1 = HTML::a(array('href' => "javascript:clickRating('$reImgPrefix','$rePagename','$version',"
376                 . "'$reImgId','$dimension',$j)"));
377             $img_attr = array();
378             $img_attr['src'] = $nk[$i % 2];
379             if ($this->rating) {
380                 $img_attr['src'] = $ok[$i % 2];
381                 $img_attr['onmouseover'] = "displayRating('$reImgId','$reImgPrefix',$j,0,1)";
382                 $img_attr['onmouseout'] = "displayRating('$reImgId','$reImgPrefix',$this->rating,0,1)";
383             } elseif (!$this->rating and $this->pred) {
384                 $img_attr['src'] = $rk[$i % 2];
385                 $img_attr['onmouseover'] = "displayRating('$reImgId','$reImgPrefix',$j,1,1)";
386                 $img_attr['onmouseout'] = "displayRating('$reImgId','$reImgPrefix',$this->pred,1,1)";
387             } else {
388                 $img_attr['onmouseover'] = "displayRating('$reImgId','$reImgPrefix',$j,0,1)";
389                 $img_attr['onmouseout'] = "displayRating('$reImgId','$reImgPrefix',0,0,1)";
390             }
391             //$imgName = 'RateIt'.$reImgId.$i;
392             $img_attr['id'] = $imgId . $i;
393             $img_attr['alt'] = $img_attr['id'];
394             $a1->pushContent(HTML::img($img_attr));
395             //$a1->addToolTip(_("Rate the topic of this page"));
396             $html->pushContent($a1);
397
398             //This adds a space between the rating smilies:
399             //if (($i%2) == 0) $html->pushContent("\n");
400         }
401         $html->pushContent(HTML::raw("&nbsp;"));
402
403         $a0 = HTML::a(array('href' => "javascript:clickRating('$reImgPrefix','$rePagename','$version',"
404             . "'$reImgId','$dimension','X')"));
405         $msg = _("Cancel your rating");
406         $imgprops = array('src' => $WikiTheme->getImageUrl("RateIt" . $imgPrefix . "Cancel"),
407             'id' => $imgId . $imgPrefix . 'Cancel',
408             'alt' => $msg,
409             'title' => $msg);
410         if (!$this->rating)
411             $imgprops['style'] = 'display:none';
412         $a0->pushContent(HTML::img($imgprops));
413         $a0->addToolTip($msg);
414         $html->pushContent($a0);
415
416         /*} elseif ($pred) {
417             $msg = _("No opinion");
418             $html->pushContent(HTML::img(array('src' => $WikiTheme->getImageUrl("RateItCancelN"),
419                                                'id'  => $imgPrefix.'Cancel',
420                                                'alt' => $msg)));
421             //$a0->addToolTip($msg);
422             //$html->pushContent($a0);
423         }*/
424         $img_attr = array();
425         $img_attr['src'] = $WikiTheme->_findData("images/spacer.png");
426         $img_attr['id'] = $actionImgName;
427         $img_attr['alt'] = $img_attr['id'];
428         $img_attr['height'] = 15;
429         $img_attr['width'] = 20;
430         $html->pushContent(HTML::img($img_attr));
431
432         // Display your current rating if there is one, or the current prediction
433         // or the empty widget.
434         $pred = empty($this->pred) ? 0 : $this->pred;
435         $js = '';
436         if (!empty($this->avg))
437             $js .= "avg['$reImgId']=$this->avg; numusers['$reImgId']=$this->numusers;\n";
438         if ($this->rating) {
439             $js .= "rating['$reImgId']=$this->rating; prediction['$reImgId']=$pred;\n";
440             $html->pushContent(JavaScript($js
441                 . "displayRating('$reImgId','$reImgPrefix',$this->rating,0,1);"));
442         } elseif (!empty($this->pred)) {
443             $js .= "rating['$reImgId']=0; prediction['$reImgId']=$this->pred;\n";
444             $html->pushContent(JavaScript($js
445                 . "displayRating('$reImgId','$reImgPrefix',$this->pred,1,1);"));
446         } else {
447             $js .= "rating['$reImgId']=0; prediction['$reImgId']=0;\n";
448             $html->pushContent(JavaScript($js
449                 . "displayRating('$reImgId','$reImgPrefix',0,0,1);"));
450         }
451         return $html;
452     }
453
454 }
455
456 // Local Variables:
457 // mode: php
458 // tab-width: 8
459 // c-basic-offset: 4
460 // c-hanging-comment-ender-p: nil
461 // indent-tabs-mode: nil
462 // End: