PDF rausgenommen

This commit is contained in:
aschwarz
2023-01-23 11:03:31 +01:00
parent 82d562a322
commit a6523903eb
28078 changed files with 4247552 additions and 2 deletions

View File

@ -0,0 +1,541 @@
<?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\MultiSites;
use Exception;
use Piwik\API\Request;
use Piwik\Archive;
use Piwik\Common;
use Piwik\Container\StaticContainer;
use Piwik\DataTable;
use Piwik\DataTable\Row;
use Piwik\Period\Range;
use Piwik\Piwik;
use Piwik\Plugins\Goals\Archiver;
use Piwik\Plugins\SitesManager\API as APISitesManager;
use Piwik\Scheduler\Scheduler;
use Piwik\SettingsPiwik;
use Piwik\Site;
/**
* The MultiSites API lets you request the key metrics (visits, page views, revenue) for all Websites in Matomo.
* @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_COL_NAME_KEY = 'metric_column_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';
private static $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_COL_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_COL_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
* @param array $showColumns If specified, only the requested columns will be fetched
* @return DataTable
*/
public function getAll($period, $date, $segment = false, $_restrictSitesToLogin = false, $enhanced = false, $pattern = false, $showColumns = array())
{
Piwik::checkUserHasSomeViewAccess();
$sites = $this->getSitesIdFromPattern($pattern, $_restrictSitesToLogin);
if (!empty($showColumns) && !is_array($showColumns)) {
$showColumns = explode(',', $showColumns);
}
if (empty($sites)) {
return new DataTable();
}
return $this->buildDataTable(
$sites,
$period,
$date,
$segment,
$_restrictSitesToLogin,
$enhanced,
$multipleWebsitesRequested = true,
$showColumns
);
}
/**
* Fetches the list of sites which names match the string pattern
*
* @param string $pattern
* @param bool $_restrictSitesToLogin
* @return array|string
*/
private function getSitesIdFromPattern($pattern, $_restrictSitesToLogin)
{
// First clear cache
Site::clearCache();
if (empty($pattern)) {
/** @var Scheduler $scheduler */
$scheduler = StaticContainer::getContainer()->get('Piwik\Scheduler\Scheduler');
// 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
&& !$scheduler->isRunningTask()
) {
APISitesManager::getInstance()->getAllSites();
} else {
APISitesManager::getInstance()->getSitesWithAtLeastViewAccess($limit = false, $_restrictSitesToLogin);
}
} else {
$sites = Request::processRequest('SitesManager.getPatternMatchSites',
array('pattern' => $pattern,
// added because caller could overwrite these
'limit' => SettingsPiwik::getWebsitesCountToDisplay(),
'showColumns' => '',
'hideColumns' => '',
'format' => 'original'));
if (!empty($sites)) {
Site::setSitesFromArray($sites);
}
}
// Both calls above have called Site::setSitesFromArray. We now get these sites:
$sitesToProblablyAdd = Site::getSites();
return $sitesToProblablyAdd;
}
/**
* Same as getAll but for a unique Matomo site
* @see Piwik\Plugins\MultiSites\API::getAll()
*
* @param int $idSite Id of the Matomo 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);
$sites = $this->getSiteFromId($idSite);
return $this->buildDataTable(
$sites,
$period,
$date,
$segment,
$_restrictSitesToLogin,
$enhanced,
$multipleWebsitesRequested = false,
$showColumns = array()
);
}
private function getSiteFromId($idSite)
{
$idSite = (int) $idSite;
$sites = array(APISitesManager::getInstance()->getSiteFromId($idSite));
return $sites;
}
private function buildDataTable($sitesToProblablyAdd, $period, $date, $segment, $_restrictSitesToLogin, $enhanced, $multipleWebsitesRequested, $showColumns)
{
$idSites = array();
if (!empty($sitesToProblablyAdd)) {
foreach ($sitesToProblablyAdd as $site) {
$idSites[] = $site['idsite'];
}
}
// build the archive type used to query archive data
$archive = Archive::build(
$idSites,
$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) {
if (!empty($showColumns) && !in_array($metricName, $showColumns)) {
unset($apiMetrics[$metricName]);
continue;
}
$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;
}
}
$dataTable = $archive->getDataTableFromNumericAndMergeChildren($fieldsToGet);
$this->populateLabel($dataTable);
$totalMetrics = $this->preformatApiMetricsForTotalsCalculation($apiMetrics);
$this->setMetricsTotalsMetadata($dataTable, $totalMetrics);
// 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($idSites, $period, $strLastDate, $segment, $_restrictSitesToLogin);
$pastData = $pastArchive->getDataTableFromNumericAndMergeChildren($fieldsToGet);
$this->populateLabel($pastData); // labels are needed to calculate evolution
$this->calculateEvolutionPercentages($dataTable, $pastData, $apiMetrics);
$this->setPastTotalVisitsMetadata($dataTable, $pastData);
if ($dataTable instanceof DataTable) {
// needed for MultiSites\Dashboard
$dataTable->setMetadata('pastData', $pastData);
}
}
// move the site id to a metadata column
$dataTable->queueFilter('MetadataCallbackAddMetadata', array('idsite', 'group', array('\Piwik\Site', 'getGroupFor'), array()));
$dataTable->queueFilter('MetadataCallbackAddMetadata', array('idsite', 'main_url', array('\Piwik\Site', 'getMainUrlFor'), array()));
// set the label of each row to the site name
if ($multipleWebsitesRequested) {
$dataTable->queueFilter('ColumnCallbackReplace', array('label', '\Piwik\Site::getNameFor'));
} else {
$dataTable->queueFilter('ColumnDelete', array('label'));
}
// replace record names with user friendly metric names
$dataTable->queueFilter('ReplaceColumnNames', array($columnNameRewrites));
// 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 && (empty($showColumns) || in_array(self::NB_VISITS_METRIC, $showColumns))
) {
$dataTable->filter(
'ColumnCallbackDeleteRow',
array(
self::NB_VISITS_METRIC,
function ($value) {
return $value == 0;
}
)
);
}
if ($multipleWebsitesRequested && $dataTable->getRowsCount() === 1 && $dataTable instanceof DataTable\Simple) {
$simpleTable = $dataTable;
$dataTable = $simpleTable->getEmptyClone();
$dataTable->addRow($simpleTable->getFirstRow());
unset($simpleTable);
}
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 {
$extraProcessedMetrics = $currentData->getMetadata(DataTable::EXTRA_PROCESSED_METRICS_METADATA_NAME);
foreach ($apiMetrics as $metricSettings) {
$evolutionMetricClass = $this->isEcommerceEvolutionMetric($metricSettings)
? "Piwik\\Plugins\\MultiSites\\Columns\\Metrics\\EcommerceOnlyEvolutionMetric"
: "Piwik\\Plugins\\CoreHome\\Columns\\Metrics\\EvolutionMetric";
$extraProcessedMetrics[] = new $evolutionMetricClass(
$metricSettings[self::METRIC_RECORD_NAME_KEY],
$pastData,
$metricSettings[self::METRIC_EVOLUTION_COL_NAME_KEY],
$quotientPrecision = 1
);
}
$currentData->setMetadata(DataTable::EXTRA_PROCESSED_METRICS_METADATA_NAME, $extraProcessedMetrics);
}
}
/**
* @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_COL_NAME_KEY => self::NB_PAGEVIEWS_LABEL,
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_COL_NAME_KEY => 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_COL_NAME_KEY => 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_COL_NAME_KEY => self::ECOMMERCE_ORDERS_METRIC,
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_COL_NAME_KEY => self::ECOMMERCE_REVENUE_METRIC,
self::METRIC_IS_ECOMMERCE_KEY => true,
);
}
}
return $metrics;
}
private function preformatApiMetricsForTotalsCalculation($apiMetrics)
{
$metrics = array();
foreach ($apiMetrics as $label => $metricsInfo) {
$totalMetadataName = self::getTotalMetadataName($label);
$metrics[$totalMetadataName] = $metricsInfo[self::METRIC_RECORD_NAME_KEY];
}
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 {
$totals = array();
foreach ($apiMetrics as $label => $recordName) {
$totals[$label] = 0;
}
$rows = $dataTable->getRows();
$rows = $this->filterRowsForTotalsCalculation($rows);
foreach ($rows as $row) {
foreach ($apiMetrics as $totalMetadataName => $recordName) {
$totals[$totalMetadataName] += $row->getColumn($recordName);
}
}
$dataTable->setMetadataValues($totals);
}
}
/**
* Sets the number of total visits in tha pastTable on the dataTable as metadata.
*
* @param DataTable $dataTable
* @param DataTable $pastTable
*/
private function setPastTotalVisitsMetadata($dataTable, $pastTable)
{
if ($pastTable instanceof DataTable) {
$total = 0;
$metric = 'nb_visits';
$rows = $pastTable->getRows();
$rows = $this->filterRowsForTotalsCalculation($rows);
foreach ($rows as $row) {
$total += $row->getColumn($metric);
}
$dataTable->setMetadata(self::getTotalMetadataName($metric . '_lastdate'), $total);
}
}
/**
* @param Row[] $rows
* @return mixed
*/
private function filterRowsForTotalsCalculation($rows)
{
/**
* Triggered to filter / restrict which rows should be included in the MultiSites (All Websites Dashboard)
* totals calculation
*
* **Example**
*
* public function filterMultiSitesRows(&$rows)
* {
* foreach ($rows as $index => $row) {
* if ($row->getColumn('label') === 5) {
* unset($rows[$index]); // remove idSite 5 from totals
* }
* }
* }
*
* @param Row[] &$rows An array containing rows, one row for each site. The label columns equals the idSite.
*/
Piwik::postEvent('MultiSites.filterRowsForTotalsCalculation', array(&$rows));
return $rows;
}
private static function getTotalMetadataName($name)
{
return 'total_' . $name;
}
private static function getLastPeriodMetadataName($name)
{
return 'last_period_' . $name;
}
private function populateLabel($dataTable)
{
$dataTable->filter(function (DataTable $table) {
foreach ($table->getRowsWithoutSummaryRow() as $row) {
$row->setColumn('label', $row->getMetadata('idsite'));
}
});
// make sure label column is always first column
$dataTable->queueFilter(function (DataTable $table) {
foreach ($table->getRowsWithoutSummaryRow() as $row) {
$row->setColumns(array_merge(array('label' => $row->getColumn('label')), $row->getColumns()));
}
});
}
private function isEcommerceEvolutionMetric($metricSettings)
{
return in_array($metricSettings[self::METRIC_EVOLUTION_COL_NAME_KEY], array(
self::GOAL_REVENUE_METRIC . '_evolution',
self::ECOMMERCE_ORDERS_METRIC . '_evolution',
self::ECOMMERCE_REVENUE_METRIC . '_evolution'
));
}
}

View File

@ -0,0 +1,17 @@
<?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\MultiSites\Categories;
use Piwik\Category\Category;
class MultiSitesCategory extends Category
{
protected $id = 'General_MultiSitesSummary';
protected $order = 3;
}

View File

@ -0,0 +1,50 @@
<?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\MultiSites\Columns\Metrics;
use Piwik\DataTable;
use Piwik\DataTable\Row;
use Piwik\Plugins\CoreHome\Columns\Metrics\EvolutionMetric;
use Piwik\Site;
/**
* Ecommerce evolution metric adapter. This is a special processed metric for MultiSites API methods. It will
* only be calculated for sites that have ecommerce enabled. The site is determined by the label
* of each row.
*/
class EcommerceOnlyEvolutionMetric extends EvolutionMetric
{
private $isRevenueEvolution;
public function __construct($wrapped, DataTable $pastData, $evolutionMetricName = false, $quotientPrecision = 0)
{
parent::__construct($wrapped, $pastData, $evolutionMetricName, $quotientPrecision);
$this->isRevenueEvolution = $this->getName() == 'revenue_evolution';
}
public function compute(Row $row)
{
$columnName = $this->getWrappedName();
$currentValue = $this->getMetric($row, $columnName);
// if the site this is for doesn't support ecommerce & this is for the revenue_evolution column,
// we don't add the new column
if ($currentValue === false || !$this->isRevenueEvolution) {
$idSite = $row->getMetadata('idsite');
if (!$idSite || !Site::isEcommerceEnabledFor($idSite)) {
$row->deleteColumn($columnName);
return false;
}
}
return parent::compute($row);
}
}

View File

@ -0,0 +1,20 @@
<?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\MultiSites\Columns;
use Piwik\Columns\Dimension;
use Piwik\Piwik;
class Website extends Dimension
{
public function getName()
{
return Piwik::translate('General_Website');
}
}

View File

@ -0,0 +1,121 @@
<?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\MultiSites;
use Piwik\Common;
use Piwik\Config;
use Piwik\Date;
use Piwik\Period;
use Piwik\DataTable;
use Piwik\DataTable\Row;
use Piwik\Piwik;
use Piwik\Translation\Translator;
use Piwik\View;
class Controller extends \Piwik\Plugin\Controller
{
/**
* @var Translator
*/
private $translator;
public function __construct(Translator $translator)
{
parent::__construct();
$this->translator = $translator;
}
public function index()
{
return $this->getSitesInfo($isWidgetized = false);
}
public function standalone()
{
return $this->getSitesInfo($isWidgetized = true);
}
public function getAllWithGroups()
{
Piwik::checkUserHasSomeViewAccess();
$period = Common::getRequestVar('period', null, 'string');
$date = Common::getRequestVar('date', null, 'string');
$segment = Common::getRequestVar('segment', false, 'string');
$pattern = Common::getRequestVar('pattern', '', 'string');
$limit = Common::getRequestVar('filter_limit', 0, 'int');
$segment = $segment ?: false;
$request = $_GET + $_POST;
$dashboard = new Dashboard($period, $date, $segment);
if ($pattern !== '') {
$dashboard->search(strtolower($pattern));
}
$response = array(
'numSites' => $dashboard->getNumSites(),
'totals' => $dashboard->getTotals(),
'lastDate' => $dashboard->getLastDate(),
'sites' => $dashboard->getSites($request, $limit)
);
return json_encode($response);
}
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);
$view->siteName = $this->translator->translate('General_AllWebsitesDashboard');
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);
$view->requestConfig->totals = 0;
return $this->renderView($view);
}
}

View File

@ -0,0 +1,352 @@
<?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\MultiSites;
use Piwik\API\DataTablePostProcessor;
use Piwik\API\Request;
use Piwik\API\ResponseBuilder;
use Piwik\Config;
use Piwik\Metrics\Formatter;
use Piwik\NumberFormatter;
use Piwik\Period;
use Piwik\DataTable;
use Piwik\DataTable\Row;
use Piwik\DataTable\Row\DataTableSummaryRow;
use Piwik\Site;
use Piwik\View;
/**
* Fetches and formats the response of `MultiSites.getAll` in a way that it can be used by the All Websites AngularJS
* widget. Eg sites are moved into groups if one is assigned, stats are calculated for groups, etc.
*/
class Dashboard
{
/** @var DataTable */
private $sitesByGroup;
/**
* @var int
*/
private $numSites = 0;
/**
* Array of metrics that will be displayed and will be number formatted
* @var array
*/
private $displayedMetricColumns = array('nb_visits', 'nb_pageviews', 'nb_actions', 'revenue');
/**
* @param string $period
* @param string $date
* @param string|false $segment
*/
public function __construct($period, $date, $segment)
{
$sites = Request::processRequest('MultiSites.getAll', [
'period' => $period,
'date' => $date,
'segment' => $segment,
'enhanced' => '1',
// NOTE: have to select everything since with queued filters disabled some metrics won't be renamed to
// their display name, and so showColumns will end up removing those.
'showColumns' => '',
'disable_queued_filters' => '1',
'filter_limit' => '-1',
'filter_offset' => '0',
'totals' => 0
], $default = []);
$sites->deleteRow(DataTable::ID_SUMMARY_ROW);
/** @var DataTable $pastData */
$pastData = $sites->getMetadata('pastData');
$sites->filter(function (DataTable $table) use ($pastData) {
$pastRow = null;
foreach ($table->getRows() as $row) {
$idSite = $row->getColumn('label');
$site = Site::getSite($idSite);
// we cannot queue label and group as we might need them for search and sorting!
$row->setColumn('label', $site['name']);
$row->setMetadata('group', $site['group']);
if ($pastData) {
// if we do not update the pastData labels, the evolution cannot be calculated correctly.
$pastRow = $pastData->getRowFromLabel($idSite);
if ($pastRow) {
$pastRow->setColumn('label', $site['name']);
}
}
}
if ($pastData && $pastRow) {
$pastData->setLabelsHaveChanged();
}
});
$this->setSitesTable($sites);
}
public function setSitesTable(DataTable $sites)
{
$this->sitesByGroup = $this->moveSitesHavingAGroupIntoSubtables($sites);
$this->rememberNumberOfSites();
}
public function getSites($request, $limit)
{
$request['filter_limit'] = $limit;
$request['filter_offset'] = isset($request['filter_offset']) ? $request['filter_offset'] : 0;
$this->makeSitesFlatAndApplyGenericFilters($this->sitesByGroup, $request);
$sites = $this->convertDataTableToArrayAndApplyQueuedFilters($this->sitesByGroup, $request);
$sites = $this->enrichValues($sites);
return $sites;
}
public function getTotals()
{
$totals = array(
'nb_pageviews' => $this->sitesByGroup->getMetadata('total_nb_pageviews'),
'nb_visits' => $this->sitesByGroup->getMetadata('total_nb_visits'),
'nb_actions' => $this->sitesByGroup->getMetadata('total_nb_actions'),
'revenue' => $this->sitesByGroup->getMetadata('total_revenue'),
'nb_visits_lastdate' => $this->sitesByGroup->getMetadata('total_nb_visits_lastdate') ? : 0,
);
$this->formatMetrics($totals);
return $totals;
}
private function formatMetrics(&$metrics)
{
$formatter = NumberFormatter::getInstance();
foreach($metrics as $metricName => &$value) {
if(in_array($metricName, $this->displayedMetricColumns)) {
if( strpos($metricName, 'revenue') !== false) {
$currency = isset($metrics['idsite']) ? Site::getCurrencySymbolFor($metrics['idsite']) : '';
$value = $formatter->formatCurrency($value, $currency);
continue;
}
$value = $formatter->format($value);
}
}
}
public function getNumSites()
{
return $this->numSites;
}
public function search($pattern)
{
$this->nestedSearch($this->sitesByGroup, $pattern);
$this->rememberNumberOfSites();
}
private function rememberNumberOfSites()
{
$this->numSites = $this->sitesByGroup->getRowsCountRecursive();
}
private function nestedSearch(DataTable $sitesByGroup, $pattern)
{
foreach ($sitesByGroup->getRows() as $index => $site) {
$label = strtolower($site->getColumn('label'));
$labelMatches = false !== strpos($label, $pattern);
if ($site->getMetadata('isGroup')) {
$subtable = $site->getSubtable();
$this->nestedSearch($subtable, $pattern);
if (!$labelMatches && !$subtable->getRowsCount()) {
// we keep the group if at least one site within the group matches the pattern
$sitesByGroup->deleteRow($index);
}
} elseif (!$labelMatches) {
$group = $site->getMetadata('group');
if (!$group || false === strpos(strtolower($group), $pattern)) {
$sitesByGroup->deleteRow($index);
}
}
}
}
/**
* @return string
*/
public function getLastDate()
{
$lastPeriod = $this->sitesByGroup->getMetadata('last_period_date');
if (!empty($lastPeriod)) {
$lastPeriod = $lastPeriod->toString();
} else {
$lastPeriod = '';
}
return $lastPeriod;
}
private function convertDataTableToArrayAndApplyQueuedFilters(DataTable $table, $request)
{
$request['serialize'] = 0;
$request['expanded'] = 0;
$request['totals'] = 0;
$request['format_metrics'] = 1;
$request['disable_generic_filters'] = 1;
$responseBuilder = new ResponseBuilder('php', $request);
$rows = $responseBuilder->getResponse($table, 'MultiSites', 'getAll');
return $rows;
}
private function moveSitesHavingAGroupIntoSubtables(DataTable $sites)
{
/** @var DataTableSummaryRow[] $groups */
$groups = array();
$sitesByGroup = $this->makeCloneOfDataTableSites($sites);
$sitesByGroup->enableRecursiveFilters(); // we need to make sure filters get applied to subtables (groups)
foreach ($sites->getRows() as $site) {
$group = $site->getMetadata('group');
if (!empty($group) && !array_key_exists($group, $groups)) {
$row = new DataTableSummaryRow();
$row->setColumn('label', $group);
$row->setMetadata('isGroup', 1);
$row->setSubtable($this->createGroupSubtable($sites));
$sitesByGroup->addRow($row);
$groups[$group] = $row;
}
if (!empty($group)) {
$groups[$group]->getSubtable()->addRow($site);
} else {
$sitesByGroup->addRow($site);
}
}
foreach ($groups as $group) {
// we need to recalculate as long as all rows are there, as soon as some rows are removed
// we can no longer recalculate the correct value. We might even calculate values for groups
// that are not returned. If this becomes a problem we need to keep a copy of this to recalculate
// only actual returned groups.
$group->recalculate();
}
return $sitesByGroup;
}
private function createGroupSubtable(DataTable $sites)
{
$table = new DataTable();
$processedMetrics = $sites->getMetadata(DataTable::EXTRA_PROCESSED_METRICS_METADATA_NAME);
$table->setMetadata(DataTable::EXTRA_PROCESSED_METRICS_METADATA_NAME, $processedMetrics);
return $table;
}
private function makeCloneOfDataTableSites(DataTable $sites)
{
$sitesByGroup = $sites->getEmptyClone(true);
// we handle them ourselves for faster performance etc. This way we also avoid to apply them twice.
$sitesByGroup->disableFilter('ColumnCallbackReplace');
$sitesByGroup->disableFilter('MetadataCallbackAddMetadata');
return $sitesByGroup;
}
/**
* Makes sure to not have any subtables anymore.
*
* So if $table is
* array(
* site1
* site2
* subtable => site3
* site4
* site5
* site6
* site7
* )
*
* it will return
*
* array(
* site1
* site2
* site3
* site4
* site5
* site6
* site7
* )
*
* in a sorted order
*
* @param DataTable $table
* @param array $request
*/
private function makeSitesFlatAndApplyGenericFilters(DataTable $table, $request)
{
// we handle limit here as we have to apply sort filter, then make sites flat, then apply limit filter.
$filterOffset = $request['filter_offset'];
$filterLimit = $request['filter_limit'];
unset($request['filter_offset']);
unset($request['filter_limit']);
// filter_sort_column does not work correctly is a bug in MultiSites.getAll
if (!empty($request['filter_sort_column']) && $request['filter_sort_column'] === 'nb_pageviews') {
$request['filter_sort_column'] = 'Actions_nb_pageviews';
} elseif (!empty($request['filter_sort_column']) && $request['filter_sort_column'] === 'revenue') {
$request['filter_sort_column'] = 'Goal_revenue';
}
// make sure no limit filter is applied, we will do this manually
$table->disableFilter('Limit');
// this will apply the sort filter
/** @var DataTable $table */
$genericFilter = new DataTablePostProcessor('MultiSites', 'getAll', $request);
$table = $genericFilter->applyGenericFilters($table);
// make sure from now on the sites will be no longer sorted, they were already sorted
$table->disableFilter('Sort');
// make sites flat and limit
$table->filter('Piwik\Plugins\MultiSites\DataTable\Filter\NestedSitesLimiter', array($filterOffset, $filterLimit));
}
private function enrichValues($sites)
{
foreach ($sites as &$site) {
if (!isset($site['idsite'])) {
continue;
}
$site['main_url'] = Site::getMainUrlFor($site['idsite']);
$this->formatMetrics($site);
}
return $sites;
}
}

View File

@ -0,0 +1,138 @@
<?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\MultiSites\DataTable\Filter;
use Piwik\DataTable\BaseFilter;
use Piwik\DataTable\Row;
use Piwik\DataTable;
/**
* Makes sure to not have any subtables anymore and applies the limit to the flattened table.
*
* So if $table is
* array(
* site1
* group2
* subtable => site3
* site4
* site5
* site6
* site7
* )
*
* it will format this to
*
* array(
* site1
* group2
* site3
* site4
* site5
* site6
* site7
* )
*
* and then apply the limit filter.
*
* Each group will not count into the limit/offset. This way, if one requests a limit of 50 sites,
* we make sure to return 50 sites.
*
* @param $sites
* @return array
*/
class NestedSitesLimiter extends BaseFilter
{
private $offset = 0;
private $limit = 0;
/**
* @var Row[]
*/
private $rows = array();
/**
* Constructor.
*
* @param DataTable $table The table to eventually filter.
*/
public function __construct($table, $offset, $limit)
{
parent::__construct($table);
$this->offset = $offset;
$this->limit = $limit;
}
/**
* @param DataTable $table
*/
public function filter($table)
{
$numRows = 0;
$lastGroupFromPreviousPage = null;
foreach ($table->getRows() as $row) {
$this->addRowIfNeeded($row, $numRows);
$numRows++;
$subtable = $row->getSubtable();
if ($subtable) {
if (!$this->hasRows()) {
$lastGroupFromPreviousPage = $row;
}
foreach ($subtable->getRows() as $subRow) {
$this->addRowIfNeeded($subRow, $numRows);
$numRows++;
}
$row->removeSubtable();
}
if ($this->hasNumberOfRequestedRowsFound()) {
break;
}
}
$this->prependGroupIfFirstSiteBelongsToAGroupButGroupIsMissingInRows($lastGroupFromPreviousPage);
$table->setRows($this->rows);
}
private function hasNumberOfRequestedRowsFound()
{
return count($this->rows) >= $this->limit;
}
private function hasRows()
{
return count($this->rows) !== 0;
}
private function addRowIfNeeded(Row $row, $numRows)
{
$inOffset = $numRows >= $this->offset;
if ($inOffset && !$this->hasNumberOfRequestedRowsFound()) {
$this->rows[] = $row;
}
}
/**
* @param Row $lastGroupFromPreviousPage
*/
private function prependGroupIfFirstSiteBelongsToAGroupButGroupIsMissingInRows($lastGroupFromPreviousPage)
{
if ($lastGroupFromPreviousPage && !empty($this->rows)) {
// the result starts with a row that does belong to a group, we make sure to show this group before that site
$group = reset($this->rows)->getMetadata('group');
if ($group && $lastGroupFromPreviousPage->getColumn('label') === $group) {
array_unshift($this->rows, $lastGroupFromPreviousPage);
// we do not remove the last item as it could result in errors, instead we show $limit+1 entries
}
}
}
}

View File

@ -0,0 +1,23 @@
<?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\MultiSites;
use Piwik\Menu\MenuTop;
use Piwik\Piwik;
class Menu extends \Piwik\Plugin\Menu
{
public function configureTopMenu(MenuTop $menu)
{
$urlParams = $this->urlForActionWithDefaultUserParams('index', array('segment' => false, 'idSite' => false));
$tooltip = Piwik::translate('MultiSites_TopLinkTooltip');
$menu->addItem('General_MultiSitesSummary', null, $urlParams, 3, $tooltip);
}
}

View File

@ -0,0 +1,84 @@
<?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\MultiSites;
use Piwik\Piwik;
class MultiSites extends \Piwik\Plugin
{
/**
* @see Piwik\Plugin::registerEvents
*/
public function registerEvents()
{
return array(
'AssetManager.getStylesheetFiles' => 'getStylesheetFiles',
'AssetManager.getJavaScriptFiles' => 'getJsFiles',
'Translate.getClientSideTranslationKeys' => 'getClientSideTranslationKeys',
'Metrics.getDefaultMetricTranslations' => 'addMetricTranslations'
);
}
public function addMetricTranslations(&$translations)
{
$appendix = " " . Piwik::translate('MultiSites_Evolution');
$metrics = array(
'visits_evolution' => Piwik::translate('General_ColumnNbVisits') . $appendix,
'actions_evolution' => Piwik::translate('General_ColumnNbActions') . $appendix,
'pageviews_evolution' => Piwik::translate('General_ColumnPageviews') . $appendix,
'revenue_evolution' => Piwik::translate('General_ColumnRevenue') . $appendix,
'nb_conversions_evolution' => Piwik::translate('Goals_ColumnConversions') . $appendix,
'orders_evolution' => Piwik::translate('General_EcommerceOrders') . $appendix,
'ecommerce_revenue_evolution' => Piwik::translate('General_ProductRevenue') . $appendix,
);
$translations = array_merge($translations, $metrics);
}
public function getClientSideTranslationKeys(&$translations)
{
$translations[] = 'General_Website';
$translations[] = 'General_ColumnNbVisits';
$translations[] = 'General_ColumnPageviews';
$translations[] = 'General_ColumnRevenue';
$translations[] = 'General_TotalVisitsPageviewsActionsRevenue';
$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';
$translations[] = 'General_Pagination';
$translations[] = 'General_ClickToSearch';
$translations[] = 'General_NeedMoreHelp';
$translations[] = 'General_Faq';
$translations[] = 'Feedback_CommunityHelp';
$translations[] = 'Feedback_ProfessionalHelp';
}
public function getJsFiles(&$jsFiles)
{
$jsFiles[] = "plugins/MultiSites/angularjs/dashboard/dashboard-model.service.js";
$jsFiles[] = "plugins/MultiSites/angularjs/dashboard/dashboard.controller.js";
$jsFiles[] = "plugins/MultiSites/angularjs/dashboard/dashboard.directive.js";
$jsFiles[] = "plugins/MultiSites/angularjs/site/site.controller.js";
$jsFiles[] = "plugins/MultiSites/angularjs/site/site.directive.js";
}
public function getStylesheetFiles(&$stylesheets)
{
$stylesheets[] = "plugins/MultiSites/angularjs/dashboard/dashboard.directive.less";
}
}

View File

@ -0,0 +1,37 @@
<?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\MultiSites\Reports;
use Piwik\Piwik;
use Piwik\Plugins\MultiSites\API;
abstract class Base extends \Piwik\Plugin\Report
{
protected function init()
{
$this->categoryId = 'General_MultiSitesSummary';
$allMetricsInfo = API::getApiMetrics($enhanced = true);
$metadataMetrics = array();
$processedMetricsMetadata = array();
foreach ($allMetricsInfo as $metricName => $metricSettings) {
$metadataMetrics[$metricName] =
Piwik::translate($metricSettings[API::METRIC_TRANSLATION_KEY]);
$processedMetricsMetadata[$metricSettings[API::METRIC_EVOLUTION_COL_NAME_KEY]] =
Piwik::translate($metricSettings[API::METRIC_TRANSLATION_KEY]) . " " . Piwik::translate('MultiSites_Evolution');
}
$this->metrics = array_keys($metadataMetrics);
$this->processedMetrics = array_keys($processedMetricsMetadata);
}
}

View File

@ -0,0 +1,25 @@
<?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\MultiSites\Reports;
use Piwik\Piwik;
use Piwik\Plugins\MultiSites\Columns\Website;
class GetAll extends Base
{
protected function init()
{
parent::init();
$this->dimension = new Website();
$this->name = Piwik::translate('General_AllWebsitesDashboard');
$this->documentation = ''; // TODO
$this->constantRowsCount = false;
$this->order = 4;
}
}

View File

@ -0,0 +1,26 @@
<?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\MultiSites\Reports;
use Piwik\Piwik;
use Piwik\Plugins\MultiSites\Columns\Website;
class GetOne extends Base
{
protected function init()
{
parent::init();
$this->dimension = new Website();
$this->name = Piwik::translate('General_SingleWebsitesDashboard');
$this->documentation = ''; // TODO
$this->constantRowsCount = false;
$this->order = 5;
}
}

View File

@ -0,0 +1,184 @@
/**
* Model for Multisites Dashboard aka All Websites Dashboard.
*/
(function () {
angular.module('piwikApp').factory('multisitesDashboardModel', multisitesDashboardModel);
multisitesDashboardModel.$inject = ['piwikApi', '$filter', '$timeout'];
function multisitesDashboardModel(piwikApi, $filter, $timeout) {
var refreshPromise = null;
// those sites are going to be displayed
var model = {
sites : [],
isLoading : false,
pageSize : 25,
currentPage : 0,
totalVisits : '?',
totalPageviews : '?',
totalActions : '?',
totalRevenue : '?',
searchTerm : '',
lastVisits : '?',
lastVisitsDate : '?',
numberOfSites : 0,
loadingMessage: _pk_translate('MultiSites_LoadingWebsites'),
updateWebsitesList: updateWebsitesList,
getNumberOfFilteredSites: getNumberOfFilteredSites,
getNumberOfPages: getNumberOfPages,
getPaginationLowerBound: getPaginationLowerBound,
getPaginationUpperBound: getPaginationUpperBound,
previousPage: previousPage,
nextPage: nextPage,
searchSite: searchSite,
sortBy: sortBy,
reverse: true,
sortColumn: 'nb_visits',
fetchAllSites: fetchAllSites,
refreshInterval: 0
};
return model;
function cancelRefereshInterval()
{
if (refreshPromise) {
$timeout.cancel(refreshPromise);
refreshPromise = null;
};
}
function onError () {
model.errorLoadingSites = true;
model.sites = [];
}
function updateWebsitesList(report) {
if (!report) {
onError();
return;
}
var allSites = report.sites;
angular.forEach(allSites, function (site, index) {
site.visits_evolution = parseInt(site.visits_evolution, 10);
site.pageviews_evolution = parseInt(site.pageviews_evolution, 10);
site.revenue_evolution = parseInt(site.revenue_evolution, 10);
});
model.totalVisits = report.totals.nb_visits;
model.totalPageviews = report.totals.nb_pageviews;
model.totalActions = report.totals.nb_actions;
model.totalRevenue = report.totals.revenue;
model.lastVisits = report.totals.nb_visits_lastdate;
model.sites = allSites;
model.numberOfSites = report.numSites;
model.lastVisitsDate = report.lastDate;
}
function getNumberOfFilteredSites () {
return model.numberOfSites;
}
function getNumberOfPages() {
return Math.ceil(getNumberOfFilteredSites() / model.pageSize - 1);
}
function getCurrentPagingOffset() {
return Math.ceil(model.currentPage * model.pageSize);
}
function getPaginationLowerBound() {
return getCurrentPagingOffset() + 1;
}
function getPaginationUpperBound() {
var end = getCurrentPagingOffset() + parseInt(model.pageSize, 10);
var max = getNumberOfFilteredSites();
if (end > max) {
end = max;
}
return parseInt(end, 10);
}
function previousPage() {
model.currentPage = model.currentPage - 1;
fetchAllSites();
}
function sortBy(metric) {
if (model.sortColumn == metric) {
model.reverse = !model.reverse;
}
model.sortColumn = metric;
fetchAllSites();
};
function previousPage() {
model.currentPage = model.currentPage - 1;
fetchAllSites();
}
function nextPage() {
model.currentPage = model.currentPage + 1;
fetchAllSites();
}
function searchSite (term) {
model.searchTerm = term;
model.currentPage = 0;
fetchAllSites();
}
function fetchAllSites() {
if (model.isLoading) {
piwikApi.abort();
cancelRefereshInterval();
}
model.isLoading = true;
model.errorLoadingSites = false;
var params = {
module: 'MultiSites',
action: 'getAllWithGroups',
hideMetricsDoc: '1',
filter_sort_order: 'asc',
filter_limit: model.pageSize,
filter_offset: getCurrentPagingOffset(),
showColumns: 'label,nb_visits,nb_pageviews,visits_evolution,pageviews_evolution,revenue_evolution,nb_actions,revenue'
};
if (model.searchTerm) {
params.pattern = model.searchTerm;
}
if (model.sortColumn) {
params.filter_sort_column = model.sortColumn;
}
if (model.reverse) {
params.filter_sort_order = 'desc';
}
return piwikApi.fetch(params).then(function (response) {
updateWebsitesList(response);
}, onError).finally(function () {
model.isLoading = false;
if (model.refreshInterval && model.refreshInterval > 0) {
cancelRefereshInterval();
refreshPromise = $timeout(function () {
refreshPromise = null;
fetchAllSites(model.refreshInterval);
}, model.refreshInterval * 1000);
}
});
}
}
})();

View File

@ -0,0 +1,29 @@
/*!
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
(function () {
angular.module('piwikApp').controller('MultiSitesDashboardController', MultiSitesDashboardController);
MultiSitesDashboardController.$inject = ['$scope', 'piwik', 'multisitesDashboardModel'];
function MultiSitesDashboardController($scope, piwik, multisitesDashboardModel){
$scope.model = multisitesDashboardModel;
$scope.evolutionSelector = 'visits_evolution';
$scope.hasSuperUserAccess = piwik.hasSuperUserAccess;
$scope.date = piwik.broadcast.getValueFromUrl('date');
$scope.idSite = piwik.broadcast.getValueFromUrl('idSite');
$scope.url = piwik.piwik_url;
$scope.period = piwik.period;
$scope.areAdsForProfessionalServicesEnabled = piwik.config && piwik.config.are_ads_enabled;
this.refresh = function (interval) {
multisitesDashboardModel.refreshInterval = interval;
multisitesDashboardModel.fetchAllSites();
};
}
})();

View File

@ -0,0 +1,129 @@
<div>
<h2 piwik-enriched-headline
class="card-title"
help-url="https://matomo.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_TotalVisitsPageviewsActionsRevenue' | translate:('<strong>'+model.totalVisits+'</strong>'):('<strong>'+model.totalPageviews+'</strong>'):('<strong>'+model.totalActions+'</strong>'):('<strong>' + model.totalRevenue + '</strong>')">
</span>
</h2>
<table id="mt" class="dataTable card-table" cellspacing="0">
<thead>
<tr>
<th id="names" class="label" ng-click="model.sortBy('label')" ng-class="{columnSorted: 'label' == model.sortColumn}">
<span class="heading">{{ 'General_Website'|translate }}</span>
<span ng-class="{multisites_asc: !model.reverse && 'label' == model.sortColumn, multisites_desc: model.reverse && 'label' == model.sortColumn}" class="arrow"></span>
</th>
<th id="visits" class="multisites-column" ng-click="model.sortBy('nb_visits')" ng-class="{columnSorted: 'nb_visits' == model.sortColumn}">
<span ng-class="{multisites_asc: !model.reverse && 'nb_visits' == model.sortColumn, multisites_desc: model.reverse && 'nb_visits' == model.sortColumn}" class="arrow"></span>
<span class="heading">{{ 'General_ColumnNbVisits'|translate }}</span>
</th>
<th id="pageviews" class="multisites-column" ng-click="model.sortBy('nb_pageviews')" ng-class="{columnSorted: 'nb_pageviews' == model.sortColumn}">
<span ng-class="{multisites_asc: !model.reverse && 'nb_pageviews' == model.sortColumn, multisites_desc: model.reverse && 'nb_pageviews' == model.sortColumn}" class="arrow"></span>
<span class="heading">{{ 'General_ColumnPageviews'|translate }}</span>
</th>
<th ng-if="displayRevenueColumn" id="revenue" class="multisites-column" ng-click="model.sortBy('revenue')" ng-class="{columnSorted: 'revenue' == model.sortColumn}">
<span ng-class="{multisites_asc: !model.reverse && 'revenue' == model.sortColumn, multisites_desc: model.reverse && 'revenue' == model.sortColumn}" class="arrow"></span>
<span class="heading">{{ 'General_ColumnRevenue'|translate }}</span>
</th>
<th id="evolution" colspan="{{ showSparklines ? 2 : 1 }}" ng-class="{columnSorted: evolutionSelector == model.sortColumn}">
<span class="arrow" ng-class="{multisites_asc: !model.reverse && evolutionSelector == model.sortColumn, multisites_desc: model.reverse && evolutionSelector == model.sortColumn}"></span>
<span class="evolution"
ng-click="model.sortBy(evolutionSelector)"> {{ 'MultiSites_Evolution'|translate }}</span>
<select class="selector browser-default" id="evolution_selector" ng-model="evolutionSelector"
ng-change="model.sortBy(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">
<div piwik-activity-indicator loading-message="model.loadingMessage" loading="model.isLoading"></div>
</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:(''):('') }}
<br /><br />
{{ 'General_NeedMoreHelp'|translate }}
<a rel="noreferrer noopener" target="_blank" href="https://matomo.org/faq/troubleshooting/faq_19489/">{{ 'General_Faq'|translate }}</a>
<a rel="noreferrer noopener" target="_blank" href="https://forum.matomo.org/">{{ 'Feedback_CommunityHelp'|translate }}</a>
<span ng-show="areAdsForProfessionalServicesEnabled"> </span>
<a ng-show="areAdsForProfessionalServicesEnabled" rel="noreferrer noopener" target="_blank" href="https://matomo.org/support/?pk_campaign=Help&pk_medium=AjaxError&pk_content=MultiSites&pk_source=Piwik_App">{{ 'Feedback_ProfessionalHelp'|translate }}</a>.
</div>
</td>
</tr>
<tr website="website"
evolution-metric="evolutionSelector"
piwik-multisites-site
date-sparkline="dateSparkline"
show-sparklines="showSparklines"
metric="model.sortColumn"
display-revenue-column="displayRevenueColumn"
ng-repeat="website in model.sites">
</tr>
</tbody>
<tfoot>
<tr>
<td colspan="8" class="paging" ng-hide="model.numberOfPages() <= 1">
<div class="row">
<div class="col s3 add_new_site">
<a ng-if="hasSuperUserAccess" href="{{ url }}?module=SitesManager&action=index&showaddsite=1&period={{ period }}&date={{ date }}&idSite={{ idSite }}">
<span class="icon-add"></span> {{ 'SitesManager_AddSite'|translate }}
</a>
</div>
<div class="col s6">
<span id="prev" class="previous dataTablePrevious" 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">
{{ 'General_Pagination'|translate:model.getPaginationLowerBound():model.getPaginationUpperBound():model.getNumberOfFilteredSites() }}
</span>
</span>
<span id="next" class="next dataTableNext" ng-hide="model.currentPage >= model.getNumberOfPages()" ng-click="model.nextPage()">
<span style="cursor:pointer;" class="pointer">{{ 'General_Next'|translate }} &#187;</span>
</span>
</div>
<div class="col s3">&nbsp;</div>
</div>
</td>
</tr>
<tr row_id="last">
<td colspan="8" class="input-field site_search">
<input type="text"
ng-model="searchTerm"
class="browser-default"
piwik-onenter="model.searchSite(searchTerm)"
placeholder="{{ 'Actions_SubmenuSitesearch' | translate }}">
<span title="{{ 'General_ClickToSearch' | translate }}"
ng-click="model.searchSite(searchTerm)"
class="icon-search search_ico"></span>
</td>
</tr>
</tfoot>
</table>
</div>

View File

@ -0,0 +1,46 @@
/*!
* Piwik - free/libre analytics platform
*
* @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>
*/
(function () {
angular.module('piwikApp').directive('piwikMultisitesDashboard', piwikMultisitesDashboard);
piwikMultisitesDashboard.$inject = ['piwik'];
function piwikMultisitesDashboard(piwik){
return {
restrict: 'A',
scope: {
displayRevenueColumn: '@',
showSparklines: '@',
dateSparkline: '@'
},
templateUrl: 'plugins/MultiSites/angularjs/dashboard/dashboard.directive.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,275 @@
.smallTitle {
font-size: 15px;
}
.widget {
#multisites {
padding: 15px;
}
}
#mt {
table.dataTable td.label img {
margin-top: -8px;
}
}
#multisites {
> .col {
padding-left: 0;
padding-right: 0;
}
.notification-error {
margin-top: 15px;
a {
text-decoration: underline !important;
}
}
.add_new_site {
border: 0 !important;
font-size: 13px;
text-align: left;
padding-left: 27px;
a {
color: @theme-color-text;
&:hover {
text-decoration: underline !important;
}
}
}
.clean {
border: 0 !important;
text-align: right;
padding-right:10px;
padding-top: 19px;
padding-bottom: 5px;
}
@media print{
.add_new_site{
display: none;
}
.row .col.s6{
width: 100%;
}
.row .col.s3{
display: none;
}
}
.site_search {
padding: 0;
text-align: center;
border: 0 !important;
@media print{
display: none;
}
}
.multisites-evolution,
.multisites-column {
text-align: right;
}
.multisites-evolution {
width:170px;
}
td, tr, .sparkline {
text-align: center;
vertical-align: middle;
}
td.empty-row {
border-bottom: none !important;
}
.paging {
padding: 5px;
border-bottom: 0 !important;
.row {
margin-top: 16px;
}
.previous {
visibility: visible;
}
.next {
visibility: visible;
}
}
th:first-child {
text-align:left;
padding-left: 32px;
}
th {
cursor: pointer;
text-align: right;
padding-right: 0 !important;
&#evolution {
text-align: center;
}
&.columnSorted {
font-weight: normal !important;
}
}
.site_search input {
margin-right: 0;
margin-left: 25px;
padding-right: 25px;
width: 250px;
height: 3rem;
}
.search_ico {
position: relative;
left: -20px;
top: 1px;
cursor: pointer;
font-size: 16px;
}
.reset {
position: relative;
left: -25px;
cursor: pointer;
margin-right: 0;
}
tr.group {
font-weight: bold;
height: 30px;
}
tr.groupedWebsite .label {
padding-left: 50px;
}
td.multisites-label {
text-align: left;
width: 250px;
max-width: 250px;
padding-left: 32px;
a {
width: auto;
}
}
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
}
.heading {
display: inline;
margin-top: 4px;
}
#evolution_selector {
height: 28px;
margin: -9px 0 0 5px;
width: 80px;
display: inline-block;
}
.label .arrow {
margin-left: 2px;
}
.multisites_asc,
.multisites_desc {
float: none;
display: inline-block;
vertical-align: top;
margin: 0;
margin-left: 6px;
margin-top: -1px;
}
#evolution {
.multisites_asc,
.multisites_desc {
margin-right: 6px;
margin-left: 0;
}
.evolution {
vertical-align: top;
}
}
.multisites_asc {
margin-top: -7px;
vertical-align: top;
}
.multisites_desc:after {
content: " \25BC";
font-size: 1px;
color: @theme-color-brand;
border-left: 4px solid rgba(0, 0, 0, 0);
border-right: 4px solid rgba(0, 0, 0, 0);
border-top: 5px solid @theme-color-brand;
}
.multisites_asc:after {
content: " \25BC";
font-size: 1px;
color: @theme-color-brand;
border-left: 4px solid rgba(0, 0, 0, 0);
border-right: 4px solid rgba(0, 0, 0, 0);
border-bottom: 5px solid @theme-color-brand;
}
div.sparkline {
float:none;
}
tfoot td {
border-bottom: 0;
}
}

View File

@ -0,0 +1,47 @@
/*!
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
(function () {
angular.module('piwikApp').controller('MultiSitesSiteController', MultiSitesSiteController);
MultiSitesSiteController.$inject = ['$scope', 'piwik'];
function MultiSitesSiteController($scope, piwik){
$scope.period = piwik.period;
$scope.date = piwik.broadcast.getValueFromUrl('date');
$scope.sparklineImage = sparklineImage;
$scope.website.label = piwik.helper.htmlDecode($scope.website.label);
this.getWebsite = function () {
return $scope.website;
};
function sparklineImage(website){
var append = '';
var token_auth = piwik.broadcast.getValueFromUrl('token_auth');
if (token_auth.length) {
append = '&token_auth=' + token_auth;
}
var metric = $scope.metric;
switch ($scope.evolutionMetric) {
case 'visits_evolution':
metric = 'nb_visits';
break;
case 'pageviews_evolution':
metric = 'nb_pageviews';
break;
case 'revenue_evolution':
metric = 'revenue';
break;
}
return piwik.piwik_url + '?module=MultiSites&action=getEvolutionGraph&period=' + $scope.period + '&date=' + $scope.dateSparkline + '&evolutionBy=' + metric + '&columns=' + 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 }}" class="truncated-text-line">{{ website.label }}</a>
<span style="width: 10px; margin-left:3px;">
<a rel="noreferrer noopener" target="_blank" title="{{ 'General_GoTo'|translate:website.main_url }}" ng-href="{{ website.main_url }}">
<img src="plugins/Morpheus/images/link.png"/></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'" class="multisites-evolution">
<div class="visits" ng-if="!website.isGroup">
<span ng-show="website[evolutionMetric] > 0"><img src="plugins/MultiSites/images/arrow_up.png" alt="" /> <span style="color: green;">{{ website[evolutionMetric] }}%</span></span>
<span ng-show="website[evolutionMetric] == 0"><img src="plugins/MultiSites/images/stop.png" alt="" /> <span>{{ website[evolutionMetric] }}%</span></span>
<span ng-show="website[evolutionMetric] < 0"><img src="plugins/MultiSites/images/arrow_down.png" alt="" /> <span style="color: red;">{{ website[evolutionMetric] }}%</span></span>
</div>
</td>
<td ng-if="showSparklines" style="width:180px;">
<div ng-if="!website.isGroup" class="sparkline" style="width: 100px; margin: auto;">
<a rel="noreferrer noopener" 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>

View File

@ -0,0 +1,42 @@
/*!
* Piwik - free/libre analytics platform
*
* @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>
*/
(function () {
angular.module('piwikApp').directive('piwikMultisitesSite', piwikMultisitesSite);
piwikMultisitesSite.$inject = ['piwik'];
function piwikMultisitesSite(piwik){
return {
restrict: 'AC',
replace: true,
scope: {
website: '=',
evolutionMetric: '=',
showSparklines: '=',
dateSparkline: '=',
displayRevenueColumn: '=',
metric: '='
},
templateUrl: 'plugins/MultiSites/angularjs/site/site.directive.html?cb=' + piwik.cacheBuster,
controller: 'MultiSitesSiteController'
};
}
})();

Binary file not shown.

After

Width:  |  Height:  |  Size: 358 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 365 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: 307 B

View File

@ -0,0 +1,6 @@
{
"MultiSites": {
"Evolution": "التطور",
"Pagination": "%1$s - %2$s من %3$s"
}
}

View File

@ -0,0 +1,5 @@
{
"MultiSites": {
"Evolution": "Змены"
}
}

View File

@ -0,0 +1,8 @@
{
"MultiSites": {
"Evolution": "Развитие",
"LoadingWebsites": "Зарежда сайтовете",
"TopLinkTooltip": "Сравнете статистиката за всички ваши уебсайтове.",
"Pagination": "%1$s - %2$s като %3$s"
}
}

View File

@ -0,0 +1,7 @@
{
"MultiSites": {
"Evolution": "Evolució",
"TopLinkTooltip": "Compareu les estadístiques anàlitiques de tots els vostres llocs web.",
"Pagination": "%1$s - %2$s de %3$s"
}
}

View File

@ -0,0 +1,9 @@
{
"MultiSites": {
"Evolution": "Vývoj",
"LoadingWebsites": "Načítání webových stránek",
"PluginDescription": "Zobrazte a porovnejte všechny vaše stránky a aplikace na této užitečné nástěnce všech stránek.",
"TopLinkTooltip": "Porovnejte webové analytické statistiky pro všechny vaše weby.",
"Pagination": "%1$s - %2$s z %3$s"
}
}

View File

@ -0,0 +1,9 @@
{
"MultiSites": {
"Evolution": "Udvikling",
"LoadingWebsites": "Loading websites",
"PluginDescription": "Se og sammenlign alle dine websider og apps i dette brugbare 'Alle websider' oversigtsbillede.",
"TopLinkTooltip": "Sammenlign analyse statistik for alle hjemmesider.",
"Pagination": "%1$s - %2$s af %3$s"
}
}

View File

@ -0,0 +1,9 @@
{
"MultiSites": {
"Evolution": "Entwicklung",
"LoadingWebsites": "Lade Websites",
"PluginDescription": "Zeige und vergleiche alle Websites und Apps im hilfreichen 'Alle Websites' Dashboard.",
"TopLinkTooltip": "Vergleichen Sie Webanalytikstatistiken für alle Ihre Websites.",
"Pagination": "%1$s - %2$s von %3$s"
}
}

View File

@ -0,0 +1,9 @@
{
"MultiSites": {
"Evolution": "Πρόοδος",
"LoadingWebsites": "Γίνεται φόρτωση των ιστοτόπων",
"PluginDescription": "Δείτε και συγκρίνετε όλους τους ιστοτόπους σας και τις εφαρμογές σας σε αυτό τον χρήσιμο πίνακα 'Όλοι οι Ιστοτόποι'.",
"TopLinkTooltip": "Σύγκριση στατιστικών Ιστού για όλες τις ιστοσελίδες σας.",
"Pagination": "%1$s - %2$s από %3$s"
}
}

View File

@ -0,0 +1,9 @@
{
"MultiSites": {
"Evolution": "Evolution",
"LoadingWebsites": "Loading websites",
"PluginDescription": "View and compare all your websites and apps in this useful 'All Websites' dashboard. ",
"TopLinkTooltip": "Compare Web Analytics stats for all of your Websites.",
"Pagination": "%1$s - %2$s of %3$s"
}
}

View File

@ -0,0 +1,5 @@
{
"MultiSites": {
"Pagination": "%1$s - %2$s de %3$s"
}
}

View File

@ -0,0 +1,7 @@
{
"MultiSites": {
"Evolution": "Evolución",
"TopLinkTooltip": "Comparar la estadísticas de Análisis Web para todos tus Sitios Web.",
"Pagination": "%1$s - %2$s de %3$s"
}
}

View File

@ -0,0 +1,9 @@
{
"MultiSites": {
"Evolution": "Evolución",
"LoadingWebsites": "Cargando sitios de internet",
"PluginDescription": "Vea y compare todos sus sitios de internet y aplicaciones en este tablero 'Todos los sitios de internet'",
"TopLinkTooltip": "Comparar la estadísticas de Análisis de internet para todos sus sitios.",
"Pagination": "%1$s - %2$s de %3$s"
}
}

View File

@ -0,0 +1,7 @@
{
"MultiSites": {
"Evolution": "Muutus ajas",
"LoadingWebsites": "Veebilehtede avamine",
"Pagination": "%1$s - %2$s \/ %3$s"
}
}

View File

@ -0,0 +1,8 @@
{
"MultiSites": {
"Evolution": "تکامل",
"LoadingWebsites": "درحال بارگزاری وب سایت ها",
"TopLinkTooltip": "اطلاعات آماری همه وب سایت هایتان را مقایسه کنید.",
"Pagination": "%1$s-%2$s از %3$s"
}
}

View File

@ -0,0 +1,9 @@
{
"MultiSites": {
"Evolution": "Kehitys",
"LoadingWebsites": "Lataa verkkosivuja",
"PluginDescription": "Katso ja vertaile kaikkia verkkosivujasi tällä \"Kaikki verkkosivut\"-työpöydällä.",
"TopLinkTooltip": "Vertaile eri sivujen tilastoja.",
"Pagination": "%1$s - %2$s %3$s:sta."
}
}

View File

@ -0,0 +1,9 @@
{
"MultiSites": {
"Evolution": "Évolution",
"LoadingWebsites": "Chargement des sites",
"PluginDescription": "Visualisez et comparez tous vos sites web et application en utilisant le tableau de bord \"tous les sites web\".",
"TopLinkTooltip": "Comparez les statistiques pour tous vos sites web.",
"Pagination": "%1$s - %2$s de %3$s"
}
}

View File

@ -0,0 +1,5 @@
{
"MultiSites": {
"Pagination": "%1$s - %2$s of %3$s"
}
}

View File

@ -0,0 +1,9 @@
{
"MultiSites": {
"Evolution": "विकास",
"LoadingWebsites": "लोड हो रहा है वेबसाइट",
"PluginDescription": "देखें और इस उपयोगी 'सभी वेबसाइटों के' डैशबोर्ड में अपने सभी वेबसाइटों और क्षुधा की तुलना करें।",
"TopLinkTooltip": "आपकी वेबसाइट सभी के लिए वेब विश्लेषिकी आँकड़ों की तुलना करें.",
"Pagination": "%1$s - %2$s के लिए %3$s"
}
}

View File

@ -0,0 +1,5 @@
{
"MultiSites": {
"Evolution": "Trendek"
}
}

View File

@ -0,0 +1,8 @@
{
"MultiSites": {
"Evolution": "Perkembangan",
"LoadingWebsites": "Membuat website",
"TopLinkTooltip": "Bandingkan statistik Analisis Ramatraya untuk seluruh Situs Anda.",
"Pagination": "%1$s - %2$s dari %3$s"
}
}

View File

@ -0,0 +1,9 @@
{
"MultiSites": {
"Evolution": "Evoluzione",
"LoadingWebsites": "Caricamento siti web",
"PluginDescription": "Guarda e confronta tutti i siti e le app in questa utile dashboard 'Tutti i Siti'.",
"TopLinkTooltip": "Compara le statistiche web di tutti i tuoi siti web.",
"Pagination": "%1$s - %2$s di %3$s"
}
}

View File

@ -0,0 +1,9 @@
{
"MultiSites": {
"Evolution": "推移",
"LoadingWebsites": "ウェブサイトをロード中",
"PluginDescription": "この便利な「すべてのウェブサイト」ダッシュボードで、すべてのウェブサイトとアプリケーションを見て、比較してください。",
"TopLinkTooltip": "あなたの全ウェブサイトに対するウェブ解析統計情報を比較してください。",
"Pagination": "%1$s の %2$s - %3$s"
}
}

View File

@ -0,0 +1,5 @@
{
"MultiSites": {
"Evolution": "ევოლუცია"
}
}

View File

@ -0,0 +1,9 @@
{
"MultiSites": {
"Evolution": "변화 추이",
"LoadingWebsites": "웹사이트 로딩중",
"PluginDescription": "유용한 '모든 웹사이트' 대시보드에서 모든 웹사이트와 앱을 보고 비교함",
"TopLinkTooltip": "웹사이트의 모든 웹분석 통계를 비교합니다.",
"Pagination": "%1$s - %2$s 의 %3$s"
}
}

View File

@ -0,0 +1,7 @@
{
"MultiSites": {
"Evolution": "Raida",
"LoadingWebsites": "Įkeliamos svetainės",
"Pagination": "%1$s - %2$s iš %3$s"
}
}

View File

@ -0,0 +1,9 @@
{
"MultiSites": {
"Evolution": "Utvikling",
"LoadingWebsites": "Laster nettsteder",
"PluginDescription": "Se og sammenligne alle dine nettsteder og apper i denne nyttige «Alle nettsteder»-oversikten.",
"TopLinkTooltip": "Sammenligne web-statistikk for alle dine nettsteder.",
"Pagination": "%1$s %2$s av %3$s"
}
}

View File

@ -0,0 +1,9 @@
{
"MultiSites": {
"Evolution": "Trends",
"LoadingWebsites": "Websites laden",
"PluginDescription": "Bekijk en vergelijk al uw websites en apps in dit handige 'Alle Websites' paneel.",
"TopLinkTooltip": "Vergelijk Website statistieken van al je websites.",
"Pagination": "%1$s - %2$s van %3$s"
}
}

View File

@ -0,0 +1,5 @@
{
"MultiSites": {
"Evolution": "Evolusjon"
}
}

View File

@ -0,0 +1,9 @@
{
"MultiSites": {
"Evolution": "Postęp",
"LoadingWebsites": "Ładowanie stron",
"PluginDescription": "Przeglądaj i porównuj wszystkie swoje serwisy i aplikacje w tym użytecznym panelu 'Wszystkie serwisy'",
"TopLinkTooltip": "Porównaj statystyki stron wszystkich swoich Serwisów.",
"Pagination": "%1$s - %2$s z %3$s"
}
}

View File

@ -0,0 +1,9 @@
{
"MultiSites": {
"Evolution": "Evolução",
"LoadingWebsites": "Carregando sites",
"PluginDescription": "Veja e compare todos os seus sites e aplicativos neste útil painel 'Todos Websites'.",
"TopLinkTooltip": "Comparar estatísticas de todos os websites.",
"Pagination": "%1$s - %2$s de %3$s"
}
}

View File

@ -0,0 +1,9 @@
{
"MultiSites": {
"Evolution": "Evolução",
"LoadingWebsites": "A carregar sites",
"PluginDescription": "Veja e compare todos os seus sites e aplicações neste útil painel 'Todos os sites'.",
"TopLinkTooltip": "Compare as estatísticas de todos os seus sites.",
"Pagination": "%1$s - %2$s de %3$s"
}
}

View File

@ -0,0 +1,8 @@
{
"MultiSites": {
"Evolution": "Evoluţie",
"LoadingWebsites": "încărcare site-uri web",
"TopLinkTooltip": "Compara statisticiLE Web Analytics pentru toate site-urile dvs..",
"Pagination": "%1$s - %2$s of %3$s"
}
}

View File

@ -0,0 +1,9 @@
{
"MultiSites": {
"Evolution": "Динамика",
"LoadingWebsites": "Загрузка сайтов",
"PluginDescription": "Смотреть и сравнивать все ваши веб-сайты и приложения на этой полезной панели управления \"Все веб-сайты\".",
"TopLinkTooltip": "Сравните веб-аналитику для всех ваших сайтов.",
"Pagination": "%1$s - %2$s из %3$s"
}
}

View File

@ -0,0 +1,6 @@
{
"MultiSites": {
"Evolution": "Vývoj",
"Pagination": "%1$s - %2$s of %3$s"
}
}

View File

@ -0,0 +1,7 @@
{
"MultiSites": {
"Evolution": "Evolucija",
"LoadingWebsites": "Nalaganje strani",
"Pagination": "%1$s - %2$s od %3$s"
}
}

View File

@ -0,0 +1,9 @@
{
"MultiSites": {
"Evolution": "Evolucion",
"LoadingWebsites": "Po ngarkohen sajte",
"PluginDescription": "Shihni dhe krahasoni krejt sajtet dhe aplikacionet tuaja te ky pult i dobishëm 'Krejt Sajtet'.",
"TopLinkTooltip": "Krahasoni Analiza Web për krejt Sajtet tuaj.",
"Pagination": "%1$s - %2$s of %3$s"
}
}

View File

@ -0,0 +1,9 @@
{
"MultiSites": {
"Evolution": "Razvoj",
"LoadingWebsites": "Učitavanje sajtova",
"PluginDescription": "Pregledajte i uporedite sve vaše sajtove i aplikacije preko ovog korisnog 'Svi sajtovi' dodatka.",
"TopLinkTooltip": "Uporedi statistiku za sve vaše sajtove",
"Pagination": "%1$s - %2$s od %3$s"
}
}

View File

@ -0,0 +1,9 @@
{
"MultiSites": {
"Evolution": "Utveckling",
"LoadingWebsites": "Laddar webbplatser",
"PluginDescription": "Visa och jämför alla dina webbplatser i panelen 'All Websites'.",
"TopLinkTooltip": "Jämför statistik för alla dina webbplatser.",
"Pagination": "%1$s - %2$s av %3$s"
}
}

View File

@ -0,0 +1,5 @@
{
"MultiSites": {
"Evolution": "பரிணாமம்"
}
}

View File

@ -0,0 +1,5 @@
{
"MultiSites": {
"Evolution": "ความเปลี่ยนแปลง"
}
}

View File

@ -0,0 +1,8 @@
{
"MultiSites": {
"Evolution": "Ebolusyon",
"LoadingWebsites": "Naglo-load ang mga website",
"TopLinkTooltip": "Ikumpara ang stats ng Web Analytics para sa lahat ng iyong Website.",
"Pagination": "%1$s - %2$s ng %3$s"
}
}

View File

@ -0,0 +1,9 @@
{
"MultiSites": {
"Evolution": "Gelişme",
"LoadingWebsites": "Web siteleri yükleniyor",
"PluginDescription": "Tüm web sitesi ve uygulamalarınızı kullanışlı 'Tüm Web Siteleri' panosu ile görüp karşılaştırabilirsiniz.",
"TopLinkTooltip": "Tüm web sitelerinizin istatistiklerini karşılaştırın.",
"Pagination": "%1$s - %2$s of %3$s"
}
}

View File

@ -0,0 +1,9 @@
{
"MultiSites": {
"Evolution": "Еволюція",
"LoadingWebsites": "Завантаження сайтів",
"PluginDescription": "Подивитися і порівняти всі ваші веб-сайти і додатки в цій корисній 'Усі сайти' панелі приладів.",
"TopLinkTooltip": "Порівняйте веб-аналітику для всіх ваших сайтів.",
"Pagination": "%1$s - %2$s з %3$s"
}
}

View File

@ -0,0 +1,9 @@
{
"MultiSites": {
"Evolution": "Sự tiến triển",
"LoadingWebsites": "Đang tải website",
"PluginDescription": "Xem và so sánh toàn bộ các website bạn quản lý",
"TopLinkTooltip": "So sánh các số liệu thống kê Web Analytics cho tất cả các trang web của bạn.",
"Pagination": "%1$s - %2$s trên %3$s"
}
}

View File

@ -0,0 +1,8 @@
{
"MultiSites": {
"Evolution": "趋势",
"LoadingWebsites": "正在加载网站",
"TopLinkTooltip": "比较所有网站的分析数据",
"Pagination": "%1$s - %2$s of %3$s"
}
}

View File

@ -0,0 +1,9 @@
{
"MultiSites": {
"Evolution": "發展趨勢",
"LoadingWebsites": "網站載入中",
"PluginDescription": "在這個方便的「所有網站」展示板中查看並比較你所有的網站和應用程式。",
"TopLinkTooltip": "比較你所有網站的網頁分析統計。",
"Pagination": "第 %1$s - %2$s 項,共 %3$s 項"
}
}

View File

@ -0,0 +1,33 @@
{% extends isWidgetized ? 'empty.twig' : 'dashboard.twig' %}
{% block topcontrols %}
{% if not isWidgetized %}
{% include "@CoreHome/_periodSelect.twig" %}
{% include "@CoreHome/_headerMessage.twig" %}
{% endif %}
{% endblock %}
{% block content %}
<div class="row" id="multisites">
<div class="col s12 m12 l10 offset-l1">
{% if isWidgetized %}
<div id="main">
{% else %}
<div id="main" class="card">
<div class="card-content">
{% endif %}
<div piwik-multisites-dashboard
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>
{% if isWidgetized %}
</div>
{% else %}
</div></div>
{% endif %}
</div>
</div>
{% endblock %}