gecko-dev/webtools/uninstall_survey/models/result.php

554 строки
22 KiB
PHP

<?php
/* ***** BEGIN LICENSE BLOCK *****
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
*
* The contents of this file are subject to the Mozilla Public License Version
* 1.1 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* http://www.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
* for the specific language governing rights and limitations under the
* License.
*
* The Original Code is survey.mozilla.com site.
*
* The Initial Developer of the Original Code is
* The Mozilla Foundation.
* Portions created by the Initial Developer are Copyright (C) 2006
* the Initial Developer. All Rights Reserved.
*
* Contributor(s):
* Wil Clouser <clouserw@mozilla.com> (Original Author)
*
* Alternatively, the contents of this file may be used under the terms of
* either the GNU General Public License Version 2 or later (the "GPL"), or
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
* in which case the provisions of the GPL or the LGPL are applicable instead
* of those above. If you wish to allow use of your version of this file only
* under the terms of either the GPL or the LGPL, and not to allow others to
* use your version of this file under the terms of the MPL, indicate your
* decision by deleting the provisions above and replace them with the notice
* and other provisions required by the GPL or the LGPL. If you do not delete
* the provisions above, a recipient may use your version of this file under
* the terms of any one of the MPL, the GPL or the LGPL.
*
* ***** END LICENSE BLOCK ***** */
class Result extends AppModel {
var $name = 'Result';
var $belongsTo = array('Application');
var $hasAndBelongsToMany = array('Choice' =>
array('className' => 'Choice',
'uniq' => true
)
);
var $Sanitize;
function Result()
{
parent::appModel();
$this->Sanitize = new Sanitize();
}
/**
* Count's all the comments. To speed things up I'm using found_rows(). That
* means this must be called directly after getComments()!
* @return int count value
*/
function getCommentCount()
{
$comments = $this->query("SELECT FOUND_ROWS() as count");
return $comments[0][0]['count'];
}
/**
* Will retrieve all the comments within param's and pagination's parameters
* @param array URL parameters
* @param array pagination values from the controller
* @param boolean if privacy is true phone numbers and email addresses will be
* masked
* @return cake result set
*/
function getComments($params, $pagination, $privacy=true)
{
$params = $this->cleanArrayForSql($params);
$_application_id = $this->Application->getIdFromUrl($params);
// We only want to see rows with comments
$_conditions = array("comments NOT LIKE ''");
if (!empty($params['start_date'])) {
$_timestamp = strtotime($params['start_date']);
if (!($_timestamp == -1) || $_timestamp == false) {
$_date = date('Y-m-d H:i:s', $_timestamp);//sql format
array_push($_conditions, "`created` >= '{$_date}'");
}
}
if (!empty($params['end_date'])) {
$_timestamp = strtotime($params['end_date']);
if (!($_timestamp == -1) || $_timestamp == false) {
$_date = date('Y-m-d 23:59:59', $_timestamp);//sql format
array_push($_conditions, "`created` <= '{$_date}'");
}
}
$_application_id = $this->Application->getIdFromUrl($params);
array_push($_conditions, "`Result`.`application_id`={$_application_id}");
// Next determine our collection
if (!empty($params['collection'])) {
$_collection_id = $this->Choice->Collection->findByDescription($params['collection']);
$clear = true;
foreach ($_collection_id['Application'] as $var => $val) {
if ($_application_id == $val['id']) {
$clear = false;
}
}
if ($clear) {
$_id = $this->Application->getMaxCollectionId($_application_id, 'issue');
$_collection_id['Collection']['id'] = $_id[0][0]['max'];
}
} else {
$_id = $this->Application->getMaxCollectionId($_application_id, 'issue');
$_collection_id['Collection']['id'] = $_id[0][0]['max'];
}
// SQL_CALC_FOUND_ROWS used for counting comments
$_query = "
SELECT
SQL_CALC_FOUND_ROWS DISTINCT
`Result`.`id`,
`Result`.`comments`,
`Result`.`created`
FROM
`results` AS `Result`
JOIN choices_results ON choices_results.result_id = Result.id
JOIN choices ON choices_results.choice_id = choices.id
JOIN choices_collections ON choices_collections.choice_id = choices.id
WHERE
comments != ''
AND
collection_id={$_collection_id['Collection']['id']}
AND
Result.application_id={$_application_id}
";
$_start =($pagination['page'] -1) * $pagination['show'];
$_query .= "
ORDER BY `Result`.`created` {$pagination['direction']}
LIMIT {$_start},{$pagination['show']}
";
$comments = $this->query($_query);
if ($privacy) {
// Pull out all the email addresses and phone numbers. The original
// lines are below, but commented out for the sake of speed.
// preg_replace() will replace a single level of an array according to a
// pattern. This behavior doesn't seem to be documented (at this time), so I'm not sure
// if they are going to "fix" it later. If they do, you can replace the
// current code with the commented ones, but realize it will take about
// twice as long.
foreach ($comments as $var => $val) {
// Handle foo@bar.com
$_email_regex = '/\ ?(.+)?@(.+)?\.(.+)?\ ?/';
$comments[$var]['Result'] = preg_replace($_email_regex,'$1@****.$3',$comments[$var]['Result']);
//$comments[$var]['Result']['comments'] = preg_replace($_email_regex,'$1@****.$3',$comments[$var]['Result']['comments']);
//$comments[$var]['Result']['intention_text'] = preg_replace($_email_regex,'$1@****.$3',$comments[$var]['Result']['intention_text']);
// Handle xxx-xxx-xxxx
$_phone_regex = '/([0-9]{3})[ .-]?[0-9]{4}/';
$comments[$var]['Result'] = preg_replace($_phone_regex,'$1-****',$comments[$var]['Result']);
//$comments[$var]['Result']['comments'] = preg_replace($_phone_regex,'$1-****',$comments[$var]['Result']['comments']);
//$comments[$var]['Result']['intention_text'] = preg_replace($_phone_regex,'$1-****',$comments[$var]['Result']['intention_text']);
}
}
return $comments;
}
/**
* This function runs the query to get the export data for the CSV file.
*
* @param array URL parameters
* @param boolean if privacy is true phone numbers and email addresses will be
* masked
* @return array two dimensional array that should be pretty easy to transform
* into a CSV.
*/
function getCsvExportData($params, $privacy=true)
{
$params = $this->cleanArrayForSql($params);
// We have to use a left join here because there isn't always an intention
$_query = "
SELECT
`results`.`id`,
`results`.`created`,
`results`.`intention_text` as `intention_other`,
`results`.`comments`,
`intentions`.`description` as `intention`
FROM `results`
LEFT JOIN `intentions` ON `results`.`intention_id`=`intentions`.`id`
INNER JOIN `applications` ON `applications`.`id` = `results`.`application_id`
WHERE
1=1
";
if (!empty($params['start_date'])) {
$_timestamp = strtotime($params['start_date']);
if (!($_timestamp == -1) || $_timestamp == false) {
$_date = date('Y-m-d H:i:s', $_timestamp);//sql format
$_query .= " AND `results`.`created` >= '{$_date}'";
}
}
if (!empty($params['end_date'])) {
$_timestamp = strtotime($params['end_date']);
if (!($_timestamp == -1) || $_timestamp == false) {
$_date = date('Y-m-d 23:59:59', $_timestamp);//sql format
$_query .= " AND `results`.`created` <= '{$_date}'";
}
}
if (!empty($params['product'])) {
// product's come in looking like:
// Mozilla Firefox 1.5.0.1
$_exp = explode(' ',urldecode($params['product']));
if(count($_exp) == 3) {
$_product = $_exp[0].' '.$_exp[1];
$_version = $_exp[2];
$_query .= " AND `applications`.`name` LIKE '{$_product}'";
$_query .= " AND `applications`.`version` LIKE '{$_version}'";
} else {
// defaults I guess?
$_query .= " AND `applications`.`name` LIKE '".DEFAULT_APP_NAME."'";
$_query .= " AND `applications`.`version` LIKE '".DEFAULT_APP_VERSION."'";
}
} else {
// I'm providing a default here, because otherwise all results will be
// returned (across all applications) and that is not desired
$_query .= " AND `applications`.`name` LIKE '".DEFAULT_APP_NAME."'";
$_query .= " AND `applications`.`version` LIKE '".DEFAULT_APP_VERSION."'";
}
$_query .= " ORDER BY `results`.`created` ASC";
$res = $this->query($_query);
// Since we're exporting to a CSV, we need to flatten the results into a 2
// dimensional table array
foreach ($res as $result) {
$newdata[] = array_merge($result['results'], $result['intentions']);
}
if ($privacy) {
// Pull out all the email addresses and phone numbers. The original
// lines are below, but commented out for the sake of speed.
// preg_replace() will replace a single level of an array according to a
// pattern. This behavior doesn't seem to be documented (at this time), so I'm not sure
// if they are going to "fix" it later. If they do, you can replace the
// current code with the commented ones, but realize it will take about
// twice as long.
foreach ($newdata as $var => $val) {
// Handle foo@bar.com
$_email_regex = '/\ ?(.+)?@(.+)?\.(.+)?\ ?/';
$newdata[$var] = preg_replace($_email_regex,'$1@****.$3',$newdata[$var]);
//$newdata[$var]['comments'] = preg_replace($_email_regex,'$1@****.$3',$newdata[$var]['comments']);
//$newdata[$var]['intention_other'] = preg_replace($_email_regex,'$1@****.$3',$newdata[$var]['intention_other']);
// Handle xxx-xxx-xxxx
$_phone_regex = '/([0-9]{3})[ .-]?[0-9]{4}/';
$newdata[$var] = preg_replace($_phone_regex,'$1-****',$newdata[$var]);
//$newdata[$var]['comments'] = preg_replace($_phone_regex,'$1-****',$newdata[$var]['comments']);
//$newdata[$var]['intention_other'] = preg_replace($_phone_regex,'$1-****',$newdata[$var]['intention_other']);
}
}
// Our CSV library just prints out everything in order, so we have to put the
// column labels on here ourselves
$newdata = array_merge(array(array_keys($newdata[0])), $newdata);
return $newdata;
}
/**
* Will retrieve the information used for graphing. This function has undergone
* quite an evolution in the name of speed, from 1 query to 3, speedwise from
* 5.5s to just under a half a second. Thanks to morgamic for some crazy
* sql kung fu. :)
* @param the url parameters (unescaped)
* @return a result set
*/
function getDescriptionAndTotalsData($params)
{
// Clean parameters for inserting into SQL
$params = $this->cleanArrayForSql($params);
/* Below is the original query for this function. It was beautiful and
* brought back just what we needed. However, it took 5.5s to run with 43000
* results, and since that number is just going up, it won't do to have a
* query take that long (especially one on the front page).
//It would be nice to drop something like this in the SELECT:
// CONCAT(COUNT(*)/(SELECT COUNT(*) FROM our_giant_query_all_over_again)*100,'%') AS `percentage`
$_query = "
SELECT
issues.description,
COUNT( DISTINCT results.id ) AS total
FROM
issues
LEFT JOIN issues_results ON issues_results.issue_id=issues.id
LEFT JOIN results ON results.id=issues_results.result_id AND results.application_id=applications.id
JOIN applications_issues ON applications_issues.issue_id=issues.id
JOIN applications ON applications.id=applications_issues.application_id
WHERE 1=1
";
// Any other restraints (date, app, etc.)
$_query .= " GROUP BY `issues`.`description`
ORDER BY `issues`.`description` DESC";
*
*/
$_conditions = "1=1";
// Firstly, determine our application
$_application_id = $this->Application->getIdFromUrl($params);
$_conditions .= " AND `Result`.`application_id`={$_application_id}";
// Next determine our collection
if (!empty($params['collection'])) {
$_collection_id = $this->Choice->Collection->findByDescription($params['collection']);
// Check if the current app has a collection by that name. If it doesn't
// fall back to max (this way they can jump between apps without having
// messages that say "no data found"
$clear = true;
foreach ($_collection_id['Application'] as $var => $val) {
if ($_application_id == $val['id']) {
$clear = false;
}
}
if ($clear) {
$_id = $this->Application->getMaxCollectionId($_application_id, 'issue');
$_collection_id['Collection']['id'] = $_id[0][0]['max'];
}
} else {
// If collection isn't set, default to the highest (newest) one
$_id = $this->Application->getMaxCollectionId($_application_id, 'issue');
$_collection_id['Collection']['id'] = $_id[0][0]['max'];
}
// The second query will retrieve all the issues that are related to our
// application.
$_query = "
SELECT
choices.description, choices.id
FROM
choices
JOIN choices_collections ON choices_collections.choice_id = choices.id
JOIN collections ON collections.id = choices_collections.collection_id
JOIN applications_collections ON applications_collections.collection_id = collections.id
JOIN applications ON applications.id = applications_collections.application_id
AND applications.id = {$_application_id}
AND collections.id = {$_collection_id['Collection']['id']}
AND choices.type = 'issue'
ORDER BY choices.pos ASC
";
$_issues = $this->query($_query);
$_query = '';
$_issue_ids = '';//used in the query
$_results = array();
foreach ($_issues as $var => $val) {
// Cake has a pretty specific way it stores data, and this is consistent
// with the old query. Here we start our results array so it's holding the
// descriptions and a zeroed total
$_results[$val['choices']['id']]['choices']['description'] = $val['choices']['description'];
$_results[$val['choices']['id']][0]['total'] = 0; // default to nothing - this will get filled in later
// Since we're already walking through this loop, we might as well build
// up a query string to get our totals
$_issue_ids .= empty($_issue_ids) ? $val['choices']['id'] : ',
'.$val['choices']['id'];
}
$_query = "
SELECT
choices_results.choice_id, count(results.id)
AS
total
FROM
results
JOIN choices_results ON results.id = choices_results.result_id
WHERE
results.application_id = {$_application_id}
AND
choices_results.choice_id in ({$_issue_ids})
";
if (!empty($params['start_date'])) {
$_timestamp = strtotime($params['start_date']);
if (!($_timestamp == -1) || $_timestamp == false) {
$_date = date('Y-m-d H:i:s', $_timestamp);//sql format
$_query.= " AND `results`.`created` >= '{$_date}'";
}
}
if (!empty($params['end_date'])) {
$_timestamp = strtotime($params['end_date']);
if (!($_timestamp == -1) || $_timestamp == false) {
$_date = date('Y-m-d 23:59:59', $_timestamp);//sql format
$_query .= " AND `results`.`created` <= '{$_date}'";
}
}
$_query .= " GROUP BY choices_results.choice_id";
$ret = $this->query($_query);
foreach ($ret as $var => $val) {
// fill in the totals we retrieved
$_results[$val['choices_results']['choice_id']][0]['total'] = $val[0]['total'];
}
return $_results;
}
/**
* We've got complex info to save, so I'm overriding the default save method.
* Too bad we can't leverage some cake awesomeness. :(
*
* @param array Big cake array, filled with juicy $_POST goodness. It should be
* the same structure as any other array created by the html helper.
* @return boolean true on success, false on failure
*/
function save($data)
{
// Apparently these are all escaped for us by cake. It still makes me
// nervous.
$_application_id = $data['Application']['id'];
$_intention_id = $data['Intention']['id']; // Doesn't quite conform to cake standards
$_comments = $data['Result']['comments'];
$_issues_text = $this->Sanitize->Sql($data['Issue']['text']);
$_intention_text = $this->Sanitize->Sql($data['Intention']['text']);
// Joined for legacy reasons
$_user_agent = mysql_real_escape_string("{$data['ua'][0]} {$data['lang'][0]}");
$_http_user_agent = mysql_real_escape_string($_SERVER['HTTP_USER_AGENT']);
// Make sure our required variables are set and correct
if (!is_numeric($_application_id)) {
return false;
}
// Special cases for the "other" fields. If their corresponding option isn't
// set, we don't want the field values.
// issue is determined below
$this->Choice->unBindModel(array('hasAndBelongsToMany' => array('Result')));
$this->Choice->unBindModel(array('hasAndBelongsToMany' => array('Collection')));
//$_issue_array = $this->Issue->findByDescription('other');
$_conditions = "description LIKE 'Other' AND type='intention'";
$_intention_array = $this->Choice->findAll($_conditions);
$_conditions = "description LIKE 'Other' AND type='issue'";
$_issue_array = $this->Choice->findAll($_conditions);
if ($_intention_id != $_intention_array[0]['Choice']['id']) {
$_intention_text = '';
}
$this->set('application_id', $_application_id);
$this->set('comments', $_comments);
$this->set('useragent', $_user_agent);
$this->set('http_user_agent', $_http_user_agent);
// We kinda overrode $this's save(), so we'll have to ask our guardians
parent::save();
$_result_id = $this->getLastInsertID();
// Insert our intention
if (!empty($_intention_id)) {
$_query = "
INSERT INTO choices_results(
result_id,
choice_id,
other
) VALUES (
{$_result_id},
{$_intention_id},
'{$_intention_text}'
)
";
$this->query($_query);
}
// The choices_results table isn't represented by a class in cake, so we have
// to do the query manually.
if (!empty($data['Issue']['id'])) {
$_query = '';
foreach ($data['Issue']['id'] as $var => $val) {
// This should never happen, but hey...
if (!is_numeric($val)) {
continue;
}
// If the 'other' id matches the id we're putting in, add the issue text
$_other_text = ($val == $_issue_array[0]['Choice']['id']) ? $_issues_text : '';
$_query .= empty($_query) ? "({$_result_id},{$val},'{$_other_text}')" : ",({$_result_id},{$val},'{$_other_text}')";
}
$_query = "
INSERT INTO choices_results(
result_id,
choice_id,
other
) VALUES
{$_query}
";
$this->query($_query);
}
return false;
}
}
?>