diff --git a/plugins/CustomAlerts/API.php b/plugins/CustomAlerts/API.php new file mode 100755 index 00000000000..d8b8c1a012c --- /dev/null +++ b/plugins/CustomAlerts/API.php @@ -0,0 +1,386 @@ +getSitesIdWithAtLeastViewAccess(); + } + + $alerts = Db::fetchAll(("SELECT * FROM " + . Common::prefixTable('alert') + . " WHERE idalert IN ( + SELECT pas.idalert FROM " . Common::prefixTable('alert_site') + . " pas WHERE idsite IN (" . implode(",", $idSites) . ")) " + . "AND deleted = 0" + )); + + return $alerts; + } + + public function getTriggeredAlerts($period, $date, $login = false) + { + Piwik::checkUserIsSuperUserOrTheUser($login); + + $this->checkPeriod($period); + $piwikDate = Date::factory($date); + $date = Period::factory($period, $piwikDate); + + $db = Db::get(); + + $sql = "SELECT pa.idalert AS idalert, + pal.idsite AS idsite, + pa.name AS alert_name, + ps.name AS site_name, + login, + period, + report, + report_condition, + report_matched, + metric, + metric_condition, + metric_matched + FROM ". Common::prefixTable('alert_log') ." pal + JOIN ". Common::prefixTable('alert') ." pa + ON pal.idalert = pa.idalert + JOIN ". Common::prefixTable('site') ." ps + ON pal.idsite = ps.idsite + WHERE period = ? + AND ts_triggered BETWEEN ? AND ?"; + + if ($login !== false) { + $sql .= " AND login = \"" . $login . "\""; + } + + return $db->fetchAll($sql, array( + $period, + $date->getDateStart()->getDateStartUTC(), + $date->getDateEnd()->getDateEndUTC()) + ); + + } + + public function getAllAlerts($period) + { + Piwik::checkUserIsSuperUser(); + + $sql = "SELECT * FROM " + . Common::prefixTable('alert_site') . " alert, " + . Common::prefixTable('alert') . " alert_site " + . "WHERE alert.idalert = alert_site.idalert " + . "AND deleted = 0 "; + + if ($this->isValidPeriod($period)) { + $sql .= sprintf("AND period = '%s'", $period); + } else { + throw new Exception("Invalid period given."); + } + + return Db::fetchAll($sql); + } + + /** + * Creates an Alert for given website(s). + * + * @param string $name + * @param mixed $idSites + * @param string $period + * @param bool $email + * @param string $metric (nb_uniq_visits, sum_visit_length, ..) + * @param string $metricCondition + * @param float $metricValue + * @param string $report + * @param string $reportCondition + * @param string $reportValue + * @return int ID of new Alert + */ + public function addAlert($name, $idSites, $period, $email, $metric, $metricCondition, $metricValue, $report, $reportCondition = '', $reportValue = '') + { + if (!is_array($idSites)) { + $idSites = array($idSites); + } + + Piwik::checkUserHasViewAccess($idSites); + + $name = $this->checkName($name); + $this->checkPeriod($period); + + // save in db + $db = Db::get(); + $idAlert = Db::fetchOne("SELECT max(idalert) + 1 FROM " . Common::prefixTable('alert')); + if ($idAlert == false) { + $idAlert = 1; + } + + $newAlert = array( + 'idalert' => $idAlert, + 'name' => $name, + 'period' => $period, + 'login' => Piwik::getCurrentUserLogin(), + 'enable_mail' => (int) $email, + 'metric' => $metric, + 'metric_condition' => $metricCondition, + 'metric_matched' => (float) $metricValue, + 'report' => $report, + 'deleted' => 0, + ); + + if (!empty($reportCondition) && !empty($reportCondition)) { + $newAlert['report_condition'] = $reportCondition; + $newAlert['report_matched'] = $reportValue; + } + + // Do we have a valid alert for all given idSites? + foreach ($idSites as $idSite) { + if (!$this->isValidAlert($newAlert, $idSite)) { + throw new Exception(Piwik::translate('Alerts_ReportOrMetricIsInvalid')); + } + } + + $db->insert(Common::prefixTable('alert'), $newAlert); + foreach ($idSites as $idSite) { + $db->insert(Common::prefixTable('alert_site'), array( + 'idalert' => $idAlert, + 'idsite' => $idSite + )); + } + return $idAlert; + } + + /** + * Edits an Alert for given website(s). + * + * @param string $idalert ID of the Alert to edit. + * @param string $name Name of Alert + * @param mixed $idSites Single int or array of ints of idSites. + * @param string $period Period the alert is defined on. + * @param bool $email + * @param string $metric (nb_uniq_visits, sum_visit_length, ..) + * @param string $metricCondition + * @param float $metricValue + * @param string $report + * @param string $reportCondition + * @param string $reportValue + * @return boolean + */ + public function editAlert($idAlert, $name, $idSites, $period, $email, $metric, $metricCondition, $metricValue, $report, $reportCondition = '', $reportValue = '') + { + if (!is_array($idSites)) { + $idSites = array($idSites); + } + + Piwik::checkUserHasViewAccess($idSites); + + // Is the name in a valid format? + $name = $this->checkName($name); + + // Is the period valid? + $this->checkPeriod($period); + + // Save in DB + $db = Db::get(); + + $alert = array( + 'name' => $name, + 'period' => $period, + 'login' => 'admin', //Piwik::getCurrentUserLogin(), + 'enable_mail' => (boolean) $email, + 'metric' => $metric, + 'metric_condition' => $metricCondition, + 'metric_matched' => (float) $metricValue, + 'report' => $report, + 'deleted' => 0, + ); + + // + if (!empty($reportCondition) && !empty($reportCondition)) { + $alert['report_condition'] = $reportCondition; + $alert['report_matched'] = $reportValue; + } else { + $alert['report_condition'] = null; + $alert['report_matched'] = null; + } + + // Do we have a valid alert for all given idSites? + foreach ($idSites as $idSite) { + if (!$this->isValidAlert($alert, $idSites)) { + throw new Exception(Piwik::translate('CustomAlerts_ReportOrMetricIsInvalid')); + } + } + + $db->update(Common::prefixTable('alert'), $alert, "idalert = " . $idAlert); + + $db->query("DELETE FROM " . Common::prefixTable("alert_site") . " + WHERE idalert = ?", $idAlert); + + foreach ($idSites as $idSite) { + $db->insert(Common::prefixTable('alert_site'), array( + 'idalert' => $idAlert, + 'idsite' => $idSite + )); + } + return $idAlert; + } + + /** + * Delete alert by id. + * + * @param int $idAlert + */ + public function deleteAlert($idAlert) + { + $alert = $this->getAlert($idAlert); + + if (!$alert) { + throw new Exception(Piwik::translate('CustomAlerts_AlertDoesNotExist', $idAlert)); + } + + Piwik::checkUserIsSuperUserOrTheUser($alert['login']); + + $db = Db::get(); + $db->update( + Common::prefixTable('alert'), + array("deleted" => 1), + "idalert = " . $idAlert + ); + } + + /** + * Checks whether a report + metric exists for + * the given idSites and if the a dimension is + * given (requires report_condition, report_matched) + * + * @param array alert + * @param int $idSite + * @return boolean + */ + private function isValidAlert($alert, $idSite) + { + list($module, $action) = explode(".", $alert['report']); + + $report = MetadataApi::getInstance()->getMetadata($idSite, $module, $action); + + // If there is no report matching module + action for idSite it's not valid. + if(count($report) == 0) { + return false; + } + + // Merge all available metrics + $allMetrics = $report[0]['metrics']; + if (isset($report[0]['processedMetrics'])) { + $allMetrics = array_merge($allMetrics, $report[0]['processedMetrics']); + } + if (isset($report[0]['metricsGoal'])) { + $allMetrics = array_merge($allMetrics, $report[0]['metricsGoal']); + } + if (isset($report[0]['processedMetricsGoal'])) { + $allMetrics = array_merge($allMetrics, $report[0]['processedMetricsGoal']); + } + + if (!in_array($alert['metric'], array_keys($allMetrics))) { + return false; + } + + // If we have a dimension, we need to check if + // report_condition and report_matched is given. + if (isset($report[0]['dimension']) + && (!isset($alert['report_condition']) || !isset($alert['report_matched']))) { + return false; + } else { + return true; + } + + return false; + } + + private function checkName($name) + { + return urldecode($name); + } + + private function checkPeriod($period) + { + if (!$this->isValidPeriod($period)) { + throw new Exception(Piwik::translate('CustomAlerts_InvalidPeriod')); + } + } + + private function isValidPeriod($period) + { + return in_array($period, array('day', 'week', 'month', 'year')); + } + +} +?> \ No newline at end of file diff --git a/plugins/CustomAlerts/Controller.php b/plugins/CustomAlerts/Controller.php new file mode 100755 index 00000000000..666262ac43c --- /dev/null +++ b/plugins/CustomAlerts/Controller.php @@ -0,0 +1,118 @@ + 'matches_exactly', + 'CustomAlerts_DoesNotMatchExactly' => 'does_not_match_exactly', + 'CustomAlerts_MatchesRegularExpression' => 'matches_regex', + 'CustomAlerts_DoesNotMatchRegularExpression' => 'does_not_match_regex', + 'CustomAlerts_Contains' => 'contains', + 'CustomAlerts_DoesNotContain' => 'does_not_contain', + 'CustomAlerts_StartsWith' => 'starts_with', + 'CustomAlerts_DoesNotStartWith' => 'does_not_start_with', + 'CustomAlerts_EndsWith' => 'ends_with', + 'CustomAlerts_DoesNotEndWith' => 'does_not_end_with', + ); + private $alertMetricConditions = array( + 'CustomAlerts_IsLessThan' => 'less_than', + 'CustomAlerts_IsGreaterThan' => 'greater_than', + 'CustomAlerts_DecreasesMoreThan' => 'decrease_more_than', + 'CustomAlerts_IncreasesMoreThan' => 'increase_more_than', + 'CustomAlerts_PercentageDecreasesMoreThan' => 'percentage_decrease_more_than', + 'CustomAlerts_PercentageIncreasesMoreThan' => 'percentage_increase_more_than', + ); + + /** + * Shows all Alerts of the current selected idSite. + */ + public function index() + { + $view = new View('@CustomAlerts/index'); + $this->setGeneralVariablesView($view); + + $idSite = Common::getRequestVar('idSite'); + + $alertList = API::getInstance()->getAlerts(array($idSite)); + + $view->alertList = $alertList; + + return $view->render(); + } + + public function addNewAlert() + { + $view = new View('@CustomAlerts/addNewAlert'); + $this->setGeneralVariablesView($view); + + $sitesList = SitesManagerApi::getInstance()->getSitesWithAtLeastViewAccess(); + $view->sitesList = $sitesList; + + $availableReports = MetadataApi::getInstance()->getReportMetadata(); + + // ToDo need to collect metrics,processedMetrics,goalMetrics, goalProcessedMetric + + $view->alertGroups = array(); + $view->alerts = $availableReports; + $view->alertGroupConditions = $this->alertGroupConditions; + $view->alertMetricConditions = $this->alertMetricConditions; + + return $view->render(); + } + + public function editAlert() + { + $idAlert = Common::getRequestVar('idalert'); + + $view = new View('@CustomAlerts/editAlert'); + $this->setGeneralVariablesView($view); + + $alert = API::getInstance()->getAlert($idAlert); + $view->alert = $alert; + + $sitesList = SitesManagerApi::getInstance()->getSitesWithAtLeastViewAccess(); + $view->sitesList = $sitesList; + + // Fetch sites that the alert was defined on. + $sql = "SELECT idsite FROM ".Common::prefixTable('alert_site')." WHERE idalert = ?"; + $sites = Db::fetchAll($sql, $idAlert, \PDO::FETCH_COLUMN); + $idSites = array(); + foreach ($sites as $site) { + $idSites[] = $site['idsite']; + } + $view->sitesDefined = $idSites; + + $availableReports = MetadataApi::getInstance()->getReportMetadata(); + + $view->alerts = $availableReports; + $view->alertGroupConditions = $this->alertGroupConditions; + $view->alertMetricConditions = $this->alertMetricConditions; + + return $view->render(); + } +} +?> diff --git a/plugins/CustomAlerts/CustomAlerts.php b/plugins/CustomAlerts/CustomAlerts.php new file mode 100755 index 00000000000..4333de2a366 --- /dev/null +++ b/plugins/CustomAlerts/CustomAlerts.php @@ -0,0 +1,371 @@ + 'addTopMenu', + 'TaskScheduler.getScheduledTasks' => 'getScheduledTasks', + 'AssetManager.getJavaScriptFiles' => 'getJsFiles', + 'AssetManager.getStylesheetFiles' => 'getCssFiles', + ); + } + + public function getJsFiles(&$jsFiles) + { + $jsFiles[] = "plugins/CustomAlerts/javascripts/ui.dropdownchecklist.js"; + } + + public function getCssFiles(&$cssFiles) + { + $cssFiles[] = "plugins/CustomAlerts/stylesheets/ui.dropdownchecklist.css"; + } + + public function install() + { + $tableAlert = "CREATE TABLE " . Common::prefixTable('alert') . " ( + `idalert` INT NOT NULL PRIMARY KEY , + `name` VARCHAR(100) NOT NULL , + `login` VARCHAR(100) NOT NULL , + `period` VARCHAR(5) NOT NULL , + `report` VARCHAR(150) NOT NULL , + `report_condition` VARCHAR(50) , + `report_matched` VARCHAR(255) , + `metric` VARCHAR(150) NOT NULL , + `metric_condition` VARCHAR(50) NOT NULL , + `metric_matched` FLOAT NOT NULL , + `enable_mail` BOOLEAN NOT NULL , + `deleted` BOOLEAN NOT NULL + ) DEFAULT CHARSET=utf8 ;"; + + $tableAlertSite = "CREATE TABLE " . Common::prefixTable('alert_site') . "( + `idalert` INT( 11 ) NOT NULL , + `idsite` INT( 11 ) NOT NULL , + PRIMARY KEY ( idalert, idsite ) + ) DEFAULT CHARSET=utf8 ;"; + + $tableAlertLog = "CREATE TABLE " . Common::prefixTable('alert_log') . " ( + `idalert` INT( 11 ) NOT NULL , + `idsite` INT( 11 ) NOT NULL , + `ts_triggered` timestamp NOT NULL default CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP, + KEY `ts_triggered` (`ts_triggered`) + )"; + + try { + Db::exec($tableAlert); + Db::exec($tableAlertLog); + Db::exec($tableAlertSite); + } catch (Exception $e) { + // mysql code error 1050:table already exists + // see bug #153 http://dev.piwik.org/trac/ticket/153 + if (!Db::get()->isErrNo($e, '1050')) { + throw $e; + } + } + } + + public function uninstall() + { + $tables = array('alert', 'alert_log', 'alert_site'); + foreach ($tables as $table) { + $sql = "DROP TABLE " . Common::prefixTable($table); + Db::exec($sql); + } + } + + public function addTopMenu() + { + MenuTop::addEntry("Alerts", array("module" => "CustomAlerts", "action" => "index"), true, 9); + } + + public function getScheduledTasks(&$tasks) + { + $tasks[] = new ScheduledTask( + __CLASS__, + 'processDailyAlerts', + null, + ScheduledTime::factory('daily') + ); + + $tasks[] = new ScheduledTask( + __CLASS__, + 'processWeeklyAlerts', + null, + ScheduledTime::factory('weekly') + ); + + $tasks[] = new ScheduledTask( + __CLASS__, + 'processMonthlyAlerts', + null, + ScheduledTime::factory('monthly') + ); + } + + public function processDailyAlerts() + { + $this->processAlerts('day'); + } + + public function processWeeklyAlerts() + { + $this->processAlerts('week'); + } + + public function processMonthlyAlerts() + { + $this->processAlerts('month'); + } + + public function processAlerts($period) + { + $alerts = API::getInstance()->getAllAlerts($period); + + foreach ($alerts as $alert) { + $report = $alert['report']; + $metric = $alert['metric']; + $idSite = $alert['idsite']; + $idAlert = $alert['idalert']; + + $params = array( + "method" => $report, + "format" => "original", + "idSite" => $idSite, + "period" => $period, + "date" => Date::today()->subPeriod(1, $period)->toString() + ); + + // Get the data for the API request + $request = new Piwik\API\Request($params); + $result = $request->process(); + + $metric_one = $this->getMetricFromTable($result, $alert['metric'], $alert['report_condition'], $alert['report_matched']); + + // Do we have data? Continue otherwise. + if (is_null($metric_one)) { + continue; + } + + // Can we already trigger the alert? + switch ($alert['metric_condition']) { + case 'greater_than': + if ($metric_one > floatval($alert['metric_matched'])) { + $this->triggerAlert($idAlert, $idSite); + } + continue; + break; + case 'less_than': + if ($metric_one < floatval($alert['metric_matched'])) { + $this->triggerAlert($idAlert, $idSite); + } + continue; + break; + default: + break; + } + + $params['date'] = Date::today()->subPeriod(2, $period)->toString(); + + // Get the data for the API request + $request = new Piwik\API\Request($params); + $result = $request->process(); + + $metric_two = $this->getMetricFromTable($result, $alert['metric'], $alert['report_condition'], $alert['report_matched']); + + switch ($alert['metric_condition']) { + case 'decrease_more_than': + if (($metric_two - $metric_one) > $alert['metric_matched']) + $this->triggerAlert($idAlert, $idSite); + break; + case 'increase_more_than': + if (($metric_one - $metric_two) > $alert['metric_matched']) + $this->triggerAlert($idAlert, $idSite); + break; + case 'percentage_decrease_more_than': + // ToDo + break; + case 'percentage_increase_more_than': + // ToDo + break; + } + } + + //$this->sendNewAlerts($period); + } + + private function triggerAlert($idAlert, $idSite) + { + $db = Db::get(); + $db->insert( + Common::prefixTable('alert_log'), + array( + 'idalert' => $idAlert, + 'idsite' => $idSite, + 'ts_triggered' => Date::now()->getDatetime() + ) + ); + } + + /** + * + * @param array $dataTable DataTable + * @param string $metric Metric to fetch from row. + * @param string $filterCond Condition to filter for. + * @param string $filterValue Value to find + */ + private function getMetricFromTable($dataTable, $metric, $filterCond = '', $filterValue = '') + { + // Do we have a condition? Then filter.. + if (!empty($filterValue)) { + + $value = $filterValue; + + $invert = false; + + // Some escaping? + switch ($filterCond) { + case 'matches_exactly': + $pattern = sprintf("^%s$", $value); + break; + case 'matches_regex': + $pattern = $value; + break; + case 'does_not_match_exactly': + $pattern = sprintf("^%s$", $value); + $invert = true; + case 'does_not_match_regex': + $pattern = sprintf("%s", $value); + $invert = true; + break; + case 'contains': + $pattern = $value; + break; + case 'does_not_contain': + $pattern = sprintf("[^%s]", $value); + $invert = true; + break; + case 'starts_with': + $pattern = sprintf("^%s", $value); + break; + case 'does_not_start_with': + $pattern = sprintf("^%s", $value); + $invert = true; + break; + case 'ends_with': + $pattern = sprintf("%s$", $value); + break; + case 'does_not_end_with': + $pattern = sprintf("%s$", $value); + $invert = true; + break; + } + + $dataTable->filter('Pattern', array('label', $pattern, $invert)); + } + + if ($dataTable->getRowsCount() > 1) { + $dataTable->filter('Truncate'); + } + + // ToDo + //$dataTable->filter('AddColumnsProcessedMetrics'); + + $dataRow = $dataTable->getFirstRow(); + + if ($dataRow) { + return $dataRow->getColumn($metric); + } else { + return null; + } + } + + /** + * Sends a list of the triggered alerts to + * $recipient. + * + * @param string $recipient Email address of recipient. + */ + private function sendNewAlerts($period) + { + $triggeredAlerts = API::getInstance()->getTriggeredAlerts($period, Date::today()); + + foreach($triggeredAlerts as $triggeredAlert) { + // collect $triggered[$login] = array(of Alerts) + } + + $mail = new Piwik\Mail(); + $mail->addTo($recipient); + $mail->setSubject('Piwik alert [' . Date::today() . ']'); + + $viewHtml = new Piwik\View('@CustomAlerts/alertHtmlMail'); + $viewHtml->assign('triggeredAlerts', $this->getTriggeredAlerts('html')); + $mail->setBodyHtml($viewHtml->render()); + + $viewText = new Piwik\View('@CustomAlerts/alertTextMail'); + $viewText->assign('triggeredAlerts', $this->getTriggeredAlerts('tsv')); + $viewText->setContentType('text/plain'); + $mail->setBodyText($viewText->render()); + + $mail->send(); + } + + /** + * Returns the Alerts that were triggered in $format. + * + * @param string $format Can be 'html', 'tsv' or empty for php array + */ + private function getTriggeredAlerts($format = null) + { + switch ($format) { + case 'html': + $view = new Piwik\View('@CustomAlerts/htmlTriggeredAlerts'); + $view->triggeredAlerts = $this->triggeredAlerts; + return $view->render(); + break; + case 'tsv': + $tsv = ''; + $showedTitle = false; + foreach ($this->triggeredAlerts as $alert) { + if (!$showedTitle) { + $showedTitle = true; + $tsv .= implode("\t", array_keys($alert)) . "\n"; + } + $tsv .= implode("\t", array_values($alert)) . "\n"; + } + return $tsv; + break; + default: + return $this->triggeredAlerts; + } + } + +} +?> diff --git a/plugins/CustomAlerts/images/dropdown.png b/plugins/CustomAlerts/images/dropdown.png new file mode 100755 index 00000000000..1e86c61e49a Binary files /dev/null and b/plugins/CustomAlerts/images/dropdown.png differ diff --git a/plugins/CustomAlerts/images/dropdown_hover.png b/plugins/CustomAlerts/images/dropdown_hover.png new file mode 100755 index 00000000000..f3bbf75aece Binary files /dev/null and b/plugins/CustomAlerts/images/dropdown_hover.png differ diff --git a/plugins/CustomAlerts/javascripts/ui.dropdownchecklist.js b/plugins/CustomAlerts/javascripts/ui.dropdownchecklist.js new file mode 100755 index 00000000000..98ee0a9cb78 --- /dev/null +++ b/plugins/CustomAlerts/javascripts/ui.dropdownchecklist.js @@ -0,0 +1,487 @@ +;(function($) { + /* + * ui.dropdownchecklist + * + * Copyright (c) 2008-2009 Adrian Tosca + * Dual licensed under the MIT (MIT-LICENSE.txt) + * and GPL (GPL-LICENSE.txt) licenses. + * + */ + // The dropdown check list jQuery plugin transforms a regular select html element into a dropdown check list. + $.widget("ui.dropdownchecklist", { + // Creates the drop container that keeps the items and appends it to the document + _appendDropContainer: function() { + var wrapper = $("
"); + // the container is wrapped in a div + wrapper.addClass("ui-dropdownchecklist-dropcontainer-wrapper"); + // initially hidden + wrapper.css({ + position: 'absolute', + left: "-33000", + top: "-33000px", + width: '3000px', + height: '30000px' + }); + var container = $(""); // the actual container + container.addClass("ui-dropdownchecklist-dropcontainer") + .css("overflow-y", "auto"); + wrapper.append(container); + $(document.body).append(wrapper); + //wrapper.insertAfter(this.sourceSelect); + // flag that tells if the drop container is shown or not + wrapper.drop = false; + return wrapper; + }, + _isDropDownKeyShortcut: function(e) { + return e.altKey && ($.ui.keyCode.DOWN == (e.keyCode || e.which));// Alt + Down Arrow + }, + _isDroDownCloseKey: function(e) { + return $.ui.keyCode.ESCAPE == (e.keyCode || e.which); + }, + _handleKeyboard: function(e) { + var self = this; + if (self._isDropDownKeyShortcut(e)) { + e.stopPropagation(); + self._toggleDropContainer(); + self.dropWrapper.find("input:first").focus(); + } else if (self.dropWrapper.drop && self._isDroDownCloseKey(e)) { + self._toggleDropContainer(); + } + }, + // Creates the control that will replace the source select and appends it to the document + // The control resembles a regular select with single selection + _appendControl: function() { + var self = this, sourceSelect = this.sourceSelect; + + // the controls is wrapped in a span with inline-block display + var wrapper = $(""); + wrapper.addClass("ui-dropdownchecklist-wrapper"); + wrapper.css({ + display: "inline-block", + cursor: "default" + }); + + // the actual control, can be styled to set the border and drop right image + var control = $(""); + control.addClass("ui-dropdownchecklist"); + control.css({ + display: "inline-block" + }); + control.attr("tabIndex", 0); + control.keyup(function(e) { + self._handleKeyboard(e) + }); + wrapper.append(control); + + // the text container keeps the control text that is build from the selected (checked) items + var textContainer = $(""); + textContainer.addClass("ui-dropdownchecklist-text") + textContainer.css({ + display: "inline-block", + overflow: "hidden" + }); + control.append(textContainer); + + // add the hover styles to the control + wrapper.hover(function() { + if (!self.disabled) { + control.toggleClass("ui-dropdownchecklist-hover") + } + }, function() { + if (!self.disabled) { + control.toggleClass("ui-dropdownchecklist-hover") + } + }); + + // clicking on the control toggles the drop container + wrapper.click(function(event) { + if (!self.disabled) { + event.stopPropagation(); + self._toggleDropContainer(); + } + }) + + wrapper.insertAfter(sourceSelect); + + $(window).resize(function() { + if (!self.disabled && self.dropWrapper.drop) { + self._toggleDropContainer(); + } + }) + + return wrapper; + }, + // Creates a drop item that coresponds to an option element in the source select + _createDropItem: function(index, value, text, checked, disabled, indent) { + var self = this; + // the item contains a div that contains a checkbox input and a span for text + // the div + var item = $(""); + item.addClass("ui-dropdownchecklist-item"); + item.css({ + whiteSpace: "nowrap" + }); + var checkedString = checked ? ' checked="checked"' : ''; + var disabledString = disabled ? ' disabled="disabled"' : ''; + var idBase = (self.sourceSelect.attr("id") || "ddcl"); + var id = idBase + index; + var checkBox; + if (self.initialMultiple) { // the checkbox + checkBox = $(''); + } else { // the radiobutton + checkBox = $(''); + } + checkBox = checkBox.attr("index", index).val(value); + item.append(checkBox); + // the text + var label = $(""); + label.addClass("ui-dropdownchecklist-text") + .css({ + cursor: "default", + width: "100%" + }) + .text(text); + if (indent) { + item.addClass("ui-dropdownchecklist-indent"); + } + if (disabled) { + item.addClass("ui-dropdownchecklist-item-disabled"); + } + item.append(label); + item.hover(function() { + item.addClass("ui-dropdownchecklist-item-hover") + }, function() { + item.removeClass("ui-dropdownchecklist-item-hover") + }); + // clicking on the checkbox synchronizes the source select + checkBox.click(function(e) { + e.stopPropagation(); + if (!disabled) { + self._syncSelected($(this)); + self.sourceSelect.trigger("change", 'ddcl_internal'); + } + }); + // check/uncheck the item on clicks on the entire item div + var checkItem = function(e) { + e.stopPropagation(); + if (!disabled) { + var checked = checkBox.attr("checked"); + checkBox.attr("checked", !checked) + self._syncSelected(checkBox); + self.sourceSelect.trigger("change", 'ddcl_internal'); + } + } + label.click(function(e) { + e.stopPropagation() + }); + item.click(checkItem); + item.keyup(function(e) { + self._handleKeyboard(e) + }); + return item; + }, + _createGroupItem: function(text) { + var group = $("") + group.addClass("ui-dropdownchecklist-group"); + group.css({ + whiteSpace: "nowrap" + }); + var label = $(""); + label.addClass("ui-dropdownchecklist-text") + .css({ + cursor: "default", + width: "100%" + }) + .text(text); + group.append(label); + return group; + }, + // Creates the drop items and appends them to the drop container + // Also calculates the size needed by the drop container and returns it + _appendItems: function() { + var self = this, sourceSelect = this.sourceSelect, dropWrapper = this.dropWrapper; + var dropContainerDiv = dropWrapper.find(".ui-dropdownchecklist-dropcontainer"); + dropContainerDiv.css({ + "float": "left" + }); // to allow getting the actual width of the container + sourceSelect.children().each(function(index) { // when the select has groups + var opt = $(this); + if (opt.is("option")) { + self._appendOption(opt, dropContainerDiv, index, false); + } else { + var text = opt.attr("label"); + var group = self._createGroupItem(text); + dropContainerDiv.append(group); + self._appendOptions(opt, dropContainerDiv, index, true); + } + }); + //self._appendOptions(sourceSelect, dropContainerDiv, false); // when no groups + var divWidth = dropContainerDiv.outerWidth(); + var divHeight = dropContainerDiv.outerHeight(); + dropContainerDiv.css({ + "float": "" + }); // set it back + return { + width: divWidth, + height: divHeight + }; + }, + _appendOptions: function(parent, container, parentIndex, indent) { + var self = this; + parent.children("option").each(function(index) { + var option = $(this); + var childIndex = (parentIndex + "." + index); + self._appendOption(option, container, childIndex, indent); + }) + }, + _appendOption: function(option, container, index, indent) { + var self = this; + var text = option.text(); + var value = option.val(); + var selected = option.attr("selected"); + var disabled = option.attr("disabled"); + var item = self._createDropItem(index, value, text, selected, disabled, indent); + container.append(item); + }, + // Synchronizes the items checked and the source select + // When firstItemChecksAll option is active also synchronizes the checked items + // senderCheckbox parameters is the checkbox input that generated the synchronization + _syncSelected: function(senderCheckbox) { + var self = this, options = this.options, sourceSelect = this.sourceSelect, dropWrapper = this.dropWrapper; + var allEnabledCheckboxes = dropWrapper.find("input:not([disabled])"); + if (options.firstItemChecksAll) { + // if firstItemChecksAll is true, check all checkboxes if the first one is checked + if (senderCheckbox.attr("index") == 0) { + allEnabledCheckboxes.attr("checked", senderCheckbox.attr("checked")); + } else { + // check the first checkbox if all the other checkboxes are checked + var allChecked; + allChecked = true; + allEnabledCheckboxes.each(function(index) { + if (index > 0) { + var checked = $(this).attr("checked"); + if (!checked) allChecked = false; + } + }); + var firstCheckbox = allEnabledCheckboxes.filter(":first"); + firstCheckbox.attr("checked", false); + if (allChecked) { + firstCheckbox.attr("checked", true); + } + } + } + + var allCheckboxes = dropWrapper.find("input"); + // do the actual synch with the source select + var selectOptions = sourceSelect.get(0).options; + allCheckboxes.each(function(index) { + $(selectOptions[index]).attr("selected", $(this).attr("checked")); + }); + + // update the text shown in the control + self._updateControlText(); + }, + _sourceSelectChangeHandler: function(event) { + var self = this, dropWrapper = this.dropWrapper; + dropWrapper.find("input").val(self.sourceSelect.val()); + + // update the text shown in the control + self._updateControlText(); + }, + // Updates the text shown in the control depending on the checked (selected) items + _updateControlText: function() { + var self = this, sourceSelect = this.sourceSelect, options = this.options, controlWrapper = this.controlWrapper; + var firstSelect = sourceSelect.find("option:first"); + var allSelected = null != firstSelect && firstSelect.attr("selected"); + var selectOptions = sourceSelect.find("option"); + var text = self._formatText(selectOptions, options.firstItemChecksAll, allSelected); + var controlLabel = controlWrapper.find(".ui-dropdownchecklist-text"); + controlLabel.text(text); + controlLabel.attr("title", text); + }, + // Formats the text that is shown in the control + _formatText: function(selectOptions, firstItemChecksAll, allSelected) { + var text; + if (null != this.options.textFormatFunction) { + return this.options.textFormatFunction(selectOptions); + } else if (firstItemChecksAll && allSelected) { + // just set the text from the first item + text = selectOptions.filter(":first").text(); + } else { + // concatenate the text from the checked items + text = ""; + selectOptions.each(function() { + if ($(this).attr("selected")) { + text += $(this).text() + ", "; + } + }); + if (text.length > 0) { + text = text.substring(0, text.length - 2); + } + } + if (text == "") text = this.options.emptyText; + return text; + }, + // Shows and hides the drop container + _toggleDropContainer: function() { + var self = this, dropWrapper = this.dropWrapper, controlWrapper = this.controlWrapper; + // hides the last shown drop container + var hide = function() { + var instance = $.ui.dropdownchecklist.drop; + if (null != instance) { + instance.dropWrapper.css({ + top: "-33000px", + left: "-33000px" + }); + instance.controlWrapper.find(".ui-dropdownchecklist").toggleClass("ui-dropdownchecklist-active"); + instance.dropWrapper.find("input").attr("tabIndex", -1); + instance.dropWrapper.drop = false; + $.ui.dropdownchecklist.drop = null; + $(document).unbind("click", hide); + self.sourceSelect.trigger("blur"); + } + } + // shows the given drop container instance + var show = function(instance) { + if (null != $.ui.dropdownchecklist.drop) { + hide(); + } + instance.dropWrapper.css({ + top: instance.controlWrapper.offset().top + instance.controlWrapper.outerHeight() + "px", + left: instance.controlWrapper.offset().left + "px" + }) + var ancestorsZIndexes = controlWrapper.parents().map( + function() { + var zIndex = $(this).css("z-index"); + return isNaN(zIndex) ? 0 : zIndex + } + ).get(); + var parentZIndex = Math.max.apply(Math, ancestorsZIndexes); + if (parentZIndex > 0) { + instance.dropWrapper.css({ + zIndex: (parentZIndex+1) + }) + } + instance.controlWrapper.find(".ui-dropdownchecklist").toggleClass("ui-dropdownchecklist-active"); + instance.dropWrapper.find("input").attr("tabIndex", 0); + instance.dropWrapper.drop = true; + $.ui.dropdownchecklist.drop = instance; + $(document).bind("click", hide); + self.sourceSelect.trigger("focus"); + } + if (dropWrapper.drop) { + hide(self); + } else { + show(self); + } + }, + // Set the size of the control and of the drop container + _setSize: function(dropCalculatedSize) { + var options = this.options, dropWrapper = this.dropWrapper, controlWrapper = this.controlWrapper; + + var controlWidth; + // use the width from options if set, otherwise set the same width as the drop container + if (options.width) { + controlWidth = parseInt(options.width); + } else { + controlWidth = dropCalculatedSize.width; + var minWidth = options.minWidth; + // if the width is to small (usually when there are no items) set a minimum width + if (controlWidth < minWidth) { + controlWidth = minWidth; + } + } + controlWrapper.find(".ui-dropdownchecklist-text").css({ + width: controlWidth + "px" + }); + + // for the drop container get the actual (outer) width of the control. + // this can be different than the set one depening on paddings, borders etc set on the control + var controlOuterWidth = controlWrapper.outerWidth(); + + // the drop container height can be set from options + var dropHeight = options.maxDropHeight ? parseInt(options.maxDropHeight) : dropCalculatedSize.height; + // ensure the drop container is not less than the control width (would be ugly) + var dropWidth = dropCalculatedSize.width < controlOuterWidth ? controlOuterWidth : dropCalculatedSize.width; + + $(dropWrapper).css({ + width: dropWidth + "px", + height: dropHeight + "px" + }); + + dropWrapper.find(".ui-dropdownchecklist-dropcontainer").css({ + height: dropHeight + "px" + }); + }, + // Initializes the plugin + _init: function() { + var self = this, options = this.options; + + // sourceSelect is the select on which the plugin is applied + var sourceSelect = self.element; + self.initialDisplay = sourceSelect.css("display"); + sourceSelect.css("display", "none"); + self.initialMultiple = sourceSelect.attr("multiple"); + sourceSelect.attr("multiple", "multiple"); + self.sourceSelect = sourceSelect; + + // create the drop container where the items are shown + var dropWrapper = self._appendDropContainer(); + self.dropWrapper = dropWrapper; + + // append the items from the source select element + var dropCalculatedSize = self._appendItems(); + + // append the control that resembles a single selection select + var controlWrapper = self._appendControl(); + self.controlWrapper = controlWrapper; + + // updates the text shown in the control + self._updateControlText(controlWrapper, dropWrapper, sourceSelect); + + // set the sizes of control and drop container + self._setSize(dropCalculatedSize); + + // BGIFrame for IE6 + if (options.bgiframe && typeof self.dropWrapper.bgiframe == "function") { + self.dropWrapper.bgiframe(); + } + + // listen for change events on the source select element + // ensure we avoid processing internally triggered changes + self.sourceSelect.change(function(event, eventName) { + if (eventName != 'ddcl_internal') { + self._sourceSelectChangeHandler(event); + } + }); + }, + enable: function() { + this.controlWrapper.find(".ui-dropdownchecklist").removeClass("ui-dropdownchecklist-disabled"); + this.disabled = false; + }, + disable: function() { + this.controlWrapper.find(".ui-dropdownchecklist").addClass("ui-dropdownchecklist-disabled"); + this.disabled = true; + }, + destroy: function() { + $.widget.prototype.destroy.apply(this, arguments); + this.sourceSelect.css("display", this.initialDisplay); + this.sourceSelect.attr("multiple", this.initialMultiple); + this.controlWrapper.unbind().remove(); + this.dropWrapper.remove(); + } + }); + + $.extend($.ui.dropdownchecklist, { + defaults: { + width: null, + maxDropHeight: null, + firstItemChecksAll: false, + minWidth: 50, + bgiframe: false, + emptyText: "", + textFormatFunction: null + } + }); + +})(jQuery); \ No newline at end of file diff --git a/plugins/CustomAlerts/lang/en.json b/plugins/CustomAlerts/lang/en.json new file mode 100755 index 00000000000..54f89953a27 --- /dev/null +++ b/plugins/CustomAlerts/lang/en.json @@ -0,0 +1,51 @@ +{ + "CustomAlerts": { + "Alerts": "Alerts", + "PluginDescription": "The Alerts plugin lets you define custom Alerts on almost every metric available in Piwik.", + "CreateNewAlert": "Create new Alert", + "AlertConditions": "Alert Conditions", + "AlertDoesNotExist": "Alert with ID %d does not exist!", + "AccessException": "You are not the owner of the Alert with ID %d", + "EditAlert": "Edit Alert", + "SendEmail": "Email me when this alert is triggered", + "InvalidPeriod": "Invalid period given!", + "ReportOrMetricIsInvalid": "The report or metric given is invalid!", + "AlertName": "Alert Name", + "ApplyTo": "Apply to", + "Value": "Value", + "Condition": "Condition", + "AlertMeWhen": "Alert me when", + "ThisAppliesTo": "This applies to", + "Period": "Period", + "Day": "Day", + "Week": "Week", + "Month": "Month", + "Year": "Year", + "MatchesExactly": "matches exactly", + "DoesNotMatchExactly": "does not match exactly", + "MatchesRegularExpression": "matches regular expression", + "DoesNotMatchRegularExpression": "does not match regular expression", + "Contains": "contains", + "DoesNotContain": "does not contains", + "StartsWith": "starts with", + "DoesNotStartWith": "does not start with", + "EndsWith": "ends with", + "DoesNotEndWith": "does not end with", + "Visits": "Unique Visits", + "Visitors": "Visitors", + "Pageviews": "Pageviews", + "BounceRate": "Bounce Rate", + "AverageTimeOnSite": "Average Time On Site", + "Conversions": "Conversions", + "IsLessThan": "is less than", + "IsGreaterThan": "is greater than", + "DecreasesMoreThan": "decreases more than", + "IncreasesMoreThan": "increases more than", + "PercentageDecreasesMoreThan": "% decreases more than", + "PercentageIncreasesMoreThan": "% increases more than", + "MailGreeting": "Dear Piwik User", + "MailText": "The custom alerts you requested from Piwik Alerts are listed in the table below. To see more details or to adjust your custom alert settings, please sign in to your Piwik Open Source Analytics account and access the Alerts page.", + "MailEnd": "Happy analyzing!", + "NoAlertsDefined": "You haven't created any Alerts for this site yet, create one now!" + } +} \ No newline at end of file diff --git a/plugins/CustomAlerts/plugin.json b/plugins/CustomAlerts/plugin.json new file mode 100644 index 00000000000..8f55ee15a71 --- /dev/null +++ b/plugins/CustomAlerts/plugin.json @@ -0,0 +1,5 @@ +{ + "name": "CustomAlerts", + "description": "Alerts are a great way to get notified of changes on your website.", + "version": "0.0.1" +} \ No newline at end of file diff --git a/plugins/CustomAlerts/stylesheets/ui.dropdownchecklist.css b/plugins/CustomAlerts/stylesheets/ui.dropdownchecklist.css new file mode 100755 index 00000000000..52eb4de4619 --- /dev/null +++ b/plugins/CustomAlerts/stylesheets/ui.dropdownchecklist.css @@ -0,0 +1,47 @@ +.ui-dropdownchecklist { + height: 20px; + border: 1px solid #ddd; + border-right: 0; + background: #fff url(../images/dropdown.png) no-repeat center right; +} +.ui-dropdownchecklist-hover, .ui-dropdownchecklist-active { + background-image: url(../images/dropdown_hover.png); + border-color: #5794bf; +} + +.ui-dropdownchecklist-text { + font-size: 14px; + height: 20px; + line-height: 20px; + margin-right: 17px; /* background dropdown.png image */ +} + +.ui-dropdownchecklist-dropcontainer { + background-color: #fff; + border: 1px solid #999; +} + +.ui-dropdownchecklist-item { + +} + +.ui-dropdownchecklist-item-hover { + background-color: #39f; +} + +.ui-dropdownchecklist-item-disabled label { + color: #ccc; +} + +.ui-dropdownchecklist-item-hover .ui-dropdownchecklist-text { + color: #fff; +} + +.ui-dropdownchecklist-group { + font-weight: bold; + font-style: italic; +} + +.ui-dropdownchecklist-indent { + padding-left: 20px; +} diff --git a/plugins/CustomAlerts/templates/addNewAlert.twig b/plugins/CustomAlerts/templates/addNewAlert.twig new file mode 100755 index 00000000000..d94d1cb77c6 --- /dev/null +++ b/plugins/CustomAlerts/templates/addNewAlert.twig @@ -0,0 +1,255 @@ +{% extends 'dashboard.twig' %} + +{% block content %} + + + + + +{{ key }} | + {% endfor %} +
{{ row }} | + {% endfor %} +
{{ 'CustomAlerts_PluginDescription'|translate }}
ID | +Alert Name | +Period | +Report | +Send Email? | +|
---|---|---|---|---|---|
+ + {{ 'CustomAlerts_NoAlertsDefined'|translate }}. + + |
+ |||||
{{ alert.idalert }} | +{{ alert.name }} | +{{ alert.period|capitalize }} | +{{ alert.report }} | +{% if alert.enable_mail %}Yes{% else %}No{% endif %} | + {% set idalert=alert.idalert %} +Edit | +
{'Alerts_PluginDescription'|translate}
+ID | +Name | +Period | +Report | +Send Email? | +|
---|---|---|---|---|---|
{$alert.idalert} | +{$alert.name} | +{$alert.period|ucfirst} | +{$alert.report} | +{if $alert.enable_mail==true}Yes{else}No{/if} | + {assign var="idalert" value=$alert.idalert} +Edit | +