diff options
33 files changed, 1029 insertions, 45 deletions
diff --git a/core/Application/Kernel/PluginList.php b/core/Application/Kernel/PluginList.php index c960b9fd89..2a79d9a8e3 100644 --- a/core/Application/Kernel/PluginList.php +++ b/core/Application/Kernel/PluginList.php @@ -39,6 +39,7 @@ class PluginList 'ExampleVisualization', 'ExamplePluginTemplate', 'ExampleTracker', + 'ExampleLogTables', 'ExampleReport', 'MobileAppMeasurable', 'Provider', diff --git a/core/DataAccess/LogQueryBuilder/JoinGenerator.php b/core/DataAccess/LogQueryBuilder/JoinGenerator.php index 506ee5e9d6..f964ed6594 100644 --- a/core/DataAccess/LogQueryBuilder/JoinGenerator.php +++ b/core/DataAccess/LogQueryBuilder/JoinGenerator.php @@ -53,6 +53,19 @@ class JoinGenerator if (!$logTable->getColumnToJoinOnIdVisit()) { $tableNameToJoin = $logTable->getLinkTableToBeAbleToJoinOnVisit(); + if (empty($tableNameToJoin) && $logTable->getWaysToJoinToOtherLogTables()) { + foreach ($logTable->getWaysToJoinToOtherLogTables() as $otherLogTable => $column) { + if ($this->tables->hasJoinedTable($otherLogTable)) { + $this->tables->addTableDependency($table, $otherLogTable); + continue; + } + if ($this->tables->isTableJoinableOnVisit($otherLogTable) || $this->tables->isTableJoinableOnAction($otherLogTable)) { + $this->addMissingTablesForOtherTableJoin($otherLogTable, $table); + } + } + continue; + } + if ($index > 0 && !$this->tables->hasJoinedTable($tableNameToJoin)) { $this->tables->addTableToJoin($tableNameToJoin); } @@ -96,6 +109,30 @@ class JoinGenerator } } + private function addMissingTablesForOtherTableJoin($tableName, $dependentTable) + { + $this->tables->addTableDependency($dependentTable, $tableName); + + if ($this->tables->hasJoinedTable($tableName)) { + return; + } + + $table = $this->tables->getLogTable($tableName); + + if ($table->getColumnToJoinOnIdAction() || $table->getColumnToJoinOnIdAction() || $table->getLinkTableToBeAbleToJoinOnVisit()) { + $this->tables->addTableToJoin($tableName); + return; + } + + $otherTableJoins = $table->getWaysToJoinToOtherLogTables(); + + foreach ($otherTableJoins as $logTable => $column) { + $this->addMissingTablesForOtherTableJoin($logTable, $tableName); + } + + $this->tables->addTableToJoin($tableName); + } + /** * Generate the join sql based on the needed tables * @throws Exception if tables can't be joined @@ -206,6 +243,15 @@ class JoinGenerator break; } + + $otherJoins = $logTable->getWaysToJoinToOtherLogTables(); + foreach ($otherJoins as $joinTable => $column) { + if($availableLogTable->getName() == $joinTable) { + $join = sprintf("`%s`.`%s` = `%s`.`%s`", $table, $column, $availableLogTable->getName(), $column); + break; + } + } + } if (!isset($join)) { diff --git a/core/DataAccess/LogQueryBuilder/JoinTables.php b/core/DataAccess/LogQueryBuilder/JoinTables.php index 70d4b75610..cf06382904 100644 --- a/core/DataAccess/LogQueryBuilder/JoinTables.php +++ b/core/DataAccess/LogQueryBuilder/JoinTables.php @@ -19,6 +19,40 @@ class JoinTables extends \ArrayObject */ private $logTableProvider; + // NOTE: joins can be specified explicitly as arrays w/ 'joinOn' keys or implicitly as table names. when + // table names are used, the joins dependencies are assumed based on how we want to order those joins. + // the below table list the possible dependencies of each table, and is specifically designed to enforce + // the following order: + // log_link_visit_action, log_action, log_visit, log_conversion, log_conversion_item + // which means if an array is supplied where log_visit comes before log_link_visitAction, it will + // be moved to after it. + private $implicitTableDependencies = [ + 'log_link_visit_action' => [ + // empty + ], + 'log_action' => [ + 'log_link_visit_action', + 'log_conversion', + 'log_conversion_item', + 'log_visit', + ], + 'log_visit' => [ + 'log_link_visit_action', + 'log_action', + ], + 'log_conversion' => [ + 'log_link_visit_action', + 'log_action', + 'log_visit', + ], + 'log_conversion_item' => [ + 'log_link_visit_action', + 'log_action', + 'log_visit', + 'log_conversion', + ], + ]; + /** * Tables constructor. * @param LogTablesProvider $logTablesProvider @@ -125,6 +159,73 @@ class JoinTables extends \ArrayObject $this->exchangeArray($sorted); } + public function isTableJoinableOnVisit($tableToCheck) + { + $table = $this->getLogTable($tableToCheck); + + if (empty($table)) { + return false; + } + + if ($table->getColumnToJoinOnIdVisit()) { + return true; + } + + if ($table->getLinkTableToBeAbleToJoinOnVisit()) { + return true; + } + + $otherWays = $table->getWaysToJoinToOtherLogTables(); + + if (empty($otherWays)) { + return false; + } + + foreach ($otherWays as $logTable => $column) { + if ($logTable == 'log_visit' || $this->isTableJoinableOnVisit($logTable)) { + return true; + } + } + + return false; + } + + public function isTableJoinableOnAction($tableToCheck) + { + $table = $this->getLogTable($tableToCheck); + + if (empty($table)) { + return false; + } + + if ($table->getColumnToJoinOnIdAction()) { + return true; + } + + $otherWays = $table->getWaysToJoinToOtherLogTables(); + + if (empty($otherWays)) { + return false; + } + + foreach ($otherWays as $logTable => $column) { + if ($logTable == 'log_action' || $this->isTableJoinableOnAction($logTable)) { + return true; + } + } + + return false; + } + + public function addTableDependency($table, $dependentTable) + { + if (!empty($this->implicitTableDependencies[$table])) { + return; + } + + $this->implicitTableDependencies[$table] = [$dependentTable]; + } + private function checkTableCanBeUsedForSegmentation($tableName) { if (!is_array($tableName) && !$this->getLogTable($tableName)) { @@ -154,39 +255,7 @@ class JoinTables extends \ArrayObject private function assumeImplicitJoinDependencies($allTablesToQuery, $table) { - // NOTE: joins can be specified explicitly as arrays w/ 'joinOn' keys or implicitly as table names. when - // table names are used, the joins dependencies are assumed based on how we want to order those joins. - // the below table list the possible dependencies of each table, and is specifically designed to enforce - // the following order: - // log_link_visit_action, log_action, log_visit, log_conversion, log_conversion_item - // which means if an array is supplied where log_visit comes before log_link_visitAction, it will - // be moved to after it. - $implicitTableDependencies = [ - 'log_link_visit_action' => [ - // empty - ], - 'log_action' => [ - 'log_link_visit_action', - 'log_conversion', - 'log_conversion_item', - 'log_visit', - ], - 'log_visit' => [ - 'log_link_visit_action', - 'log_action', - ], - 'log_conversion' => [ - 'log_link_visit_action', - 'log_action', - 'log_visit', - ], - 'log_conversion_item' => [ - 'log_link_visit_action', - 'log_action', - 'log_visit', - 'log_conversion', - ], - ]; + $implicitTableDependencies = $this->implicitTableDependencies; $result = []; if (isset($implicitTableDependencies[$table])) { diff --git a/plugins/ExampleLogTables/Columns/GroupAttributeAdmin.php b/plugins/ExampleLogTables/Columns/GroupAttributeAdmin.php new file mode 100644 index 0000000000..3c11627058 --- /dev/null +++ b/plugins/ExampleLogTables/Columns/GroupAttributeAdmin.php @@ -0,0 +1,14 @@ +<?php +namespace Piwik\Plugins\ExampleLogTables\Columns; + +use Piwik\Columns\Dimension; + +class GroupAttributeAdmin extends Dimension +{ + protected $dbTableName = 'log_group'; + protected $category = 'General_Visitors'; + protected $type = self::TYPE_BOOL; + protected $columnName = 'is_admin'; + protected $segmentName = 'isadmin'; + protected $nameSingular = 'Admin privileges'; +} diff --git a/plugins/ExampleLogTables/Columns/UserAttributeGender.php b/plugins/ExampleLogTables/Columns/UserAttributeGender.php new file mode 100644 index 0000000000..4a5ef0ab2c --- /dev/null +++ b/plugins/ExampleLogTables/Columns/UserAttributeGender.php @@ -0,0 +1,14 @@ +<?php +namespace Piwik\Plugins\ExampleLogTables\Columns; + +use Piwik\Columns\Dimension; + +class UserAttributeGender extends Dimension +{ + protected $dbTableName = 'log_custom'; + protected $category = 'General_Visitors'; + protected $type = self::TYPE_TEXT; + protected $columnName = 'gender'; + protected $segmentName = 'attrgender'; + protected $nameSingular = 'Gender'; +} diff --git a/plugins/ExampleLogTables/Dao/CustomGroupLog.php b/plugins/ExampleLogTables/Dao/CustomGroupLog.php new file mode 100644 index 0000000000..2b71ff33e6 --- /dev/null +++ b/plugins/ExampleLogTables/Dao/CustomGroupLog.php @@ -0,0 +1,66 @@ +<?php +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + * + */ +namespace Piwik\Plugins\ExampleLogTables\Dao; + +use Piwik\Common; +use Piwik\Db; +use Piwik\DbHelper; + +class CustomGroupLog +{ + private $table = 'log_group'; + private $tablePrefixed = ''; + + public function __construct() + { + $this->tablePrefixed = Common::prefixTable($this->table); + } + + public function install() + { + DbHelper::createTable($this->table, " + `group` VARCHAR(30) NOT NULL, + `is_admin` TINYINT(1) NOT NULL, + PRIMARY KEY (`group`)"); + } + + public function uninstall() + { + Db::query(sprintf('DROP TABLE IF EXISTS `%s`', $this->tablePrefixed)); + } + + private function getDb() + { + return Db::get(); + } + + public function getAllRecords() + { + return $this->getDb()->fetchAll('SELECT * FROM ' . $this->tablePrefixed); + } + + public function addGroupInformation($group, $isAdmin) + { + $columns = array( + 'group' => $group, + 'is_admin' => $isAdmin + ); + + $bind = array_values($columns); + $placeholder = Common::getSqlStringFieldsArray($columns); + + $sql = sprintf('INSERT INTO %s (`%s`) VALUES(%s)', + $this->tablePrefixed, implode('`,`', array_keys($columns)), $placeholder); + + $db = $this->getDb(); + + $db->query($sql, $bind); + } +} + diff --git a/plugins/ExampleLogTables/Dao/CustomUserLog.php b/plugins/ExampleLogTables/Dao/CustomUserLog.php new file mode 100644 index 0000000000..50f3016301 --- /dev/null +++ b/plugins/ExampleLogTables/Dao/CustomUserLog.php @@ -0,0 +1,68 @@ +<?php +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + * + */ +namespace Piwik\Plugins\ExampleLogTables\Dao; + +use Piwik\Common; +use Piwik\Db; +use Piwik\DbHelper; + +class CustomUserLog +{ + private $table = 'log_custom'; + private $tablePrefixed = ''; + + public function __construct() + { + $this->tablePrefixed = Common::prefixTable($this->table); + } + + public function install() + { + DbHelper::createTable($this->table, " + `user_id` VARCHAR(200) NOT NULL, + `gender` VARCHAR(30) NOT NULL, + `group` VARCHAR(30) NOT NULL, + PRIMARY KEY (user_id)"); + } + + public function uninstall() + { + Db::query(sprintf('DROP TABLE IF EXISTS `%s`', $this->tablePrefixed)); + } + + private function getDb() + { + return Db::get(); + } + + public function getAllRecords() + { + return $this->getDb()->fetchAll('SELECT * FROM ' . $this->tablePrefixed); + } + + public function addUserInformation($userId, $group, $gender) + { + $columns = array( + 'user_id' => $userId, + 'group' => $group, + 'gender' => $gender + ); + + $bind = array_values($columns); + $placeholder = Common::getSqlStringFieldsArray($columns); + + $sql = sprintf('INSERT INTO %s (`%s`) VALUES(%s)', + $this->tablePrefixed, implode('`,`', array_keys($columns)), $placeholder); + + $db = $this->getDb(); + + $db->query($sql, $bind); + } +} + diff --git a/plugins/ExampleLogTables/ExampleLogTables.php b/plugins/ExampleLogTables/ExampleLogTables.php new file mode 100644 index 0000000000..caf83d594b --- /dev/null +++ b/plugins/ExampleLogTables/ExampleLogTables.php @@ -0,0 +1,26 @@ +<?php +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + * + */ +namespace Piwik\Plugins\ExampleLogTables; + +use Piwik\Plugins\ExampleLogTables\Dao\CustomUserLog; +use Piwik\Plugins\ExampleLogTables\Dao\CustomGroupLog; + +class ExampleLogTables extends \Piwik\Plugin +{ + public function install() + { + // Install custom log table [disabled as example only] + + // $userLog = new CustomUserLog(); + // $userLog->install(); + + // $userLog = new CustomGroupLog(); + // $userLog->install(); + } +}
\ No newline at end of file diff --git a/plugins/ExampleLogTables/Tracker/LogTable/CustomGroupLog.php b/plugins/ExampleLogTables/Tracker/LogTable/CustomGroupLog.php new file mode 100644 index 0000000000..7ee199bc11 --- /dev/null +++ b/plugins/ExampleLogTables/Tracker/LogTable/CustomGroupLog.php @@ -0,0 +1,34 @@ +<?php +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + * + */ +namespace Piwik\Plugins\ExampleLogTables\Tracker\LogTable; + +use Piwik\Tracker\LogTable; + +class CustomGroupLog extends LogTable +{ + public function getName() + { + return 'log_group'; + } + + public function getIdColumn() + { + return 'group'; + } + + public function getPrimaryKey() + { + return ['group']; + } + + public function getWaysToJoinToOtherLogTables() + { + return ['log_custom' => 'group']; + } +} diff --git a/plugins/ExampleLogTables/Tracker/LogTable/CustomUserLog.php b/plugins/ExampleLogTables/Tracker/LogTable/CustomUserLog.php new file mode 100644 index 0000000000..5cbc20d83a --- /dev/null +++ b/plugins/ExampleLogTables/Tracker/LogTable/CustomUserLog.php @@ -0,0 +1,34 @@ +<?php +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + * + */ +namespace Piwik\Plugins\ExampleLogTables\Tracker\LogTable; + +use Piwik\Tracker\LogTable; + +class CustomUserLog extends LogTable +{ + public function getName() + { + return 'log_custom'; + } + + public function getIdColumn() + { + return 'user_id'; + } + + public function getPrimaryKey() + { + return ['user_id']; + } + + public function getWaysToJoinToOtherLogTables() + { + return ['log_visit' => 'user_id']; + } +} diff --git a/plugins/ExampleLogTables/plugin.json b/plugins/ExampleLogTables/plugin.json new file mode 100644 index 0000000000..3e6442de06 --- /dev/null +++ b/plugins/ExampleLogTables/plugin.json @@ -0,0 +1,29 @@ +{ + "name": "ExampleLogTables", + "description": "Matomo Platform showcase: how to create custom log tables.", + "version": "0.1.0", + "theme": false, + "require": { + "piwik": ">=3.0.0-b1,<4.0.0-b1" + }, + "authors": [ + { + "name": "Matomo", + "email": "", + "homepage": "" + } + ], + "support": { + "email": "", + "issues": "", + "forum": "", + "irc": "", + "wiki": "", + "source": "", + "docs": "", + "rss": "" + }, + "homepage": "", + "license": "GPL v3+", + "keywords": [] +} diff --git a/plugins/ExampleLogTables/tests/Fixtures/VisitsWithUserIdAndCustomData.php b/plugins/ExampleLogTables/tests/Fixtures/VisitsWithUserIdAndCustomData.php new file mode 100644 index 0000000000..4324e58d54 --- /dev/null +++ b/plugins/ExampleLogTables/tests/Fixtures/VisitsWithUserIdAndCustomData.php @@ -0,0 +1,110 @@ +<?php +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ +namespace Piwik\Plugins\ExampleLogTables\tests\Fixtures; + +use Piwik\Date; +use Piwik\Plugins\ExampleLogTables\Dao\CustomGroupLog; +use Piwik\Plugins\ExampleLogTables\Dao\CustomUserLog; +use Piwik\Tests\Framework\Fixture; + +class VisitsWithUserIdAndCustomData extends Fixture +{ + public $dateTime = '2018-02-01 11:22:33'; + public $idSite = 1; + + private static $countryCodes = ['CA', 'CN', 'DE', 'ES', 'FR', 'IE', 'IN', 'IT', 'MX', 'PT', 'RU', 'GB', 'US']; + + public function setUp() + { + if (!self::siteCreated($idSite = 1)) { + self::createWebsite($this->dateTime); + } + + // set up database tables + $userLog = new CustomUserLog(); + $userLog->install(); + $groupLog = new CustomGroupLog(); + $groupLog->install(); + + $this->trackVisits(); + $this->insertCustomUserLogData(); + $this->insertCustomGroupLogData(); + } + + private function trackVisits() + { + $t = self::getTracker($this->idSite, $this->dateTime, $defaultInit = true); + $t->setTokenAuth(self::getTokenAuth()); + $t->enableBulkTracking(); + + foreach (array('user1', 'user2', 'user3', 'user4', false) as $key => $userId) { + for ($numVisits = 0; $numVisits < ($key+1) * 10; $numVisits++) { + $t->setUserId($userId); + $t->setPlugins($numVisits % 3 == 0, $numVisits % 5 == 0, $numVisits % 7 == 0); + $t->setBrowserHasCookies($numVisits % 3 == 0); + $t->setCountry(self::$countryCodes[$numVisits % count(self::$countryCodes)]); + + if ($numVisits % 5 == 0) { + $t->doTrackSiteSearch('some search term' . $numVisits); + } + + if ($numVisits % 4 == 0) { + $t->doTrackEvent('Event action ' . $numVisits, 'event cat ' . $numVisits); + } + + if ($numVisits % 7 == 0) { + $t->doTrackContentInteraction('click', 'slider ' . $numVisits%4); + } + + if ($numVisits % 7 == 4) { + $t->doTrackAction('http://out.link', 'outlink'); + } + + if ($numVisits % 5 == 3) { + $t->setEcommerceView('SKU VERY nice indeed ' . ($numVisits%3), 'PRODUCT name ' . ($numVisits%4), 'category ' . ($numVisits%5), $numVisits*2.79); + } + + $t->setForceNewVisit(); + $t->setUrl('http://example.org/my/dir/page' . ($numVisits % 4)); + + $visitDateTime = Date::factory($this->dateTime)->addHour($numVisits*6)->getDatetime(); + $t->setForceVisitDateTime($visitDateTime); + + if ($numVisits % 7 == 0) { + $t->doTrackAction('http://example.org/download.pdf', 'download'); + } + + self::assertTrue($t->doTrackPageView('incredible title ' . ($numVisits % 3))); + + if ($numVisits % 9 == 0) { + $t->setForceVisitDateTime(Date::factory($this->dateTime)->addHour($numVisits*6.1)->getDatetime()); + $t->addEcommerceItem('SKU VERY nice indeed ' . ($numVisits%3), 'PRODUCT name ' . ($numVisits%4), 'category ' . ($numVisits%5), $numVisits*2.79); + self::assertTrue($t->doTrackEcommerceCartUpdate($numVisits*17)); + } + } + } + + self::checkBulkTrackingResponse($t->doBulkTrack()); + } + + private function insertCustomUserLogData() + { + $customLog = new CustomUserLog(); + $customLog->addUserInformation('user1', 'admin', 'men'); + $customLog->addUserInformation('user2', 'user', 'women'); + $customLog->addUserInformation('user3', 'admin', 'women'); + $customLog->addUserInformation('user4', '', 'men'); + } + + private function insertCustomGroupLogData() + { + $customGroup = new CustomGroupLog(); + $customGroup->addGroupInformation('admin', 1); + $customGroup->addGroupInformation('user', 0); + } +}
\ No newline at end of file diff --git a/plugins/ExampleLogTables/tests/System/CustomLogTablesTest.php b/plugins/ExampleLogTables/tests/System/CustomLogTablesTest.php new file mode 100644 index 0000000000..1cf254f3dc --- /dev/null +++ b/plugins/ExampleLogTables/tests/System/CustomLogTablesTest.php @@ -0,0 +1,138 @@ +<?php +namespace Piwik\Plugins\ExampleLogTables\tests\System; + +use Piwik\Plugins\ExampleLogTables\tests\Fixtures\VisitsWithUserIdAndCustomData; +use Piwik\Tests\Framework\TestCase\SystemTestCase; +use Piwik\Tests\Framework\TestRequest\ApiTestConfig; +use Piwik\Tests\Framework\TestRequest\Response; + +/** + * Testing Custom Log Tables + * + * @group ExampleLogTables + * @group Plugins + */ +class CustomLogTablesTest extends SystemTestCase +{ + /** + * @var VisitsWithUserIdAndCustomData + */ + public static $fixture = null; // initialized below class definition + + /** + * @dataProvider getApiForTesting + */ + public function testApi($api, $params) + { + $this->runApiTests($api, $params); + } + + /** + * @dataProvider getSegmentsToTest + */ + public function testNoApiReturnsError($segment) + { + $dateTime = self::$fixture->dateTime; + $idSite1 = self::$fixture->idSite; + + $params = [ + 'idSite' => $idSite1, + 'date' => $dateTime, + 'periods' => 'month', + 'setDateLastN' => false, + 'format' => 'JSON', + 'segment' => $segment, + 'testSuffix' => '' + ]; + + $testConfig = new ApiTestConfig($params); + $testRequests = $this->getTestRequestsCollection('all', $testConfig, 'all'); + + foreach ($testRequests->getRequestUrls() as $apiId => $requestUrl) { + $response = Response::loadFromApi($params, $requestUrl, false); + $decoded = json_decode($response->getResponseText(), true); + + if (is_array($decoded) && isset($decoded['result']) && $decoded['result'] == 'error') { + $this->fail('API returned an error when requesting ' . http_build_query($requestUrl) . "\nMessage: " . $decoded['message']); + } + } + } + + public function getSegmentsToTest() + { + return [ + ['attrgender==men'], + ['isadmin==1'], + ]; + } + + public function getApiForTesting() + { + $dateTime = self::$fixture->dateTime; + $idSite1 = self::$fixture->idSite; + + $result = [ + [[ + 'Actions.get', + 'UserId.getUsers', + 'VisitsSummary.get' + ], [ + 'idSite' => $idSite1, + 'date' => $dateTime, + 'periods' => 'month', + 'setDateLastN' => false, + 'segment' => 'attrgender==men', + 'testSuffix' => '_men'] + ], + [[ + 'Actions.get', + 'UserId.getUsers', + 'VisitsSummary.get' + ], [ + 'idSite' => $idSite1, + 'date' => $dateTime, + 'periods' => 'month', + 'setDateLastN' => false, + 'segment' => 'attrgender==women', + 'testSuffix' => '_women'] + ], + [[ + 'Actions.get', + 'UserId.getUsers', + 'VisitsSummary.get' + ], [ + 'idSite' => $idSite1, + 'date' => $dateTime, + 'periods' => 'month', + 'setDateLastN' => false, + 'segment' => 'isadmin==1', + 'testSuffix' => '_admin'] + ], + [[ + 'Actions.get', + 'UserId.getUsers', + 'VisitsSummary.get' + ], [ + 'idSite' => $idSite1, + 'date' => $dateTime, + 'periods' => 'month', + 'setDateLastN' => false, + 'testSuffix' => '_all'] + ], + ]; + + return $result; + } + + public static function getOutputPrefix() + { + return 'ExampleLogTables'; + } + + public static function getPathToTestDirectory() + { + return dirname(__FILE__); + } +} + +CustomLogTablesTest::$fixture = new VisitsWithUserIdAndCustomData(); diff --git a/plugins/ExampleLogTables/tests/System/expected/test_ExampleLogTables_admin__Actions.get_month.xml b/plugins/ExampleLogTables/tests/System/expected/test_ExampleLogTables_admin__Actions.get_month.xml new file mode 100644 index 0000000000..d73153a828 --- /dev/null +++ b/plugins/ExampleLogTables/tests/System/expected/test_ExampleLogTables_admin__Actions.get_month.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="utf-8" ?> +<result> + <nb_pageviews>45</nb_pageviews> + <nb_uniq_pageviews>40</nb_uniq_pageviews> + <nb_downloads>7</nb_downloads> + <nb_uniq_downloads>7</nb_uniq_downloads> + <nb_outlinks>0</nb_outlinks> + <nb_uniq_outlinks>0</nb_uniq_outlinks> + <nb_searches>8</nb_searches> + <nb_keywords>6</nb_keywords> +</result>
\ No newline at end of file diff --git a/plugins/ExampleLogTables/tests/System/expected/test_ExampleLogTables_admin__UserId.getUsers_month.xml b/plugins/ExampleLogTables/tests/System/expected/test_ExampleLogTables_admin__UserId.getUsers_month.xml new file mode 100644 index 0000000000..e9d5ed7b27 --- /dev/null +++ b/plugins/ExampleLogTables/tests/System/expected/test_ExampleLogTables_admin__UserId.getUsers_month.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="utf-8" ?> +<result> + <row> + <label>user3</label> + <nb_visits>34</nb_visits> + <nb_actions>53</nb_actions> + <max_actions>3</max_actions> + <sum_visit_length>20</sum_visit_length> + <bounce_count>19</bounce_count> + <nb_visits_converted>0</nb_visits_converted> + <sum_daily_nb_uniq_visitors>8</sum_daily_nb_uniq_visitors> + <sum_daily_nb_users>8</sum_daily_nb_users> + + </row> + <row> + <label>user1</label> + <nb_visits>12</nb_visits> + <nb_actions>18</nb_actions> + <max_actions>3</max_actions> + <sum_visit_length>6</sum_visit_length> + <bounce_count>7</bounce_count> + <nb_visits_converted>0</nb_visits_converted> + <sum_daily_nb_uniq_visitors>3</sum_daily_nb_uniq_visitors> + <sum_daily_nb_users>3</sum_daily_nb_users> + + </row> +</result>
\ No newline at end of file diff --git a/plugins/ExampleLogTables/tests/System/expected/test_ExampleLogTables_admin__VisitsSummary.get_month.xml b/plugins/ExampleLogTables/tests/System/expected/test_ExampleLogTables_admin__VisitsSummary.get_month.xml new file mode 100644 index 0000000000..eeeb691530 --- /dev/null +++ b/plugins/ExampleLogTables/tests/System/expected/test_ExampleLogTables_admin__VisitsSummary.get_month.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="utf-8" ?> +<result> + <nb_uniq_visitors>2</nb_uniq_visitors> + <nb_users>2</nb_users> + <nb_visits>46</nb_visits> + <nb_actions>71</nb_actions> + <nb_visits_converted>0</nb_visits_converted> + <bounce_count>26</bounce_count> + <sum_visit_length>26</sum_visit_length> + <max_actions>3</max_actions> + <bounce_rate>57%</bounce_rate> + <nb_actions_per_visit>1.5</nb_actions_per_visit> + <avg_time_on_site>1</avg_time_on_site> +</result>
\ No newline at end of file diff --git a/plugins/ExampleLogTables/tests/System/expected/test_ExampleLogTables_all__Actions.get_month.xml b/plugins/ExampleLogTables/tests/System/expected/test_ExampleLogTables_all__Actions.get_month.xml new file mode 100644 index 0000000000..951eb7a039 --- /dev/null +++ b/plugins/ExampleLogTables/tests/System/expected/test_ExampleLogTables_all__Actions.get_month.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="utf-8" ?> +<result> + <nb_pageviews>171</nb_pageviews> + <nb_uniq_pageviews>156</nb_uniq_pageviews> + <nb_downloads>24</nb_downloads> + <nb_uniq_downloads>24</nb_uniq_downloads> + <nb_outlinks>0</nb_outlinks> + <nb_uniq_outlinks>0</nb_uniq_outlinks> + <nb_searches>30</nb_searches> + <nb_keywords>10</nb_keywords> +</result>
\ No newline at end of file diff --git a/plugins/ExampleLogTables/tests/System/expected/test_ExampleLogTables_all__UserId.getUsers_month.xml b/plugins/ExampleLogTables/tests/System/expected/test_ExampleLogTables_all__UserId.getUsers_month.xml new file mode 100644 index 0000000000..537029b08e --- /dev/null +++ b/plugins/ExampleLogTables/tests/System/expected/test_ExampleLogTables_all__UserId.getUsers_month.xml @@ -0,0 +1,51 @@ +<?xml version="1.0" encoding="utf-8" ?> +<result> + <row> + <label>user4</label> + <nb_visits>45</nb_visits> + <nb_actions>70</nb_actions> + <max_actions>3</max_actions> + <sum_visit_length>25</sum_visit_length> + <bounce_count>25</bounce_count> + <nb_visits_converted>0</nb_visits_converted> + <sum_daily_nb_uniq_visitors>11</sum_daily_nb_uniq_visitors> + <sum_daily_nb_users>11</sum_daily_nb_users> + + </row> + <row> + <label>user3</label> + <nb_visits>34</nb_visits> + <nb_actions>53</nb_actions> + <max_actions>3</max_actions> + <sum_visit_length>20</sum_visit_length> + <bounce_count>19</bounce_count> + <nb_visits_converted>0</nb_visits_converted> + <sum_daily_nb_uniq_visitors>8</sum_daily_nb_uniq_visitors> + <sum_daily_nb_users>8</sum_daily_nb_users> + + </row> + <row> + <label>user2</label> + <nb_visits>22</nb_visits> + <nb_actions>35</nb_actions> + <max_actions>3</max_actions> + <sum_visit_length>12</sum_visit_length> + <bounce_count>12</bounce_count> + <nb_visits_converted>0</nb_visits_converted> + <sum_daily_nb_uniq_visitors>6</sum_daily_nb_uniq_visitors> + <sum_daily_nb_users>6</sum_daily_nb_users> + + </row> + <row> + <label>user1</label> + <nb_visits>12</nb_visits> + <nb_actions>18</nb_actions> + <max_actions>3</max_actions> + <sum_visit_length>6</sum_visit_length> + <bounce_count>7</bounce_count> + <nb_visits_converted>0</nb_visits_converted> + <sum_daily_nb_uniq_visitors>3</sum_daily_nb_uniq_visitors> + <sum_daily_nb_users>3</sum_daily_nb_users> + + </row> +</result>
\ No newline at end of file diff --git a/plugins/ExampleLogTables/tests/System/expected/test_ExampleLogTables_all__VisitsSummary.get_month.xml b/plugins/ExampleLogTables/tests/System/expected/test_ExampleLogTables_all__VisitsSummary.get_month.xml new file mode 100644 index 0000000000..f22cfae43b --- /dev/null +++ b/plugins/ExampleLogTables/tests/System/expected/test_ExampleLogTables_all__VisitsSummary.get_month.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="utf-8" ?> +<result> + <nb_uniq_visitors>54</nb_uniq_visitors> + <nb_users>4</nb_users> + <nb_visits>195</nb_visits> + <nb_actions>264</nb_actions> + <nb_visits_converted>0</nb_visits_converted> + <bounce_count>130</bounce_count> + <sum_visit_length>80</sum_visit_length> + <max_actions>3</max_actions> + <bounce_rate>67%</bounce_rate> + <nb_actions_per_visit>1.4</nb_actions_per_visit> + <avg_time_on_site>0</avg_time_on_site> +</result>
\ No newline at end of file diff --git a/plugins/ExampleLogTables/tests/System/expected/test_ExampleLogTables_men__Actions.get_month.xml b/plugins/ExampleLogTables/tests/System/expected/test_ExampleLogTables_men__Actions.get_month.xml new file mode 100644 index 0000000000..052c729dda --- /dev/null +++ b/plugins/ExampleLogTables/tests/System/expected/test_ExampleLogTables_men__Actions.get_month.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="utf-8" ?> +<result> + <nb_pageviews>57</nb_pageviews> + <nb_uniq_pageviews>50</nb_uniq_pageviews> + <nb_downloads>8</nb_downloads> + <nb_uniq_downloads>8</nb_uniq_downloads> + <nb_outlinks>0</nb_outlinks> + <nb_uniq_outlinks>0</nb_uniq_outlinks> + <nb_searches>10</nb_searches> + <nb_keywords>8</nb_keywords> +</result>
\ No newline at end of file diff --git a/plugins/ExampleLogTables/tests/System/expected/test_ExampleLogTables_men__UserId.getUsers_month.xml b/plugins/ExampleLogTables/tests/System/expected/test_ExampleLogTables_men__UserId.getUsers_month.xml new file mode 100644 index 0000000000..f88f48454a --- /dev/null +++ b/plugins/ExampleLogTables/tests/System/expected/test_ExampleLogTables_men__UserId.getUsers_month.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="utf-8" ?> +<result> + <row> + <label>user4</label> + <nb_visits>45</nb_visits> + <nb_actions>70</nb_actions> + <max_actions>3</max_actions> + <sum_visit_length>25</sum_visit_length> + <bounce_count>25</bounce_count> + <nb_visits_converted>0</nb_visits_converted> + <sum_daily_nb_uniq_visitors>11</sum_daily_nb_uniq_visitors> + <sum_daily_nb_users>11</sum_daily_nb_users> + + </row> + <row> + <label>user1</label> + <nb_visits>12</nb_visits> + <nb_actions>18</nb_actions> + <max_actions>3</max_actions> + <sum_visit_length>6</sum_visit_length> + <bounce_count>7</bounce_count> + <nb_visits_converted>0</nb_visits_converted> + <sum_daily_nb_uniq_visitors>3</sum_daily_nb_uniq_visitors> + <sum_daily_nb_users>3</sum_daily_nb_users> + + </row> +</result>
\ No newline at end of file diff --git a/plugins/ExampleLogTables/tests/System/expected/test_ExampleLogTables_men__VisitsSummary.get_month.xml b/plugins/ExampleLogTables/tests/System/expected/test_ExampleLogTables_men__VisitsSummary.get_month.xml new file mode 100644 index 0000000000..4e9a3bd824 --- /dev/null +++ b/plugins/ExampleLogTables/tests/System/expected/test_ExampleLogTables_men__VisitsSummary.get_month.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="utf-8" ?> +<result> + <nb_uniq_visitors>2</nb_uniq_visitors> + <nb_users>2</nb_users> + <nb_visits>57</nb_visits> + <nb_actions>88</nb_actions> + <nb_visits_converted>0</nb_visits_converted> + <bounce_count>32</bounce_count> + <sum_visit_length>31</sum_visit_length> + <max_actions>3</max_actions> + <bounce_rate>56%</bounce_rate> + <nb_actions_per_visit>1.5</nb_actions_per_visit> + <avg_time_on_site>1</avg_time_on_site> +</result>
\ No newline at end of file diff --git a/plugins/ExampleLogTables/tests/System/expected/test_ExampleLogTables_women__Actions.get_month.xml b/plugins/ExampleLogTables/tests/System/expected/test_ExampleLogTables_women__Actions.get_month.xml new file mode 100644 index 0000000000..4269b53424 --- /dev/null +++ b/plugins/ExampleLogTables/tests/System/expected/test_ExampleLogTables_women__Actions.get_month.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="utf-8" ?> +<result> + <nb_pageviews>57</nb_pageviews> + <nb_uniq_pageviews>50</nb_uniq_pageviews> + <nb_downloads>8</nb_downloads> + <nb_uniq_downloads>8</nb_uniq_downloads> + <nb_outlinks>0</nb_outlinks> + <nb_uniq_outlinks>0</nb_uniq_outlinks> + <nb_searches>10</nb_searches> + <nb_keywords>6</nb_keywords> +</result>
\ No newline at end of file diff --git a/plugins/ExampleLogTables/tests/System/expected/test_ExampleLogTables_women__UserId.getUsers_month.xml b/plugins/ExampleLogTables/tests/System/expected/test_ExampleLogTables_women__UserId.getUsers_month.xml new file mode 100644 index 0000000000..d9abec4587 --- /dev/null +++ b/plugins/ExampleLogTables/tests/System/expected/test_ExampleLogTables_women__UserId.getUsers_month.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="utf-8" ?> +<result> + <row> + <label>user3</label> + <nb_visits>34</nb_visits> + <nb_actions>53</nb_actions> + <max_actions>3</max_actions> + <sum_visit_length>20</sum_visit_length> + <bounce_count>19</bounce_count> + <nb_visits_converted>0</nb_visits_converted> + <sum_daily_nb_uniq_visitors>8</sum_daily_nb_uniq_visitors> + <sum_daily_nb_users>8</sum_daily_nb_users> + + </row> + <row> + <label>user2</label> + <nb_visits>22</nb_visits> + <nb_actions>35</nb_actions> + <max_actions>3</max_actions> + <sum_visit_length>12</sum_visit_length> + <bounce_count>12</bounce_count> + <nb_visits_converted>0</nb_visits_converted> + <sum_daily_nb_uniq_visitors>6</sum_daily_nb_uniq_visitors> + <sum_daily_nb_users>6</sum_daily_nb_users> + + </row> +</result>
\ No newline at end of file diff --git a/plugins/ExampleLogTables/tests/System/expected/test_ExampleLogTables_women__VisitsSummary.get_month.xml b/plugins/ExampleLogTables/tests/System/expected/test_ExampleLogTables_women__VisitsSummary.get_month.xml new file mode 100644 index 0000000000..9c82720353 --- /dev/null +++ b/plugins/ExampleLogTables/tests/System/expected/test_ExampleLogTables_women__VisitsSummary.get_month.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="utf-8" ?> +<result> + <nb_uniq_visitors>2</nb_uniq_visitors> + <nb_users>2</nb_users> + <nb_visits>56</nb_visits> + <nb_actions>88</nb_actions> + <nb_visits_converted>0</nb_visits_converted> + <bounce_count>31</bounce_count> + <sum_visit_length>32</sum_visit_length> + <max_actions>3</max_actions> + <bounce_rate>55%</bounce_rate> + <nb_actions_per_visit>1.6</nb_actions_per_visit> + <avg_time_on_site>1</avg_time_on_site> +</result>
\ No newline at end of file diff --git a/plugins/UserId/Archiver.php b/plugins/UserId/Archiver.php index 28c32bf3c6..584f364d3c 100644 --- a/plugins/UserId/Archiver.php +++ b/plugins/UserId/Archiver.php @@ -80,7 +80,7 @@ class Archiver extends \Piwik\Plugin\Archiver /** @var \Zend_Db_Statement $query */ $query = $this->getLogAggregator()->queryVisitsByDimension( array(self::USER_ID_FIELD), - "$userIdFieldName IS NOT NULL AND $userIdFieldName != ''", + "log_visit.$userIdFieldName IS NOT NULL AND log_visit.$userIdFieldName != ''", array("LOWER(HEX($visitorIdFieldName)) as $visitorIdFieldName") ); diff --git a/tests/PHPUnit/Framework/Mock/Plugin/CustomUserLogTable.php b/tests/PHPUnit/Framework/Mock/Plugin/CustomUserLogTable.php new file mode 100644 index 0000000000..e06e732d16 --- /dev/null +++ b/tests/PHPUnit/Framework/Mock/Plugin/CustomUserLogTable.php @@ -0,0 +1,28 @@ +<?php + +namespace Piwik\Tests\Framework\Mock\Plugin; + +use Piwik\Tracker\LogTable; + +class CustomUserLogTable extends LogTable +{ + public function getName() + { + return 'log_custom'; + } + + public function getIdColumn() + { + return 'user_id'; + } + + public function getPrimaryKey() + { + return ['user_id']; + } + + public function getWaysToJoinToOtherLogTables() + { + return ['log_visit' => 'user_id']; + } +} diff --git a/tests/PHPUnit/Framework/Mock/Plugin/LogTablesProvider.php b/tests/PHPUnit/Framework/Mock/Plugin/LogTablesProvider.php index c563e250cd..f4a643018a 100644 --- a/tests/PHPUnit/Framework/Mock/Plugin/LogTablesProvider.php +++ b/tests/PHPUnit/Framework/Mock/Plugin/LogTablesProvider.php @@ -21,7 +21,9 @@ class LogTablesProvider extends \Piwik\Plugin\LogTablesProvider new Action(), new LinkVisitAction(), new ConversionItem(), - new Conversion() + new Conversion(), + new CustomUserLogTable(), + new OtherCustomUserLogTable() ); } diff --git a/tests/PHPUnit/Framework/Mock/Plugin/OtherCustomUserLogTable.php b/tests/PHPUnit/Framework/Mock/Plugin/OtherCustomUserLogTable.php new file mode 100644 index 0000000000..68a84855cd --- /dev/null +++ b/tests/PHPUnit/Framework/Mock/Plugin/OtherCustomUserLogTable.php @@ -0,0 +1,28 @@ +<?php + +namespace Piwik\Tests\Framework\Mock\Plugin; + +use Piwik\Tracker\LogTable; + +class OtherCustomUserLogTable extends LogTable +{ + public function getName() + { + return 'log_custom_other'; + } + + public function getIdColumn() + { + return 'other_id'; + } + + public function getPrimaryKey() + { + return ['other_id']; + } + + public function getWaysToJoinToOtherLogTables() + { + return ['log_custom' => 'other_id']; + } +} diff --git a/tests/PHPUnit/Unit/DataAccess/LogQueryBuilder/JoinGeneratorTest.php b/tests/PHPUnit/Unit/DataAccess/LogQueryBuilder/JoinGeneratorTest.php index 1012e58ec3..6f77720372 100644 --- a/tests/PHPUnit/Unit/DataAccess/LogQueryBuilder/JoinGeneratorTest.php +++ b/tests/PHPUnit/Unit/DataAccess/LogQueryBuilder/JoinGeneratorTest.php @@ -67,12 +67,6 @@ class JoinGeneratorTest extends \PHPUnit_Framework_TestCase $this->assertEquals($expected, $generator->getJoinString()); } - public function test_generate_getJoinString_OnlyOneTable() - { - $generator = $this->generate(array('log_visit')); - $this->assertEquals('log_visit AS log_visit', $generator->getJoinString()); - } - public function test_generate_getJoinString_OnlyOneActionTable() { $generator = $this->generate(array('log_action')); @@ -87,6 +81,43 @@ class JoinGeneratorTest extends \PHPUnit_Framework_TestCase $this->assertEquals($expected, $generator->getJoinString()); } + public function test_generate_getJoinString_JoinCustomVisitTable() + { + $generator = $this->generate(array('log_visit', 'log_custom')); + $this->assertEquals('log_visit AS log_visit LEFT JOIN log_custom AS log_custom ON `log_custom`.`user_id` = `log_visit`.`user_id`', $generator->getJoinString()); + } + + public function test_generate_getJoinString_JoinMultipleCustomVisitTable() + { + $generator = $this->generate(array('log_visit', 'log_custom_other', 'log_custom')); + $this->assertEquals('log_visit AS log_visit LEFT JOIN log_custom AS log_custom ON `log_custom`.`user_id` = `log_visit`.`user_id` LEFT JOIN log_custom_other AS log_custom_other ON `log_custom_other`.`other_id` = `log_custom`.`other_id`', $generator->getJoinString()); + } + + public function test_generate_getJoinString_JoinMultipleCustomVisitTableWithMissingOne() + { + $generator = $this->generate(array('log_visit', 'log_custom_other')); + $this->assertEquals('log_visit AS log_visit LEFT JOIN log_custom AS log_custom ON `log_custom`.`user_id` = `log_visit`.`user_id` LEFT JOIN log_custom_other AS log_custom_other ON `log_custom_other`.`other_id` = `log_custom`.`other_id`', $generator->getJoinString()); + } + + /** + * @expectedException \Exception + * @expectedExceptionMessage Table 'log_visit' can't be joined for segmentation + * + * Note: the exception reports `log_visit` and not `log_custom` as it resolves the dependencies as so resolves + * from `log_custom` to `log_visit` but is then not able to find a way to join `log_visit` with `log_action` + */ + public function test_generate_getJoinString_CustomVisitTableCantBeJoinedWithAction() + { + $generator = $this->generate(array('log_action', 'log_custom')); + $generator->getJoinString(); + } + + public function test_generate_getJoinString_JoinCustomVisitTableMultiple() + { + $generator = $this->generate(array('log_visit', 'log_action', 'log_custom')); + $this->assertEquals('log_visit AS log_visit LEFT JOIN log_link_visit_action AS log_link_visit_action ON log_link_visit_action.idvisit = log_visit.idvisit LEFT JOIN log_action AS log_action ON log_link_visit_action.idaction_url = log_action.idaction LEFT JOIN log_custom AS log_custom ON `log_custom`.`user_id` = `log_visit`.`user_id`', $generator->getJoinString()); + } + public function test_generate_getJoinString_manuallyJoinedAlready() { $generator = $this->generate(array( diff --git a/tests/PHPUnit/Unit/DataAccess/LogQueryBuilder/JoinTablesTest.php b/tests/PHPUnit/Unit/DataAccess/LogQueryBuilder/JoinTablesTest.php index 33f5c4cdd0..6bf7395549 100644 --- a/tests/PHPUnit/Unit/DataAccess/LogQueryBuilder/JoinTablesTest.php +++ b/tests/PHPUnit/Unit/DataAccess/LogQueryBuilder/JoinTablesTest.php @@ -39,6 +39,20 @@ class JoinTablesTest extends \PHPUnit_Framework_TestCase $this->makeTables(array('log_visit', 'log_foo_bar_baz')); } + public function test_hasJoinedTable_custom() + { + $tables = $this->makeTables(array('log_visit', 'log_custom')); + $this->assertTrue($tables->hasJoinedTable('log_visit')); + $this->assertTrue($tables->hasJoinedTable('log_custom')); + } + + public function test_hasJoinedTable_custom2() + { + $tables = $this->makeTables(array('log_visit', 'log_custom_other')); + $this->assertTrue($tables->hasJoinedTable('log_visit')); + $this->assertTrue($tables->hasJoinedTable('log_custom_other')); + } + public function test_hasJoinedTable_shouldDetectIfTableIsAlreadyAdded() { $this->assertTrue($this->tables->hasJoinedTable('log_visit')); diff --git a/tests/UI/expected-screenshots/UIIntegrationTest_admin_plugins.png b/tests/UI/expected-screenshots/UIIntegrationTest_admin_plugins.png index 97f91fba8e..75088cdb4a 100644 --- a/tests/UI/expected-screenshots/UIIntegrationTest_admin_plugins.png +++ b/tests/UI/expected-screenshots/UIIntegrationTest_admin_plugins.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e25803802e946ac8d73770defc818e29ef83265cad9d4692dc5d3bdeeb197013 -size 1058541 +oid sha256:3dcf01d06e413501b56bd58e24738864406092435974b3dac45650d46dee4dfa +size 1068670 diff --git a/tests/UI/expected-screenshots/UIIntegrationTest_admin_plugins_no_internet.png b/tests/UI/expected-screenshots/UIIntegrationTest_admin_plugins_no_internet.png index 8371292920..fe3156ddae 100644 --- a/tests/UI/expected-screenshots/UIIntegrationTest_admin_plugins_no_internet.png +++ b/tests/UI/expected-screenshots/UIIntegrationTest_admin_plugins_no_internet.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:62a82dd8080ba626cb4350493e55173a5008a34b2e3036ed8631b37e70146571 -size 1059908 +oid sha256:e0869595907cda432fd31b52977c3c055df0a9969b4bd69fd9eb86b17e25e4d5 +size 1069291 |