add icons for Character groups

This commit is contained in:
coderkun 2014-04-29 14:18:04 +02:00
commit 2d9a41a5fe
3461 changed files with 594457 additions and 0 deletions

View file

@ -0,0 +1,496 @@
<?php
/**
* Piwik - Open source web analytics
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\Plugins\MultiSites;
use Exception;
use Piwik\API\Request;
use Piwik\Archive;
use Piwik\Common;
use Piwik\DataTable\Filter\CalculateEvolutionFilter;
use Piwik\DataTable;
use Piwik\Period\Range;
use Piwik\Piwik;
use Piwik\Plugins\Goals\Archiver;
use Piwik\Plugins\SitesManager\API as APISitesManager;
use Piwik\Site;
use Piwik\TaskScheduler;
/**
* The MultiSites API lets you request the key metrics (visits, page views, revenue) for all Websites in Piwik.
* @method static \Piwik\Plugins\MultiSites\API getInstance()
*/
class API extends \Piwik\Plugin\API
{
const METRIC_TRANSLATION_KEY = 'translation';
const METRIC_EVOLUTION_COL_NAME_KEY = 'evolution_column_name';
const METRIC_RECORD_NAME_KEY = 'record_name';
const METRIC_IS_ECOMMERCE_KEY = 'is_ecommerce';
const NB_VISITS_METRIC = 'nb_visits';
const NB_ACTIONS_METRIC = 'nb_actions';
const NB_PAGEVIEWS_LABEL = 'nb_pageviews';
const NB_PAGEVIEWS_METRIC = 'Actions_nb_pageviews';
const GOAL_REVENUE_METRIC = 'revenue';
const GOAL_CONVERSION_METRIC = 'nb_conversions';
const ECOMMERCE_ORDERS_METRIC = 'orders';
const ECOMMERCE_REVENUE_METRIC = 'ecommerce_revenue';
static private $baseMetrics = array(
self::NB_VISITS_METRIC => array(
self::METRIC_TRANSLATION_KEY => 'General_ColumnNbVisits',
self::METRIC_EVOLUTION_COL_NAME_KEY => 'visits_evolution',
self::METRIC_RECORD_NAME_KEY => self::NB_VISITS_METRIC,
self::METRIC_IS_ECOMMERCE_KEY => false,
),
self::NB_ACTIONS_METRIC => array(
self::METRIC_TRANSLATION_KEY => 'General_ColumnNbActions',
self::METRIC_EVOLUTION_COL_NAME_KEY => 'actions_evolution',
self::METRIC_RECORD_NAME_KEY => self::NB_ACTIONS_METRIC,
self::METRIC_IS_ECOMMERCE_KEY => false,
)
);
/**
* Returns a report displaying the total visits, actions and revenue, as
* well as the evolution of these values, of all existing sites over a
* specified period of time.
*
* If the specified period is not a 'range', this function will calculcate
* evolution metrics. Evolution metrics are metrics that display the
* percent increase/decrease of another metric since the last period.
*
* This function will merge the result of the archive query so each
* row in the result DataTable will correspond to the metrics of a single
* site. If a date range is specified, the result will be a
* DataTable\Map, but it will still be merged.
*
* @param string $period The period type to get data for.
* @param string $date The date(s) to get data for.
* @param bool|string $segment The segments to get data for.
* @param bool|string $_restrictSitesToLogin Hack used to enforce we restrict the returned data to the specified username
* Only used when a scheduled task is running
* @param bool|string $enhanced When true, return additional goal & ecommerce metrics
* @param bool|string $pattern If specified, only the website which names (or site ID) match the pattern will be returned using SitesManager.getPatternMatchSites
* @return DataTable
*/
public function getAll($period, $date, $segment = false, $_restrictSitesToLogin = false, $enhanced = false, $pattern = false)
{
Piwik::checkUserHasSomeViewAccess();
$idSites = $this->getSitesIdFromPattern($pattern);
if (empty($idSites)) {
return new DataTable();
}
return $this->buildDataTable(
$idSites,
$period,
$date,
$segment,
$_restrictSitesToLogin,
$enhanced,
$multipleWebsitesRequested = true
);
}
/**
* Fetches the list of sites which names match the string pattern
*
* @param $pattern
* @return array|string
*/
private function getSitesIdFromPattern($pattern)
{
$idSites = 'all';
if (empty($pattern)) {
return $idSites;
}
$idSites = array();
$sites = Request::processRequest('SitesManager.getPatternMatchSites',
array('pattern' => $pattern,
// added because caller could overwrite these
'serialize' => 0,
'format' => 'original'));
if (!empty($sites)) {
foreach ($sites as $site) {
$idSites[] = $site['idsite'];
}
}
return $idSites;
}
/**
* Same as getAll but for a unique Piwik site
* @see Piwik\Plugins\MultiSites\API::getAll()
*
* @param int $idSite Id of the Piwik site
* @param string $period The period type to get data for.
* @param string $date The date(s) to get data for.
* @param bool|string $segment The segments to get data for.
* @param bool|string $_restrictSitesToLogin Hack used to enforce we restrict the returned data to the specified username
* Only used when a scheduled task is running
* @param bool|string $enhanced When true, return additional goal & ecommerce metrics
* @return DataTable
*/
public function getOne($idSite, $period, $date, $segment = false, $_restrictSitesToLogin = false, $enhanced = false)
{
Piwik::checkUserHasViewAccess($idSite);
return $this->buildDataTable(
$idSite,
$period,
$date,
$segment,
$_restrictSitesToLogin,
$enhanced,
$multipleWebsitesRequested = false
);
}
private function buildDataTable($idSitesOrIdSite, $period, $date, $segment, $_restrictSitesToLogin, $enhanced, $multipleWebsitesRequested)
{
$allWebsitesRequested = ($idSitesOrIdSite == 'all');
if ($allWebsitesRequested) {
// First clear cache
Site::clearCache();
// Then, warm the cache with only the data we should have access to
if (Piwik::hasUserSuperUserAccess()
// Hack: when this API function is called as a Scheduled Task, Super User status is enforced.
// This means this function would return ALL websites in all cases.
// Instead, we make sure that only the right set of data is returned
&& !TaskScheduler::isTaskBeingExecuted()
) {
APISitesManager::getInstance()->getAllSites();
} else {
APISitesManager::getInstance()->getSitesWithAtLeastViewAccess($limit = false, $_restrictSitesToLogin);
}
// Both calls above have called Site::setSitesFromArray. We now get these sites:
$sitesToProblablyAdd = Site::getSites();
} else {
$sitesToProblablyAdd = array(APISitesManager::getInstance()->getSiteFromId($idSitesOrIdSite));
}
// build the archive type used to query archive data
$archive = Archive::build(
$idSitesOrIdSite,
$period,
$date,
$segment,
$_restrictSitesToLogin
);
// determine what data will be displayed
$fieldsToGet = array();
$columnNameRewrites = array();
$apiECommerceMetrics = array();
$apiMetrics = API::getApiMetrics($enhanced);
foreach ($apiMetrics as $metricName => $metricSettings) {
$fieldsToGet[] = $metricSettings[self::METRIC_RECORD_NAME_KEY];
$columnNameRewrites[$metricSettings[self::METRIC_RECORD_NAME_KEY]] = $metricName;
if ($metricSettings[self::METRIC_IS_ECOMMERCE_KEY]) {
$apiECommerceMetrics[$metricName] = $metricSettings;
}
}
// get the data
// $dataTable instanceOf Set
$dataTable = $archive->getDataTableFromNumeric($fieldsToGet);
$dataTable = $this->mergeDataTableMapAndPopulateLabel($idSitesOrIdSite, $multipleWebsitesRequested, $dataTable);
if ($dataTable instanceof DataTable\Map) {
foreach ($dataTable->getDataTables() as $table) {
$this->addMissingWebsites($table, $fieldsToGet, $sitesToProblablyAdd);
}
} else {
$this->addMissingWebsites($dataTable, $fieldsToGet, $sitesToProblablyAdd);
}
// calculate total visits/actions/revenue
$this->setMetricsTotalsMetadata($dataTable, $apiMetrics);
// if the period isn't a range & a lastN/previousN date isn't used, we get the same
// data for the last period to show the evolution of visits/actions/revenue
list($strLastDate, $lastPeriod) = Range::getLastDate($date, $period);
if ($strLastDate !== false) {
if ($lastPeriod !== false) {
// NOTE: no easy way to set last period date metadata when range of dates is requested.
// will be easier if DataTable\Map::metadata is removed, and metadata that is
// put there is put directly in DataTable::metadata.
$dataTable->setMetadata(self::getLastPeriodMetadataName('date'), $lastPeriod);
}
$pastArchive = Archive::build($idSitesOrIdSite, $period, $strLastDate, $segment, $_restrictSitesToLogin);
$pastData = $pastArchive->getDataTableFromNumeric($fieldsToGet);
$pastData = $this->mergeDataTableMapAndPopulateLabel($idSitesOrIdSite, $multipleWebsitesRequested, $pastData);
// use past data to calculate evolution percentages
$this->calculateEvolutionPercentages($dataTable, $pastData, $apiMetrics);
Common::destroy($pastData);
}
// remove eCommerce related metrics on non eCommerce Piwik sites
// note: this is not optimal in terms of performance: those metrics should not be retrieved in the first place
if ($enhanced) {
if ($dataTable instanceof DataTable\Map) {
foreach ($dataTable->getDataTables() as $table) {
$this->removeEcommerceRelatedMetricsOnNonEcommercePiwikSites($table, $apiECommerceMetrics);
}
} else {
$this->removeEcommerceRelatedMetricsOnNonEcommercePiwikSites($dataTable, $apiECommerceMetrics);
}
}
// move the site id to a metadata column
$dataTable->filter('ColumnCallbackAddMetadata', array('label', 'group', array('\Piwik\Site', 'getGroupFor'), array()));
$dataTable->filter('ColumnCallbackAddMetadata', array('label', 'main_url', array('\Piwik\Site', 'getMainUrlFor'), array()));
$dataTable->filter('ColumnCallbackAddMetadata', array('label', 'idsite'));
// set the label of each row to the site name
if ($multipleWebsitesRequested) {
$dataTable->filter('ColumnCallbackReplace', array('label', '\Piwik\Site::getNameFor'));
} else {
$dataTable->filter('ColumnDelete', array('label'));
}
Site::clearCache();
// replace record names with user friendly metric names
$dataTable->filter('ReplaceColumnNames', array($columnNameRewrites));
// Ensures data set sorted, for Metadata output
$dataTable->filter('Sort', array(self::NB_VISITS_METRIC, 'desc', $naturalSort = false));
// filter rows without visits
// note: if only one website is queried and there are no visits, we can not remove the row otherwise
// ResponseBuilder throws 'Call to a member function getColumns() on a non-object'
if ($multipleWebsitesRequested
// We don't delete the 0 visits row, if "Enhanced" mode is on.
&& !$enhanced
) {
$dataTable->filter(
'ColumnCallbackDeleteRow',
array(
self::NB_VISITS_METRIC,
function ($value) {
return $value == 0;
}
)
);
}
return $dataTable;
}
/**
* Performs a binary filter of two
* DataTables in order to correctly calculate evolution metrics.
*
* @param DataTable|DataTable\Map $currentData
* @param DataTable|DataTable\Map $pastData
* @param array $apiMetrics The array of string fields to calculate evolution
* metrics for.
* @throws Exception
*/
private function calculateEvolutionPercentages($currentData, $pastData, $apiMetrics)
{
if (get_class($currentData) != get_class($pastData)) { // sanity check for regressions
throw new Exception("Expected \$pastData to be of type " . get_class($currentData) . " - got "
. get_class($pastData) . ".");
}
if ($currentData instanceof DataTable\Map) {
$pastArray = $pastData->getDataTables();
foreach ($currentData->getDataTables() as $subTable) {
$this->calculateEvolutionPercentages($subTable, current($pastArray), $apiMetrics);
next($pastArray);
}
} else {
foreach ($apiMetrics as $metricSettings) {
$currentData->filter(
'CalculateEvolutionFilter',
array(
$pastData,
$metricSettings[self::METRIC_EVOLUTION_COL_NAME_KEY],
$metricSettings[self::METRIC_RECORD_NAME_KEY],
$quotientPrecision = 1)
);
}
}
}
/**
* @ignore
*/
public static function getApiMetrics($enhanced)
{
$metrics = self::$baseMetrics;
if(Common::isActionsPluginEnabled()) {
$metrics[self::NB_PAGEVIEWS_LABEL] = array(
self::METRIC_TRANSLATION_KEY => 'General_ColumnPageviews',
self::METRIC_EVOLUTION_COL_NAME_KEY => 'pageviews_evolution',
self::METRIC_RECORD_NAME_KEY => self::NB_PAGEVIEWS_METRIC,
self::METRIC_IS_ECOMMERCE_KEY => false,
);
}
if (Common::isGoalPluginEnabled()) {
// goal revenue metric
$metrics[self::GOAL_REVENUE_METRIC] = array(
self::METRIC_TRANSLATION_KEY => 'General_ColumnRevenue',
self::METRIC_EVOLUTION_COL_NAME_KEY => self::GOAL_REVENUE_METRIC . '_evolution',
self::METRIC_RECORD_NAME_KEY => Archiver::getRecordName(self::GOAL_REVENUE_METRIC),
self::METRIC_IS_ECOMMERCE_KEY => false,
);
if ($enhanced) {
// number of goal conversions metric
$metrics[self::GOAL_CONVERSION_METRIC] = array(
self::METRIC_TRANSLATION_KEY => 'Goals_ColumnConversions',
self::METRIC_EVOLUTION_COL_NAME_KEY => self::GOAL_CONVERSION_METRIC . '_evolution',
self::METRIC_RECORD_NAME_KEY => Archiver::getRecordName(self::GOAL_CONVERSION_METRIC),
self::METRIC_IS_ECOMMERCE_KEY => false,
);
// number of orders
$metrics[self::ECOMMERCE_ORDERS_METRIC] = array(
self::METRIC_TRANSLATION_KEY => 'General_EcommerceOrders',
self::METRIC_EVOLUTION_COL_NAME_KEY => self::ECOMMERCE_ORDERS_METRIC . '_evolution',
self::METRIC_RECORD_NAME_KEY => Archiver::getRecordName(self::GOAL_CONVERSION_METRIC, 0),
self::METRIC_IS_ECOMMERCE_KEY => true,
);
// eCommerce revenue
$metrics[self::ECOMMERCE_REVENUE_METRIC] = array(
self::METRIC_TRANSLATION_KEY => 'General_ProductRevenue',
self::METRIC_EVOLUTION_COL_NAME_KEY => self::ECOMMERCE_REVENUE_METRIC . '_evolution',
self::METRIC_RECORD_NAME_KEY => Archiver::getRecordName(self::GOAL_REVENUE_METRIC, 0),
self::METRIC_IS_ECOMMERCE_KEY => true,
);
}
}
return $metrics;
}
/**
* Sets the total visits, actions & revenue for a DataTable returned by
* $this->buildDataTable.
*
* @param DataTable $dataTable
* @param array $apiMetrics Metrics info.
* @return array Array of three values: total visits, total actions, total revenue
*/
private function setMetricsTotalsMetadata($dataTable, $apiMetrics)
{
if ($dataTable instanceof DataTable\Map) {
foreach ($dataTable->getDataTables() as $table) {
$this->setMetricsTotalsMetadata($table, $apiMetrics);
}
} else {
$revenueMetric = '';
if (Common::isGoalPluginEnabled()) {
$revenueMetric = Archiver::getRecordName(self::GOAL_REVENUE_METRIC);
}
$totals = array();
foreach ($apiMetrics as $label => $metricInfo) {
$totalMetadataName = self::getTotalMetadataName($label);
$totals[$totalMetadataName] = 0;
}
foreach ($dataTable->getRows() as $row) {
foreach ($apiMetrics as $label => $metricInfo) {
$totalMetadataName = self::getTotalMetadataName($label);
$totals[$totalMetadataName] += $row->getColumn($metricInfo[self::METRIC_RECORD_NAME_KEY]);
}
}
foreach ($totals as $name => $value) {
$dataTable->setMetadata($name, $value);
}
}
}
private static function getTotalMetadataName($name)
{
return 'total_' . $name;
}
private static function getLastPeriodMetadataName($name)
{
return 'last_period_' . $name;
}
/**
* @param DataTable|DataTable\Map $dataTable
* @param $fieldsToGet
* @param $sitesToProblablyAdd
*/
private function addMissingWebsites($dataTable, $fieldsToGet, $sitesToProblablyAdd)
{
$siteIdsInDataTable = array();
foreach ($dataTable->getRows() as $row) {
/** @var DataTable\Row $row */
$siteIdsInDataTable[] = $row->getColumn('label');
}
foreach ($sitesToProblablyAdd as $site) {
if (!in_array($site['idsite'], $siteIdsInDataTable)) {
$siteRow = array_combine($fieldsToGet, array_pad(array(), count($fieldsToGet), 0));
$siteRow['label'] = (int) $site['idsite'];
$dataTable->addRowFromSimpleArray($siteRow);
}
}
}
private function removeEcommerceRelatedMetricsOnNonEcommercePiwikSites($dataTable, $apiECommerceMetrics)
{
// $dataTableRows instanceOf Row[]
$dataTableRows = $dataTable->getRows();
foreach ($dataTableRows as $dataTableRow) {
$siteId = $dataTableRow->getColumn('label');
if (!Site::isEcommerceEnabledFor($siteId)) {
foreach ($apiECommerceMetrics as $metricSettings) {
$dataTableRow->deleteColumn($metricSettings[self::METRIC_RECORD_NAME_KEY]);
$dataTableRow->deleteColumn($metricSettings[self::METRIC_EVOLUTION_COL_NAME_KEY]);
}
}
}
}
private function mergeDataTableMapAndPopulateLabel($idSitesOrIdSite, $multipleWebsitesRequested, $dataTable)
{
// get rid of the DataTable\Map that is created by the IndexedBySite archive type
if ($dataTable instanceof DataTable\Map && $multipleWebsitesRequested) {
return $dataTable->mergeChildren();
} else {
if (!$dataTable instanceof DataTable\Map && $dataTable->getRowsCount() > 0) {
$firstSite = is_array($idSitesOrIdSite) ? reset($idSitesOrIdSite) : $idSitesOrIdSite;
$firstDataTableRow = $dataTable->getFirstRow();
$firstDataTableRow->setColumn('label', $firstSite);
}
}
return $dataTable;
}
}

View file

@ -0,0 +1,87 @@
<?php
/**
* Piwik - Open source web analytics
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\Plugins\MultiSites;
use Piwik\Common;
use Piwik\Config;
use Piwik\Date;
use Piwik\MetricsFormatter;
use Piwik\Period;
use Piwik\Piwik;
use Piwik\Plugins\MultiSites\API as APIMultiSites;
use Piwik\Plugins\SitesManager\API as APISitesManager;
use Piwik\Site;
use Piwik\View;
/**
*
*/
class Controller extends \Piwik\Plugin\Controller
{
public function __construct()
{
parent::__construct();
}
public function index()
{
return $this->getSitesInfo($isWidgetized = false);
}
public function standalone()
{
return $this->getSitesInfo($isWidgetized = true);
}
public function getSitesInfo($isWidgetized = false)
{
Piwik::checkUserHasSomeViewAccess();
$date = Common::getRequestVar('date', 'today');
$period = Common::getRequestVar('period', 'day');
$view = new View("@MultiSites/getSitesInfo");
$view->isWidgetized = $isWidgetized;
$view->displayRevenueColumn = Common::isGoalPluginEnabled();
$view->limit = Config::getInstance()->General['all_websites_website_per_page'];
$view->show_sparklines = Config::getInstance()->General['show_multisites_sparklines'];
$view->autoRefreshTodayReport = 0;
// if the current date is today, or yesterday,
// in case the website is set to UTC-12), or today in UTC+14, we refresh the page every 5min
if (in_array($date, array('today', date('Y-m-d'),
'yesterday', Date::factory('yesterday')->toString('Y-m-d'),
Date::factory('now', 'UTC+14')->toString('Y-m-d')))
) {
$view->autoRefreshTodayReport = Config::getInstance()->General['multisites_refresh_after_seconds'];
}
$params = $this->getGraphParamsModified();
$view->dateSparkline = $period == 'range' ? $date : $params['date'];
$this->setGeneralVariablesView($view);
return $view->render();
}
public function getEvolutionGraph($columns = false)
{
if (empty($columns)) {
$columns = Common::getRequestVar('columns');
}
$api = "API.get";
if ($columns == 'revenue') {
$api = "Goals.get";
}
$view = $this->getLastUnitGraph($this->pluginName, __FUNCTION__, $api);
return $this->renderView($view);
}
}

View file

@ -0,0 +1,116 @@
<?php
/**
* Piwik - Open source web analytics
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\Plugins\MultiSites;
use Piwik\Menu\MenuTop;
use Piwik\Piwik;
/**
*
*/
class MultiSites extends \Piwik\Plugin
{
public function getInformation()
{
$info = parent::getInformation();
$info['authors'] = array(array('name' => 'Piwik PRO', 'homepage' => 'http://piwik.pro'));
return $info;
}
/**
* @see Piwik\Plugin::getListHooksRegistered
*/
public function getListHooksRegistered()
{
return array(
'AssetManager.getStylesheetFiles' => 'getStylesheetFiles',
'AssetManager.getJavaScriptFiles' => 'getJsFiles',
'Menu.Top.addItems' => 'addTopMenu',
'API.getReportMetadata' => 'getReportMetadata',
'Translate.getClientSideTranslationKeys' => 'getClientSideTranslationKeys',
);
}
public function getClientSideTranslationKeys(&$translations)
{
$translations[] = 'General_Website';
$translations[] = 'General_ColumnNbVisits';
$translations[] = 'General_ColumnPageviews';
$translations[] = 'General_ColumnRevenue';
$translations[] = 'General_TotalVisitsPageviewsRevenue';
$translations[] = 'General_EvolutionSummaryGeneric';
$translations[] = 'General_AllWebsitesDashboard';
$translations[] = 'General_NVisits';
$translations[] = 'MultiSites_Evolution';
$translations[] = 'SitesManager_AddSite';
$translations[] = 'General_Next';
$translations[] = 'General_Previous';
$translations[] = 'General_GoTo';
$translations[] = 'Dashboard_DashboardOf';
$translations[] = 'Actions_SubmenuSitesearch';
$translations[] = 'MultiSites_LoadingWebsites';
$translations[] = 'General_ErrorRequest';
}
public function getReportMetadata(&$reports)
{
$metadataMetrics = array();
foreach (API::getApiMetrics($enhanced = true) as $metricName => $metricSettings) {
$metadataMetrics[$metricName] =
Piwik::translate($metricSettings[API::METRIC_TRANSLATION_KEY]);
$metadataMetrics[$metricSettings[API::METRIC_EVOLUTION_COL_NAME_KEY]] =
Piwik::translate($metricSettings[API::METRIC_TRANSLATION_KEY]) . " " . Piwik::translate('MultiSites_Evolution');
}
$reports[] = array(
'category' => Piwik::translate('General_MultiSitesSummary'),
'name' => Piwik::translate('General_AllWebsitesDashboard'),
'module' => 'MultiSites',
'action' => 'getAll',
'dimension' => Piwik::translate('General_Website'), // re-using translation
'metrics' => $metadataMetrics,
'processedMetrics' => false,
'constantRowsCount' => false,
'order' => 4
);
$reports[] = array(
'category' => Piwik::translate('General_MultiSitesSummary'),
'name' => Piwik::translate('General_SingleWebsitesDashboard'),
'module' => 'MultiSites',
'action' => 'getOne',
'dimension' => Piwik::translate('General_Website'), // re-using translation
'metrics' => $metadataMetrics,
'processedMetrics' => false,
'constantRowsCount' => false,
'order' => 5
);
}
public function addTopMenu()
{
$urlParams = array('module' => 'MultiSites', 'action' => 'index', 'segment' => false);
$tooltip = Piwik::translate('MultiSites_TopLinkTooltip');
MenuTop::addEntry('General_MultiSitesSummary', $urlParams, true, 3, $isHTML = false, $tooltip);
}
public function getJsFiles(&$jsFiles)
{
$jsFiles[] = "plugins/MultiSites/angularjs/dashboard/dashboard-model.js";
$jsFiles[] = "plugins/MultiSites/angularjs/dashboard/dashboard-controller.js";
$jsFiles[] = "plugins/MultiSites/angularjs/dashboard/dashboard-filter.js";
$jsFiles[] = "plugins/MultiSites/angularjs/dashboard/dashboard-directive.js";
$jsFiles[] = "plugins/MultiSites/angularjs/site/site-directive.js";
}
public function getStylesheetFiles(&$stylesheets)
{
$stylesheets[] = "plugins/MultiSites/angularjs/dashboard/dashboard.less";
}
}

View file

@ -0,0 +1,34 @@
/*!
* Piwik - Web Analytics
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
angular.module('piwikApp').controller('MultiSitesDashboardController', function($scope, piwik, multisitesDashboardModel){
$scope.model = multisitesDashboardModel;
$scope.reverse = true;
$scope.predicate = 'nb_visits';
$scope.evolutionSelector = 'visits_evolution';
$scope.hasSuperUserAccess = piwik.hasSuperUserAccess;
$scope.date = piwik.broadcast.getValueFromUrl('date');
$scope.url = piwik.piwik_url;
$scope.period = piwik.period;
$scope.sortBy = function (metric) {
var reverse = $scope.reverse;
if ($scope.predicate == metric) {
reverse = !reverse;
}
$scope.predicate = metric;
$scope.reverse = reverse;
};
this.refresh = function (interval) {
multisitesDashboardModel.fetchAllSites(interval);
};
});

View file

@ -0,0 +1,40 @@
/*!
* Piwik - Web Analytics
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
/**
* Renders the multisites dashboard
* Example usage:
*
* <div piwik-multisites-dashboard
* display-revenue-column="true"
* show-sparklines="true"
* date-sparkline="true"
* page-size="50"
* auto-refresh-today-report="500" // or 0 to disable
* ></div>
*/
angular.module('piwikApp').directive('piwikMultisitesDashboard', function($document, piwik, $filter){
return {
restrict: 'A',
scope: {
displayRevenueColumn: '@',
showSparklines: '@',
dateSparkline: '@'
},
templateUrl: 'plugins/MultiSites/angularjs/dashboard/dashboard.html?cb=' + piwik.cacheBuster,
controller: 'MultiSitesDashboardController',
link: function (scope, element, attrs, controller) {
if (attrs.pageSize) {
scope.model.pageSize = attrs.pageSize;
}
controller.refresh(attrs.autoRefreshTodayReport);
}
};
});

View file

@ -0,0 +1,63 @@
/*!
* Piwik - Web Analytics
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
/**
* Filters a given list of websites and groups and makes sure only the websites within a given offset and limit are
* displayed. It also makes sure sites are displayed under the groups. That means it flattens a structure like this:
*
* - website1
* - website2
* - website3.sites // this is a group
* - website4
* - website5
* - website6
*
* to the following structure
* - website1
* - website2
* - website3.sites // this is a group
* - website4
* - website5
* - website6
*/
angular.module('piwikApp').filter('multiSitesGroupFilter', function() {
return function(websites, from, to) {
var offsetEnd = parseInt(from, 10) + parseInt(to, 10);
var groups = {};
var sites = [];
for (var index = 0; index < websites.length; index++) {
var website = websites[index];
sites.push(website);
if (website.sites && website.sites.length) {
groups[website.label] = website;
for (var innerIndex = 0; innerIndex < website.sites.length; innerIndex++) {
sites.push(website.sites[innerIndex]);
}
}
if (sites.length >= offsetEnd) {
break;
}
}
// if the first site is a website having a group, then try to find the related group and prepend it to the list
// of sites to make sure we always display the name of the group that belongs to a website.
var filteredSites = sites.slice(from, offsetEnd);
if (filteredSites.length && filteredSites[0] && filteredSites[0].group) {
var groupName = filteredSites[0].group;
if (groups[groupName]) {
filteredSites.unshift(groups[groupName]);
}
}
return filteredSites;
};
});

View file

@ -0,0 +1,273 @@
/**
* Model for Multisites Dashboard aka All Websites Dashboard.
*
*/
angular.module('piwikApp').factory('multisitesDashboardModel', function (piwikApi, $filter, $timeout) {
/**
*
* this is the list of all available sites. For performance reason this list is different to model.sites. ngRepeat
* won't operate on the whole list this way. The allSites array contains websites and groups in the following
* structure
*
* - website1
* - website2
* - website3.sites = [ // this is a group
* - website4
* - website5
* ]
* - website6
*
* This structure allows us to display the sites within a group directly under the group without big logic and also
* allows us to calculate the summary for each group easily
*/
var allSitesByGroup = [];
var model = {};
// those sites are going to be displayed
model.sites = [];
model.isLoading = false;
model.pageSize = 5;
model.currentPage = 0;
model.totalVisits = '?';
model.totalActions = '?';
model.totalRevenue = '?';
model.searchTerm = '';
model.lastVisits = '?';
model.lastVisitsDate = '?';
fetchPreviousSummary();
// create a new group object which has similar structure than a website
function createGroup(name){
return {
label: name,
sites: [],
nb_visits: 0,
nb_pageviews: 0,
revenue: 0,
isGroup: true
};
}
// create a new group with empty site to make sure we do not change the original group in $allSites
function copyGroup(group)
{
return {
label: group.label,
sites: [],
nb_visits: group.nb_visits,
nb_pageviews: group.nb_pageviews,
revenue: group.revenue,
isGroup: true
};
}
function onError () {
model.errorLoadingSites = true;
model.sites = [];
allSitesByGroup = [];
}
function calculateMetricsForEachGroup(groups)
{
angular.forEach(groups, function (group) {
angular.forEach(group.sites, function (site) {
var revenue = (site.revenue+'').match(/(\d+\.?\d*)/); // convert $ 0.00 to 0.00 or 5€ to 5
group.nb_visits += parseInt(site.nb_visits, 10);
group.nb_pageviews += parseInt(site.nb_pageviews, 10);
if (revenue.length) {
group.revenue += parseInt(revenue[0], 10);
}
});
});
}
function createGroupsAndMoveSitesIntoRelatedGroup(allSitesUnordered, reportMetadata)
{
var sitesByGroup = [];
var groups = {};
// we do 3 things (complete site information, create groups, move sites into group) in one step for
// performance reason, there can be > 20k sites
angular.forEach(allSitesUnordered, function (site, index) {
site.idsite = reportMetadata[index].idsite;
site.group = reportMetadata[index].group;
site.main_url = reportMetadata[index].main_url;
if (site.group) {
if (!groups[site.group]) {
var group = createGroup(site.group);
groups[site.group] = group;
sitesByGroup.push(group);
}
groups[site.group].sites.push(site);
} else {
sitesByGroup.push(site);
}
});
calculateMetricsForEachGroup(groups);
return sitesByGroup;
}
model.updateWebsitesList = function (processedReport) {
if (!processedReport) {
onError();
return;
}
var allSitesUnordered = processedReport.reportData;
model.totalVisits = processedReport.reportTotal.nb_visits;
model.totalActions = processedReport.reportTotal.nb_actions;
model.totalRevenue = processedReport.reportTotal.revenue;
allSitesByGroup = createGroupsAndMoveSitesIntoRelatedGroup(allSitesUnordered, processedReport.reportMetadata);
if (!allSitesByGroup.length) {
return;
}
if (model.searchTerm) {
model.searchSite(model.searchTerm);
} else {
model.sites = allSitesByGroup;
}
};
model.getNumberOfFilteredSites = function () {
var numSites = model.sites.length;
var groupNames = {};
for (var index = 0; index < model.sites.length; index++) {
var site = model.sites[index];
if (site && site.isGroup) {
numSites += site.sites.length;
}
}
return numSites;
};
model.getNumberOfPages = function () {
return Math.ceil(model.getNumberOfFilteredSites() / model.pageSize - 1);
};
model.getCurrentPagingOffsetStart = function() {
return Math.ceil(model.currentPage * model.pageSize);
};
model.getCurrentPagingOffsetEnd = function() {
var end = model.getCurrentPagingOffsetStart() + parseInt(model.pageSize, 10);
var max = model.getNumberOfFilteredSites();
if (end > max) {
end = max;
}
return parseInt(end, 10);
};
model.previousPage = function () {
model.currentPage = model.currentPage - 1;
};
model.nextPage = function () {
model.currentPage = model.currentPage + 1;
};
function nestedSearch(sitesByGroup, term)
{
var filteredSites = [];
for (var index in sitesByGroup) {
var site = sitesByGroup[index];
if (site.isGroup) {
var matchingSites = nestedSearch(site.sites, term);
if (matchingSites.length || (''+site.label).toLowerCase().indexOf(term) > -1) {
var clonedGroup = copyGroup(site);
clonedGroup.sites = matchingSites;
filteredSites.push(clonedGroup);
}
} else if ((''+site.label).toLowerCase().indexOf(term) > -1) {
filteredSites.push(site);
} else if (site.group && (''+site.group).toLowerCase().indexOf(term) > -1) {
filteredSites.push(site);
}
}
return filteredSites;
}
model.searchSite = function (term) {
model.searchTerm = term;
model.currentPage = 0;
model.sites = nestedSearch(allSitesByGroup, term);
};
function fetchPreviousSummary () {
piwikApi.fetch({
method: 'API.getLastDate'
}).then(function (response) {
if (response && response.value) {
return response.value;
}
}).then(function (lastDate) {
if (!lastDate) {
return;
}
model.lastVisitsDate = lastDate;
return piwikApi.fetch({
method: 'API.getProcessedReport',
apiModule: 'MultiSites',
apiAction: 'getAll',
hideMetricsDoc: '1',
filter_limit: '0',
showColumns: 'label,nb_visits',
enhanced: 1,
date: lastDate
});
}).then(function (response) {
if (response && response.reportTotal) {
model.lastVisits = response.reportTotal.nb_visits;
}
});
}
model.fetchAllSites = function (refreshInterval) {
if (model.isLoading) {
piwikApi.abort();
}
model.isLoading = true;
model.errorLoadingSites = false;
return piwikApi.fetch({
method: 'API.getProcessedReport',
apiModule: 'MultiSites',
apiAction: 'getAll',
hideMetricsDoc: '1',
filter_limit: '-1',
showColumns: 'label,nb_visits,nb_pageviews,visits_evolution,pageviews_evolution,revenue_evolution,nb_actions,revenue',
enhanced: 1
}).then(function (response) {
model.updateWebsitesList(response);
}, onError)['finally'](function () {
model.isLoading = false;
if (refreshInterval && refreshInterval > 0) {
$timeout(function () {
model.fetchAllSites(refreshInterval);
}, refreshInterval * 1000);
}
});
};
return model;
});

View file

@ -0,0 +1,128 @@
<div>
<h2 piwik-enriched-headline
help-url="http://piwik.org/docs/manage-websites/#all-websites-dashboard"
feature-name="{{ 'General_AllWebsitesDashboard'|translate }}">
{{ 'General_AllWebsitesDashboard'|translate }}
<span class='smallTitle'
title="{{ 'General_EvolutionSummaryGeneric'|translate:('General_NVisits'|translate:model.totalVisits):date:model.lastVisits:model.lastVisitsDate:(model.totalVisits|evolution:model.lastVisits)}}"
ng-bind-html="'General_TotalVisitsPageviewsRevenue' | translate:('<strong>'+model.totalVisits+'</strong>'):('<strong>'+model.totalActions+'</strong>'):('<strong>' + model.totalRevenue + '</strong>')">
</span>
</h2>
<table id="mt" class="dataTable" cellspacing="0">
<thead>
<tr>
<th id="names" class="label" ng-click="sortBy('label')" ng-class="{columnSorted: 'label' == predicate}">
<span class="heading">{{ 'General_Website'|translate }}</span>
<span ng-class="{multisites_asc: !reverse && 'label' == predicate, multisites_desc: reverse && 'label' == predicate}" class="arrow"></span>
</th>
<th id="visits" class="multisites-column" ng-click="sortBy('nb_visits')" ng-class="{columnSorted: 'nb_visits' == predicate}">
<span class="heading">{{ 'General_ColumnNbVisits'|translate }}</span>
<span ng-class="{multisites_asc: !reverse && 'nb_visits' == predicate, multisites_desc: reverse && 'nb_visits' == predicate}" class="arrow"></span>
</th>
<th id="pageviews" class="multisites-column" ng-click="sortBy('nb_pageviews')" ng-class="{columnSorted: 'nb_pageviews' == predicate}">
<span class="heading">{{ 'General_ColumnPageviews'|translate }}</span>
<span ng-class="{multisites_asc: !reverse && 'nb_pageviews' == predicate, multisites_desc: reverse && 'nb_pageviews' == predicate}" class="arrow"></span>
</th>
<th ng-if="displayRevenueColumn" id="revenue" class="multisites-column" ng-click="sortBy('revenue')" ng-class="{columnSorted: 'revenue' == predicate}">
<span class="heading">{{ 'General_ColumnRevenue'|translate }}</span>
<span ng-class="{multisites_asc: !reverse && 'revenue' == predicate, multisites_desc: reverse && 'revenue' == predicate}" class="arrow"></span>
</th>
<th id="evolution" colspan="{{ showSparklines ? 2 : 1 }}" ng-class="{columnSorted: evolutionSelector == predicate}">
<span class="arrow" ng-class="{multisites_asc: !reverse && evolutionSelector == predicate, multisites_desc: reverse && evolutionSelector == predicate}"></span>
<span class="evolution"
ng-click="sortBy(evolutionSelector)"> {{ 'MultiSites_Evolution'|translate }}</span>
<select class="selector" id="evolution_selector" ng-model="evolutionSelector"
ng-change="predicate = evolutionSelector">
<option value="visits_evolution">{{ 'General_ColumnNbVisits'|translate }}</option>
<option value="pageviews_evolution">{{ 'General_ColumnPageviews'|translate }}</option>
<option ng-if="displayRevenueColumn" value="revenue_evolution">{{ 'General_ColumnRevenue'|translate }}</option>
</select>
</th>
</tr>
</thead>
<tbody id="tb" ng-if="model.isLoading">
<tr>
<td colspan="7" class="allWebsitesLoading">
{{ 'MultiSites_LoadingWebsites' | translate }}
<span class="allWebsitesLoadingIndicator"> </span>
</td>
</tr>
</tbody>
<tbody id="tb" ng-if="!model.isLoading">
<tr ng-if="model.errorLoadingSites">
<td colspan="7">
<div class="notification system notification-error">
{{ 'General_ErrorRequest'|translate }}
</div>
</td>
</tr>
<tr website="website"
evolution-metric="evolutionSelector"
piwik-multisites-site
date-sparkline="dateSparkline"
show-sparklines="showSparklines"
metric="predicate"
ng-class-odd="'columnodd'"
display-revenue-column="displayRevenueColumn"
ng-repeat="website in model.sites | orderBy:predicate:reverse | multiSitesGroupFilter:model.getCurrentPagingOffsetStart():model.pageSize">
</tr>
</tbody>
<tfoot>
<tr ng-if="hasSuperUserAccess">
<td colspan="8" class="add_new_site">
<a href="{{ url }}?module=SitesManager&action=index&showaddsite=1&period={{ period }}&date={{ date }}">
<img src='plugins/UsersManager/images/add.png' alt=""/> {{ 'SitesManager_AddSite'|translate }}
</a>
</td>
</tr>
<tr ng-if="!hasSuperUserAccess">
<td colspan="8">
<br/>
</td>
</tr>
<tr>
<td colspan="8" class="site_search">
<input type="text"
ng-change="model.searchSite(searchTerm)"
ng-model="searchTerm"
placeholder="{{ 'Actions_SubmenuSitesearch' | translate }}">
<img title="Search"
ng-show="!searchTerm"
class="search_ico"
src="plugins/Zeitgeist/images/search_ico.png"/>
<img title="Clear"
ng-show="searchTerm"
ng-click="searchTerm='';model.searchSite('')"
class="reset"
src="plugins/CoreHome/images/reset_search.png"/>
</td>
</tr>
<tr row_id="last">
<td colspan="8" class="paging" ng-hide="model.numberOfPages() <= 1">
<span id="prev" class="previous" ng-hide="model.currentPage == 0" ng-click="model.previousPage()">
<span style="cursor:pointer;">&#171; {{ 'General_Previous'|translate }}</span>
</span>
<span class="dataTablePages">
<span id="counter">
{{ model.getCurrentPagingOffsetStart() }} - {{ model.getCurrentPagingOffsetEnd() }} of {{ model.getNumberOfFilteredSites() }}
</span>
</span>
<span id="next" class="next" ng-hide="model.currentPage >= model.getNumberOfPages()" ng-click="model.nextPage()">
<span style="cursor:pointer;" class="pointer">{{ 'General_Next'|translate }} &#187;</span>
</span>
</td>
</tr>
</tfoot>
</table>
</div>

View file

@ -0,0 +1,185 @@
.smallTitle {
font-size: 15px;
}
#multisites {
border: 0;
padding: 0 15px;
font-size: 14px;
.notification-error {
margin-top: 15px;
}
.add_new_site,
.clean {
border: 0 !important;
text-align: right; padding-top: 15px;padding-right:10px;
}
.add_new_site {
img {
margin: 0px;
}
}
.site_search {
padding: 0px;
text-align: center;
border: 0 !important;
}
td, tr, .sparkline {
text-align: center;
vertical-align: middle;
padding: 1px;
margin: 0;
}
.indicator {
background: url(plugins/MultiSites/images/loading-blue.gif) no-repeat center;
height: 20px;
width: 60px;
margin: auto;
border: 0 !important;
}
.paging {
padding: 20px;
.previous {
padding-right: 20px;
}
.next {
padding-left: 20px;
}
}
.top_controls {
height: 10px;
}
th {
cursor: pointer;
}
.site_search input {
margin-right: 0px;
margin-left: 25px;
}
.search_ico {
position: relative;
left: -25px;
margin-right: 0px;
}
.reset {
position: relative;
left: -25px;
cursor: pointer;
margin-right: 0px;
}
tr.columnodd:hover td, tr.columnodd td {
background: #F2F2F2 !important;
}
tr:hover td {
background: #FFF !important;
}
tr.group {
font-weight: bold;
height: 30px;
}
tr.groupedWebsite .label {
padding-left: 50px;
}
td.multisites-label {
padding-left: 15px;
text-align: left;
width: 250px;
}
td.multisites-label a:hover {
text-decoration: underline;
}
td.multisites-column,
th.multisites-column {
width: 70px;
white-space: nowrap;
}
td.multisites-column-evolution,
th.multisites-column-evolution {
width: 70px;
}
th#evolution {
width:350px;
}
th#visits {
width: 100px;
}
th#pageviews {
width: 110px;
}
th#revenue {
width: 110px;
}
.evolution {
cursor:pointer;
}
.allWebsitesLoading {
padding:20px
}
.allWebsitesLoadingIndicator {
background: url(plugins/Zeitgeist/images/loading-blue.gif) no-repeat right 3px;
display: inline-block;
width: 16px;
height: 16px;
}
.heading {
display: inline-block;
margin-top: 4px;
}
.multisites_desc {
width: 16px;
height: 13px;
display: inline-block;
background-image: url(plugins/Zeitgeist/images/sortdesc.png);
}
.multisites_asc {
width: 16px;
height: 13px;
display: inline-block;
background-image: url(plugins/Zeitgeist/images/sortasc.png);
}
div.sparkline {
float:none;
}
tfoot td {
border-bottom: 0px;
}
}
#mt thead {
line-height: 2.5em;
}
#mt thead *:first-child {
border-top-left-radius: 7px
}
#mt thead *:last-child {
border-top-right-radius: 7px;
}

View file

@ -0,0 +1,55 @@
/*!
* Piwik - Web Analytics
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
/**
* Renders a single website row, for instance to be used within the MultiSites Dashboard.
*
* Usage:
* <div piwik-multisites-site>
* website="{label: 'Name', main_url: 'http://...', idsite: '...'}"
* evolution-metric="visits_evolution"
* show-sparklines="true"
* date-sparkline="2014-01-01,2014-02-02"
* display-revenue-column="true"
* </div>
*/
angular.module('piwikApp').directive('piwikMultisitesSite', function($document, piwik, $filter){
return {
restrict: 'AC',
replace: true,
scope: {
website: '=',
evolutionMetric: '=',
showSparklines: '=',
dateSparkline: '=',
displayRevenueColumn: '=',
metric: '='
},
templateUrl: 'plugins/MultiSites/angularjs/site/site.html?cb=' + piwik.cacheBuster,
controller: function ($scope) {
$scope.period = piwik.period;
$scope.date = piwik.broadcast.getValueFromUrl('date');
$scope.parseInt = parseInt;
this.getWebsite = function () {
return $scope.website;
};
$scope.sparklineImage = function(website){
var append = '';
var token_auth = piwik.broadcast.getValueFromUrl('token_auth');
if (token_auth.length) {
append = '&token_auth=' + token_auth;
}
return piwik.piwik_url + '?module=MultiSites&action=getEvolutionGraph&period=' + $scope.period + '&date=' + $scope.dateSparkline + '&evolutionBy=' +$scope.metric + '&columns=' + $scope.metric + '&idSite=' + website.idsite + '&idsite=' + website.idsite + '&viewDataTable=sparkline' + append + '&colors=' + encodeURIComponent(JSON.stringify(piwik.getSparklineColors()));
};
}
};
});

View file

@ -0,0 +1,39 @@
<tr ng-class="{'groupedWebsite': website.group, 'website': !website.group, 'group': website.isGroup}">
<td ng-if="!website.isGroup" class="multisites-label label">
<a title="View reports" ng-href="index.php?module=CoreHome&action=index&date={{ date }}&period={{ period }}&idSite={{ website.idsite }}">{{ website.label }}</a>
<span style="width: 10px; margin-left:3px;">
<a target="_blank" title="{{ 'General_GoTo'|translate:website.main_url }}" ng-href="{{ website.main_url }}">
<img src="plugins/MultiSites/images/link.gif"/></a>
</span>
</td>
<td ng-if="website.isGroup" class="multisites-label label">
{{ website.label }}
</td>
<td class="multisites-column">
{{ website.nb_visits }}
</td>
<td class="multisites-column">
{{ website.nb_pageviews }}
</td>
<td ng-if="displayRevenueColumn" class="multisites-column">
{{ website.revenue }}
</td>
<td ng-if="period != 'range'" style="width:170px;">
<div class="visits" ng-if="!website.isGroup">
<span ng-show="parseInt(website[evolutionMetric]) > 0"><img src="plugins/MultiSites/images/arrow_up.png" alt="" /> <span style="color: green;">{{ website[evolutionMetric] }}&nbsp;</span></span>
<span ng-show="parseInt(website[evolutionMetric]) == 0"><img src="plugins/MultiSites/images/stop.png" alt="" /> <span>{{ website[evolutionMetric] }}</span></span>
<span ng-show="parseInt(website[evolutionMetric]) < 0"><img src="plugins/MultiSites/images/arrow_down.png" alt="" /> <span style="color: red;">{{ website[evolutionMetric] }}&nbsp;</span></span>
</div>
</td>
<td ng-if="showSparklines" style="width:180px;">
<div ng-if="!website.isGroup" class="sparkline" style="width: 100px; margin: auto;">
<a target="_blank" ng-href="index.php?module=CoreHome&action=index&date={{ date }}&period={{ period }}&idSite={{ website.idsite }}"
title="{{ 'General_GoTo'|translate:('Dashboard_DashboardOf'|translate:website.label) }}">
<img alt="" ng-src="{{ sparklineImage(website) }}" width="100" height="25" />
</a>
</div>
</td>
</tr>

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 234 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 221 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 222 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 248 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 693 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 307 B

View file

@ -0,0 +1,23 @@
{% extends isWidgetized ? 'empty.twig' : 'dashboard.twig' %}
{% block content %}
{% if not isWidgetized %}
<div class="top_controls">
{% include "@CoreHome/_periodSelect.twig" %}
{% include "@CoreHome/_headerMessage.twig" %}
</div>
{% endif %}
<div class="pageWrap" id="multisites">
<div id="main">
<div piwik-multisites-dashboard
class="centerLargeDiv"
display-revenue-column="{% if displayRevenueColumn %}true{% else %}false{%endif%}"
page-size="{{ limit }}"
show-sparklines="{% if show_sparklines %}true{% else %}false{%endif%}"
date-sparkline="{{ dateSparkline }}"
auto-refresh-today-report="{{ autoRefreshTodayReport }}">
</div>
</div>
</div>
{% endblock %}