A web developer's blog. PHP, MySQL, CakePHP, Zend Framework, Wordpress, Code Igniter, Django, Python, CSS, Javascript, jQuery, Knockout.js, and other web development topics.

Getting the Bayesian Average for rankings (PHP / MySQL)

When you need to correctly display the leader-board based on ratings, you can’t just display the average rating for each entry.

------------------------------------
Restaurant A | 1 Vote(s) | Rating 10
------------------------------------
Restaurant B | 3 Vote(s) | Rating 6
                         | Rating 5
                         | Rating 4
------------------------------------
Restaurant C | 2 Vote(s) | Rating 3
                         | Rating 10

To correctly rank the data above you need to get the Bayesian Average for each restaurant. We have to take into account the number of votes for each restaurant. More votes would push it up the ranking while less votes would have little weight. This means that we if 1 restaurant had 1 vote and with rating of 10, it would not be the number one. (If we just got the average, it would be at the top! – and that would be wrong)

//In PHP
$avg_num_votes = 2;   //The average number of votes for all restaurants (1+3+2)/3 = 2
$avg_rating = 6.3333; //The average rating for all restaurants (10+6+5+4+3+10)/6 = 6.3333
$this_num_votes = 3;  //The number of votes for current restaurant (Restaurant B)
$this_rating = 5;     //The average rating for current restaurant (Restaurant B: (6+5+4)/3 = 5)
 
$bayesian_average = (($avg_num_votes * $avg_rating) + ($this_num_votes * $this_rating)) / ($avg_num_votes + $this_num_votes);

Using the formula above, we would have the following ratings for each restaurant:

Restaurant A would have 7.55553 = ((2*6.3333) + (1*10)) / (2+1)
Restaurant B would have 5.53332 = ((2*6.3333) + (3*5)) / (2+3)
Restaurant C would have 6.41665 = ((2*6.3333) + (2*6.5)) / (2+2)

A quick solution using MySQL View Tables

Create View `ratings` AS
SELECT
    restaurant_id,
    (SELECT count(restaurant_id) FROM ratings) / (SELECT count(DISTINCT restaurant_id) FROM ratings) AS avg_num_votes,
    (SELECT avg(rating) FROM ratings) AS avg_rating,
    count(restaurant_id) as this_num_votes,
    avg(rating) as this_rating
FROM
    ratings
GROUP BY 
    restaurant_id

To get the ratings for the restaurants:

SELECT 
    restaurant_id, 
    ((avg_num_votes * avg_rating) + (this_num_votes * this_rating)) / (avg_num_votes + this_num_votes) as real_rating 
FROM `ratings`

Would output something like:

restaurant_id    real_rating
1                7.555533333333
2                5.533320000000
3                6.416650000000

Sources:
http://blog.linkibol.com/2010/05/07/how-to-build-a-popularity-algorithm-you-can-be-proud-of/
http://snipplr.com/view/22559/

Posted in Uncategorized | Leave a comment

CakePHP Facebook Component

A simple CakePHP component that uses the Facebook PHP SDK (https://github.com/facebook/facebook-php-sdk).

Setup the component like this:

public $components = array(
    'Facebook' => array(
        'appId' => 'xxx',
        'secret' => 'xxx',
        'cookie' => true,
        'fileUpload' => 1,
        'canvas' => 1,
        'fbconnect' => 1,
        'display' => 'page',
        'scope' => 'user_about_me,email,publish_actions,publish_stream,photo_upload',
        'redirect_uri' => 'https://www.facebook.com/pages/My-Test-Page/12345?id=12345&sk=app_12345'
    )
);

Then you should be able to do this in your controllers:

debug($this->userProfile);
debug($this->loginUrl);
debug($this->logoutUrl);
debug($this->hasLiked);
debug($this->signed_request);

Grab it from Github: https://github.com/wenbert/CakePHP-Facebook-Component

Posted in Uncategorized | Leave a comment

Facebook Page Tab: Remove scrollbars and auto-resize height

I have come across many ones, but this one always worked for me without problems.

CSS

html, body { overflow: hidden; }

Put this just after the tag:

<div id="fb-root"></div>

Put this after you have jQuery and other libraries loaded. Don’t forget to put in your App ID.

$(document).ready(function() {
    window.fbAsyncInit = function() {
        FB.init({
            appId: FB_APP_ID,
            cookie: true,
            xfbml: true,
            oauth: true
        });
 
        FB.Canvas.setSize({height:600});
        setTimeout("FB.Canvas.setAutoGrow()",500);
 
    };
    (function() {
        var e = document.createElement('script'); e.async = true;
        e.src = document.location.protocol +
            '//connect.facebook.net/en_US/all.js';
        document.getElementById('fb-root').appendChild(e);
    }());
});

I forgot where I got this from but if anyone knows, flick me an email!

Posted in Uncategorized | Tagged | Leave a comment

Twitter: Get tweets from User Timeline with API 1.1 (PHP)

This code is from http://stackoverflow.com/q/12916539/66767 combined with http://stackoverflow.com/a/15387732/66767 to auto-link URLs and hashtags.

Quick and dirty… copy and paste below!

<?php
function buildBaseString($baseURI, $method, $params) {
    $r = array();
    ksort($params);
    foreach($params as $key=>$value){
        $r[] = "$key=" . rawurlencode($value);
    }
    return $method."&" . rawurlencode($baseURI) . '&' . rawurlencode(implode('&', $r));
}
 
function buildAuthorizationHeader($oauth) {
    $r = 'Authorization: OAuth ';
    $values = array();
    foreach($oauth as $key=>$value)
        $values[] = "$key=\"" . rawurlencode($value) . "\"";
    $r .= implode(', ', $values);
    return $r;
}
 
$url = "https://api.twitter.com/1.1/statuses/user_timeline.json";
 
$oauth_access_token = "XXX";
$oauth_access_token_secret = "XXX";
$consumer_key = "XXX";
$consumer_secret = "XXX";
 
$oauth = array( 'screen_name' => 'wenbert',
                'count' => 2,
                'oauth_consumer_key' => $consumer_key,
                'oauth_nonce' => time(),
                'oauth_signature_method' => 'HMAC-SHA1',
                'oauth_token' => $oauth_access_token,
                'oauth_timestamp' => time(),
                'oauth_version' => '1.0');
 
$base_info = buildBaseString($url, 'GET', $oauth);
$composite_key = rawurlencode($consumer_secret) . '&' . rawurlencode($oauth_access_token_secret);
$oauth_signature = base64_encode(hash_hmac('sha1', $base_info, $composite_key, true));
$oauth['oauth_signature'] = $oauth_signature;
 
// Make Requests
$header = array(buildAuthorizationHeader($oauth), 'Expect:');
$options = array( CURLOPT_HTTPHEADER => $header,
                  //CURLOPT_POSTFIELDS => $postfields,
                  CURLOPT_HEADER => false,
                  CURLOPT_URL => $url.'?screen_name=wenbert&count=2',
                  CURLOPT_RETURNTRANSFER => true,
                  CURLOPT_SSL_VERIFYPEER => false);
 
$feed = curl_init();
curl_setopt_array($feed, $options);
$json = curl_exec($feed);
curl_close($feed);
 
$twitter_data = json_decode($json);
?>
 
<b>Twitter Updates:</b>
<div>
    <?php
    foreach($twitter_data AS $single_tweet){
        //http://stackoverflow.com/a/15387732/66767
        $tweet = $single_tweet->text;
        $tweet = preg_replace("/([\w]+\:\/\/[\w-?&;#~=\.\/\@]+[\w\/])/", "<a target=\"_blank\" href=\"$1\">$1</a>", $tweet);
        $tweet = preg_replace("/#([A-Za-z0-9\/\.]*)/", "<a target=\"_new\" href=\"http://twitter.com/search?q=$1\">#$1</a>", $tweet);
        $tweet = preg_replace("/@([A-Za-z0-9\/\.]*)/", "<a href=\"http://www.twitter.com/$1\">@$1</a>", $tweet);
        echo($tweet);
    }
    ?>
</div>
Posted in Uncategorized | Leave a comment

Setting up File Uploader / FineUploader Basic with CakePHP

The configuration:

<?php
// app/Config/core.php
Configure::write("App.SITE_URL", 'http://www.mysite.com');
Configure::write("App.UPLOAD_PATH", '/usr/local/www/vhosts/mysite.com/httpdocs/app/webroot/uploads'); //no trailing slash
Configure::write("App.UPLOAD_PATH_URL", 'http://mysite/uploads'); //no trailing slash

The setting up the .json and .txt extensions in the routes file.
I need the txt. This is because Internet Explorer does not accept 'application/json'.
Without the .txt, IE will send a “Save as” dialog box when the upload is successful.

// app/Config/routes.php
Router::parseExtensions('json', 'txt');

I have the following for the View:

<?php
// app/View/News/json/fineupload.ctp
echo json_encode($result);

And

<?php
// app/View/News/txt/fineupload.ctp
echo json_encode($result);

Take note that both are in different directories.

In my controller:

<?php
class NewsController extends AppController {
    public $helpers = array('Html', 'Form');
    public $components = array('RequestHandler', 'Security');
 
    public function beforeFilter() {
        parent::beforeFilter();
 
        if (AuthComponent::user('role') === 'admin' OR AuthComponent::user('role') === 'user') {
            $this->Auth->allow('index', 'add', 'edit', 'fineupload');
            if(isset($this->Security) && ($this->RequestHandler->isAjax() || $this->RequestHandler->isPost()) && $this->action == 'fineupload'){
                $this->Security->validatePost = false;
                $this->Security->enabled = false;
                $this->Security->csrfCheck = false;
            }
        }
    }
 
    /* --- snipped --- */
 
    public function edit($id = null) {
        $this->layout = 'admin';
 
        $this->News->id = $id;
 
        if(!$this->News->exists()) {
            throw new NotFoundException('News not found');
        }
        if(!($this->request->is('post') || $this->request->is('put'))) {
            $this->set('id', $id);
            $this->request->data = $this->News->read(null, $id);
        } else {
            if($this->News->save($this->request->data)) {
                //$this->Session->setFlash( __('The news has been updated'), 'success');
                return $this->redirect(array('action' => 'index'));
            } else {
                //$this->Session->setFlash('The news could not be saved.', 'error');
            }
        }
    }
 
    /* --- snipped --- */
 
    public function fineupload() {
        $this->layout = false;
 
        $result = array();
 
        // debug($_FILES);
        // array(
        //     [qqfile] => array(
        //         [name] => [share1_glass.gif],
        //         [type] => [image/gif],
        //         [tmp_name] => [C:\wamp\tmp\phpA755.tmp],
        //         [error] => (int) 0,
        //         [size] => (int) 120358
        //     )
        // )
 
        $temp_file       = $_FILES['qqfile']['tmp_name'];
        $target_filename = $_FILES['qqfile']['name'];
        $upload_path     = Configure::read("App.UPLOAD_PATH");
        $target_filepath = $upload_path.'/'.$target_filename;
        move_uploaded_file($temp_file, $target_filepath);
 
        $result['status']   = 'success';
        $result['message']  = 'Upload successful.';
        $result['filename'] = $target_filename;
        $result['webpath']  = Configure::read("App.UPLOAD_PATH_URL").'/'.$target_filename;
        $result['upload_dir'] = Configure::read("App.UPLOAD_PATH_URL");
 
        $this->set('result', $result);
    }

Then in the HTML part – this is the part where we setup the FineUpload Javascript.

<!-- Fine Uploader -->
<!-- You could load these javascript files the CakePHP way -->
<script type="text/javascript" src="/js/fineuploader/header.js"></script>
<script type="text/javascript" src="/js/fineuploader/util.js"></script>
<script type="text/javascript" src="/js/fineuploader/handler.xhr.js"></script>
<script type="text/javascript" src="/js/fineuploader/handler.base.js"></script>
<script type="text/javascript" src="/js/fineuploader/handler.form.js"></script>
<script type="text/javascript" src="/js/fineuploader/button.js"></script>
 
<script type="text/javascript" src="/js/fineuploader/uploader.js"></script>
<script type="text/javascript" src="/js/fineuploader/uploader.basic.js"></script>
 
<script>
    $(document).ready(function() {
    $fub = $('#fine-uploader-basic');
    $messages = $('#messages');
 
    //for IE, we would load the mysite.com/news/fineupload.txt
    //it would return text instead of application/json
    //if we do not do this, Internet Explorer would display a "Save as" dialog box
    if($.browser.msie) {
        var request_ext = 'txt';
        //http://mysite.com/news/fineupload.txt
    } else {
        var request_ext = 'json';
        //http://mysite.com/news/fineupload.json
    }
    var uploader = new qq.FineUploaderBasic({
            button: $fub[0],
            multiple: false,
            request: {
                endpoint: '<?php echo $this->webroot ?>news/fineupload.'+request_ext
            },
            validation: {
            allowedExtensions: ['jpeg', 'jpg', 'gif', 'png', 'pdf'],
            sizeLimit: 204800 // 200 kB = 200 * 1024 bytes
        },
        callbacks: {
            onSubmit: function(id, fileName) {
                $messages.html('<div id="file-' + id + '" class="alert" style="margin: 20px 0 0"></div>');
            },
            onUpload: function(id, fileName) {
                $('#file-' + id).addClass('alert-info')
                                .html('<img src="<?php echo Configure::read("App.SITE_URL") ?>/loading.gif" alt="Initializing. Please hold."> ' +
                                    'Initializing ' +
                                    '“' + fileName + '”');
            },
            onProgress: function(id, fileName, loaded, total) {
                if (loaded < total) {
                progress = Math.round(loaded / total * 100) + '% of ' + Math.round(total / 1024) + ' kB';
                $('#file-' + id).removeClass('alert-info')
                                .html('<img src="<?php echo Configure::read("App.SITE_URL") ?>/loading.gif" alt="In progress. Please hold."> ' +
                                        'Uploading ' +
                                        '“' + fileName + '” ' +
                                        progress);
                } else {
                $('#file-' + id).addClass('alert-info')
                                .html('<img src="<?php echo Configure::read("App.SITE_URL") ?>/loading.gif" alt="Saving. Please hold."> ' +
                                        'Saving ' +
                                        '“' + fileName + '”');
                }
            },
            onComplete: function(id, fileName, responseJSON) {
                console.log('ID: '+id);
                console.log('fileName: '+fileName);
                if (responseJSON.status == 'success') {
                    $('#file-' + id).removeClass('alert-info')
                                    .addClass('alert-success')
                                    .html('<i class="icon-ok"></i> ' +
                                            'Successfully saved ' +
                                            '“' + fileName + '”');
                    $('#NewsExternalUrl').val(responseJSON.webpath);
                } else {
                $('#file-' + id).removeClass('alert-info')
                                .addClass('alert-error')
                                .html('<i class="icon-exclamation-sign"></i> ' +
                                        'Error with ' +
                                        '“' + fileName + '”: ' +
                                        responseJSON.error);
                }
            }
        }
    });
    });
</script>
<div id="fine-uploader-basic" class="btn btn-small">
    <i class="icon-upload icon"></i> Click to upload a file instead of using the Content textarea below.
</div>
(pdf, jpg, gif, png files only)
<div id="messages"></div>

FineUploader / File Uploader
http://fineuploader.com/
https://github.com/valums/file-uploader/

Posted in Uncategorized | Leave a comment

CakePHP: Disabling the Security Component for Specific Actions in a Controller

Sometimes, I need to disable the Security component for certain actions in the controller. For example, if I need to handle FineUpload.

If I do not disable the Security component, I get a “request has been blackholed” error.

<?php
class NewsController extends AppController {
    public $helpers = array('Html', 'Form');
    public $components = array('RequestHandler', 'Security');
 
    public function beforeFilter() {
        parent::beforeFilter();
 
        if (AuthComponent::user('role') === 'admin' OR AuthComponent::user('role') === 'user') {
            //only allow access to these actions when the role is admin/user
            $this->Auth->allow('index', 'add', 'edit', 'delete', 'view', 'fineupload');
 
            //Here, we disable the Security component for Ajax requests and for the "fineupload" action
            if(isset($this->Security) &&  ($this->RequestHandler->isAjax() || $this->RequestHandler->isPost()) && $this->action == 'fineupload'){
                $this->Security->validatePost = false;
                $this->Security->enabled = false;
                $this->Security->csrfCheck = false;
            }
        }
    }
/*rest of the code*/
Posted in Uncategorized | 1 Comment

CakePHP: Select Box / Drop-down List linked from related Model

For example, an Article has an Author. In the database, the Article table would have an author_id field. Assuming that we are following the CakePHP Model and Database Conventions (http://book.cakephp.org/2.0/en/getting-started/cakephp-conventions.html#model-and-database-conventions), the models would look something like these:

<?php
## /Model/Article.php
App::uses('AuthComponent', 'Controller/Component');
 
class Article extends AppModel {
    public $name = 'Article';
 
    public $belongsTo = array(
        'Author' => array(
            'className' => 'Author',
            'foreignKey' => 'author_id'
        ),
    );
}

An Article that uses the articles table which belongs to an Author. The article table has a field named author_id. The author_id is linked to the id found in the author table.

The Author Model would be:

<?php
## /Model/Author.php
App::uses('AuthComponent', 'Controller/Component');
 
class Author extends AppModel {
    public $name = 'Author';
 
    public $hasMany = array(
        'Article' => array(
            'className' => 'Article',
            'foreignKey' => 'author_id'
        ),
    );
}

An Author uses the author table and has many Article. The article table has an author_id to link to it’s Author.

Now in one of my controllers, I would have a method that will allow me to add an Article and specify the Author. The Author field would be a dropdown / selectbox.

<?php
## /Controllers/ArticlesAdminController.php
//snip    
    public function add() {
        $this->layout = 'admin';
        $this->loadModel('Author');
        $this->loadModel('Article');
        $authors = $this->Author->find('list'); //we get the authors from the database
        $this->set('authors', $authors);
 
        if ($this->request->is('post')) {
            $this->Article->create();
            if ($this->Article->save($this->request->data)) {
                $this->Session->setFlash( __('The Article has been saved'), 'success');
                return $this->redirect(array('action' => 'index'));
            } else {
                $this->Session->setFlash(__('The Article could not be saved. Please, try again'), 'error');
                //throw new InternalErrorException (__('The user could not be saved. Please, try again'));
            }
 
        }
    }

Finally, we have the view (/View/ArticlesAdmin/add.ctp).

 
<h1><?php echo __('Add Article'); ?></h1>
<?php
    echo $this->Form->create('Article', array('class' => 'well'));
 
    echo $this->Form->input('title', 
        array(
            'class' => 'span5',
            'after' => ' <span class="required_indicator">(required)</span> ',
            'error' => array('attributes' => array('wrap' => 'span', 'class' => 'label custom-inline-error label-important help-inline'))
        ));
 
    //the rest of the Article fields here...
 
    //The selectbox / dropdown menu for the Author field
    echo $this->Form->input('author_id', 
        array(
            'options' => $authors,
            'class' => 'span5',
            'error' => array('attributes' => array('wrap' => 'span', 'class' => 'label custom-inline-error label-important help-inline'))
        ));
 
?>
<?php echo $this->Form->end(__('Submit')) ?>
Posted in Uncategorized | Leave a comment

CakePHP: Displaying images that are outside the public accessible directory (webroot) using Media Views

Let’s say that Configure::read('App.uploads_temp') is somewhere outside the public accessible directory: /var/www/yourapp/private

//This goes into the controller
    /**
     * @param $filename A filename without the path. EG: default-guy.jpg
     */
    public function downloadimage($filename) {
        $file_parts = pathinfo($filename);
        $this->viewClass = 'Media';
        $params = array(
            'id'        => $filename,
            'name'      => $file_parts['basename'],
            'download'  => false,
            'extension' => $file_parts['extension'],
            'path'      =>Configure::read('App.uploads_temp'). DS
        );
        $this->set($params);
    }

You can view the image through the URL:

    http://yourapp.com/entries/downloadimage/sample-image.jpg

Reference: http://book.cakephp.org/2.0/en/views/media-view.html

Posted in Uncategorized | Leave a comment

Some useful Knockout.JS examples from my JSFiddle

I am learning Knockout.js

Accessing an observable array outside the foreach loop from the HTML
http://jsfiddle.net/wenbert/MEKLN/

Setting a default / selected option in a Select box
http://jsfiddle.net/wenbert/ZpGeS/

Working with child models
http://jsfiddle.net/wenbert/Ev8H6/

Thanks to the guys at Stackoverflow and #javascript in Freenode for replying to my questions. RP Niemeyer from Knockmeout.net, nemesv and tyrsius.

Posted in Uncategorized | Leave a comment

CakePHP 2.x: CSV File download from a database query

Here is a quick way to output/stream/download a CSV file from your database using CakePHP.

For simplicity, I have placed everything in the controller. Not good practice but it quickly does the job.

<?php
class UsersController extends AppController {
 
// ...snip...
// the rest of your code here
 
    public function downloadentries() {
 
        if($this->request->is('post')) {
            $this->loadModel('Member');
            $filename = "myfile.csv";
            $start_date = '2012-10-08 00:00:00';
            $end_date = '2012-10-08 23:59:59';
            $results = $this->Member->find('all', array(
                        'conditions' => array('Member.created >=' => $start_date, 
                                        'Member.created <=' => $end_date)
                    ));
 
            $csv_file = fopen('php://output', 'w');
 
            header('Content-type: application/csv');
            header('Content-Disposition: attachment; filename="'.$filename.'"');
 
            $header_row = array("id", "name", "email");
 
            fputcsv($csv_file,$header_row,',','"');
            foreach($results AS $result) {
                $row = array(
                $result['Member']['id'],
                $result['Member']['name'],
                $result['Member']['email']
                );
 
                fputcsv($csv_file,$row,',','"');
            }
            fclose($csv_file);
        }
        $this->layout = false;
        $this->render(false);
        return false;
    }
 
// ... more code here...
}

You might be also interested in outputting it as an Excel File: Zend Framework: View File downloaded as Excel File. Different framework but the same idea.

Sources: A, B

Posted in Uncategorized | Tagged , , | 3 Comments