diff options
Diffstat (limited to 'plugins/API')
-rw-r--r-- | plugins/API/API.php | 39 | ||||
-rw-r--r-- | plugins/API/Filter/DataComparisonFilter.php | 590 | ||||
-rw-r--r-- | plugins/API/Filter/DataComparisonFilter/ComparisonRowGenerator.php | 239 | ||||
-rw-r--r-- | plugins/API/ProcessedReport.php | 37 | ||||
-rw-r--r-- | plugins/API/RowEvolution.php | 20 | ||||
-rw-r--r-- | plugins/API/tests/Integration/Filter/DataComparisonFilter/ComparisonRowGeneratorTest.php | 613 | ||||
-rw-r--r-- | plugins/API/tests/Unit/XmlRendererTest.php | 5 |
7 files changed, 1534 insertions, 9 deletions
diff --git a/plugins/API/API.php b/plugins/API/API.php index cfdbe92985..8d21ab11a8 100644 --- a/plugins/API/API.php +++ b/plugins/API/API.php @@ -476,7 +476,7 @@ class API extends \Piwik\Plugin\API * @param bool|int $idDimension * @return array */ - public function getRowEvolution($idSite, $period, $date, $apiModule, $apiAction, $label = false, $segment = false, $column = false, $language = false, $idGoal = false, $legendAppendMetric = true, $labelUseAbsoluteUrl = true, $idDimension = false) + public function getRowEvolution($idSite, $period, $date, $apiModule, $apiAction, $label = false, $segment = false, $column = false, $language = false, $idGoal = false, $legendAppendMetric = true, $labelUseAbsoluteUrl = true, $idDimension = false, $labelSeries = false) { // check if site exists $idSite = (int) $idSite; @@ -504,7 +504,7 @@ class API extends \Piwik\Plugin\API $rowEvolution = new RowEvolution(); return $rowEvolution->getRowEvolution($idSite, $period, $date, $apiModule, $apiAction, $label, $segment, $column, - $language, $apiParameters, $legendAppendMetric, $labelUseAbsoluteUrl); + $language, $apiParameters, $legendAppendMetric, $labelUseAbsoluteUrl, $labelSeries); } /** @@ -526,6 +526,10 @@ class API extends \Piwik\Plugin\API foreach ($urls as $url) { $params = Request::getRequestArrayFromString($url . '&format=php&serialize=0'); + if (!empty($params['method']) && $params['method'] === 'API.getBulkRequest') { + continue; + } + if (isset($params['urls']) && $params['urls'] == $urls) { // by default 'urls' is added to $params as Request::getRequestArrayFromString adds all $_GET/$_POST // default parameters @@ -620,6 +624,37 @@ class API extends \Piwik\Plugin\API return $values; } + /** + * Returns category/subcategory pairs as "CategoryId.SubcategoryId" for whom comparison features should + * be disabled. + * + * @return string[] + */ + public function getPagesComparisonsDisabledFor() + { + $pages = []; + + /** + * If your plugin has pages where you'd like comparison features to be disabled, you can add them + * via this event. Add the pages as "CategoryId.SubcategoryId". + * + * **Example** + * + * ``` + * public function getPagesComparisonsDisabledFor(&$pages) + * { + * $pages[] = "General_Visitors.MyPlugin_MySubcategory"; + * $pages[] = "MyPlugin.myControllerAction"; // if your plugin defines a whole page you want comparison disabled for + * } + * ``` + * + * @param string[] &$pages + */ + Piwik::postEvent('API.getPagesComparisonsDisabledFor', [&$pages]); + + return $pages; + } + private function findSegment($segmentName, $idSite) { $segmentsMetadata = $this->getSegmentsMetadata($idSite, $_hideImplementationData = false); diff --git a/plugins/API/Filter/DataComparisonFilter.php b/plugins/API/Filter/DataComparisonFilter.php new file mode 100644 index 0000000000..78872f3b41 --- /dev/null +++ b/plugins/API/Filter/DataComparisonFilter.php @@ -0,0 +1,590 @@ +<?php +/** + * Piwik - free/libre analytics platform + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +namespace Piwik\Plugins\API\Filter; + +use Piwik\API\Request; +use Piwik\Common; +use Piwik\Config; +use Piwik\DataTable; +use Piwik\DataTable\DataTableInterface; +use Piwik\DataTable\Simple; +use Piwik\Http\BadRequestException; +use Piwik\Metrics; +use Piwik\Period; +use Piwik\Period\Factory; +use Piwik\Piwik; +use Piwik\Plugin\Report; +use Piwik\Plugins\API\Filter\DataComparisonFilter\ComparisonRowGenerator; +use Piwik\Segment; +use Piwik\Segment\SegmentExpression; +use Piwik\Site; + +/** + * Handles the API portion of the data comparison feature. + * + * If the `compareSegments`/`comparePeriods`/`compareDates` parameters are supplied this class will fetch + * the data to compare with and store this data next to each row in the root report. + * + * Additionally, `..._change` columns will be added that show the percentage change for a column. This is only + * done when comparing periods, since segments are subsets of visits, so it doesn't make sense to consider + * differences between them as "changes". + * + * ### Comparing multiple periods + * + * It is possible to compare multiple periods with other multiple periods. For example, it + * is possible to compare period=day, date=2018-02-03,2018-02-13 with period=day, date=2018-03-01,2018-03-15. + * When done, this filter will compare the first period in the first set w/ the first period in the second set, + * etc. So in the previous example, 2018-02-03 will be compared with 2018-03-01, 2018-02-04 with 2018-03-02, etc. + * + * ### Metadata + * + * This filter adds the following metadata to DataTables: + * + * - 'compareSegments': The list of segments being compared. The first entry will always be the value of the `segment` query param. + * - 'comparePeriods': The list of labels of periods being compared. The first entry will always be the value of the + * `period` query param. + * - 'compareDates': The list of dates being compared. The first entry will always be the value of the `date` query param. + * - 'comparisonSeries': Prettified labels for every comparison series in order. + * + * This filter adds the following metadata to rows in the comparison DataTables: + * + * - 'compareSegment': The segment of the data for the comparison row. + * - 'compareSegmentPretty': The prettified label for the segment. + * - 'comparePeriod': The period label for the data in the comparison row. This does not have to match a value in the + * `comparePeriods` query parameter if comparing multiple periods. + * - 'compareDate': The date for the period in the data in the comparison row. This does not have to match a value in the + * `compareDates` query parameter if comparing multiple periods. + * - 'comparePeriodPretty': The prettified label for the period. + * - 'compareSeriesPretty': Prettified label for the comparison data represented by the row. This will match an entry + * in the DataTable's `comparisonSeries` metadata. + */ +class DataComparisonFilter +{ + /** + * @var array + */ + private $request; + + /** + * @var int + */ + private $segmentCompareLimit; + + /** + * @var int + */ + private $periodCompareLimit; + + /** + * @var string + */ + private $segmentName; + + /** + * @var string[] + */ + private $compareSegments; + + /** + * @var string[] + */ + private $compareDates; + + /** + * @var string[] + */ + private $comparePeriods; + + /** + * @var int[] + */ + private $compareSegmentIndices; + + /** + * @var int[] + */ + private $comparePeriodIndices; + + /** + * @var bool + */ + private $isRequestMultiplePeriod; + + /** + * @var ComparisonRowGenerator + */ + private $comparisonRowGenerator; + + /** + * @var array + */ + private $columnMappings; + + public function __construct($request, Report $report = null) + { + $this->request = $request; + + $generalConfig = Config::getInstance()->General; + $this->segmentCompareLimit = (int) $generalConfig['data_comparison_segment_limit']; + $this->checkComparisonLimit($this->segmentCompareLimit, 'data_comparison_segment_limit'); + + $this->periodCompareLimit = (int) $generalConfig['data_comparison_period_limit']; + $this->checkComparisonLimit($this->periodCompareLimit, 'data_comparison_period_limit'); + + $this->segmentName = $this->getSegmentNameFromReport($report); + + $this->compareSegments = self::getCompareSegments(); + if (count($this->compareSegments) > $this->segmentCompareLimit + 1) { + throw new BadRequestException(Piwik::translate('General_MaximumNumberOfSegmentsComparedIs', [$this->segmentCompareLimit])); + } + + $this->compareDates = self::getCompareDates($request); + $this->comparePeriods = self::getComparePeriods($request); + + if (count($this->compareDates) !== count($this->comparePeriods)) { + throw new BadRequestException(Piwik::translate('General_CompareDatesParamMustMatchComparePeriods', ['compareDates', 'comparePeriods'])); + } + + if (count($this->compareDates) > $this->periodCompareLimit + 1) { + throw new BadRequestException(Piwik::translate('General_MaximumNumberOfPeriodsComparedIs', [$this->periodCompareLimit])); + } + + if (count($this->compareSegments) == 1 + && count($this->comparePeriods) == 1 + ) { + return; + } + + $this->checkMultiplePeriodCompare(); + + // map segments/periods to their indexes in the query parameter arrays for comparisonIdSubtable matching + $this->compareSegmentIndices = array_flip($this->compareSegments); + foreach ($this->comparePeriods as $index => $period) { + $date = $this->compareDates[$index]; + $this->comparePeriodIndices[$period][$date] = $index; + } + + $this->columnMappings = $this->getColumnMappings(); + $this->comparisonRowGenerator = new ComparisonRowGenerator($this->segmentName, $this->isRequestMultiplePeriod(), $this->columnMappings); + } + + public static function isCompareParamsPresent($request = null) + { + return !empty(Common::getRequestVar('compareSegments', [], $type = 'array', $request)) + || !empty(Common::getRequestVar('comparePeriods', [], $type = 'array', $request)) + || !empty(Common::getRequestVar('compareDates', [], $type = 'array', $request)); + } + + /** + * @param DataTable\DataTableInterface $table + */ + public function compare(DataTable\DataTableInterface $table) + { + if (empty($this->compareSegments) + && empty($this->comparePeriods) + ) { + return; + } + + $method = Common::getRequestVar('method', $default = null, $type = 'string', $this->request); + if ($method == 'Live') { + throw new \Exception("Data comparison is not enabled for the Live API."); + } + + // optimization, if empty, single table, don't need to make extra queries + if ($table->getRowsCount() == 0) { + return; + } + + $comparisonSeries = []; + + // fetch data first + $reportsToCompare = self::getReportsToCompare($this->compareSegments, $this->comparePeriods, $this->compareDates); + foreach ($reportsToCompare as $index => $modifiedParams) { + $compareMetadata = $this->getMetadataFromModifiedParams($modifiedParams); + $comparisonSeries[] = $compareMetadata['compareSeriesPretty']; + + $compareTable = $this->requestReport($method, $modifiedParams); + $this->comparisonRowGenerator->compareTables($compareMetadata, $table, $compareTable); + } + + // calculate changes (including processed metric changes) + // NOTE: it doesn't make to sense to calculate these values for segments, since segments are subsets of all visits, where periods are + // time periods (so things can change from one to another). + if (count($this->comparePeriods) > 1) { + $this->compareChangePercents($table); + } + + // format comparison table metrics + $this->formatComparisonTables($table); + + // add comparison parameters as metadata + $table->filter(function (DataTable $singleTable) use ($comparisonSeries) { + if (isset($this->compareSegments)) { + $singleTable->setMetadata('compareSegments', $this->compareSegments); + } + + if (isset($this->comparePeriods)) { + $singleTable->setMetadata('comparePeriods', $this->comparePeriods); + } + + if (isset($this->compareDates)) { + $singleTable->setMetadata('compareDates', $this->compareDates); + } + + $singleTable->setMetadata('comparisonSeries', $comparisonSeries); + }); + } + + public static function getReportsToCompare($compareSegments, $comparePeriods, $compareDates) + { + $permutations = []; + + // NOTE: the order of these loops determines the order of the rows in the comparison table. ie, + // if we loop over dates then segments, then we'll see comparison rows change segments before changing + // periods. this is because this loop determines in what order we fetch report data. + foreach ($compareDates as $index => $date) { + foreach ($compareSegments as $segment) { + $period = $comparePeriods[$index]; + + $params = []; + $params['segment'] = $segment; + + if (!empty($period) + && !empty($date) + ) { + $params['date'] = $date; + $params['period'] = $period; + } + + $permutations[] = $params; + } + } + + return $permutations; + } + + /** + * @param $paramsToModify + * @return DataTable + */ + private function requestReport($method, $paramsToModify) + { + $params = array_merge( + [ + 'filter_limit' => -1, + 'filter_offset' => 0, + 'filter_sort_column' => '', + 'filter_truncate' => -1, + 'compare' => 0, + 'totals' => 1, + 'disable_queued_filters' => 1, + 'format_metrics' => 0, + 'label' => '', + 'flat' => Common::getRequestVar('flat', 0, 'int', $this->request), + ], + $paramsToModify + ); + + $params['keep_totals_row'] = Common::getRequestVar('keep_totals_row', 0, 'int', $this->request); + $params['keep_totals_row_label'] = Common::getRequestVar('keep_totals_row_label', '', 'string', $this->request); + + if (!isset($params['idSite'])) { + $params['idSite'] = Common::getRequestVar('idSite', null, 'string', $this->request); + } + if (!isset($params['period'])) { + $params['period'] = Common::getRequestVar('period', null, 'string', $this->request); + } + if (!isset($params['date'])) { + $params['date'] = Common::getRequestVar('date', null, 'string', $this->request); + } + + $idSubtable = Common::getRequestVar('idSubtable', 0, 'int', $this->request); + if ($idSubtable > 0) { + $comparisonIdSubtables = Common::getRequestVar('comparisonIdSubtables', $default = false, 'json', $this->request); + if (empty($comparisonIdSubtables)) { + throw new \Exception("Comparing segments/periods with subtables only works when the comparison idSubtables are supplied as well."); + } + + $segmentIndex = empty($paramsToModify['segment']) ? 0 : $this->compareSegmentIndices[$paramsToModify['segment']]; + $periodIndex = empty($paramsToModify['period']) ? 0 : $this->comparePeriodIndices[$paramsToModify['period']][$paramsToModify['date']]; + $seriesIndex = self::getComparisonSeriesIndex(null, $periodIndex, $segmentIndex, count($this->compareSegments)); + + if (!isset($comparisonIdSubtables[$seriesIndex])) { + throw new \Exception("Invalid comparisonIdSubtables parameter: no idSubtable found for segment $segmentIndex and period $periodIndex"); + } + + $comparisonIdSubtable = $comparisonIdSubtables[$seriesIndex]; + if ($comparisonIdSubtable === -1) { // no subtable in comparison row + $table = new DataTable(); + $table->setMetadata('site', new Site($params['idSite'])); + $table->setMetadata('period', Period\Factory::build($params['period'], $params['date'])); + return $table; + } + + $params['idSubtable'] = $comparisonIdSubtable; + } + + return Request::processRequest($method, $params); + } + + private function formatComparisonTables(DataTableInterface $tableOrMap) + { + $tableOrMap->filter(function (DataTable $table) { + $rows = $table->getRows(); + + $totalRow = $table->getTotalsRow(); + if ($totalRow) { + $rows[] = $totalRow; + } + + foreach ($rows as $row) { + /** @var DataTable $comparisonTable */ + $comparisonTable = $row->getComparisons(); + if (!empty($comparisonTable)) { // sanity check + $columnMappings = $this->columnMappings; + $comparisonTable->filter(DataTable\Filter\ReplaceColumnNames::class, [$columnMappings]); + } + + $subtable = $row->getSubtable(); + if ($subtable) { + $this->formatComparisonTables($subtable); + } + } + }); + } + + private function checkComparisonLimit($n, $configName) + { + if ($n <= 1) { + throw new \Exception("The [General] $configName INI config option must be greater than 1."); + } + } + + private function getMetadataFromModifiedParams($modifiedParams) + { + $metadata = []; + + $period = isset($modifiedParams['period']) ? $modifiedParams['period'] : reset($this->comparePeriods); + $date = isset($modifiedParams['date']) ? $modifiedParams['date'] : reset($this->compareDates); + $segment = isset($modifiedParams['segment']) ? $modifiedParams['segment'] : reset($this->compareSegments); + + $metadata['compareSegment'] = $segment; + + $segmentObj = new Segment($segment, []); + $metadata['compareSegmentPretty'] = $segmentObj->getStoredSegmentName(false); + + $metadata['comparePeriod'] = $period; + $metadata['compareDate'] = $date; + + $prettyPeriod = Factory::build($period, $date)->getLocalizedLongString(); + $metadata['comparePeriodPretty'] = ucfirst($prettyPeriod); + + $metadata['compareSeriesPretty'] = self::getComparisonSeriesLabelSuffixFromParts( + $metadata['comparePeriodPretty'], $metadata['compareSegmentPretty']); + + return $metadata; + } + + private static function getComparisonSeriesLabelSuffixFromParts($periodPretty, $segmentPretty) + { + $comparisonLabels = [ + $periodPretty, + $segmentPretty, + ]; + $comparisonLabels = array_filter($comparisonLabels); + + return '(' . implode(') (', $comparisonLabels) . ')'; + } + + private function getSegmentNameFromReport(Report $report = null) + { + if (empty($report)) { + return null; + } + + $dimension = $report->getDimension(); + if (empty($dimension)) { + return null; + } + + $segments = $dimension->getSegments(); + if (empty($segments)) { + return null; + } + + /** @var \Piwik\Plugin\Segment $segment */ + $segment = reset($segments); + $segmentName = $segment->getSegment(); + return $segmentName; + } + + private function checkMultiplePeriodCompare() + { + if ($this->isRequestMultiplePeriod()) { + foreach ($this->comparePeriods as $index => $period) { + if (!Period::isMultiplePeriod($this->compareDates[$index], $period)) { + throw new \Exception("Cannot compare: original request is multiple period and cannot be compared with single periods."); + } + } + } else { + foreach ($this->comparePeriods as $index => $period) { + if (Period::isMultiplePeriod($this->compareDates[$index], $period)) { + throw new \Exception("Cannot compare: original request is single period and cannot be compared with multiple periods."); + } + } + } + } + + private function isRequestMultiplePeriod() + { + if ($this->isRequestMultiplePeriod === null) { + $period = Common::getRequestVar('period', $default = null, 'string', $this->request); + $date = Common::getRequestVar('date', $default = null, 'string', $this->request); + + $this->isRequestMultiplePeriod = Period::isMultiplePeriod($date, $period); + } + return $this->isRequestMultiplePeriod; + } + + private function compareChangePercents(DataTableInterface $result) + { + $segmentCount = count($this->compareSegments); + + $result->filter(function (DataTable $table) use ($segmentCount) { + $rows = $table->getRows(); + + $totalRow = $table->getTotalsRow(); + if ($totalRow) { + $rows[] = $totalRow; + } + + foreach ($rows as $row) { + $comparisons = $row->getComparisons(); + if (empty($comparisons)) { + continue; + } + + /** @var DataTable\Row[] $rows */ + $rows = array_values($comparisons->getRows()); + foreach ($rows as $index => $compareRow) { + if ($index < $segmentCount) { + continue; // do not calculate for first period + } + + list($periodIndex, $segmentIndex) = self::getIndividualComparisonRowIndices($table, $index, $segmentCount); + + $otherPeriodRowIndex = $segmentIndex; + $otherPeriodRow = $comparisons[$otherPeriodRowIndex]; + + foreach ($compareRow->getColumns() as $name => $value) { + $valueToCompare = $otherPeriodRow ? $otherPeriodRow->getColumn($name) : 0; + $valueToCompare = $valueToCompare ?: 0; + + $change = DataTable\Filter\CalculateEvolutionFilter::calculate($value, $valueToCompare, $precision = 1, $appendPercent = false); + + if ($change >= 0) { + $change = '+' . $change; + } + $change .= '%'; + + $compareRow->addColumn($name . '_change', $change); + } + } + } + }); + } + + /** + * Returns the period and segment indices for a given comparison index. + * + * @param DataTable|null $table + * @param $comparisonRowIndex + * @param null $segmentCount + * @return array + */ + public static function getIndividualComparisonRowIndices($table, $comparisonRowIndex, $segmentCount = null) + { + $segmentCount = $segmentCount ?: count($table->getMetadata('compareSegments')); + $segmentIndex = $comparisonRowIndex % $segmentCount; + $periodIndex = floor($comparisonRowIndex / $segmentCount); + return [$periodIndex, $segmentIndex]; + } + + /** + * Returns the series index for a comparison based on the period and segment indices. + * + * @param DataTable|null $table + * @param int $periodIndex + * @param int $segmentIndex + * @param int|null $segmentCount + * @return int + */ + public static function getComparisonSeriesIndex($table, $periodIndex, $segmentIndex, $segmentCount = null) + { + $segmentCount = $segmentCount ?: count($table->getMetadata('compareSegments')); + return $periodIndex * $segmentCount + $segmentIndex; + } + + private static function getCompareSegments($request = null) + { + $segments = Common::getRequestVar('compareSegments', $default = [], $type = 'array', $request); + array_unshift($segments, Common::getRequestVar('segment', '', 'string', $request)); + $segments = Common::unsanitizeInputValues($segments); + return $segments; + } + + private static function getComparePeriods($request = null) + { + $periods = Common::getRequestVar('comparePeriods', $default = [], $type = 'array', $request); + array_unshift($periods, Common::getRequestVar('period', '', 'string', $request)); + return array_values($periods); + } + + private static function getCompareDates($request = null) + { + $dates = Common::getRequestVar('compareDates', $default = [], $type = 'array', $request); + array_unshift($dates, Common::getRequestVar('date', '', 'string', $request)); + return array_values($dates); + } + + /** + * Returns the pretty series label for a specific comparison based on the currently set comparison query parameters. + * + * @param int $labelSeriesIndex The index of the comparison. Comparison series order is determined by {@see self::getReportsToCompare()}. + */ + public static function getPrettyComparisonLabelFromSeriesIndex($labelSeriesIndex) + { + $compareSegments = self::getCompareSegments(); + $comparePeriods = self::getComparePeriods(); + $compareDates = self::getCompareDates(); + + list($periodIndex, $segmentIndex) = self::getIndividualComparisonRowIndices(null, $labelSeriesIndex, count($compareSegments)); + + $segmentObj = new Segment($compareSegments[$segmentIndex], []); + $prettySegment = $segmentObj->getStoredSegmentName(false); + + $prettyPeriod = Factory::build($comparePeriods[$periodIndex], $compareDates[$periodIndex])->getLocalizedLongString(); + $prettyPeriod = ucfirst($prettyPeriod); + + return self::getComparisonSeriesLabelSuffixFromParts($prettyPeriod, $prettySegment); + } + + private function getColumnMappings() + { + $allMappings = Metrics::getMappingFromIdToName(); + + $mappings = []; + foreach ($allMappings as $index => $name) { + $mappings[$index] = $name; + $mappings[$index . '_change'] = $name . '_change'; + } + return $mappings; + } +}
\ No newline at end of file diff --git a/plugins/API/Filter/DataComparisonFilter/ComparisonRowGenerator.php b/plugins/API/Filter/DataComparisonFilter/ComparisonRowGenerator.php new file mode 100644 index 0000000000..eaed08c13b --- /dev/null +++ b/plugins/API/Filter/DataComparisonFilter/ComparisonRowGenerator.php @@ -0,0 +1,239 @@ +<?php +/** + * Piwik - free/libre analytics platform + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +namespace Piwik\Plugins\API\Filter\DataComparisonFilter; + +use Piwik\DataTable; +use Piwik\DataTable\DataTableInterface; +use Piwik\DataTable\Simple; +use Piwik\Metrics; +use Piwik\Period; +use Piwik\Segment; +use Piwik\Segment\SegmentExpression; + +class ComparisonRowGenerator +{ + /** + * @var bool + */ + private $isRequestMultiplePeriod; + + /** + * @var string + */ + private $segmentNameForReport; + + /** + * @var array + */ + private $columnMappings; + + public function __construct($segmentNameForReport, $isRequestMultiplePeriod, $columnMappings) + { + $this->segmentNameForReport = $segmentNameForReport; + $this->isRequestMultiplePeriod = $isRequestMultiplePeriod; + $this->columnMappings = $columnMappings; + } + + public function compareTables($compareMetadata, DataTableInterface $tables, DataTableInterface $compareTables = null) + { + if ($tables instanceof DataTable) { + $this->compareTable($compareMetadata, $tables, $compareTables, $compareTables); + } else if ($tables instanceof DataTable\Map) { + $childTablesArray = array_values($tables->getDataTables()); + $compareTablesArray = isset($compareTables) ? array_values($compareTables->getDataTables()) : []; + + $isDatePeriod = $tables->getKeyName() == 'date'; + + foreach ($childTablesArray as $index => $childTable) { + $compareChildTable = isset($compareTablesArray[$index]) ? $compareTablesArray[$index] : null; + $this->compareTables($compareMetadata, $childTable, $compareChildTable); + } + + // in case one of the compared periods has more periods than the main one, we want to fill the result with empty datatables + // so the comparison data is still present. this allows us to see that data in an evolution report. + if ($isDatePeriod) { + $lastTable = end($childTablesArray); + + /** @var Period $lastPeriod */ + $lastPeriod = $lastTable->getMetadata('period'); + $periodType = $lastPeriod->getLabel(); + + for ($i = count($childTablesArray); $i < count($compareTablesArray); ++$i) { + $periodChangeCount = $i - count($childTablesArray) + 1; + $newPeriod = Period\Factory::build($periodType, $lastPeriod->getDateStart()->addPeriod($periodChangeCount, $periodType)); + + // create an empty table for the main request + $newTable = new DataTable(); + $newTable->setAllTableMetadata($lastTable->getAllTableMetadata()); + $newTable->setMetadata('period', $newPeriod); + + if ($newPeriod->getLabel() === 'week' || $newPeriod->getLabel() === 'range') { + $periodLabel = $newPeriod->getRangeString(); + } else { + $periodLabel = $newPeriod->getPrettyString(); + } + + $tables->addTable($newTable, $periodLabel); + + // compare with the empty table + $compareTable = $compareTablesArray[$i]; + $this->compareTables($compareMetadata, $newTable, $compareTable); + } + } + } else { + throw new \Exception("Unexpected DataTable type: " . get_class($tables)); + } + } + + private function compareTable($compareMetadata, DataTable $table, DataTable $rootCompareTable = null, DataTable $compareTable = null) + { + // if there are no rows in the table because the metrics are 0, add one so we can still set comparison values + if ($table->getRowsCount() == 0) { + $table->addRow(new DataTable\Row()); + } + + foreach ($table->getRows() as $row) { + $label = $row->getColumn('label'); + + $compareRow = null; + if ($compareTable instanceof Simple) { + $compareRow = $compareTable->getFirstRow() ?: null; + } else if ($compareTable instanceof DataTable) { + $compareRow = $compareTable->getRowFromLabel($label) ?: null; + } + + $this->compareRow($table, $compareMetadata, $row, $compareRow, $rootCompareTable); + } + + $totalsRow = $table->getTotalsRow(); + if (!empty($totalsRow)) { + $compareRow = $compareTable ? $compareTable->getTotalsRow() : null; + $this->compareRow($table, $compareMetadata, $totalsRow, $compareRow, $rootCompareTable); + } + + if ($compareTable) { + $totals = $compareTable->getMetadata('totals'); + if (!empty($totals)) { + $totals = $this->replaceIndexesInTotals($totals); + $comparisonTotalsEntry = array_merge($compareMetadata, [ + 'totals' => $totals, + ]); + + $allTotalsTables = $table->getMetadata('comparisonTotals'); + $allTotalsTables[] = $comparisonTotalsEntry; + $table->setMetadata('comparisonTotals', $allTotalsTables); + } + } + } + + private function compareRow(DataTable $table, $compareMetadata, DataTable\Row $row, DataTable\Row $compareRow = null, DataTable $rootTable = null) + { + $comparisonDataTable = $row->getComparisons(); + if (empty($comparisonDataTable)) { + $comparisonDataTable = new DataTable(); + $comparisonDataTable->setMetadata(DataTable::EXTRA_PROCESSED_METRICS_METADATA_NAME, + $table->getMetadata(DataTable::EXTRA_PROCESSED_METRICS_METADATA_NAME)); + $row->setComparisons($comparisonDataTable); + } + + $this->addIndividualChildPrettifiedMetadata($compareMetadata, $rootTable); + + $columns = []; + if ($compareRow) { + foreach ($compareRow as $name => $value) { + if (!is_numeric($value) + || $name == 'label' + ) { + continue; + } + + $columns[$name] = $value; + } + } else { + foreach ($row as $name => $value) { + if (!is_numeric($value) + || $name == 'label' + ) { + continue; + } + + $columns[$name] = 0; + } + } + + $newRow = new DataTable\Row([ + DataTable\Row::COLUMNS => $columns, + DataTable\Row::METADATA => $compareMetadata, + ]); + + // set subtable + $newRow->setMetadata('idsubdatatable', -1); + if ($compareRow) { + $subtableId = $compareRow->getMetadata('idsubdatatable_in_db') ?: $compareRow->getIdSubDataTable(); + if ($subtableId) { + $newRow->setMetadata('idsubdatatable', $subtableId); + } + } + + // add segment metadatas + if ($row->getMetadata('segment')) { + $newSegment = $row->getMetadata('segment'); + if ($newRow->getMetadata('compareSegment')) { + $newSegment = Segment::combine($newRow->getMetadata('compareSegment'), SegmentExpression::AND_DELIMITER, $newSegment); + } + $newRow->setMetadata('segment', $newSegment); + } else if ($this->segmentNameForReport + && $row->getMetadata('segmentValue') !== false + ) { + $segmentValue = $row->getMetadata('segmentValue'); + $newRow->setMetadata('segment', sprintf('%s==%s', $this->segmentNameForReport, urlencode($segmentValue))); + } + + $comparisonDataTable->addRow($newRow); + + // recurse on subtable if there + $subtable = $row->getSubtable(); + if ($subtable + && $compareRow + ) { + $this->compareTable($compareMetadata, $subtable, $rootTable, $compareRow->getSubtable()); + } + } + + private function addIndividualChildPrettifiedMetadata(array &$metadata, DataTable $parentTable = null) + { + if ($parentTable + && $this->isRequestMultiplePeriod + ) { + /** @var Period $period */ + $period = $parentTable->getMetadata('period'); + if (empty($period)) { + return; + } + + $prettyPeriod = $period->getLocalizedLongString(); + $metadata['comparePeriodPretty'] = ucfirst($prettyPeriod); + + $metadata['comparePeriod'] = $period->getLabel(); + $metadata['compareDate'] = $period->getDateStart()->toString(); + } + } + + private function replaceIndexesInTotals($totals) + { + foreach ($totals as $index => $value) { + if (isset($this->columnMappings[$index])) { + $name = $this->columnMappings[$index]; + $totals[$name] = $totals[$index]; + unset($totals[$index]); + } + } + return $totals; + } +}
\ No newline at end of file diff --git a/plugins/API/ProcessedReport.php b/plugins/API/ProcessedReport.php index 79e01da316..6e963ec416 100644 --- a/plugins/API/ProcessedReport.php +++ b/plugins/API/ProcessedReport.php @@ -600,15 +600,17 @@ class ProcessedReport * - extract row metadata to a separate Simple $rowsMetadata * * @param int $idSite enables monetary value formatting based on site currency - * @param Simple $simpleDataTable + * @param DataTable $simpleDataTable * @param array $metadataColumns * @param boolean $hasDimension * @param bool $returnRawMetrics If set to true, the original metrics will be returned * @param bool|null $formatMetrics * @return array DataTable $enhancedDataTable filtered metrics with human readable format & Simple $rowsMetadata */ - private function handleSimpleDataTable($idSite, $simpleDataTable, $metadataColumns, $hasDimension, $returnRawMetrics = false, $formatMetrics = null) + private function handleSimpleDataTable($idSite, $simpleDataTable, $metadataColumns, $hasDimension, $returnRawMetrics = false, $formatMetrics = null, $keepMetadata = false) { + $comparisonColumns = $this->getComparisonColumns($metadataColumns); + // new DataTable to store metadata $rowsMetadata = new DataTable(); @@ -634,7 +636,11 @@ class ProcessedReport } } - $enhancedRow = new Row(); + $c = []; + if ($keepMetadata) { + $c[Row::METADATA] = $row->getMetadata(); + } + $enhancedRow = new Row($c); $enhancedDataTable->addRow($enhancedRow); foreach ($rowMetrics as $columnName => $columnValue) { @@ -669,11 +675,23 @@ class ProcessedReport } } + /** @var DataTable $comparisons */ + $comparisons = $row->getComparisons(); + + if (!empty($comparisons) + && $comparisons->getRowsCount() > 0 + ) { + list($newComparisons, $ignore) = $this->handleSimpleDataTable($idSite, $comparisons, $comparisonColumns, true, $returnRawMetrics, $formatMetrics, $keepMetadata = true); + $enhancedRow->setComparisons($newComparisons); + } + // If report has a dimension, extract metadata into a distinct DataTable if ($hasDimension) { $rowMetadata = $row->getMetadata(); $idSubDataTable = $row->getIdSubDataTable(); + unset($rowMetadata[Row::COMPARISONS_METADATA_NAME]); + // always add a metadata row - even if empty, so the number of rows and metadata are equal and can be matched directly $metadataRow = new Row(); $rowsMetadata->addRow($metadataRow); @@ -830,6 +848,10 @@ class ProcessedReport return $value; } + if (strpos($columnName, '_change') !== false) { // comparison change columns are formatted by DataComparisonFilter + return $value == '0' ? '+0%' : $value; + } + // Display time in human readable if (strpos($columnName, 'time_generation') !== false) { return $formatter->getPrettyTimeFromSeconds($value, true); @@ -853,4 +875,13 @@ class ProcessedReport return $value; } + + private function getComparisonColumns(array $metadataColumns) + { + $result = $metadataColumns; + foreach ($metadataColumns as $columnName => $columnTranslation) { + $result[$columnName . '_change'] = Piwik::translate('General_ChangeInX', lcfirst($columnName)); + } + return $result; + } } diff --git a/plugins/API/RowEvolution.php b/plugins/API/RowEvolution.php index 9025eb3436..24e695c88b 100644 --- a/plugins/API/RowEvolution.php +++ b/plugins/API/RowEvolution.php @@ -19,6 +19,7 @@ use Piwik\DataTable\Filter\SafeDecodeLabel; use Piwik\DataTable\Row; use Piwik\Period; use Piwik\Piwik; +use Piwik\Plugins\API\Filter\DataComparisonFilter; use Piwik\Site; use Piwik\Url; @@ -36,7 +37,7 @@ class RowEvolution 'getPageUrl' ); - public function getRowEvolution($idSite, $period, $date, $apiModule, $apiAction, $label = false, $segment = false, $column = false, $language = false, $apiParameters = array(), $legendAppendMetric = true, $labelUseAbsoluteUrl = true) + public function getRowEvolution($idSite, $period, $date, $apiModule, $apiAction, $label = false, $segment = false, $column = false, $language = false, $apiParameters = array(), $legendAppendMetric = true, $labelUseAbsoluteUrl = true, $labelSeries = '') { // validation of requested $period & $date if ($period == 'range') { @@ -49,7 +50,7 @@ class RowEvolution } $label = DataTablePostProcessor::unsanitizeLabelParameter($label); - $labels = Piwik::getArrayFromApiParameter($label); + $labels = Piwik::getArrayFromApiParameter($label, $onlyUnique = empty($labelSeries)); $metadata = $this->getRowEvolutionMetaData($idSite, $period, $date, $apiModule, $apiAction, $language, $apiParameters); @@ -72,7 +73,8 @@ class RowEvolution $labels, $column, $legendAppendMetric, - $labelUseAbsoluteUrl + $labelUseAbsoluteUrl, + $labelSeries ); } else { $data = $this->getSingleRowEvolution( @@ -420,8 +422,13 @@ class RowEvolution /** Get row evolution for a multiple labels */ private function getMultiRowEvolution(DataTable\Map $dataTable, $metadata, $apiModule, $apiAction, $labels, $column, $legendAppendMetric = true, - $labelUseAbsoluteUrl = true) + $labelUseAbsoluteUrl = true, + $labelSeries = '') { + $labelSeries = explode(',', $labelSeries); + $labelSeries = array_filter($labelSeries, 'strlen'); + $labelSeries = array_map('intval', $labelSeries); + if (!isset($metadata['metrics'][$column])) { // invalid column => use the first one that's available $metrics = array_keys($metadata['metrics']); @@ -455,6 +462,11 @@ class RowEvolution $cleanLabel = $this->cleanOriginalLabel($label); $actualLabels[$labelIdx] = $cleanLabel; } + + if (isset($labelSeries[$labelIdx])) { + $labelSeriesIndex = $labelSeries[$labelIdx]; + $actualLabels[$labelIdx] .= ' ' . DataComparisonFilter::getPrettyComparisonLabelFromSeriesIndex($labelSeriesIndex); + } } // convert rows to be array($column.'_'.$labelIdx => $value) as opposed to diff --git a/plugins/API/tests/Integration/Filter/DataComparisonFilter/ComparisonRowGeneratorTest.php b/plugins/API/tests/Integration/Filter/DataComparisonFilter/ComparisonRowGeneratorTest.php new file mode 100644 index 0000000000..1f9f3f17ec --- /dev/null +++ b/plugins/API/tests/Integration/Filter/DataComparisonFilter/ComparisonRowGeneratorTest.php @@ -0,0 +1,613 @@ +<?php +/** + * Piwik - free/libre analytics platform + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +namespace Piwik\Plugins\API\tests\Integration\Filter\DataComparisonFilter; + +use Piwik\DataTable; +use Piwik\Period\Factory; +use Piwik\Plugins\API\Filter\DataComparisonFilter\ComparisonRowGenerator; +use Piwik\Plugins\SegmentEditor\API; +use Piwik\Tests\Framework\TestCase\IntegrationTestCase; + +class ComparisonRowGeneratorTest extends IntegrationTestCase +{ + const TEST_SEGMENT = 'browserCode==ff'; + const OTHER_SEGMENT = 'operatingSystemCode=WIN'; + + protected static function beforeTableDataCached() + { + parent::beforeTableDataCached(); + + API::getInstance()->add('test segment', self::TEST_SEGMENT); + } + + public function test_compareTables_shouldCompareTwoDataTablesCorrectly() + { + $table1 = $this->makeTable([ + ['label' => 'row1', 'nb_visits' => 5, 'nb_actions' => 10], + ['label' => 'row2', 'nb_visits' => 10, 'nb_actions' => 25], + ['label' => 'row3', 'nb_visits' => 20], + ['label' => 'row4', 'nb_actions' => 30], + ]); + + $table2 = $this->makeTable([ + ['label' => 'row1', 'nb_visits' => 10, 'nb_actions' => 5], + ['label' => 'row3', 'somethingelse' => 25], + ]); + + $compareMetadata = [ + 'compareSegment' => self::TEST_SEGMENT, + 'comparePeriod' => 'day', + 'compareDate' => '2012-03-04', + ]; + + $comparisonRowGenerator = new ComparisonRowGenerator('reportSegment', false, []); + $comparisonRowGenerator->compareTables($compareMetadata, $table1, $table2); + + $xmlContent = $this->toXml($table1); + + $expectedXml = <<<END +<?xml version="1.0" encoding="utf-8" ?> +<result> + <row> + <label>row1</label> + <nb_visits>5</nb_visits> + <nb_actions>10</nb_actions> + <comparisons> + <row> + <nb_visits>10</nb_visits> + <nb_actions>5</nb_actions> + <compareSegment>browserCode==ff</compareSegment> + <comparePeriod>day</comparePeriod> + <compareDate>2012-03-04</compareDate> + <idsubdatatable>-1</idsubdatatable> + </row> + </comparisons> + </row> + <row> + <label>row2</label> + <nb_visits>10</nb_visits> + <nb_actions>25</nb_actions> + <comparisons> + <row> + <nb_visits>0</nb_visits> + <nb_actions>0</nb_actions> + <compareSegment>browserCode==ff</compareSegment> + <comparePeriod>day</comparePeriod> + <compareDate>2012-03-04</compareDate> + <idsubdatatable>-1</idsubdatatable> + </row> + </comparisons> + </row> + <row> + <label>row3</label> + <nb_visits>20</nb_visits> + <comparisons> + <row> + <somethingelse>25</somethingelse> + <compareSegment>browserCode==ff</compareSegment> + <comparePeriod>day</comparePeriod> + <compareDate>2012-03-04</compareDate> + <idsubdatatable>-1</idsubdatatable> + </row> + </comparisons> + </row> + <row> + <label>row4</label> + <nb_actions>30</nb_actions> + <comparisons> + <row> + <nb_actions>0</nb_actions> + <compareSegment>browserCode==ff</compareSegment> + <comparePeriod>day</comparePeriod> + <compareDate>2012-03-04</compareDate> + <idsubdatatable>-1</idsubdatatable> + </row> + </comparisons> + </row> +</result> +END; + $this->assertEquals($expectedXml, $xmlContent); + } + + public function test_compareTables_shouldUseFirstTableRowsForComparisons() + { + $table1 = $this->makeTable([ + ['label' => 'row1', 'nb_visits' => 10, 'nb_actions' => 5], + ['label' => 'row3', 'somethingelse' => 25], + ]); + + $table2 = $this->makeTable([ + ['label' => 'row1', 'nb_visits' => 5, 'nb_actions' => 10], + ['label' => 'row2', 'nb_visits' => 10, 'nb_actions' => 25], + ['label' => 'row3', 'nb_visits' => 20], + ['label' => 'row4', 'nb_actions' => 30], + ]); + + $compareMetadata = [ + 'compareSegment' => self::TEST_SEGMENT, + 'comparePeriod' => 'day', + 'compareDate' => '2012-03-04', + ]; + + $comparisonRowGenerator = new ComparisonRowGenerator('reportSegment', false, []); + $comparisonRowGenerator->compareTables($compareMetadata, $table1, $table2); + + $xmlContent = $this->toXml($table1); + + $expectedXml = <<<END +<?xml version="1.0" encoding="utf-8" ?> +<result> + <row> + <label>row1</label> + <nb_visits>10</nb_visits> + <nb_actions>5</nb_actions> + <comparisons> + <row> + <nb_visits>5</nb_visits> + <nb_actions>10</nb_actions> + <compareSegment>browserCode==ff</compareSegment> + <comparePeriod>day</comparePeriod> + <compareDate>2012-03-04</compareDate> + <idsubdatatable>-1</idsubdatatable> + </row> + </comparisons> + </row> + <row> + <label>row3</label> + <somethingelse>25</somethingelse> + <comparisons> + <row> + <nb_visits>20</nb_visits> + <compareSegment>browserCode==ff</compareSegment> + <comparePeriod>day</comparePeriod> + <compareDate>2012-03-04</compareDate> + <idsubdatatable>-1</idsubdatatable> + </row> + </comparisons> + </row> +</result> +END; + $this->assertEquals($expectedXml, $xmlContent); + } + + public function test_compareTables_shouldCompareTwoDataTableMapsCorrectly() + { + $tableSet1 = $this->makeTableMap([ + '2012-01-01' => [ + ['label' => 'row1', 'nb_visits' => 10, 'nb_actions' => 15], + ['label' => 'row2', 'nb_visits' => 15, 'nb_actions' => 15], + ['label' => 'row3', 'nb_visits' => 20, 'nb_actions' => 10], + ], + '2012-02-01' => [ + ['label' => 'row2', 'nb_visits' => 25, 'nb_actions' => 25], + ], + '2012-03-01' => [ + ['label' => 'row3', 'nb_visits' => 20, 'nb_actions' => 10], + ['label' => 'row4', 'nb_visits' => 40, 'nb_actions' => 50], + ], + ]); + + $tableSet2 = $this->makeTableMap([ + '2012-01-01' => [ + ['label' => 'row1', 'nb_visits' => 10, 'nb_actions' => 15], + ], + '2012-02-01' => [ + ['label' => 'row2', 'nb_visits' => 15, 'nb_actions' => 15], + ['label' => 'row3', 'nb_visits' => 20, 'nb_actions' => 10], + ], + '2012-03-01' => [ + ['label' => 'row2', 'nb_visits' => 25, 'nb_actions' => 25], + ['label' => 'row3', 'nb_visits' => 20, 'nb_actions' => 10], + ['label' => 'row4', 'nb_visits' => 40, 'nb_actions' => 50], + ], + ]); + + $compareMetadata = [ + 'compareSegment' => self::OTHER_SEGMENT, + 'comparePeriod' => 'month', + 'compareDate' => '2012-01-01,2012-03-01', + ]; + + $comparisonRowGenerator = new ComparisonRowGenerator('reportSegment', false, []); + $comparisonRowGenerator->compareTables($compareMetadata, $tableSet1, $tableSet2); + + $xmlContent = $this->toXml($tableSet1); + + $expectedXml = <<<END +<?xml version="1.0" encoding="utf-8" ?> +<results> + <result date="2012-01"> + <row> + <label>row1</label> + <nb_visits>10</nb_visits> + <nb_actions>15</nb_actions> + <comparisons> + <row> + <nb_visits>10</nb_visits> + <nb_actions>15</nb_actions> + <compareSegment>operatingSystemCode=WIN</compareSegment> + <comparePeriod>month</comparePeriod> + <compareDate>2012-01-01,2012-03-01</compareDate> + <idsubdatatable>-1</idsubdatatable> + </row> + </comparisons> + </row> + <row> + <label>row2</label> + <nb_visits>15</nb_visits> + <nb_actions>15</nb_actions> + <comparisons> + <row> + <nb_visits>0</nb_visits> + <nb_actions>0</nb_actions> + <compareSegment>operatingSystemCode=WIN</compareSegment> + <comparePeriod>month</comparePeriod> + <compareDate>2012-01-01,2012-03-01</compareDate> + <idsubdatatable>-1</idsubdatatable> + </row> + </comparisons> + </row> + <row> + <label>row3</label> + <nb_visits>20</nb_visits> + <nb_actions>10</nb_actions> + <comparisons> + <row> + <nb_visits>0</nb_visits> + <nb_actions>0</nb_actions> + <compareSegment>operatingSystemCode=WIN</compareSegment> + <comparePeriod>month</comparePeriod> + <compareDate>2012-01-01,2012-03-01</compareDate> + <idsubdatatable>-1</idsubdatatable> + </row> + </comparisons> + </row> + </result> + <result date="2012-02"> + <row> + <label>row2</label> + <nb_visits>25</nb_visits> + <nb_actions>25</nb_actions> + <comparisons> + <row> + <nb_visits>15</nb_visits> + <nb_actions>15</nb_actions> + <compareSegment>operatingSystemCode=WIN</compareSegment> + <comparePeriod>month</comparePeriod> + <compareDate>2012-01-01,2012-03-01</compareDate> + <idsubdatatable>-1</idsubdatatable> + </row> + </comparisons> + </row> + </result> + <result date="2012-03"> + <row> + <label>row3</label> + <nb_visits>20</nb_visits> + <nb_actions>10</nb_actions> + <comparisons> + <row> + <nb_visits>20</nb_visits> + <nb_actions>10</nb_actions> + <compareSegment>operatingSystemCode=WIN</compareSegment> + <comparePeriod>month</comparePeriod> + <compareDate>2012-01-01,2012-03-01</compareDate> + <idsubdatatable>-1</idsubdatatable> + </row> + </comparisons> + </row> + <row> + <label>row4</label> + <nb_visits>40</nb_visits> + <nb_actions>50</nb_actions> + <comparisons> + <row> + <nb_visits>40</nb_visits> + <nb_actions>50</nb_actions> + <compareSegment>operatingSystemCode=WIN</compareSegment> + <comparePeriod>month</comparePeriod> + <compareDate>2012-01-01,2012-03-01</compareDate> + <idsubdatatable>-1</idsubdatatable> + </row> + </comparisons> + </row> + </result> +</results> +END; + $this->assertEquals($expectedXml, $xmlContent); + } + + public function test_compareTables_shouldCompareTwoDataTaleMapsOfDifferentLengthsCorrectly_whenFirstIsLonger() + { + $tableSet1 = $this->makeTableMap([ + '2012-01-01' => [ + ['label' => 'row1', 'nb_visits' => 10, 'nb_actions' => 15], + ['label' => 'row2', 'nb_visits' => 15, 'nb_actions' => 15], + ['label' => 'row3', 'nb_visits' => 20, 'nb_actions' => 10], + ], + '2012-02-01' => [ + ['label' => 'row2', 'nb_visits' => 25, 'nb_actions' => 25], + ], + '2012-03-01' => [ + ['label' => 'row3', 'nb_visits' => 20, 'nb_actions' => 10], + ['label' => 'row4', 'nb_visits' => 40, 'nb_actions' => 50], + ], + ]); + + $tableSet2 = $this->makeTableMap([ + '2012-01-01' => [ + ['label' => 'row1', 'nb_visits' => 10, 'nb_actions' => 15], + ], + '2012-02-01' => [ + // empty + ], + ]); + + $compareMetadata = [ + 'compareSegment' => self::OTHER_SEGMENT, + 'comparePeriod' => 'month', + 'compareDate' => '2012-01-01,2012-03-01', + ]; + + $comparisonRowGenerator = new ComparisonRowGenerator('reportSegment', false, []); + $comparisonRowGenerator->compareTables($compareMetadata, $tableSet1, $tableSet2); + + $xmlContent = $this->toXml($tableSet1); + + $expectedXml = <<<END +<?xml version="1.0" encoding="utf-8" ?> +<results> + <result date="2012-01"> + <row> + <label>row1</label> + <nb_visits>10</nb_visits> + <nb_actions>15</nb_actions> + <comparisons> + <row> + <nb_visits>10</nb_visits> + <nb_actions>15</nb_actions> + <compareSegment>operatingSystemCode=WIN</compareSegment> + <comparePeriod>month</comparePeriod> + <compareDate>2012-01-01,2012-03-01</compareDate> + <idsubdatatable>-1</idsubdatatable> + </row> + </comparisons> + </row> + <row> + <label>row2</label> + <nb_visits>15</nb_visits> + <nb_actions>15</nb_actions> + <comparisons> + <row> + <nb_visits>0</nb_visits> + <nb_actions>0</nb_actions> + <compareSegment>operatingSystemCode=WIN</compareSegment> + <comparePeriod>month</comparePeriod> + <compareDate>2012-01-01,2012-03-01</compareDate> + <idsubdatatable>-1</idsubdatatable> + </row> + </comparisons> + </row> + <row> + <label>row3</label> + <nb_visits>20</nb_visits> + <nb_actions>10</nb_actions> + <comparisons> + <row> + <nb_visits>0</nb_visits> + <nb_actions>0</nb_actions> + <compareSegment>operatingSystemCode=WIN</compareSegment> + <comparePeriod>month</comparePeriod> + <compareDate>2012-01-01,2012-03-01</compareDate> + <idsubdatatable>-1</idsubdatatable> + </row> + </comparisons> + </row> + </result> + <result date="2012-02"> + <row> + <label>row2</label> + <nb_visits>25</nb_visits> + <nb_actions>25</nb_actions> + <comparisons> + <row> + <nb_visits>0</nb_visits> + <nb_actions>0</nb_actions> + <compareSegment>operatingSystemCode=WIN</compareSegment> + <comparePeriod>month</comparePeriod> + <compareDate>2012-01-01,2012-03-01</compareDate> + <idsubdatatable>-1</idsubdatatable> + </row> + </comparisons> + </row> + </result> + <result date="2012-03"> + <row> + <label>row3</label> + <nb_visits>20</nb_visits> + <nb_actions>10</nb_actions> + <comparisons> + <row> + <nb_visits>0</nb_visits> + <nb_actions>0</nb_actions> + <compareSegment>operatingSystemCode=WIN</compareSegment> + <comparePeriod>month</comparePeriod> + <compareDate>2012-01-01,2012-03-01</compareDate> + <idsubdatatable>-1</idsubdatatable> + </row> + </comparisons> + </row> + <row> + <label>row4</label> + <nb_visits>40</nb_visits> + <nb_actions>50</nb_actions> + <comparisons> + <row> + <nb_visits>0</nb_visits> + <nb_actions>0</nb_actions> + <compareSegment>operatingSystemCode=WIN</compareSegment> + <comparePeriod>month</comparePeriod> + <compareDate>2012-01-01,2012-03-01</compareDate> + <idsubdatatable>-1</idsubdatatable> + </row> + </comparisons> + </row> + </result> +</results> +END; + $this->assertEquals($expectedXml, $xmlContent); + } + + public function test_compareTables_shouldCompareTwoDataTaleMapsOfDifferentLengthsCorrectly_whenFirstIsShorter() + { + $tableSet1 = $this->makeTableMap([ + '2012-01-01' => [ + ['label' => 'row1', 'nb_visits' => 10, 'nb_actions' => 15], + ['label' => 'row2', 'nb_visits' => 15, 'nb_actions' => 15], + ['label' => 'row3', 'nb_visits' => 20, 'nb_actions' => 10], + ], + '2012-02-01' => [ + // empty + ], + ]); + + $tableSet2 = $this->makeTableMap([ + '2012-01-01' => [ + ['label' => 'row1', 'nb_visits' => 10, 'nb_actions' => 15], + ], + '2012-02-01' => [ + ['label' => 'row2', 'nb_visits' => 15, 'nb_actions' => 15], + ['label' => 'row3', 'nb_visits' => 20, 'nb_actions' => 10], + ], + '2012-03-01' => [ + ['label' => 'row2', 'nb_visits' => 25, 'nb_actions' => 25], + ['label' => 'row3', 'nb_visits' => 20, 'nb_actions' => 10], + ['label' => 'row4', 'nb_visits' => 40, 'nb_actions' => 50], + ], + ]); + + $compareMetadata = [ + 'compareSegment' => self::OTHER_SEGMENT, + 'comparePeriod' => 'month', + 'compareDate' => '2012-01-01,2012-03-01', + ]; + + $comparisonRowGenerator = new ComparisonRowGenerator('reportSegment', false, []); + $comparisonRowGenerator->compareTables($compareMetadata, $tableSet1, $tableSet2); + + $xmlContent = $this->toXml($tableSet1); + + $expectedXml = <<<END +<?xml version="1.0" encoding="utf-8" ?> +<results> + <result date="2012-01"> + <row> + <label>row1</label> + <nb_visits>10</nb_visits> + <nb_actions>15</nb_actions> + <comparisons> + <row> + <nb_visits>10</nb_visits> + <nb_actions>15</nb_actions> + <compareSegment>operatingSystemCode=WIN</compareSegment> + <comparePeriod>month</comparePeriod> + <compareDate>2012-01-01,2012-03-01</compareDate> + <idsubdatatable>-1</idsubdatatable> + </row> + </comparisons> + </row> + <row> + <label>row2</label> + <nb_visits>15</nb_visits> + <nb_actions>15</nb_actions> + <comparisons> + <row> + <nb_visits>0</nb_visits> + <nb_actions>0</nb_actions> + <compareSegment>operatingSystemCode=WIN</compareSegment> + <comparePeriod>month</comparePeriod> + <compareDate>2012-01-01,2012-03-01</compareDate> + <idsubdatatable>-1</idsubdatatable> + </row> + </comparisons> + </row> + <row> + <label>row3</label> + <nb_visits>20</nb_visits> + <nb_actions>10</nb_actions> + <comparisons> + <row> + <nb_visits>0</nb_visits> + <nb_actions>0</nb_actions> + <compareSegment>operatingSystemCode=WIN</compareSegment> + <comparePeriod>month</comparePeriod> + <compareDate>2012-01-01,2012-03-01</compareDate> + <idsubdatatable>-1</idsubdatatable> + </row> + </comparisons> + </row> + </result> + <result date="2012-02"> + <row> + <comparisons> + <row> + <compareSegment>operatingSystemCode=WIN</compareSegment> + <comparePeriod>month</comparePeriod> + <compareDate>2012-01-01,2012-03-01</compareDate> + <idsubdatatable>-1</idsubdatatable> + </row> + </comparisons> + </row> + </result> + <result date="2012-03"> + <row> + <comparisons> + <row> + <compareSegment>operatingSystemCode=WIN</compareSegment> + <comparePeriod>month</comparePeriod> + <compareDate>2012-01-01,2012-03-01</compareDate> + <idsubdatatable>-1</idsubdatatable> + </row> + </comparisons> + </row> + </result> +</results> +END; + $this->assertEquals($expectedXml, $xmlContent); + } + + private function makeTable(array $rows) + { + $table = new DataTable(); + $table->addRowsFromSimpleArray($rows); + return $table; + } + + private function makeTableMap(array $tableRows) + { + $result = new DataTable\Map(); + $result->setKeyName('date'); + foreach ($tableRows as $label => $rows) { + $period = Factory::build('month', $label); + + $table = $this->makeTable($rows); + $table->setMetadata('period', $period); + + $result->addTable($table, $period->getPrettyString()); + } + return $result; + } + + private function toXml(DataTable\DataTableInterface $table) + { + $renderer = new DataTable\Renderer\Xml(); + $renderer->setTable($table); + return $renderer->render(); + } +}
\ No newline at end of file diff --git a/plugins/API/tests/Unit/XmlRendererTest.php b/plugins/API/tests/Unit/XmlRendererTest.php index 833d303aac..a026f7778a 100644 --- a/plugins/API/tests/Unit/XmlRendererTest.php +++ b/plugins/API/tests/Unit/XmlRendererTest.php @@ -28,6 +28,11 @@ class XmlRendererTest extends \PHPUnit_Framework_TestCase DataTable\Manager::getInstance()->deleteAll(); } + public function tearDown() + { + DataTable\Manager::getInstance()->deleteAll(); + } + public function test_renderSuccess_shouldIncludeMessage() { $response = $this->builder->renderSuccess('ok'); |