From 18501381cec766aa729fdc7bdcd282c90a7be45f Mon Sep 17 00:00:00 2001 From: Thomas Steur Date: Tue, 2 Jun 2015 01:09:15 +0000 Subject: [PATCH] refs #7893 added possibility to measure mobile apps --- config/global.ini.php | 1 + core/Application/Kernel/PluginList.php | 34 ++++ core/Db/Schema/Mysql.php | 8 + core/Measurable/Measurable.php | 32 +++ core/Measurable/MeasurableSetting.php | 70 +++++++ core/Measurable/MeasurableSettings.php | 103 ++++++++++ core/Measurable/Settings/Storage.php | 104 ++++++++++ core/Measurable/Type.php | 62 ++++++ core/Measurable/Type/Manager.php | 39 ++++ core/Plugin/Manager.php | 29 +-- core/Plugin/Report.php | 1 + core/Plugin/Settings.php | 8 +- core/Settings/Setting.php | 17 +- core/Settings/Storage.php | 9 +- core/Updates/2.14.0-b2.php | 43 ++++ lang/en.json | 2 + plugins/API/API.php | 19 ++ .../templates/pluginSettings.twig | 117 +---------- .../angularjs/common/directives/dialog.js | 5 +- .../MobileAppMeasurable.php | 13 ++ plugins/MobileAppMeasurable/Type.php | 35 ++++ plugins/MobileAppMeasurable/lang/en.json | 7 + plugins/MobileAppMeasurable/plugin.json | 4 + .../Morpheus/templates/settingsMacros.twig | 124 ++++++++++++ plugins/SitesManager/API.php | 66 +++++- plugins/SitesManager/Controller.php | 25 ++- plugins/SitesManager/Menu.php | 36 +++- plugins/SitesManager/Model.php | 16 ++ plugins/SitesManager/SitesManager.php | 10 + .../sites-manager-site.controller.js | 28 ++- .../sites-manager/sites-manager-type-model.js | 52 +++++ .../sites-manager/sites-manager.controller.js | 49 ++++- plugins/SitesManager/lang/en.json | 3 + plugins/SitesManager/templates/index.html | 2 + .../templates/measurable_type_settings.twig | 7 + .../sites-list/add-entity-dialog.html | 16 ++ .../templates/sites-list/add-site-link.html | 6 +- .../templates/sites-list/site-fields.html | 40 ++-- .../templates/sites-manager-header.html | 3 +- .../tests/Integration/ApiTest.php | 111 +++++++++++ .../tests/Integration/ModelTest.php | 66 ++++++ plugins/WebsiteMeasurable/Type.php | 19 ++ .../WebsiteMeasurable/WebsiteMeasurable.php | 13 ++ plugins/WebsiteMeasurable/lang/en.json | 7 + plugins/WebsiteMeasurable/plugin.json | 4 + tests/PHPUnit/Framework/Fixture.php | 11 +- .../Framework/TestingEnvironmentVariables.php | 5 +- .../Measurable/MeasurableSettingTest.php | 82 ++++++++ .../Measurable/MeasurableSettingsTest.php | 109 ++++++++++ .../Integration/Measurable/MeasurableTest.php | 91 +++++++++ .../Measurable/Settings/StorageTest.php | 188 ++++++++++++++++++ .../Integration/Plugin/SettingsTest.php | 4 +- .../Integration/ReleaseCheckListTest.php | 6 +- ...tReportMetadata__API.getAvailableTypes.xml | 9 + 54 files changed, 1773 insertions(+), 197 deletions(-) create mode 100644 core/Measurable/Measurable.php create mode 100644 core/Measurable/MeasurableSetting.php create mode 100644 core/Measurable/MeasurableSettings.php create mode 100644 core/Measurable/Settings/Storage.php create mode 100644 core/Measurable/Type.php create mode 100644 core/Measurable/Type/Manager.php create mode 100644 core/Updates/2.14.0-b2.php create mode 100644 plugins/MobileAppMeasurable/MobileAppMeasurable.php create mode 100644 plugins/MobileAppMeasurable/Type.php create mode 100644 plugins/MobileAppMeasurable/lang/en.json create mode 100644 plugins/MobileAppMeasurable/plugin.json create mode 100644 plugins/Morpheus/templates/settingsMacros.twig create mode 100644 plugins/SitesManager/angularjs/sites-manager/sites-manager-type-model.js create mode 100644 plugins/SitesManager/templates/measurable_type_settings.twig create mode 100644 plugins/SitesManager/templates/sites-list/add-entity-dialog.html create mode 100644 plugins/SitesManager/tests/Integration/ModelTest.php create mode 100644 plugins/WebsiteMeasurable/Type.php create mode 100644 plugins/WebsiteMeasurable/WebsiteMeasurable.php create mode 100644 plugins/WebsiteMeasurable/lang/en.json create mode 100644 plugins/WebsiteMeasurable/plugin.json create mode 100644 tests/PHPUnit/Integration/Measurable/MeasurableSettingTest.php create mode 100644 tests/PHPUnit/Integration/Measurable/MeasurableSettingsTest.php create mode 100644 tests/PHPUnit/Integration/Measurable/MeasurableTest.php create mode 100644 tests/PHPUnit/Integration/Measurable/Settings/StorageTest.php create mode 100644 tests/PHPUnit/System/expected/test_apiGetReportMetadata__API.getAvailableTypes.xml diff --git a/config/global.ini.php b/config/global.ini.php index 2793ca1b52f..1d013c81b9c 100644 --- a/config/global.ini.php +++ b/config/global.ini.php @@ -718,6 +718,7 @@ Plugins[] = CorePluginsAdmin Plugins[] = CoreAdminHome Plugins[] = CoreHome +Plugins[] = WebsiteMeasurable Plugins[] = Diagnostics Plugins[] = CoreVisualizations Plugins[] = Proxy diff --git a/core/Application/Kernel/PluginList.php b/core/Application/Kernel/PluginList.php index 66fa64eb4e7..2c93253385c 100644 --- a/core/Application/Kernel/PluginList.php +++ b/core/Application/Kernel/PluginList.php @@ -25,6 +25,27 @@ class PluginList */ private $settings; + /** + * Plugins bundled with core package, disabled by default + * @var array + */ + private $corePluginsDisabledByDefault = array( + 'DBStats', + 'ExampleCommand', + 'ExampleSettingsPlugin', + 'ExampleUI', + 'ExampleVisualization', + 'ExamplePluginTemplate', + 'ExampleTracker', + 'ExampleReport', + 'MobileAppMeasurable' + ); + + // Themes bundled with core package, disabled by default + private $coreThemesDisabledByDefault = array( + 'ExampleTheme' + ); + public function __construct(GlobalSettingsProvider $settings) { $this->settings = $settings; @@ -55,6 +76,16 @@ public function getPluginsBundledWithPiwik() return $section['Plugins']; } + /** + * Returns the plugins bundled with core package that are disabled by default. + * + * @return string[] + */ + public function getCorePluginsDisabledByDefault() + { + return array_merge($this->corePluginsDisabledByDefault, $this->coreThemesDisabledByDefault); + } + /** * Sorts an array of plugins in the order they should be loaded. * @@ -68,6 +99,9 @@ public function sortPlugins(array $plugins) return $plugins; } + // we need to make sure a possibly disabled plugin will be still loaded before any 3rd party plugin + $global = array_merge($global, $this->corePluginsDisabledByDefault); + $global = array_values($global); $plugins = array_values($plugins); diff --git a/core/Db/Schema/Mysql.php b/core/Db/Schema/Mysql.php index 1e674f2decc..8402dc5bd14 100644 --- a/core/Db/Schema/Mysql.php +++ b/core/Db/Schema/Mysql.php @@ -102,6 +102,14 @@ public function getTablesCreateSql() ) ENGINE=$engine DEFAULT CHARSET=utf8 ", + 'site_setting' => "CREATE TABLE {$prefixTables}site_setting ( + idsite INTEGER(10) UNSIGNED NOT NULL AUTO_INCREMENT, + `setting_name` VARCHAR(255) NOT NULL, + `setting_value` LONGTEXT NOT NULL, + PRIMARY KEY(idsite, setting_name) + ) ENGINE=$engine DEFAULT CHARSET=utf8 + ", + 'site_url' => "CREATE TABLE {$prefixTables}site_url ( idsite INTEGER(10) UNSIGNED NOT NULL, url VARCHAR(255) NOT NULL, diff --git a/core/Measurable/Measurable.php b/core/Measurable/Measurable.php new file mode 100644 index 00000000000..d80c1f03233 --- /dev/null +++ b/core/Measurable/Measurable.php @@ -0,0 +1,32 @@ +id, $this->getType()); + $setting = $settings->getSetting($name); + + if (!empty($setting)) { + return $setting->getValue(); // Calling `getValue` makes sure we respect read permission of this setting + } + + throw new Exception(sprintf('Setting %s does not exist', $name)); + } +} diff --git a/core/Measurable/MeasurableSetting.php b/core/Measurable/MeasurableSetting.php new file mode 100644 index 00000000000..91e0970442f --- /dev/null +++ b/core/Measurable/MeasurableSetting.php @@ -0,0 +1,70 @@ +writableByCurrentUser = Piwik::isUserHasSomeAdminAccess(); + $this->readableByCurrentUser = Piwik::isUserHasSomeViewAccess(); + } + + /** + * Returns `true` if this setting is writable for the current user, `false` if otherwise. In case it returns + * writable for the current user it will be visible in the Plugin settings UI. + * + * @return bool + */ + public function isWritableByCurrentUser() + { + return $this->writableByCurrentUser; + } + + /** + * Returns `true` if this setting can be displayed for the current user, `false` if otherwise. + * + * @return bool + */ + public function isReadableByCurrentUser() + { + return $this->readableByCurrentUser; + } +} diff --git a/core/Measurable/MeasurableSettings.php b/core/Measurable/MeasurableSettings.php new file mode 100644 index 00000000000..d462f4678f0 --- /dev/null +++ b/core/Measurable/MeasurableSettings.php @@ -0,0 +1,103 @@ +idSite = $idSite; + $this->idType = $idType; + $this->storage = new Storage(Db::get(), $this->idSite); + $this->pluginName = 'MeasurableSettings'; + + $this->init(); + } + + protected function init() + { + $typeManager = new Type\Manager(); + $type = $typeManager->getType($this->idType); + $type->configureMeasurableSettings($this); + + /** + * This event is posted when generating settings for a Measurable (website). You can add any Measurable settings + * that you wish to be shown in the Measurable manager (websites manager). If you need to add settings only for + * eg MobileApp measurables you can use eg `$type->getId() === Piwik\Plugins\MobileAppMeasurable\Type::ID` and + * add only settings if the condition is true. + * + * @since Piwik 2.14.0 + * @deprecated will be removed in Piwik 3.0.0 + * + * @param MeasurableSettings $this + * @param \Piwik\Measurable\Type $type + * @param int $idSite + */ + Piwik::postEvent('Measurable.initMeasurableSettings', array($this, $type, $this->idSite)); + } + + public function addSetting(Setting $setting) + { + if ($this->idSite && $setting instanceof MeasurableSetting) { + $setting->writableByCurrentUser = Piwik::isUserHasAdminAccess($this->idSite); + } + + parent::addSetting($setting); + } + + public function save() + { + Piwik::checkUserHasAdminAccess($this->idSite); + + $typeManager = new Type\Manager(); + $type = $typeManager->getType($this->idType); + + /** + * Triggered just before Measurable settings are about to be saved. You can use this event for example + * to validate not only one setting but multiple ssetting. For example whether username + * and password matches. + * + * @since Piwik 2.14.0 + * @deprecated will be removed in Piwik 3.0.0 + * + * @param MeasurableSettings $this + * @param \Piwik\Measurable\Type $type + * @param int $idSite + */ + Piwik::postEvent('Measurable.beforeSaveSettings', array($this, $type, $this->idSite)); + + $this->storage->save(); + } + +} + diff --git a/core/Measurable/Settings/Storage.php b/core/Measurable/Settings/Storage.php new file mode 100644 index 00000000000..df9748af5e9 --- /dev/null +++ b/core/Measurable/Settings/Storage.php @@ -0,0 +1,104 @@ +db = $db; + $this->idSite = $idSite; + } + + protected function deleteSettingsFromStorage() + { + $table = $this->getTableName(); + $sql = "DELETE FROM $table WHERE `idsite` = ?"; + $bind = array($this->idSite); + + $this->db->query($sql, $bind); + } + + public function deleteValue(Setting $setting) + { + $this->toBeDeleted[$setting->getName()] = true; + parent::deleteValue($setting); + } + + public function setValue(Setting $setting, $value) + { + $this->toBeDeleted[$setting->getName()] = false; // prevent from deleting this setting, we will create/update it + parent::setValue($setting, $value); + } + + /** + * Saves (persists) the current setting values in the database. + */ + public function save() + { + $table = $this->getTableName(); + + foreach ($this->toBeDeleted as $name => $delete) { + if ($delete) { + $sql = "DELETE FROM $table WHERE `idsite` = ? and `setting_name` = ?"; + $bind = array($this->idSite, $name); + + $this->db->query($sql, $bind); + } + } + + $this->toBeDeleted = array(); + + foreach ($this->settingsValues as $name => $value) { + $value = serialize($value); + + $sql = "INSERT INTO $table (`idsite`, `setting_name`, `setting_value`) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE `setting_value` = ?"; + $bind = array($this->idSite, $name, $value, $value); + + $this->db->query($sql, $bind); + } + } + + protected function loadSettings() + { + $sql = "SELECT `setting_name`, `setting_value` FROM " . $this->getTableName() . " WHERE idsite = ?"; + $bind = array($this->idSite); + + $settings =$this->db->fetchAll($sql, $bind); + + $flat = array(); + foreach ($settings as $setting) { + $flat[$setting['setting_name']] = unserialize($setting['setting_value']); + } + + return $flat; + } + + private function getTableName() + { + return Common::prefixTable('site_setting'); + } +} diff --git a/core/Measurable/Type.php b/core/Measurable/Type.php new file mode 100644 index 00000000000..e9457a660fb --- /dev/null +++ b/core/Measurable/Type.php @@ -0,0 +1,62 @@ +isType('website') to be true (maybe) + return $this->getId() === $typeId; + } + + public function getId() + { + $id = static::ID; + + if (empty($id)) { + $message = 'Type %s does not define an ID. Set the ID constant to fix this issue';; + throw new \Exception(sprintf($message, get_called_class())); + } + + return $id; + } + + public function getDescription() + { + return $this->description; + } + + public function getName() + { + return $this->name; + } + + public function getNamePlural() + { + return $this->namePlural; + } + + public function getHowToSetupUrl() + { + return $this->howToSetupUrl; + } + + public function configureMeasurableSettings(MeasurableSettings $settings) + { + } +} + diff --git a/core/Measurable/Type/Manager.php b/core/Measurable/Type/Manager.php new file mode 100644 index 00000000000..cbd35f9349d --- /dev/null +++ b/core/Measurable/Type/Manager.php @@ -0,0 +1,39 @@ +findComponents('Type', '\\Piwik\\Measurable\\Type'); + } + + /** + * @param string $typeId + * @return Type|null + */ + public function getType($typeId) + { + foreach ($this->getAllTypes() as $type) { + if ($type->getId() === $typeId) { + return $type; + } + } + + return new Type(); + } +} + diff --git a/core/Plugin/Manager.php b/core/Plugin/Manager.php index 62a9954d39b..25434468d14 100644 --- a/core/Plugin/Manager.php +++ b/core/Plugin/Manager.php @@ -78,28 +78,12 @@ public static function getInstance() 'API', 'Proxy', 'LanguagesManager', + 'WebsiteMeasurable', // default Piwik theme, always enabled self::DEFAULT_THEME, ); - // Plugins bundled with core package, disabled by default - protected $corePluginsDisabledByDefault = array( - 'DBStats', - 'ExampleCommand', - 'ExampleSettingsPlugin', - 'ExampleUI', - 'ExampleVisualization', - 'ExamplePluginTemplate', - 'ExampleTracker', - 'ExampleReport' - ); - - // Themes bundled with core package, disabled by default - protected $coreThemesDisabledByDefault = array( - 'ExampleTheme' - ); - private $trackerPluginsNotToLoad = array(); /** @@ -194,11 +178,6 @@ public function getTrackerPluginsNotToLoad() return $this->trackerPluginsNotToLoad; } - public function getCorePluginsDisabledByDefault() - { - return array_merge($this->corePluginsDisabledByDefault, $this->coreThemesDisabledByDefault); - } - // If a plugin hooks onto at least an event starting with "Tracker.", we load the plugin during tracker const TRACKER_EVENT_PREFIX = 'Tracker.'; @@ -668,7 +647,7 @@ protected static function isManifestFileFound($path) public function isPluginBundledWithCore($name) { return $this->isPluginEnabledByDefault($name) - || in_array($name, $this->getCorePluginsDisabledByDefault()) + || in_array($name, $this->pluginList->getCorePluginsDisabledByDefault()) || $name == self::DEFAULT_THEME; } @@ -888,9 +867,11 @@ public function getIgnoredBogusPlugins() */ public static function getAllPluginsNames() { + $pluginList = StaticContainer::get('Piwik\Application\Kernel\PluginList'); + $pluginsToLoad = array_merge( self::getInstance()->readPluginsDirectory(), - self::getInstance()->getCorePluginsDisabledByDefault() + $pluginList->getCorePluginsDisabledByDefault() ); $pluginsToLoad = array_values(array_unique($pluginsToLoad)); return $pluginsToLoad; diff --git a/core/Plugin/Report.php b/core/Plugin/Report.php index fa830aad772..5ab8583a5a3 100644 --- a/core/Plugin/Report.php +++ b/core/Plugin/Report.php @@ -841,6 +841,7 @@ public static function getAllReports() $cacheId = CacheId::languageAware('Reports' . md5(implode('', $reports))); $cache = PiwikCache::getTransientCache(); + if (!$cache->contains($cacheId)) { $instances = array(); diff --git a/core/Plugin/Settings.php b/core/Plugin/Settings.php index 1066ea6aff0..c26581e4b1e 100644 --- a/core/Plugin/Settings.php +++ b/core/Plugin/Settings.php @@ -49,12 +49,12 @@ abstract class Settings private $settings = array(); private $introduction; - private $pluginName; + protected $pluginName; /** * @var StorageInterface */ - private $storage; + protected $storage; /** * Constructor. @@ -181,8 +181,8 @@ protected function addSetting(Setting $setting) { $name = $setting->getName(); - if (!ctype_alnum($name)) { - $msg = sprintf('The setting name "%s" in plugin "%s" is not valid. Only alpha and numerical characters are allowed', $setting->getName(), $this->pluginName); + if (!ctype_alnum(str_replace('_', '', $name))) { + $msg = sprintf('The setting name "%s" in plugin "%s" is not valid. Only underscores, alpha and numerical characters are allowed', $setting->getName(), $this->pluginName); throw new \Exception($msg); } diff --git a/core/Settings/Setting.php b/core/Settings/Setting.php index 51e79421467..bf8947b1967 100644 --- a/core/Settings/Setting.php +++ b/core/Settings/Setting.php @@ -153,7 +153,7 @@ abstract class Setting * @var StorageInterface */ private $storage; - private $pluginName; + protected $pluginName; /** * Constructor. @@ -266,11 +266,7 @@ public function removeValue() */ public function setValue($value) { - $this->checkHasEnoughWritePermission(); - - if ($this->validate && $this->validate instanceof \Closure) { - call_user_func($this->validate, $value, $this); - } + $this->validateValue($value); if ($this->transform && $this->transform instanceof \Closure) { $value = call_user_func($this->transform, $value, $this); @@ -281,6 +277,15 @@ public function setValue($value) return $this->storage->setValue($this, $value); } + private function validateValue($value) + { + $this->checkHasEnoughWritePermission(); + + if ($this->validate && $this->validate instanceof \Closure) { + call_user_func($this->validate, $value, $this); + } + } + /** * @throws \Exception */ diff --git a/core/Settings/Storage.php b/core/Settings/Storage.php index 5e3b8fc7932..131c01b111e 100644 --- a/core/Settings/Storage.php +++ b/core/Settings/Storage.php @@ -24,7 +24,7 @@ class Storage implements StorageInterface * * @var array */ - private $settingsValues = array(); + protected $settingsValues = array(); // for lazy loading of setting values private $settingValuesLoaded = false; @@ -52,12 +52,17 @@ public function save() */ public function deleteAllValues() { - Option::delete($this->getOptionKey()); + $this->deleteSettingsFromStorage(); $this->settingsValues = array(); $this->settingValuesLoaded = false; } + protected function deleteSettingsFromStorage() + { + Option::delete($this->getOptionKey()); + } + /** * Returns the current value for a setting. If no value is stored, the default value * is be returned. diff --git a/core/Updates/2.14.0-b2.php b/core/Updates/2.14.0-b2.php new file mode 100644 index 00000000000..dfa8c3f58fa --- /dev/null +++ b/core/Updates/2.14.0-b2.php @@ -0,0 +1,43 @@ +getEngine(); + + $table = Common::prefixTable('site_setting'); + + $sqlarray = array( + "DROP TABLE IF EXISTS `$table`" => false, + "CREATE TABLE `$table` ( + idsite INTEGER(10) UNSIGNED NOT NULL AUTO_INCREMENT, + `setting_name` VARCHAR(255) NOT NULL, + `setting_value` LONGTEXT NOT NULL, + PRIMARY KEY(idsite, setting_name) + ) ENGINE=$engine DEFAULT CHARSET=utf8" => 1050, + ); + + return $sqlarray; + } + + public function doUpdate(Updater $updater) + { + $updater->executeMigrationQueries(__FILE__, $this->getMigrationQueries($updater)); + } +} diff --git a/lang/en.json b/lang/en.json index 9a3023d2f11..0742bb04442 100644 --- a/lang/en.json +++ b/lang/en.json @@ -299,6 +299,8 @@ "Price": "Price", "ProductConversionRate": "Product Conversion Rate", "ProductRevenue": "Product Revenue", + "Measurable": "Measurable", + "Measurables": "Measurables", "PurchasedProducts": "Purchased Products", "Quantity": "Quantity", "RangeReports": "Custom date ranges", diff --git a/plugins/API/API.php b/plugins/API/API.php index 34f0ad5bb54..d66a0b35a2e 100644 --- a/plugins/API/API.php +++ b/plugins/API/API.php @@ -27,6 +27,7 @@ use Piwik\Plugins\CoreAdminHome\CustomLogo; use Piwik\Segment\SegmentExpression; use Piwik\Translation\Translator; +use Piwik\Measurable\Type; use Piwik\Version; require_once PIWIK_INCLUDE_PATH . '/core/Config.php'; @@ -94,6 +95,24 @@ public static function getDefaultMetricTranslations() return Metrics::getDefaultMetricTranslations(); } + public function getAvailableTypes() + { + $typeManager = new Type\Manager(); + $types = $typeManager->getAllTypes(); + + $available = array(); + foreach ($types as $type) { + $available[] = array( + 'id' => $type->getId(), + 'name' => Piwik::translate($type->getName()), + 'description' => Piwik::translate($type->getDescription()), + 'howToSetupUrl' => $type->getHowToSetupUrl() + ); + } + + return $available; + } + public function getSegmentsMetadata($idSites = array(), $_hideImplementationData = true) { $segments = array(); diff --git a/plugins/CoreAdminHome/templates/pluginSettings.twig b/plugins/CoreAdminHome/templates/pluginSettings.twig index 592914b06a1..415c174f79f 100644 --- a/plugins/CoreAdminHome/templates/pluginSettings.twig +++ b/plugins/CoreAdminHome/templates/pluginSettings.twig @@ -1,9 +1,11 @@ + {% extends mode == 'user' ? "user.twig" : "admin.twig" %} {% block content %} {% import 'macros.twig' as piwik %} {% import 'ajaxMacros.twig' as ajax %} + {% import 'settingsMacros.twig' as settingsMacro %} {% if mode == 'user' %}

{{ 'CoreAdminHome_PersonalPluginSettings'|translate }}

@@ -32,118 +34,9 @@
- {% for name, setting in pluginSettings.settings %} - {% set settingValue = setting.getValue %} - -
- - {% if setting.introduction %} -

{{ setting.introduction }}

- {% endif %} - - {% if setting.uiControlType != 'checkbox' %} - - {% endif %} - - {% if setting.inlineHelp %} -
- {{ setting.inlineHelp }} - {% if setting.defaultValue and setting.uiControlType != 'checkbox' and setting.uiControlType != 'radio' %} -
- {{ 'General_Default'|translate }}: - {% if setting.defaultValue is iterable %} - {{ setting.defaultValue|join(', ')|truncate(50) }} - {% else %} - {{ setting.defaultValue|truncate(50) }} - {% endif %} - {% endif %} -
- {% endif %} - - {% if setting.uiControlType == 'select' or setting.uiControlType == 'multiselect' %} - - {% elseif setting.uiControlType == 'textarea' %} - - {% elseif setting.uiControlType == 'radio' %} - - {% for key, value in setting.availableValues %} - - {% endfor %} - - {% elseif setting.uiControlType == 'checkbox' %} - - - - {% else %} - - - - {% endif %} - - {{ setting.description }} - -
- - {% endfor %} + {% for name, setting in pluginSettings.settings %} + {{ settingsMacro.singleSetting(setting, loop.index) }} + {% endfor %}
diff --git a/plugins/CoreHome/angularjs/common/directives/dialog.js b/plugins/CoreHome/angularjs/common/directives/dialog.js index e711d3bc29d..cce88292df5 100644 --- a/plugins/CoreHome/angularjs/common/directives/dialog.js +++ b/plugins/CoreHome/angularjs/common/directives/dialog.js @@ -29,7 +29,9 @@ element.css('display', 'none'); element.on( "dialogclose", function() { - scope.$apply($parse(attrs.piwikDialog).assign(scope, false)); + setTimeout(function () { + scope.$apply($parse(attrs.piwikDialog).assign(scope, false)); + }, 0); }); scope.$watch(attrs.piwikDialog, function(newValue, oldValue) { @@ -37,6 +39,7 @@ piwik.helper.modalConfirm(element, {yes: function() { if (attrs.yes) { scope.$eval(attrs.yes); + setTimeout(function () { scope.$apply(); }, 0); } }}); } diff --git a/plugins/MobileAppMeasurable/MobileAppMeasurable.php b/plugins/MobileAppMeasurable/MobileAppMeasurable.php new file mode 100644 index 00000000000..ded345ca3a2 --- /dev/null +++ b/plugins/MobileAppMeasurable/MobileAppMeasurable.php @@ -0,0 +1,13 @@ +validate = function ($value) { + if (strlen($value) > 100) { + throw new \Exception('Only 100 characters are allowed'); + } + }; + + $settings->addSetting($appId); + } + +} + diff --git a/plugins/MobileAppMeasurable/lang/en.json b/plugins/MobileAppMeasurable/lang/en.json new file mode 100644 index 00000000000..de6c59c8d21 --- /dev/null +++ b/plugins/MobileAppMeasurable/lang/en.json @@ -0,0 +1,7 @@ +{ + "MobileAppMeasurable": { + "MobileApp": "Mobile App", + "MobileApps": "Mobile Apps", + "MobileAppDescription": " A native mobile app for iOS, Android or any other mobile operating system." + } +} \ No newline at end of file diff --git a/plugins/MobileAppMeasurable/plugin.json b/plugins/MobileAppMeasurable/plugin.json new file mode 100644 index 00000000000..fa99a021bfe --- /dev/null +++ b/plugins/MobileAppMeasurable/plugin.json @@ -0,0 +1,4 @@ +{ + "name": "MobileAppMeasurable", + "description": "Analytics for Mobile: lets you measure and analyze Mobile Apps with an optimized perspective of your mobile data." +} \ No newline at end of file diff --git a/plugins/Morpheus/templates/settingsMacros.twig b/plugins/Morpheus/templates/settingsMacros.twig new file mode 100644 index 00000000000..da9cb43709a --- /dev/null +++ b/plugins/Morpheus/templates/settingsMacros.twig @@ -0,0 +1,124 @@ +{% macro singleSetting(setting, index = 0) %} + + {% set settingValue = setting.getValue %} + +
+ + {% if setting.introduction %} +

{{ setting.introduction }}

+ {% endif %} + + {{ _self.field(setting, index) }} + + {{ setting.description }} + +
+ +{% endmacro %} + +{% macro field(setting, index = -1) %} + + {% if index == -1 %} + {% set index = setting.getName %} + {% endif %} + + {% set settingValue = setting.getValue %} + + {% if setting.uiControlType != 'checkbox' %} + + {% endif %} + + {% if setting.inlineHelp %} +
+ {{ setting.inlineHelp }} + {% if setting.defaultValue and setting.uiControlType != 'checkbox' and setting.uiControlType != 'radio' %} +
+ {{ 'General_Default'|translate }}: + {% if setting.defaultValue is iterable %} + {{ setting.defaultValue|join(', ')|truncate(50) }} + {% else %} + {{ setting.defaultValue|truncate(50) }} + {% endif %} + {% endif %} +
+ {% endif %} + + {% if setting.uiControlType == 'select' or setting.uiControlType == 'multiselect' %} + + {% elseif setting.uiControlType == 'textarea' %} + + {% elseif setting.uiControlType == 'radio' %} + + {% for key, value in setting.availableValues %} + + {% endfor %} + + {% elseif setting.uiControlType == 'checkbox' %} + + + + {% else %} + + + + {% endif %} +{% endmacro %} diff --git a/plugins/SitesManager/API.php b/plugins/SitesManager/API.php index 2076fb2914d..59af2e66a41 100644 --- a/plugins/SitesManager/API.php +++ b/plugins/SitesManager/API.php @@ -18,6 +18,7 @@ use Piwik\Network\IPUtils; use Piwik\Option; use Piwik\Piwik; +use Piwik\Measurable\MeasurableSettings; use Piwik\ProxyHttp; use Piwik\Scheduler\Scheduler; use Piwik\SettingsPiwik; @@ -26,6 +27,7 @@ use Piwik\Tracker; use Piwik\Tracker\Cache; use Piwik\Tracker\TrackerCodeGenerator; +use Piwik\Measurable\Type; use Piwik\Url; use Piwik\UrlHelper; @@ -501,6 +503,7 @@ public function getSitesIdFromTimezones($timezones) * @param null|string $excludedUserAgents * @param int $keepURLFragments If 1, URL fragments will be kept when tracking. If 2, they * will be removed. If 0, the default global behavior will be used. + * @param array|null $settings JSON serialized settings eg {settingName: settingValue, ...} * @see getKeepURLFragmentsGlobal. * @param string $type The website type, defaults to "website" if not set. * @@ -520,7 +523,8 @@ public function addSite($siteName, $startDate = null, $excludedUserAgents = null, $keepURLFragments = null, - $type = null) + $type = null, + $settings = null) { Piwik::checkUserHasSuperUserAccess(); @@ -549,9 +553,7 @@ public function addSite($siteName, $urls = array_slice($urls, 1); $bind = array('name' => $siteName, - 'main_url' => $url, - - ); + 'main_url' => $url); $bind['excluded_ips'] = $this->checkAndReturnExcludedIps($excludedIps); $bind['excluded_parameters'] = $this->checkAndReturnCommaSeparatedStringList($excludedQueryParameters); @@ -578,12 +580,21 @@ public function addSite($siteName, $bind['group'] = ""; } + if (!empty($settings)) { + $this->validateMeasurableSettings($bind['type'], $settings); + } + $idSite = $this->getModel()->createSite($bind); $this->insertSiteUrls($idSite, $urls); // we reload the access list which doesn't yet take in consideration this new website Access::getInstance()->reloadAccess(); + + if (!empty($settings)) { + $this->updateMeasurableSettings($idSite, $settings); + } + $this->postUpdateWebsite($idSite); /** @@ -596,6 +607,36 @@ public function addSite($siteName, return (int) $idSite; } + private function validateMeasurableSettings($idType, $settings) + { + $measurableSettings = new MeasurableSettings(0, $idType); + + foreach ($measurableSettings->getSettingsForCurrentUser() as $measurableSetting) { + $name = $measurableSetting->getName(); + if (!empty($settings[$name])) { + $measurableSetting->setValue($settings[$name]); + } + } + } + + private function updateMeasurableSettings($idSite, $settings) + { + $idType = Site::getTypeFor($idSite); + + $measurableSettings = new MeasurableSettings($idSite, $idType); + + foreach ($measurableSettings->getSettingsForCurrentUser() as $measurableSetting) { + $name = $measurableSetting->getName(); + if (!empty($settings[$name])) { + $measurableSetting->setValue($settings[$name]); + } + // we do not clear existing settings if the value is missing. + // There can be so many settings added by random plugins one would always clear some settings. + } + + $measurableSettings->save(); + } + private function postUpdateWebsite($idSite) { Site::clearCache(); @@ -1045,6 +1086,7 @@ public function setDefaultTimezone($defaultTimezone) * @param int|null $keepURLFragments If 1, URL fragments will be kept when tracking. If 2, they * will be removed. If 0, the default global behavior will be used. * @param string $type The Website type, default value is "website" + * @param array|null $settings JSON serialized settings eg {settingName: settingValue, ...} * @throws Exception * @see getKeepURLFragmentsGlobal. If null, the existing value will * not be modified. @@ -1066,7 +1108,8 @@ public function updateSite($idSite, $startDate = null, $excludedUserAgents = null, $keepURLFragments = null, - $type = null) + $type = null, + $settings = null) { Piwik::checkUserHasAdminAccess($idSite); @@ -1128,10 +1171,21 @@ public function updateSite($idSite, list($searchKeywordParameters, $searchCategoryParameters) = $this->checkSiteSearchParameters($searchKeywordParameters, $searchCategoryParameters); $bind['sitesearch_keyword_parameters'] = $searchKeywordParameters; $bind['sitesearch_category_parameters'] = $searchCategoryParameters; - $bind['type'] = $this->checkAndReturnType($type); + + if (!is_null($type)) { + $bind['type'] = $this->checkAndReturnType($type); + } + + if (!empty($settings)) { + $this->validateMeasurableSettings(Site::getTypeFor($idSite), $settings); + } $this->getModel()->updateSite($bind, $idSite); + if (!empty($settings)) { + $this->updateMeasurableSettings($idSite, $settings); + } + // we now update the main + alias URLs $this->getModel()->deleteSiteAliasUrls($idSite); diff --git a/plugins/SitesManager/Controller.php b/plugins/SitesManager/Controller.php index 33b00235f61..bbf4bd52c4f 100644 --- a/plugins/SitesManager/Controller.php +++ b/plugins/SitesManager/Controller.php @@ -12,6 +12,8 @@ use Piwik\API\ResponseBuilder; use Piwik\Common; use Piwik\Piwik; +use Piwik\Measurable\MeasurableSetting; +use Piwik\Measurable\MeasurableSettings; use Piwik\SettingsPiwik; use Piwik\Site; use Piwik\Tracker\TrackerCodeGenerator; @@ -33,8 +35,29 @@ public function index() return $this->renderTemplate('index'); } - public function getGlobalSettings() { + public function getMeasurableTypeSettings() + { + $idSite = Common::getRequestVar('idSite', 0, 'int'); + $idType = Common::getRequestVar('idType', '', 'string'); + + if ($idSite >= 1) { + Piwik::checkUserHasAdminAccess($idSite); + } else if ($idSite === 0) { + Piwik::checkUserHasSomeAdminAccess(); + } else { + throw new Exception('Invalid idSite parameter. IdSite has to be zero or higher'); + } + + $view = new View('@SitesManager/measurable_type_settings'); + + $propSettings = new MeasurableSettings($idSite, $idType); + $view->settings = $propSettings->getSettingsForCurrentUser(); + return $view->render(); + } + + public function getGlobalSettings() + { Piwik::checkUserHasSomeViewAccess(); $response = new ResponseBuilder(Common::getRequestVar('format')); diff --git a/plugins/SitesManager/Menu.php b/plugins/SitesManager/Menu.php index 3f4d1b02adf..43447f14948 100644 --- a/plugins/SitesManager/Menu.php +++ b/plugins/SitesManager/Menu.php @@ -10,15 +10,49 @@ use Piwik\Menu\MenuAdmin; use Piwik\Piwik; +use Piwik\Measurable\Type; class Menu extends \Piwik\Plugin\Menu { + private $typeManager; + + public function __construct(Type\Manager $typeManager) + { + $this->typeManager = $typeManager; + } + public function configureAdminMenu(MenuAdmin $menu) { if (Piwik::isUserHasSomeAdminAccess()) { - $menu->addManageItem('SitesManager_Sites', + $type = $this->getFirstTypeIfOnlyOneIsInUse(); + + $menuName = 'General_Measurables'; + if ($type) { + $menuName = $type->getNamePlural(); + } + + $menu->addManageItem($menuName, $this->urlForAction('index'), $order = 1); } } + + private function getFirstTypeIfOnlyOneIsInUse() + { + $types = $this->typeManager->getAllTypes(); + + if (count($types) === 1) { + // only one type is in use, use this one for the wording + return reset($types); + } else { + // multiple types are activated, check whether only one is actually in use + $model = new Model(); + $typeIds = $model->getUsedTypeIds(); + + if (count($typeIds) === 1) { + $typeManager = new Type\Manager(); + return $typeManager->getType(reset($typeIds)); + } + } + } } diff --git a/plugins/SitesManager/Model.php b/plugins/SitesManager/Model.php index 676c5a6f12c..ed16f79a061 100644 --- a/plugins/SitesManager/Model.php +++ b/plugins/SitesManager/Model.php @@ -331,6 +331,22 @@ public function updateSiteCreatedTime($idSites, $minDateSql) Db::query($query, $bind); } + /** + * Returns all used type ids (unique) + * @return array of used type ids + */ + public function getUsedTypeIds() + { + $types = array(); + $rows = $this->getDb()->fetchAll("SELECT DISTINCT `type` as typeid FROM " . $this->table); + + foreach ($rows as $row) { + $types[] = $row['typeid']; + } + + return $types; + } + /** * Insert the list of alias URLs for the website. * The URLs must not exist already for this website! diff --git a/plugins/SitesManager/SitesManager.php b/plugins/SitesManager/SitesManager.php index b4390f32d9d..6baa7ac427d 100644 --- a/plugins/SitesManager/SitesManager.php +++ b/plugins/SitesManager/SitesManager.php @@ -10,7 +10,9 @@ use Piwik\Common; use Piwik\Archive\ArchiveInvalidator; +use Piwik\Db; use Piwik\Plugins\PrivacyManager\PrivacyManager; +use Piwik\Measurable\Settings\Storage; use Piwik\Tracker\Cache; use Piwik\Tracker\Model as TrackerModel; @@ -69,6 +71,9 @@ public function onSiteDeleted($idSite) $archiveInvalidator = new ArchiveInvalidator(); $archiveInvalidator->forgetRememberedArchivedReportsToInvalidateForSite($idSite); + + $measurableStorage = new Storage(Db::get(), $idSite); + $measurableStorage->deleteAllValues(); } /** @@ -88,6 +93,7 @@ public function getJsFiles(&$jsFiles) $jsFiles[] = "plugins/SitesManager/angularjs/sites-manager/api-helper.service.js"; $jsFiles[] = "plugins/SitesManager/angularjs/sites-manager/api-site.service.js"; $jsFiles[] = "plugins/SitesManager/angularjs/sites-manager/api-core.service.js"; + $jsFiles[] = "plugins/SitesManager/angularjs/sites-manager/sites-manager-type-model.js"; $jsFiles[] = "plugins/SitesManager/angularjs/sites-manager/sites-manager-admin-sites-model.js"; $jsFiles[] = "plugins/SitesManager/angularjs/sites-manager/multiline-field.directive.js"; $jsFiles[] = "plugins/SitesManager/angularjs/sites-manager/edit-trigger.directive.js"; @@ -326,7 +332,11 @@ public function getClientSideTranslationKeys(&$translationKeys) $translationKeys[] = "SitesManager_SelectDefaultTimezone"; $translationKeys[] = "SitesManager_DefaultCurrencyForNewWebsites"; $translationKeys[] = "SitesManager_SelectDefaultCurrency"; + $translationKeys[] = "SitesManager_AddMeasurable"; $translationKeys[] = "SitesManager_AddSite"; + $translationKeys[] = "SitesManager_XManagement"; + $translationKeys[] = "SitesManager_ChooseMeasurableTypeHeadline"; + $translationKeys[] = "General_Measurables"; $translationKeys[] = "Goals_Ecommerce"; $translationKeys[] = "SitesManager_NotFound"; } diff --git a/plugins/SitesManager/angularjs/sites-manager/sites-manager-site.controller.js b/plugins/SitesManager/angularjs/sites-manager/sites-manager-site.controller.js index 85ad64716ff..21dc868f7ae 100644 --- a/plugins/SitesManager/angularjs/sites-manager/sites-manager-site.controller.js +++ b/plugins/SitesManager/angularjs/sites-manager/sites-manager-site.controller.js @@ -7,9 +7,9 @@ (function () { angular.module('piwikApp').controller('SitesManagerSiteController', SitesManagerSiteController); - SitesManagerSiteController.$inject = ['$scope', '$filter', 'sitesManagerApiHelper']; + SitesManagerSiteController.$inject = ['$scope', '$filter', 'sitesManagerApiHelper', 'sitesManagerTypeModel']; - function SitesManagerSiteController($scope, $filter, sitesManagerApiHelper) { + function SitesManagerSiteController($scope, $filter, sitesManagerApiHelper, sitesManagerTypeModel) { var translate = $filter('translate'); @@ -17,6 +17,16 @@ initModel(); initActions(); + + sitesManagerTypeModel.fetchTypeById($scope.site.type).then(function (type) { + if (type) { + $scope.currentType = type; + $scope.howToSetupUrl = type.howToSetupUrl; + $scope.isInternalSetupUrl = '?' === ('' + type.howToSetupUrl).substr(0, 1); + } else { + $scope.currentType = {name: $scope.site.type}; + } + }); }; var initActions = function () { @@ -77,6 +87,16 @@ }, 'GET'); } + var settings = $('.typeSettings fieldset').serializeArray(); + + var flatSettings = ''; + if (settings.length) { + flatSettings = {}; + angular.forEach(settings, function (setting) { + flatSettings[setting.name] = setting.value; + }); + } + ajaxHandler.addParams({ siteName: $scope.site.name, timezone: $scope.site.timezone, @@ -87,9 +107,11 @@ excludedUserAgents: $scope.site.excluded_user_agents.join(','), keepURLFragments: $scope.site.keep_url_fragment, siteSearch: $scope.site.sitesearch, + type: $scope.site.type, searchKeywordParameters: sendSiteSearchKeywordParams ? $scope.site.sitesearch_keyword_parameters.join(',') : null, searchCategoryParameters: sendSearchCategoryParameters ? $scope.site.sitesearch_category_parameters.join(',') : null, - urls: $scope.site.alias_urls + urls: $scope.site.alias_urls, + settings: flatSettings }, 'POST'); ajaxHandler.redirectOnSuccess($scope.redirectParams); diff --git a/plugins/SitesManager/angularjs/sites-manager/sites-manager-type-model.js b/plugins/SitesManager/angularjs/sites-manager/sites-manager-type-model.js new file mode 100644 index 00000000000..1168e82483d --- /dev/null +++ b/plugins/SitesManager/angularjs/sites-manager/sites-manager-type-model.js @@ -0,0 +1,52 @@ +/** + * Model for Sites Manager. Fetches only sites one has at least Admin permission. + */ +(function () { + angular.module('piwikApp').factory('sitesManagerTypeModel', sitesManagerTypeModel); + + sitesManagerTypeModel.$inject = ['piwikApi']; + + function sitesManagerTypeModel(piwikApi) + { + var typesPromise = null; + + var model = { + typesById: {}, + fetchTypeById: fetchTypeById, + fetchAvailableTypes: fetchAvailableTypes, + hasMultipleTypes: hasMultipleTypes + }; + + return model; + + function hasMultipleTypes(typeId) + { + return fetchAvailableTypes().then(function (types) { + return types && types.length > 1; + }); + } + + function fetchTypeById(typeId) + { + return fetchAvailableTypes().then(function () { + return model.typesById[typeId]; + }); + } + + function fetchAvailableTypes() + { + if (!typesPromise) { + typesPromise = piwikApi.fetch({method: 'API.getAvailableTypes'}).then(function (types) { + + angular.forEach(types, function (type) { + model.typesById[type.id] = type; + }); + + return types; + }); + } + + return typesPromise; + } + } +})(); diff --git a/plugins/SitesManager/angularjs/sites-manager/sites-manager.controller.js b/plugins/SitesManager/angularjs/sites-manager/sites-manager.controller.js index c657537ca72..5c8f820bfc7 100644 --- a/plugins/SitesManager/angularjs/sites-manager/sites-manager.controller.js +++ b/plugins/SitesManager/angularjs/sites-manager/sites-manager.controller.js @@ -7,9 +7,9 @@ (function () { angular.module('piwikApp').controller('SitesManagerController', SitesManagerController); - SitesManagerController.$inject = ['$scope', '$filter', 'coreAPI', 'sitesManagerAPI', 'sitesManagerAdminSitesModel', 'piwik', 'sitesManagerApiHelper']; + SitesManagerController.$inject = ['$scope', '$filter', 'coreAPI', 'sitesManagerAPI', 'piwikApi', 'sitesManagerAdminSitesModel', 'piwik', 'sitesManagerApiHelper', 'sitesManagerTypeModel']; - function SitesManagerController($scope, $filter, coreAPI, sitesManagerAPI, adminSites, piwik, sitesManagerApiHelper) { + function SitesManagerController($scope, $filter, coreAPI, sitesManagerAPI, piwikApi, adminSites, piwik, sitesManagerApiHelper, sitesManagerTypeModel) { var translate = $filter('translate'); @@ -38,10 +38,28 @@ $scope.cancelEditSite = cancelEditSite; $scope.addSite = addSite; + $scope.addNewEntity = addNewEntity; $scope.saveGlobalSettings = saveGlobalSettings; $scope.informSiteIsBeingEdited = informSiteIsBeingEdited; $scope.lookupCurrentEditSite = lookupCurrentEditSite; + + $scope.closeAddMeasurableDialog = function () { + // I couldn't figure out another way to close that jquery dialog + var element = angular.element('[piwik-dialog="$parent.showAddSiteDialog"]'); + if (element.parents('ui-dialog') && element.dialog('isOpen')) { + element.dialog('close'); + } + } + }; + + var initAvailableTypes = function () { + return sitesManagerTypeModel.fetchAvailableTypes().then(function (types) { + $scope.availableTypes = types; + $scope.typeForNewEntity = 'website'; + + return types; + }); }; var informSiteIsBeingEdited = function() { @@ -61,6 +79,8 @@ showLoading(); + var availableTypesPromise = initAvailableTypes(); + sitesManagerAPI.getGlobalSettings(function(globalSettings) { $scope.globalSettings = globalSettings; @@ -76,7 +96,9 @@ initKeepURLFragmentsList(); adminSites.fetchLimitedSitesWithAdminAccess(function () { - triggerAddSiteIfRequested(); + availableTypesPromise.then(function () { + triggerAddSiteIfRequested(); + }); }); sitesManagerAPI.getSitesIdWithAdminAccess(function (siteIds) { if (siteIds && siteIds.length) { @@ -90,7 +112,7 @@ var search = String(window.location.search); if(piwik.helper.getArrayFromQueryString(search).showaddsite == 1) - addSite(); + addNewEntity(); }; var initEcommerceSelectOptions = function() { @@ -181,8 +203,23 @@ }; }; - var addSite = function() { - $scope.adminSites.sites.push({}); + var addNewEntity = function () { + sitesManagerTypeModel.hasMultipleTypes().then(function (hasMultipleTypes) { + if (hasMultipleTypes) { + $scope.showAddSiteDialog = true; + } else if ($scope.availableTypes.length === 1) { + var type = $scope.availableTypes[0].id; + addSite(type); + } + }); + }; + + var addSite = function(type) { + if (!type) { + type = 'website'; // todo shall we really hard code this or trigger an exception or so? + } + + $scope.adminSites.sites.unshift({type: type}); }; var saveGlobalSettings = function() { diff --git a/plugins/SitesManager/lang/en.json b/plugins/SitesManager/lang/en.json index 7ee253b445b..20977db5049 100644 --- a/plugins/SitesManager/lang/en.json +++ b/plugins/SitesManager/lang/en.json @@ -1,6 +1,7 @@ { "SitesManager": { "AddSite": "Add a new website", + "AddMeasurable": "Add a new measurable", "AdvancedTimezoneSupportNotFound": "Advanced timezones support was not found in your PHP (supported in PHP>=5.2). You can still choose a manual UTC offset.", "AliasUrlHelp": "It is recommended, but not required, to specify the various URLs, one per line, that your visitors use to access this website. Alias URLs for a website will not appear in the Referrers > Websites report. Note that it is not necessary to specify the URLs with and without 'www' as Piwik automatically considers both.", "ChangingYourTimezoneWillOnlyAffectDataForward": "Changing your time zone will only affect data going forward, and will not be applied retroactively.", @@ -74,6 +75,8 @@ "Urls": "URLs", "UTCTimeIs": "UTC time is %s.", "WebsitesManagement": "Websites Management", + "XManagement": "Manage %s", + "ChooseMeasurableTypeHeadline": "What would you like to measure?", "YouCurrentlyHaveAccessToNWebsites": "You currently have access to %s websites.", "YourCurrentIpAddressIs": "Your current IP address is %s" } diff --git a/plugins/SitesManager/templates/index.html b/plugins/SitesManager/templates/index.html index 1c5b9c4087f..86041b25e4e 100644 --- a/plugins/SitesManager/templates/index.html +++ b/plugins/SitesManager/templates/index.html @@ -6,6 +6,8 @@
+
+
diff --git a/plugins/SitesManager/templates/measurable_type_settings.twig b/plugins/SitesManager/templates/measurable_type_settings.twig new file mode 100644 index 00000000000..7c1bb624f25 --- /dev/null +++ b/plugins/SitesManager/templates/measurable_type_settings.twig @@ -0,0 +1,7 @@ +{% import 'settingsMacros.twig' as settingsMacro %} + +{% for name, setting in settings %} +
+ {{ settingsMacro.singleSetting(setting, loop.index) }} +
+{% endfor %} diff --git a/plugins/SitesManager/templates/sites-list/add-entity-dialog.html b/plugins/SitesManager/templates/sites-list/add-entity-dialog.html new file mode 100644 index 00000000000..f1a16796840 --- /dev/null +++ b/plugins/SitesManager/templates/sites-list/add-entity-dialog.html @@ -0,0 +1,16 @@ +
+ +
+
+ +
+
+
\ No newline at end of file diff --git a/plugins/SitesManager/templates/sites-list/add-site-link.html b/plugins/SitesManager/templates/sites-list/add-site-link.html index 284e5bf7bff..af0266816bb 100644 --- a/plugins/SitesManager/templates/sites-list/add-site-link.html +++ b/plugins/SitesManager/templates/sites-list/add-site-link.html @@ -1,7 +1,9 @@
- - {{ 'SitesManager_AddSite'|translate }} + + {{ availableTypes.length > 1 ? ('SitesManager_AddMeasurable'|translate) : ('SitesManager_AddSite'|translate) }}
  • {{ 'SitesManager_Timezone'|translate }}: {{ site.timezone }}
  • {{ 'SitesManager_Currency'|translate }}: {{ site.currency }}
  • -
  • - {{ 'Actions_SubmenuSitesearch'|translate }}: - - {{ 'General_Yes'|translate }} - {{ 'General_No'|translate }} - +
  • + {{ 'Goals_Ecommerce'|translate }}: {{ 'General_Yes'|translate }} +
  • +
  • + {{ 'Actions_SubmenuSitesearch'|translate }}: {{ 'General_Yes'|translate }}
  • - {{ 'Goals_Ecommerce'|translate }}: - - {{ 'General_No'|translate }} - {{ 'General_Yes'|translate }} - + {{ 'SitesManager_Urls'|translate }}: + {{ site.alias_urls.join(', ') }}
  • -
  • +
  • {{ 'SitesManager_ExcludedIps'|translate }}: {{ site.excluded_ips.join(', ') }}
  • -
  • +
  • {{ 'SitesManager_ExcludedParameters'|translate }}: {{ site.excluded_parameters.join(', ') }}
  • -
  • +
  • {{ 'SitesManager_ExcludedUserAgents'|translate }}: {{ site.excluded_user_agents.join(', ') }}
  • @@ -62,8 +55,8 @@

    {{ site.name }}

    Delete -
  • - +
  • + {{ 'SitesManager_ShowTrackingTag'|translate }}
@@ -78,6 +71,11 @@

{{ site.name }}

+
+
+
@@ -143,4 +141,4 @@

{{ site.name }}

-
+
\ No newline at end of file diff --git a/plugins/SitesManager/templates/sites-manager-header.html b/plugins/SitesManager/templates/sites-manager-header.html index 59762777fdf..167b20ed838 100644 --- a/plugins/SitesManager/templates/sites-manager-header.html +++ b/plugins/SitesManager/templates/sites-manager-header.html @@ -1,8 +1,9 @@

- {{ 'SitesManager_WebsitesManagement'|translate }} + {{ 'SitesManager_XManagement'|translate:(availableTypes.length > 1 ? ('General_Measurables'|translate) : ('SitesManager_Sites'|translate)) }}

diff --git a/plugins/SitesManager/tests/Integration/ApiTest.php b/plugins/SitesManager/tests/Integration/ApiTest.php index 011ff7a344b..63ec493b0c6 100644 --- a/plugins/SitesManager/tests/Integration/ApiTest.php +++ b/plugins/SitesManager/tests/Integration/ApiTest.php @@ -9,9 +9,12 @@ namespace Piwik\Plugins\SitesManager\tests\Integration; use Piwik\Piwik; +use Piwik\Plugin; +use Piwik\Plugins\MobileAppMeasurable; use Piwik\Plugins\SitesManager\API; use Piwik\Plugins\SitesManager\Model; use Piwik\Plugins\UsersManager\API as APIUsersManager; +use Piwik\Measurable\Measurable; use Piwik\Site; use Piwik\Tests\Framework\Mock\FakeAccess; use Piwik\Tests\Framework\TestCase\IntegrationTestCase; @@ -31,6 +34,8 @@ public function setUp() { parent::setUp(); + Plugin\Manager::getInstance()->activatePlugin('MobileAppMeasurable'); + // setup the access layer FakeAccess::$superUser = true; } @@ -193,6 +198,40 @@ public function testAddSiteStrangeName() } + /** + * @expectedException \Exception + * @expectedExceptionMessage Only 100 characters are allowed + */ + public function testAddSite_ShouldFailAndNotCreatedASiteIfASettingIsInvalid() + { + try { + $type = MobileAppMeasurable\Type::ID; + $settings = array('app_id' => str_pad('test', 789, 't')); + $this->addSiteWithType($type, $settings); + } catch (Exception $e) { + + // make sure no site created + $ids = API::getInstance()->getAllSitesId(); + $this->assertEquals(array(), $ids); + + throw $e; + } + } + + public function testAddSite_ShouldSavePassedMeasurableSettings_IfSettingsAreValid() + { + $type = MobileAppMeasurable\Type::ID; + $settings = array('app_id' => 'org.piwik.mobile2'); + $idSite = $this->addSiteWithType($type, $settings); + + $this->assertSame(1, $idSite); + + $measurable = new Measurable($idSite); + $appId = $measurable->getSettingValue('app_id'); + + $this->assertSame('org.piwik.mobile2', $appId); + } + /** * adds a site * use by several other unit tests @@ -213,6 +252,42 @@ protected function _addSite() return $idsite; } + private function addSiteWithType($type, $settings) + { + return API::getInstance()->addSite("name", "http://piwik.net/", $ecommerce = 0, + $siteSearch = 1, $searchKeywordParameters = null, $searchCategoryParameters = null, + $ip = null, + $excludedQueryParameters = null, + $timezone = null, + $currency = null, + $group = null, + $startDate = null, + $excludedUserAgents = null, + $keepURLFragments = null, + $type, $settings); + } + + private function updateSiteSettings($idSite, $newSiteName, $settings) + { + return API::getInstance()->updateSite($idSite, + $newSiteName, + $urls = null, + $ecommerce = null, + $siteSearch = null, + $searchKeywordParameters = null, + $searchCategoryParameters = null, + $excludedIps = null, + $excludedQueryParameters = null, + $timezone = null, + $currency = null, + $group = null, + $startDate = null, + $excludedUserAgents = null, + $keepURLFragments = null, + $type = null, + $settings); + } + /** * no duplicate -> all the urls are saved */ @@ -795,6 +870,42 @@ public function testUpdateSiteSeveralUrlsAndGroup() $this->assertEquals($newurls, $allUrls); } + /** + * @expectedException \Exception + * @expectedExceptionMessage Only 100 characters are allowed + */ + public function testUpdateSite_ShouldFailAndNotUpdateSiteIfASettingIsInvalid() + { + $type = MobileAppMeasurable\Type::ID; + $idSite = $this->addSiteWithType($type, array()); + + try { + $this->updateSiteSettings($idSite, 'newSiteName', array('app_id' => str_pad('t', 589, 't'))); + + } catch (Exception $e) { + // verify nothing was updated (not even the name) + $measurable = new Measurable($idSite); + $this->assertNotEquals('newSiteName', $measurable->getName()); + + throw $e; + } + } + + public function testUpdateSite_ShouldSavePassedMeasurableSettings_IfSettingsAreValid() + { + $type = MobileAppMeasurable\Type::ID; + $idSite = $this->addSiteWithType($type, array()); + + $this->assertSame(1, $idSite); + + $this->updateSiteSettings($idSite, 'newSiteName', $settings = array('app_id' => 'org.piwik.mobile2')); + + // verify it was updated + $measurable = new Measurable($idSite); + $this->assertSame('newSiteName', $measurable->getName()); + $this->assertSame('org.piwik.mobile2', $measurable->getSettingValue('app_id')); + } + /** * @expectedException Exception * @expectedExceptionMessage SitesManager_ExceptionDeleteSite diff --git a/plugins/SitesManager/tests/Integration/ModelTest.php b/plugins/SitesManager/tests/Integration/ModelTest.php new file mode 100644 index 00000000000..45ebce2be82 --- /dev/null +++ b/plugins/SitesManager/tests/Integration/ModelTest.php @@ -0,0 +1,66 @@ +model = new Model(); + } + + public function test_getUsedTypeIds_shouldReturnNoType_IfNoSitesExist() + { + $this->assertSame(array(), $this->model->getUsedTypeIds()); + } + + public function test_getUsedTypeIds_shouldReturnOnlyOneType_IfAllSitesUseSameType() + { + for ($i = 0; $i < 9; $i++) { + $this->createMeasurable('website'); + } + + $this->assertSame(array('website'), $this->model->getUsedTypeIds()); + } + + public function test_getUsedTypeIds_shouldReturnAnotherType_IfDifferentOnesAreUsed() + { + for ($i = 0; $i < 9; $i++) { + $this->createMeasurable('website'); + $this->createMeasurable('universal'); + $this->createMeasurable('mobileapp'); + } + + $this->assertSame(array('website', 'universal', 'mobileapp'), $this->model->getUsedTypeIds()); + } + + private function createMeasurable($type) + { + Fixture::createWebsite('2015-01-01 00:00:00', + $ecommerce = 0, $siteName = false, $siteUrl = false, + $siteSearch = 1, $searchKeywordParameters = null, + $searchCategoryParameters = null, $timezone = null, $type); + } +} diff --git a/plugins/WebsiteMeasurable/Type.php b/plugins/WebsiteMeasurable/Type.php new file mode 100644 index 00000000000..714b9dd580c --- /dev/null +++ b/plugins/WebsiteMeasurable/Type.php @@ -0,0 +1,19 @@ +getCorePluginsDisabledByDefault(); + $disabledPlugins = $pluginList->getCorePluginsDisabledByDefault(); $disabledPlugins[] = 'LoginHttpAuth'; $disabledPlugins[] = 'ExampleVisualization'; diff --git a/tests/PHPUnit/Integration/Measurable/MeasurableSettingTest.php b/tests/PHPUnit/Integration/Measurable/MeasurableSettingTest.php new file mode 100644 index 00000000000..d0e45286e7d --- /dev/null +++ b/tests/PHPUnit/Integration/Measurable/MeasurableSettingTest.php @@ -0,0 +1,82 @@ +setStorage($storage); + return $setting; + } + + public function test_setValue_getValue_shouldSucceed_IfEnoughPermission() + { + $setting = $this->createSetting(); + $setting->setValue('test'); + $value = $setting->getValue(); + + $this->assertSame('test', $value); + } + + /** + * @expectedException \Exception + * @expectedExceptionMessage CoreAdminHome_PluginSettingChangeNotAllowed + */ + public function testSetValue_shouldThrowException_IfOnlyViewPermission() + { + FakeAccess::clearAccess(); + FakeAccess::setIdSitesView(array(1, 2, 3)); + $this->createSetting()->setValue('test'); + } + + /** + * @expectedException \Exception + * @expectedExceptionMessage CoreAdminHome_PluginSettingChangeNotAllowed + */ + public function testSetValue_shouldThrowException_IfNoPermissionAtAll() + { + FakeAccess::clearAccess(); + $this->createSetting()->setValue('test'); + } + + /** + * @expectedException \Exception + * @expectedExceptionMessage CoreAdminHome_PluginSettingReadNotAllowed + */ + public function testGetSettingValue_shouldThrowException_IfNoPermissionToRead() + { + FakeAccess::clearAccess(); + $this->createSetting()->getValue(); + } + + public function provideContainerConfig() + { + return array( + 'Piwik\Access' => new FakeAccess() + ); + } + +} diff --git a/tests/PHPUnit/Integration/Measurable/MeasurableSettingsTest.php b/tests/PHPUnit/Integration/Measurable/MeasurableSettingsTest.php new file mode 100644 index 00000000000..cf17fc634e7 --- /dev/null +++ b/tests/PHPUnit/Integration/Measurable/MeasurableSettingsTest.php @@ -0,0 +1,109 @@ +activatePlugin('MobileAppMeasurable'); + + if (!Fixture::siteCreated($this->idSite)) { + $type = MobileAppType::ID; + Fixture::createWebsite('2015-01-01 00:00:00', + $ecommerce = 0, $siteName = false, $siteUrl = false, + $siteSearch = 1, $searchKeywordParameters = null, + $searchCategoryParameters = null, $timezone = null, $type); + } + + $this->settings = $this->createSettings(); + } + + public function test_init_shouldAddSettingsFromType() + { + $this->assertNotEmpty($this->settings->getSetting('app_id')); + } + + public function test_save_shouldActuallyStoreValues() + { + $this->settings->getSetting('test2')->setValue('value2'); + $this->settings->getSetting('test3')->setValue('value3'); + + $this->assertStoredSettingsValue(null, 'test2'); + $this->assertStoredSettingsValue(null, 'test3'); + + $this->settings->save(); + + $this->assertStoredSettingsValue('value2', 'test2'); + $this->assertStoredSettingsValue('value3', 'test3'); + } + + /** + * @expectedException \Exception + * @expectedExceptionMessage checkUserHasAdminAccess + */ + public function test_save_shouldCheckAdminPermissionsForThatSite() + { + FakeAccess::clearAccess(); + + $this->settings->save(); + } + + private function createSettings() + { + $settings = new MeasurableSettings($this->idSite, MobileAppType::ID); + $settings->addSetting($this->createSetting('test2')); + $settings->addSetting($this->createSetting('test3')); + + return $settings; + } + + private function createSetting($name) + { + return new MeasurableSetting($name, $name . ' Name'); + } + + private function assertStoredSettingsValue($expectedValue, $settingName) + { + $settings = $this->createSettings(); + $value = $settings->getSetting($settingName)->getValue(); + + $this->assertSame($expectedValue, $value); + } + + public function provideContainerConfig() + { + return array( + 'Piwik\Access' => new FakeAccess() + ); + } +} diff --git a/tests/PHPUnit/Integration/Measurable/MeasurableTest.php b/tests/PHPUnit/Integration/Measurable/MeasurableTest.php new file mode 100644 index 00000000000..3da9a505449 --- /dev/null +++ b/tests/PHPUnit/Integration/Measurable/MeasurableTest.php @@ -0,0 +1,91 @@ +activatePlugin('MobileAppMeasurable'); + + if (!Fixture::siteCreated($this->idSite)) { + $type = MobileAppType::ID; + Fixture::createWebsite('2015-01-01 00:00:00', + $ecommerce = 0, $siteName = false, $siteUrl = false, + $siteSearch = 1, $searchKeywordParameters = null, + $searchCategoryParameters = null, $timezone = null, $type); + } + + $this->measurable = new Measurable($this->idSite); + } + + public function testGetSettingValue_shouldReturnValue_IfSettingExistsAndIsReadable() + { + $setting = new MeasurableSettings($this->idSite, Measurable::getTypeFor($this->idSite)); + $setting->getSetting($this->settingName)->setValue('mytest'); + + $value = $this->measurable->getSettingValue($this->settingName); + $this->assertNull($value); + + $setting->save(); // actually save value + + $value = $this->measurable->getSettingValue($this->settingName); + $this->assertSame('mytest', $value); + } + + /** + * @expectedException \Exception + * @expectedExceptionMessage does not exist + */ + public function testGetSettingValue_shouldThrowException_IfSettingDoesNotExist() + {; + $this->measurable->getSettingValue('NoTeXisTenT'); + } + + /** + * @expectedException \Exception + * @expectedExceptionMessage CoreAdminHome_PluginSettingReadNotAllowed + */ + public function testGetSettingValue_shouldThrowException_IfNoPermissionToRead() + { + FakeAccess::clearAccess(); + $this->measurable->getSettingValue('app_id'); + } + + public function provideContainerConfig() + { + return array( + 'Piwik\Access' => new FakeAccess() + ); + } + +} diff --git a/tests/PHPUnit/Integration/Measurable/Settings/StorageTest.php b/tests/PHPUnit/Integration/Measurable/Settings/StorageTest.php new file mode 100644 index 00000000000..9264a51ba34 --- /dev/null +++ b/tests/PHPUnit/Integration/Measurable/Settings/StorageTest.php @@ -0,0 +1,188 @@ +idSite)) { + Fixture::createWebsite('2015-01-01 00:00:00'); + } + + $this->storage = $this->createStorage(); + $this->setting = $this->createSetting('test'); + } + + private function createStorage($idSite = null) + { + if (!isset($idSite)) { + $idSite = $this->idSite; + } + + return new Storage(Db::get(), $idSite); + } + + private function createSetting($name) + { + return new MeasurableSetting($name, $name . ' Name'); + } + + public function test_getValue_shouldReturnNullByDefault() + { + $value = $this->storage->getValue($this->setting); + $this->assertNull($value); + } + + public function test_getValue_shouldReturnADefaultValueIfOneIsSet() + { + $this->setting->defaultValue = 194.34; + $value = $this->storage->getValue($this->setting); + $this->assertSame(194.34, $value); + } + + public function test_setValue_getValue_shouldSetAndGetActualValue() + { + $this->storage->setValue($this->setting, 'myRandomVal'); + $value = $this->storage->getValue($this->setting); + $this->assertEquals('myRandomVal', $value); + } + + public function test_setValue_shouldNotSaveItInDatabase() + { + $this->storage->setValue($this->setting, 'myRandomVal'); + + // make sure not actually stored + $this->assertSettingValue(null, $this->setting); + } + + public function test_save_shouldPersistValueInDatabase() + { + $this->storage->setValue($this->setting, 'myRandomVal'); + $this->storage->save(); + + // make sure actually stored + $this->assertSettingValue('myRandomVal', $this->setting); + } + + public function test_save_shouldPersistValueForEachSiteInDatabase() + { + $this->storage->setValue($this->setting, 'myRandomVal'); + $this->storage->save(); + + // make sure actually stored + $this->assertSettingValue('myRandomVal', $this->setting); + + $storage = $this->createStorage($idSite = 2); + $valueForDifferentSite = $storage->getValue($this->setting); + $this->assertNull($valueForDifferentSite); + } + + public function test_save_shouldPersistMultipleValues_ContainingInt() + { + $this->saveMultipleValues(); + + $this->assertSettingValue('myRandomVal', $this->setting); + $this->assertSettingValue(5, $this->createSetting('test2')); + $this->assertSettingValue(array(1, 2, '4'), $this->createSetting('test3')); + } + + public function test_deleteAll_ShouldRemoveTheEntireEntry() + { + $this->saveMultipleValues(); + + $this->assertSettingNotEmpty($this->setting); + $this->assertSettingNotEmpty($this->createSetting('test2')); + $this->assertSettingNotEmpty($this->createSetting('test3')); + + $this->storage->deleteAllValues(); + + $this->assertSettingEmpty($this->setting); + $this->assertSettingEmpty($this->createSetting('test2')); + $this->assertSettingEmpty($this->createSetting('test3')); + } + + public function test_deleteValue_ShouldOnlyDeleteOneValue() + { + $this->saveMultipleValues(); + + $this->assertSettingNotEmpty($this->setting); + $this->assertSettingNotEmpty($this->createSetting('test2')); + $this->assertSettingNotEmpty($this->createSetting('test3')); + + $this->storage->deleteValue($this->createSetting('test2')); + $this->storage->save(); + + $this->assertSettingEmpty($this->createSetting('test2')); + + $this->assertSettingNotEmpty($this->setting); + $this->assertSettingNotEmpty($this->createSetting('test3')); + } + + public function test_deleteValue_saveValue_ShouldNotResultInADeletedValue() + { + $this->saveMultipleValues(); + + $this->storage->deleteValue($this->createSetting('test2')); + $this->storage->setValue($this->createSetting('test2'), 'PiwikTest'); + $this->storage->save(); + + $this->assertSettingValue('PiwikTest', $this->createSetting('test2')); + } + + private function assertSettingValue($expectedValue, $setting) + { + $value = $this->createStorage()->getValue($setting); + $this->assertSame($expectedValue, $value); + } + + private function assertSettingNotEmpty(Setting $setting) + { + $value = $this->createStorage()->getValue($setting); + $this->assertNotNull($value); + } + + private function assertSettingEmpty(Setting $setting) + { + $value = $this->createStorage()->getValue($setting); + $this->assertNull($value); + } + + private function saveMultipleValues() + { + $this->storage->setValue($this->setting, 'myRandomVal'); + $this->storage->setValue($this->createSetting('test2'), 5); + $this->storage->setValue($this->createSetting('test3'), array(1, 2, '4')); + $this->storage->save(); + } +} diff --git a/tests/PHPUnit/Integration/Plugin/SettingsTest.php b/tests/PHPUnit/Integration/Plugin/SettingsTest.php index a7e76800914..f5ac688d310 100644 --- a/tests/PHPUnit/Integration/Plugin/SettingsTest.php +++ b/tests/PHPUnit/Integration/Plugin/SettingsTest.php @@ -48,11 +48,11 @@ public function test_addSetting_shouldThrowException_InCaseTwoSettingsHaveTheSam /** * @expectedException \Exception - * @expectedExceptionMessage The setting name "myname_" in plugin "ExampleSettingsPlugin" is not valid. Only alpha and numerical characters are allowed + * @expectedExceptionMessage The setting name "myname-" in plugin "ExampleSettingsPlugin" is not valid. Only underscores, alpha and numerical characters are allowed */ public function test_addSetting_shouldThrowException_IfTheSettingNameIsNotValid() { - $setting = $this->buildUserSetting('myname_', 'mytitle'); + $setting = $this->buildUserSetting('myname-', 'mytitle'); $this->settings->addSetting($setting); } diff --git a/tests/PHPUnit/Integration/ReleaseCheckListTest.php b/tests/PHPUnit/Integration/ReleaseCheckListTest.php index 2c92d75c61c..f147824fb40 100644 --- a/tests/PHPUnit/Integration/ReleaseCheckListTest.php +++ b/tests/PHPUnit/Integration/ReleaseCheckListTest.php @@ -10,6 +10,7 @@ use Exception; use Piwik\Config; +use Piwik\Container\StaticContainer; use Piwik\Filesystem; use Piwik\Ini\IniReader; use Piwik\Plugin\Manager; @@ -235,7 +236,10 @@ public function test_DirectoriesInPluginsFolder_areKnown() } $manager = Manager::getInstance(); $isGitSubmodule = $manager->isPluginOfficialAndNotBundledWithCore($pluginName); - $disabled = in_array($pluginName, $manager->getCorePluginsDisabledByDefault()) || $isGitSubmodule; + + $pluginList = StaticContainer::get('Piwik\Application\Kernel\PluginList'); + + $disabled = in_array($pluginName, $pluginList->getCorePluginsDisabledByDefault()) || $isGitSubmodule; $enabled = in_array($pluginName, $pluginsBundledWithPiwik); diff --git a/tests/PHPUnit/System/expected/test_apiGetReportMetadata__API.getAvailableTypes.xml b/tests/PHPUnit/System/expected/test_apiGetReportMetadata__API.getAvailableTypes.xml new file mode 100644 index 00000000000..b4433cb0f29 --- /dev/null +++ b/tests/PHPUnit/System/expected/test_apiGetReportMetadata__API.getAvailableTypes.xml @@ -0,0 +1,9 @@ + + + + website + Website + A website consists of web pages typically served from a single web domain. + ?module=CoreAdminHome&action=trackingCodeGenerator + + \ No newline at end of file