Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/matomo-org/matomo.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorbenakamoorthi <benaka.moorthi@gmail.com>2012-12-14 12:56:21 +0400
committerbenakamoorthi <benaka.moorthi@gmail.com>2012-12-14 12:56:21 +0400
commit043fc882559e3ed31c1387fbd5ef158510c5668f (patch)
treee899ada11325b777e79de2b89997a943b21d3f34
parent6720c8ae82f2f914a682c4c741fd6adc768dd230 (diff)
Fixes #1253, added annotations plugin that allows attaching notes to different days.
Notes: * Modified renderers so they would render arrays better. Before, arrays were added to DataTables and array keys were either lost or ignored, now they are rendered. * Fixed issue w/ JSON rendering that rendered arrays when the PHP arrays didn't have contiguous keys. * Augmented some exception messages. * Added utility method processRequest to Piwik_API_Request to ease use of the class. git-svn-id: http://dev.piwik.org/svn/trunk@7612 59fd770c-687e-43c8-a1e3-f5a4ff64c105
-rw-r--r--config/global.ini.php1
-rw-r--r--core/API/Request.php22
-rw-r--r--core/API/ResponseBuilder.php94
-rw-r--r--core/DataTable/Renderer.php70
-rw-r--r--core/DataTable/Renderer/Json.php23
-rw-r--r--core/DataTable/Renderer/Xml.php119
-rw-r--r--core/Date.php2
-rw-r--r--core/FrontController.php2
-rw-r--r--core/Piwik.php40
-rwxr-xr-xcore/Updates/1.9.3-b10.php34
-rw-r--r--core/Version.php2
-rw-r--r--core/ViewDataTable.php15
-rw-r--r--core/ViewDataTable/GenerateGraphHTML/ChartEvolution.php1
-rw-r--r--lang/en.php16
-rw-r--r--plugins/API/API.php2
-rwxr-xr-xplugins/Annotations/API.php364
-rwxr-xr-xplugins/Annotations/AnnotationList.php446
-rwxr-xr-xplugins/Annotations/Annotations.php70
-rwxr-xr-xplugins/Annotations/Controller.php226
-rwxr-xr-xplugins/Annotations/templates/annotation.tpl43
-rwxr-xr-xplugins/Annotations/templates/annotationManager.tpl27
-rwxr-xr-xplugins/Annotations/templates/annotations.js583
-rwxr-xr-xplugins/Annotations/templates/annotations.tpl30
-rwxr-xr-xplugins/Annotations/templates/evolutionAnnotations.tpl12
-rwxr-xr-xplugins/Annotations/templates/styles.css194
-rw-r--r--plugins/CoreHome/templates/calendar.js21
-rw-r--r--plugins/CoreHome/templates/datatable.css6
-rw-r--r--plugins/CoreHome/templates/datatable.js155
-rw-r--r--plugins/CoreHome/templates/datatable_footer.tpl11
-rw-r--r--tests/PHPUnit/Core/API/ResponseBuilderTest.php69
-rw-r--r--tests/PHPUnit/Core/DataTable/Renderer/JSONTest.php68
-rw-r--r--tests/PHPUnit/Core/DataTable/Renderer/XMLTest.php95
-rw-r--r--tests/PHPUnit/Core/PiwikTest.php25
-rwxr-xr-xtests/PHPUnit/Integration/AnnotationsTest.php415
-rwxr-xr-xtests/PHPUnit/Integration/expected/test_ImportLogs__Goals.getGoals.xml2
-rw-r--r--tests/PHPUnit/Integration/expected/test_OneVisitorTwoVisits__Goals.getGoals.xml4
-rw-r--r--tests/PHPUnit/Integration/expected/test_OneVisitorTwoVisits_withCookieSupport__Goals.getGoals.xml4
-rwxr-xr-xtests/PHPUnit/Integration/expected/test_annotations__Annotations.get.xml11
-rwxr-xr-xtests/PHPUnit/Integration/expected/test_annotations__Annotations.getAll_day.xml13
-rwxr-xr-xtests/PHPUnit/Integration/expected/test_annotations__Annotations.getAll_month.xml85
-rwxr-xr-xtests/PHPUnit/Integration/expected/test_annotations__Annotations.getAll_week.xml21
-rwxr-xr-xtests/PHPUnit/Integration/expected/test_annotations__Annotations.getAll_year.xml133
-rwxr-xr-xtests/PHPUnit/Integration/expected/test_annotations__Annotations.getAnnotationCountForDates_day.xml12
-rwxr-xr-xtests/PHPUnit/Integration/expected/test_annotations__Annotations.getAnnotationCountForDates_month.xml12
-rwxr-xr-xtests/PHPUnit/Integration/expected/test_annotations__Annotations.getAnnotationCountForDates_week.xml12
-rwxr-xr-xtests/PHPUnit/Integration/expected/test_annotations__Annotations.getAnnotationCountForDates_year.xml12
-rwxr-xr-xtests/PHPUnit/Integration/expected/test_annotations_lastN__Annotations.getAll_week.xml85
-rwxr-xr-xtests/PHPUnit/Integration/expected/test_annotations_lastN__Annotations.getAnnotationCountForDates_week.xml47
-rwxr-xr-xtests/PHPUnit/Integration/expected/test_annotations_multipleSites__Annotations.getAll_month.xml135
-rwxr-xr-xtests/PHPUnit/Integration/expected/test_annotations_multipleSites__Annotations.getAnnotationCountForDates_month.xml21
-rwxr-xr-xtests/PHPUnit/Integration/expected/test_annotations_range__Annotations.getAll_range.xml69
-rwxr-xr-xtests/PHPUnit/Integration/expected/test_annotations_range__Annotations.getAnnotationCountForDates_range.xml229
-rwxr-xr-xtests/PHPUnit/IntegrationTestCase.php1
-rwxr-xr-xthemes/default/images/grey_marker.pngbin0 -> 1627 bytes
-rwxr-xr-xthemes/default/images/star.pngbin0 -> 757 bytes
-rwxr-xr-xthemes/default/images/star_empty.pngbin0 -> 658 bytes
-rwxr-xr-xthemes/default/images/yellow_marker.pngbin0 -> 1618 bytes
57 files changed, 4024 insertions, 187 deletions
diff --git a/config/global.ini.php b/config/global.ini.php
index af26509b60..14d8bbaf8b 100644
--- a/config/global.ini.php
+++ b/config/global.ini.php
@@ -520,6 +520,7 @@ Plugins[] = CustomVariables
Plugins[] = PrivacyManager
Plugins[] = ImageGraph
Plugins[] = DoNotTrack
+Plugins[] = Annotations
[PluginsInstalled]
PluginsInstalled[] = Login
diff --git a/core/API/Request.php b/core/API/Request.php
index b7f8c62bfb..337fee441c 100644
--- a/core/API/Request.php
+++ b/core/API/Request.php
@@ -170,4 +170,26 @@ class Piwik_API_Request
}
return $a;
}
+
+ /**
+ * Helper method to process an API request using the variables in $_GET and $_POST.
+ *
+ * @param string $method The API method to call, ie, Actions.getPageTitles
+ * @param array $paramOverride The parameter name-value pairs to use instead of what's
+ * in $_GET & $_POST.
+ * @param mixed The result of the API request.
+ */
+ public static function processRequest( $method, $paramOverride = array() )
+ {
+ // set up request params
+ $params = $_GET + $_POST;
+ $params['format'] = 'original';
+ $params['module'] = 'API';
+ $params['method'] = $method;
+ $params = $paramOverride + $params;
+
+ // process request
+ $request = new Piwik_API_Request($params);
+ return $request->process();
+ }
}
diff --git a/core/API/ResponseBuilder.php b/core/API/ResponseBuilder.php
index afc43abebb..a85f1c67d8 100644
--- a/core/API/ResponseBuilder.php
+++ b/core/API/ResponseBuilder.php
@@ -159,7 +159,7 @@ class Piwik_API_ResponseBuilder
/**
* Apply the specified renderer to the DataTable
*
- * @param Piwik_DataTable $dataTable
+ * @param Piwik_DataTable|array $dataTable
* @return string
*/
protected function getRenderedDataTable($dataTable)
@@ -350,9 +350,7 @@ class Piwik_API_ResponseBuilder
return $multiDimensional;
}
- $dataTable = new Piwik_DataTable();
- $dataTable->addRowsFromSimpleArray($array);
- return $this->getRenderedDataTable($dataTable);
+ return $this->getRenderedDataTable($array);
}
/**
@@ -402,13 +400,7 @@ class Piwik_API_ResponseBuilder
return $array;
case 'xml':
- @header("Content-Type: text/xml;charset=utf-8");
- $xml =
- "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n" .
- "<result>\n".
- self::convertMultiDimensionalArrayToXml($array).
- "\n</result>";
- return $xml;
+ return $this->getRenderedDataTable($array);
default:
break;
}
@@ -420,82 +412,6 @@ class Piwik_API_ResponseBuilder
}
/**
- * Render a multidimensional array to XML
- *
- * @param array $array can contain scalar, arrays, Piwik_DataTable and Piwik_DataTable_Array
- * @param int $level
- * @return string
- */
- public static function convertMultiDimensionalArrayToXml($array, $level = 0)
- {
- $xml="";
- foreach ($array as $key=>$value)
- {
- if(is_numeric($key))
- {
- $key = "row";
- }
-
- $key = str_replace(' ', '_', $key);
- $marginLeft = str_repeat("\t", $level + 1);
-
- switch(true)
- {
- // Case dimension is a PHP array
- case (is_array($value)):
-
- if(empty($value))
- {
- $xml .= $marginLeft . "<$key/>\n";
- }
- else
- {
- $xml.= $marginLeft .
- "<$key>\n".
- self::convertMultiDimensionalArrayToXml($value, $level + 1).
- "\n". $marginLeft .
- "</$key>\n";
- }
- break;
-
- // Case dimension is a Piwik_DataTable_Array or a Piwik_DataTable
- case ($value instanceof Piwik_DataTable_Array || $value instanceof Piwik_DataTable):
-
- if($value->getRowsCount() == 0)
- {
- $xml .= $marginLeft . "<$key/>\n";
- }
- else
- {
- $XMLRenderer = new Piwik_DataTable_Renderer_Xml();
- $XMLRenderer->setTable($value);
- $renderedReport = $XMLRenderer->render();
-
- $renderedReport = preg_replace("/<\?xml.*\?>\n/", "", $renderedReport);
- $markupToRemove = $value instanceof Piwik_DataTable_Array ? "results" : "result";
- $renderedReport = preg_replace("/\n?<\/?". $markupToRemove .">\n?/", "", $renderedReport);
-
- // Add one level of margin to each line
- $renderedReport = $marginLeft . preg_replace("/\n/", "\n" . $marginLeft, $renderedReport);
-
- $xml.= $marginLeft . "<$key>\n";
- $xml.= $renderedReport;
- $xml.= "\n" . $marginLeft . "</$key>\n";
- }
-
- break;
-
- // Case scalar
- default:
-
- $xml.= $marginLeft . "<$key>".Piwik_DataTable_Renderer::formatValueXml($value)."</$key>\n";
- break;
- }
- }
- return $xml;
- }
-
- /**
* Render a multidimensional array to Json
* Handle Piwik_DataTable|Piwik_DataTable_Array elements in the first dimension only, following case does not work:
* array(
@@ -513,9 +429,7 @@ class Piwik_API_ResponseBuilder
*/
public static function convertMultiDimensionalArrayToJson($array)
{
- // Naive but works for our current use cases
- $arrayKeys = array_keys($array);
- $isAssociative = !is_numeric($arrayKeys[0]);
+ $isAssociative = Piwik::isAssociativeArray($array);
if($isAssociative)
{
diff --git a/core/DataTable/Renderer.php b/core/DataTable/Renderer.php
index b66e1f67b2..8f48e88903 100644
--- a/core/DataTable/Renderer.php
+++ b/core/DataTable/Renderer.php
@@ -130,10 +130,11 @@ abstract class Piwik_DataTable_Renderer
*/
public function setTable($table)
{
- if(!($table instanceof Piwik_DataTable)
+ if (!is_array($table)
+ && !($table instanceof Piwik_DataTable)
&& !($table instanceof Piwik_DataTable_Array))
{
- throw new Exception("The renderer accepts only a Piwik_DataTable or an array of DataTable (Piwik_DataTable_Array) object.");
+ throw new Exception("DataTable renderers renderer accepts only Piwik_DataTable and Piwik_DataTable_Array instances, and array instances.");
}
$this->table = $table;
}
@@ -355,4 +356,69 @@ abstract class Piwik_DataTable_Renderer
$this->idSite = $idSite;
}
+ /**
+ * Returns true if an array should be wrapped before rendering. This is used to
+ * mimic quirks in the old rendering logic (for backwards compatibility). The
+ * specific meaning of 'wrap' is left up to the Renderer. For XML, this means a
+ * new <row> node. For JSON, this means wrapping in an array.
+ *
+ * In the old code, arrays were added to new DataTable instances, and then rendered.
+ * This transformation wrapped associative arrays except under certain circumstances,
+ * including:
+ * - single element (ie, array('nb_visits' => 0))
+ * - empty array (ie, array())
+ * - array w/ arrays/DataTable instances as values (ie,
+ * array('name' => 'myreport',
+ * 'reportData' => new Piwik_DataTable())
+ * OR array('name' => 'myreport',
+ * 'reportData' => array(...)) )
+ *
+ * @param array $array
+ * @param bool|null $isAssociativeArray Whether the array is associative or not.
+ * If null, it is determined.
+ * @return bool
+ */
+ protected static function shouldWrapArrayBeforeRendering( $array, $isAssociativeArray = null )
+ {
+ if (empty($array))
+ {
+ return false;
+ }
+
+ if ($isAssociativeArray === null)
+ {
+ $isAssociativeArray = Piwik::isAssociativeArray($array);
+ }
+
+ $wrap = true;
+ if ($isAssociativeArray)
+ {
+ // we don't wrap if the array has one element that is a value
+ $firstValue = reset($array);
+ if (count($array) === 1
+ && (!is_array($firstValue)
+ && !is_object($firstValue)))
+ {
+ $wrap = false;
+ }
+ else
+ {
+ foreach ($array as $value)
+ {
+ if (is_array($value)
+ || is_object($value))
+ {
+ $wrap = false;
+ break;
+ }
+ }
+ }
+ }
+ else
+ {
+ $wrap = false;
+ }
+
+ return $wrap;
+ }
}
diff --git a/core/DataTable/Renderer/Json.php b/core/DataTable/Renderer/Json.php
index 16fee39e2c..812eb4e76a 100644
--- a/core/DataTable/Renderer/Json.php
+++ b/core/DataTable/Renderer/Json.php
@@ -54,12 +54,23 @@ class Piwik_DataTable_Renderer_Json extends Piwik_DataTable_Renderer
*/
protected function renderTable($table)
{
- $renderer = new Piwik_DataTable_Renderer_Php();
- $renderer->setTable($table);
- $renderer->setRenderSubTables($this->isRenderSubtables());
- $renderer->setSerialize(false);
- $renderer->setHideIdSubDatableFromResponse($this->hideIdSubDatatable);
- $array = $renderer->flatRender();
+ if (is_array($table))
+ {
+ $array = $table;
+ if (self::shouldWrapArrayBeforeRendering($array))
+ {
+ $array = array($array);
+ }
+ }
+ else
+ {
+ $renderer = new Piwik_DataTable_Renderer_Php();
+ $renderer->setTable($table);
+ $renderer->setRenderSubTables($this->isRenderSubtables());
+ $renderer->setSerialize(false);
+ $renderer->setHideIdSubDatableFromResponse($this->hideIdSubDatatable);
+ $array = $renderer->flatRender();
+ }
if(!is_array($array))
{
diff --git a/core/DataTable/Renderer/Xml.php b/core/DataTable/Renderer/Xml.php
index 74bb78be41..ea3c546f6d 100644
--- a/core/DataTable/Renderer/Xml.php
+++ b/core/DataTable/Renderer/Xml.php
@@ -60,6 +60,11 @@ class Piwik_DataTable_Renderer_Xml extends Piwik_DataTable_Renderer
*/
protected function getArrayFromDataTable($table)
{
+ if (is_array($table))
+ {
+ return $table;
+ }
+
$renderer = new Piwik_DataTable_Renderer_Php();
$renderer->setRenderSubTables($this->isRenderSubtables());
$renderer->setSerialize(false);
@@ -137,7 +142,7 @@ class Piwik_DataTable_Renderer_Xml extends Piwik_DataTable_Renderer
return $out;
}
- if($table instanceof Piwik_DataTable)
+ if ($table instanceof Piwik_DataTable)
{
$out = $this->renderDataTable($array);
if($returnOnlyDataTableXml)
@@ -147,6 +152,118 @@ class Piwik_DataTable_Renderer_Xml extends Piwik_DataTable_Renderer
$out = "<result>\n$out</result>";
return $out;
}
+
+ if (is_array($array))
+ {
+ $out = $this->renderArray($array, $prefixLines."\t");
+ if ($returnOnlyDataTableXml)
+ {
+ return $out;
+ }
+ return "<result>\n$out</result>";
+ }
+ }
+
+ /**
+ * Renders an array as XML.
+ *
+ * @param array $array The array to render.
+ * @param string $prefixLines The string to prefix each line in the output.
+ * @return string
+ */
+ private function renderArray( $array, $prefixLines )
+ {
+ $isAssociativeArray = Piwik::isAssociativeArray($array);
+
+ // check if array contains arrays, and if not wrap the result in an extra <row> element
+ // (only check if this is the root renderArray call)
+ // NOTE: this is for backwards compatibility. before, array's were added to a new DataTable.
+ // if the array had arrays, they were added as multiple rows, otherwise it was treated as
+ // one row. removing will change API output.
+ $wrapInRow = $prefixLines === "\t" && self::shouldWrapArrayBeforeRendering($array, $isAssociativeArray);
+
+ // render the array
+ $result = "";
+ if ($wrapInRow)
+ {
+ $result .= "$prefixLines<row>\n";
+ $prefixLines .= "\t";
+ }
+ foreach ($array as $key => $value)
+ {
+ // based on the type of array & the key, determine how this node will look
+ if ($isAssociativeArray)
+ {
+ if (is_numeric($key))
+ {
+ $prefix = "<row key=\"$key\">";
+ $suffix = "</row>";
+ $emptyNode = "<row key=\"$key\"/>";
+ }
+ else
+ {
+ $prefix = "<$key>";
+ $suffix = "</$key>";
+ $emptyNode = "<$key />";
+ }
+ }
+ else
+ {
+ $prefix = "<row>";
+ $suffix = "</row>";
+ $emptyNode = "<row/>";
+ }
+
+ // render the array item
+ if (is_array($value))
+ {
+ $result .= $prefixLines.$prefix."\n";
+ $result .= $this->renderArray($value, $prefixLines."\t");
+ $result .= $prefixLines.$suffix."\n";
+ }
+ else if ($value instanceof Piwik_DataTable
+ || $value instanceof Piwik_DataTable_Array)
+ {
+ if ($value->getRowsCount() == 0)
+ {
+ $result .= $prefixLines.$emptyNode."\n";
+ }
+ else
+ {
+ $result .= $prefixLines.$prefix."\n";
+ if ($value instanceof Piwik_DataTable_Array)
+ {
+ $result .= $this->renderDataTableArray($value, $this->getArrayFromDataTable($value), $prefixLines);
+ }
+ else if ($value instanceof Piwik_DataTable_Simple)
+ {
+ $result .= $this->renderDataTableSimple($this->getArrayFromDataTable($value), $prefixLines);
+ }
+ else
+ {
+ $result .= $this->renderDataTable($this->getArrayFromDataTable($value), $prefixLines);
+ }
+ $result .= $prefixLines.$suffix."\n";
+ }
+ }
+ else
+ {
+ $xmlValue = self::formatValueXml($value);
+ if (strlen($xmlValue) != 0)
+ {
+ $result .= $prefixLines.$prefix.$xmlValue.$suffix."\n";
+ }
+ else
+ {
+ $result .= $prefixLines.$emptyNode."\n";
+ }
+ }
+ }
+ if ($wrapInRow)
+ {
+ $result .= substr($prefixLines, 0, strlen($prefixLines) - 1)."</row>\n";
+ }
+ return $result;
}
/**
diff --git a/core/Date.php b/core/Date.php
index 3da0b5bdf1..a86aa5652a 100644
--- a/core/Date.php
+++ b/core/Date.php
@@ -62,7 +62,7 @@ class Piwik_Date
*/
static public function factory($dateString, $timezone = null)
{
- $invalidDateException = new Exception(Piwik_TranslateException('General_ExceptionInvalidDateFormat', array("YYYY-MM-DD, or 'today' or 'yesterday'", "strtotime", "http://php.net/strtotime")));
+ $invalidDateException = new Exception(Piwik_TranslateException('General_ExceptionInvalidDateFormat', array("YYYY-MM-DD, or 'today' or 'yesterday'", "strtotime", "http://php.net/strtotime")).": $dateString");
if($dateString instanceof self)
{
$dateString = $dateString->toString();
diff --git a/core/FrontController.php b/core/FrontController.php
index 654818abaa..80c2500910 100644
--- a/core/FrontController.php
+++ b/core/FrontController.php
@@ -126,7 +126,7 @@ class Piwik_FrontController
// Piwik::log("Dispatching $module / $action, parameters: ".var_export($parameters, $return = true));
if( !is_callable(array($controller, $action)))
{
- throw new Exception("Action $action not found in the controller $controllerClassName.");
+ throw new Exception("Action '$action' not found in the controller '$controllerClassName'.");
}
// Generic hook that plugins can use to modify any input to the function,
diff --git a/core/Piwik.php b/core/Piwik.php
index e070dc0591..c8bf3e9b5d 100644
--- a/core/Piwik.php
+++ b/core/Piwik.php
@@ -2667,4 +2667,44 @@ class Piwik
$oType = is_object($o) ? get_class($o) : gettype($o);
throw new Exception("Invalid variable type '$oType', expected one of following: ".implode(', ', $types));
}
+
+ /**
+ * Returns true if an array is an associative array, false if otherwise.
+ *
+ * This method determines if an array is associative by checking that the
+ * first element's key is 0, and that each successive element's key is
+ * one greater than the last.
+ *
+ * @param array $array
+ * @return bool
+ */
+ static public function isAssociativeArray( $array )
+ {
+ reset($array);
+ if (!is_numeric(key($array))
+ || key($array) != 0) // first key must be 0
+ {
+ return true;
+ }
+
+ // check that each key is == next key - 1 w/o actually indexing the array
+ while (true)
+ {
+ $current = key($array);
+
+ next($array);
+ $next = key($array);
+
+ if ($next === null)
+ {
+ break;
+ }
+ else if ($current + 1 != $next)
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
}
diff --git a/core/Updates/1.9.3-b10.php b/core/Updates/1.9.3-b10.php
new file mode 100755
index 0000000000..fc60dc32f3
--- /dev/null
+++ b/core/Updates/1.9.3-b10.php
@@ -0,0 +1,34 @@
+<?php
+/**
+ * Piwik - Open source web analytics
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ * @version $Id$
+ *
+ * @category Piwik
+ * @package Updates
+ */
+
+/**
+ * @package Updates
+ */
+class Piwik_Updates_1_9_3_b10 extends Piwik_Updates
+{
+ static function isMajorUpdate()
+ {
+ return false;
+ }
+
+ static function update()
+ {
+ try
+ {
+ Piwik_PluginsManager::getInstance()->activatePlugin('Annotations');
+ }
+ catch(Exception $e)
+ {
+ // pass
+ }
+ }
+}
diff --git a/core/Version.php b/core/Version.php
index a16a7a3e67..9bb369ceba 100644
--- a/core/Version.php
+++ b/core/Version.php
@@ -21,5 +21,5 @@ final class Piwik_Version
* Current Piwik version
* @var string
*/
- const VERSION = '1.9.3-b9';
+ const VERSION = '1.9.3-b10';
}
diff --git a/core/ViewDataTable.php b/core/ViewDataTable.php
index 628e05be69..c6ffb8e91a 100644
--- a/core/ViewDataTable.php
+++ b/core/ViewDataTable.php
@@ -292,6 +292,7 @@ abstract class Piwik_ViewDataTable
$this->viewProperties['show_table_all_columns'] = Piwik_Common::getRequestVar('show_table_all_columns', true);
$this->viewProperties['show_all_views_icons'] = Piwik_Common::getRequestVar('show_all_views_icons', true);
$this->viewProperties['hide_all_views_icons'] = Piwik_Common::getRequestVar('hide_all_views_icons', false);
+ $this->viewProperties['hide_annotations_view'] = Piwik_Common::getRequestVar('hide_annotations_view', true);
$this->viewProperties['show_bar_chart'] = Piwik_Common::getRequestVar('show_barchart', true);
$this->viewProperties['show_pie_chart'] = Piwik_Common::getRequestVar('show_piechart', true);
$this->viewProperties['show_tag_cloud'] = Piwik_Common::getRequestVar('show_tag_cloud', true);
@@ -959,6 +960,20 @@ abstract class Piwik_ViewDataTable
}
/**
+ * Whether or not to show the annotations view. This method has no effect if
+ * the Annotations plugin is not loaded.
+ */
+ public function showAnnotationsView()
+ {
+ if (!Piwik_PluginsManager::getInstance()->isPluginLoaded('Annotations'))
+ {
+ return;
+ }
+
+ $this->viewProperties['hide_annotations_view'] = false;
+ }
+
+ /**
* Whether or not to show the bar chart icon.
*/
public function disableShowBarChart()
diff --git a/core/ViewDataTable/GenerateGraphHTML/ChartEvolution.php b/core/ViewDataTable/GenerateGraphHTML/ChartEvolution.php
index 5974126d80..f4a7ac61b0 100644
--- a/core/ViewDataTable/GenerateGraphHTML/ChartEvolution.php
+++ b/core/ViewDataTable/GenerateGraphHTML/ChartEvolution.php
@@ -55,6 +55,7 @@ class Piwik_ViewDataTable_GenerateGraphHTML_ChartEvolution extends Piwik_ViewDat
$this->disableShowAllViewsIcons();
$this->disableShowTable();
$this->disableShowAllColumns();
+ $this->showAnnotationsView();
}
/**
diff --git a/lang/en.php b/lang/en.php
index e8011d1089..4c0f8d04c3 100644
--- a/lang/en.php
+++ b/lang/en.php
@@ -1875,4 +1875,20 @@ And thank you for using Piwik!',
'Overlay_ErrorNotLoadingDetails' => 'Maybe the page loaded on the right doesn\'t have the Piwik tracker code. In this case, try launching Overlay for a different page from the pages report.',
'Overlay_ErrorNotLoadingDetailsSSL' => 'Since you\'re using Piwik over https, the most likely cause is that your website doesn\'t support SSL. Try using Piwik over http.',
'Overlay_ErrorNotLoadingLink' => 'Click here to get more tips for troubleshooting',
+ 'Annotations_PluginDescription' => 'Allows you to attach notes to different days so you will be remember why your data looks the way it does.',
+ 'Annotations_Annotations' => 'Annotations',
+ 'Annotations_EnterAnnotationText' => 'Enter your note...',
+ 'CoreHome_Annotations_IconDesc_js' => 'View notes for this date range.',
+ 'CoreHome_Annotations_IconDescHideNotes_js' => 'Hide notes for this date range.',
+ 'Annotations_NoAnnotations' => 'There are no notes for this date range.',
+ 'CoreHome_Annotations_ViewAndAddAnnotations_js' => 'View and add annotations for %s...',
+ 'CoreHome_Annotations_HideAnnotationsFor_js' => 'Hide annotations for %s...',
+ 'CoreHome_Annotations_AddAnnotationsFor_js' => 'Add annotations for %s...',
+ 'Annotations_ClickToAdd' => 'Click to add an annotation.',
+ 'Annotations_ClickToEdit' => 'Click to edit this annotation.',
+ 'Annotations_ClickToDelete' => 'Click to delete this annotation.',
+ 'Annotations_ClickToStarOrUnstar' => 'Click to star or unstar this annotation.',
+ 'Annotations_YouCannotModifyThisNote' => 'You cannot modify this note, because you did not create it, nor do you do not have admin access for this site.',
+ 'Annotations_CreateNewAnnotation' => 'Create new annotation...',
+ 'Annotations_LoginToAnnotate' => 'Login to create an annotation.',
);
diff --git a/plugins/API/API.php b/plugins/API/API.php
index 3be22d02f0..1c173ba628 100644
--- a/plugins/API/API.php
+++ b/plugins/API/API.php
@@ -634,7 +634,7 @@ class Piwik_API_API
}
}
- return $availableReports;
+ return array_values($availableReports); // make sure array has contiguous key values
}
diff --git a/plugins/Annotations/API.php b/plugins/Annotations/API.php
new file mode 100755
index 0000000000..cc49d59492
--- /dev/null
+++ b/plugins/Annotations/API.php
@@ -0,0 +1,364 @@
+<?php
+/**
+ * Piwik - Open source web analytics
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ * @version $Id$
+ *
+ * @category Piwik_Plugins
+ * @package Piwik_Annotations
+ */
+
+/**
+ * @see plugins/Annotations/AnnotationList.php
+ */
+require_once PIWIK_INCLUDE_PATH.'/plugins/Annotations/AnnotationList.php';
+
+/**
+ * API for annotations plugin. Provides methods to create, modify, delete & query
+ * annotations.
+ *
+ * @package Piwik_Annotations
+ */
+class Piwik_Annotations_API
+{
+ static private $instance = null;
+
+ /**
+ * Returns this API's singleton instance.
+ *
+ * @return Piwik_Annotations_API
+ */
+ static public function getInstance()
+ {
+ if (self::$instance == null)
+ {
+ self::$instance = new self;
+ }
+ return self::$instance;
+ }
+
+ /**
+ * Create a new annotation for a site.
+ *
+ * @param string $idSite The site ID to add the annotation to.
+ * @param string $date The date the annotation is attached to.
+ * @param string $note The text of the annotation.
+ * @param string $starred Either 0 or 1. Whether the annotation should be starred.
+ * @return array Returns an array of two elements. The first element (indexed by
+ * 'annotation') is the new annotation. The second element (indexed
+ * by 'idNote' is the new note's ID).
+ */
+ public function add( $idSite, $date, $note, $starred = 0 )
+ {
+ // can only add a note to one site
+ if (!is_numeric($idSite))
+ {
+ throw new Exception("Invalid idSite: '$idSite'. Note: Cannot add one note to multiple sites.");
+ }
+
+ // make sure date is a valid date
+ Piwik_Date::factory($date);
+
+ // check permissions
+ $this->checkUserCanAddNotesFor($idSite);
+
+ // add, save & return a new annotation
+ $annotations = new Piwik_Annotations_AnnotationList($idSite);
+
+ $newAnnotation = $annotations->add($idSite, $date, $note, $starred);
+ $annotations->save($idSite);
+
+ return $newAnnotation;
+ }
+
+ /**
+ * Modifies an annotation for a site and returns the modified annotation
+ * and its ID.
+ *
+ * If the current user is not allowed to modify an annotation, an exception
+ * will be thrown. A user can modify a note if:
+ * - the user has admin access for the site, OR
+ * - the user has view access, is not the anonymous user and is the user that
+ * created the note
+ *
+ * @param string $idSite The site ID to add the annotation to.
+ * @param string $idNote The ID of the note.
+ * @param string|null $date The date the annotation is attached to. If null, the annotation's
+ * date is not modified.
+ * @param string|null $note The text of the annotation. If null, the annotation's text
+ * is not modified.
+ * @param string|null $starred Either 0 or 1. Whether the annotation should be starred.
+ * If null, the annotation is not starred/un-starred.
+ * @return array Returns an array of two elements. The first element (indexed by
+ * 'annotation') is the new annotation. The second element (indexed
+ * by 'idNote' is the new note's ID).
+ */
+ public function save( $idSite, $idNote, $date = null, $note = null, $starred = null )
+ {
+ // cannot update notes for multiple sites
+ if (!is_numeric($idSite))
+ {
+ throw new Exception("Invalid idSite: '$idSite'. Note: Cannot modify more than one note at a time.");
+ }
+
+ // make sure date is a valid date
+ if ($date !== null)
+ {
+ Piwik_Date::factory($date);
+ }
+
+ // get the annotations for the site
+ $annotations = new Piwik_Annotations_AnnotationList($idSite);
+
+ // check permissions
+ $this->checkUserCanModifyOrDelete($idSite, $annotations->get($idSite, $idNote));
+
+ // modify the annotation, and save the whole list
+ $annotations->update($idSite, $idNote, $date, $note, $starred);
+ $annotations->save($idSite);
+
+ return $annotations->get($idSite, $idNote);
+ }
+
+ /**
+ * Removes an annotation from a site's list of annotations.
+ *
+ * If the current user is not allowed to delete the annotation, an exception
+ * will be thrown. A user can delete a note if:
+ * - the user has admin access for the site, OR
+ * - the user has view access, is not the anonymous user and is the user that
+ * created the note
+ *
+ * @param string $idSite The site ID to add the annotation to.
+ * @param string $idNote The ID of the note to delete.
+ */
+ public function delete( $idSite, $idNote )
+ {
+ // check that $idSite is single
+ if (!is_numeric($idSite))
+ {
+ throw new Exception("Invalid idSite: '$idSite'. Note: Cannot delete multiple notes.");
+ }
+
+ $annotations = new Piwik_Annotations_AnnotationList($idSite);
+
+ // check permissions
+ $this->checkUserCanModifyOrDelete($idSite, $annotations->get($idSite, $idNote));
+
+ // remove the note & save the list
+ $annotations->remove($idSite, $idNote);
+ $annotations->save($idSite);
+ }
+
+ /**
+ * Returns a single note for one site.
+ *
+ * @param string $idSite The site ID to add the annotation to.
+ * @param string $idNote The ID of the note to get.
+ * @return array The annotation. It will contain the following properties:
+ * - date: The date the annotation was recorded for.
+ * - note: The note text.
+ * - starred: Whether the note is starred or not.
+ * - user: The user that created the note.
+ * - canEditOrDelete: Whether the user that called this method can edit or
+ * delete the annotation returned.
+ */
+ public function get( $idSite, $idNote )
+ {
+ // getting a single note means only ONE idSite
+ if (!is_numeric($idSite))
+ {
+ throw new Exception("Invalid idSite: '$idSite'. Note: Specify only one site ID when getting ONE note.");
+ }
+
+ Piwik::checkUserHasViewAccess($idSite);
+
+ // get single annotation
+ $annotations = new Piwik_Annotations_AnnotationList($idSite);
+ return $annotations->get($idSite, $idNote);
+ }
+
+ /**
+ * Returns every annotation for a specific site within a specific date range.
+ * The date range is specified by a date, the period type (day/week/month/year)
+ * and an optional number of N periods in the past to include.
+ *
+ * @param string $idSite The site ID to add the annotation to. Can be one ID or
+ * a list of site IDs.
+ * @param string|false $date The date of the period.
+ * @param string $period The period type.
+ * @param int|false $lastN Whether to include the last N number of periods in the
+ * date range or not.
+ * @return array An array that indexes arrays of annotations by site ID. ie,
+ * array(
+ * 5 => array(
+ * array(...), // annotation #1
+ * array(...), // annotation #2
+ * ),
+ * 8 => array(...)
+ * )
+ */
+ public function getAll( $idSite, $date = false, $period = 'day', $lastN = false )
+ {
+ Piwik::checkUserHasViewAccess($idSite);
+
+ $annotations = new Piwik_Annotations_AnnotationList($idSite);
+
+ // if date/period are supplied, determine start/end date for search
+ list($startDate, $endDate) = self::getDateRangeForPeriod($date, $period, $lastN);
+
+ return $annotations->search($startDate, $endDate);
+ }
+
+ /**
+ * Returns the count of annotations for a list of periods, including the count of
+ * starred annotations.
+ *
+ * @param string $idSite The site ID to add the annotation to.
+ * @param string|false $date The date of the period.
+ * @param string $period The period type.
+ * @param int|false $lastN Whether to get counts for the last N number of periods or not.
+ * @return array An array mapping site IDs to arrays holding dates & the count of
+ * annotations made for those dates. eg,
+ * array(
+ * 5 => array(
+ * array('2012-01-02', array('count' => 4, 'starred' => 2)),
+ * array('2012-01-03', array('count' => 0, 'starred' => 0)),
+ * array('2012-01-04', array('count' => 2, 'starred' => 0)),
+ * ),
+ * 6 => array(
+ * array('2012-01-02', array('count' => 1, 'starred' => 0)),
+ * array('2012-01-03', array('count' => 4, 'starred' => 3)),
+ * array('2012-01-04', array('count' => 2, 'starred' => 0)),
+ * ),
+ * ...
+ * )
+ */
+ public function getAnnotationCountForDates( $idSite, $date, $period, $lastN = false )
+ {
+ Piwik::checkUserHasViewAccess($idSite);
+
+ // get start & end date for request. lastN is ignored if $period == 'range'
+ list($startDate, $endDate) = self::getDateRangeForPeriod($date, $period, $lastN);
+ if ($period == 'range')
+ {
+ $period = 'day';
+ }
+
+ // create list of dates
+ $dates = array();
+ for (; $startDate->getTimestamp() <= $endDate->getTimestamp(); $startDate = $startDate->addPeriod(1, $period))
+ {
+ $dates[] = $startDate;
+ }
+ // we add one for the end of the last period (used in for loop below to bound annotation dates)
+ $dates[] = $startDate;
+
+ // get annotations for the site
+ $annotations = new Piwik_Annotations_AnnotationList($idSite);
+
+ // create result w/ 0-counts
+ $result = array();
+ for ($i = 0; $i != count($dates) - 1; ++$i)
+ {
+ $date = $dates[$i];
+ $nextDate = $dates[$i + 1];
+ $strDate = $date->toString();
+
+ foreach ($annotations->getIdSites() as $idSite)
+ {
+ $result[$idSite][$strDate] = $annotations->count($idSite, $date, $nextDate);
+ }
+ }
+
+ // convert associative array into array of pairs (so it can be traversed by index)
+ $pairResult = array();
+ foreach ($result as $idSite => $counts)
+ {
+ foreach ($counts as $date => $count)
+ {
+ $pairResult[$idSite][] = array($date, $count);
+ }
+ }
+ return $pairResult;
+ }
+
+ /**
+ * Throws if the current user is not allowed to modify or delete an annotation.
+ *
+ * @param int $idSite The site ID the annotation belongs to.
+ * @param array $annotation The annotation.
+ * @throws Exception if the current user is not allowed to modify/delete $annotation.
+ */
+ private function checkUserCanModifyOrDelete( $idSite, $annotation )
+ {
+ if (!$annotation['canEditOrDelete'])
+ {
+ throw new Exception(Piwik_Translate('Annotations_YouCannotModifyThisNote'));
+ }
+ }
+
+ /**
+ * Throws if the current user is not allowed to create annotations for a site.
+ *
+ * @param int $idSite The site ID.
+ * @throws Exception if the current user is anonymous or does not have view access
+ * for site w/ id=$idSite.
+ */
+ private static function checkUserCanAddNotesFor( $idSite )
+ {
+ if (!Piwik_Annotations_AnnotationList::canUserAddNotesFor($idSite))
+ {
+ throw new Exception("The current user is not allowed to add notes for site #$idSite.");
+ }
+ }
+
+ /**
+ * Returns start & end dates for the range described by a period and optional lastN
+ * argument.
+ *
+ * @param string $date|false The start date of the period (or the date range of a range
+ * period).
+ * @param string $period The period type ('day', 'week', 'month', 'year' or 'range').
+ * @param int|false $lastN Whether to include the last N periods in the range or not.
+ * Ignored if period == range.
+ *
+ * @ignore
+ */
+ public static function getDateRangeForPeriod( $date, $period, $lastN = false )
+ {
+ if ($date === false)
+ {
+ return array(false, false);
+ }
+
+ // if the range is just a normal period (or the period is a range in which case lastN is ignored)
+ if ($lastN === false
+ || $period == 'range')
+ {
+ if ($period == 'range')
+ {
+ $oPeriod = new Piwik_Period_Range('day', $date);
+ }
+ else
+ {
+ $oPeriod = Piwik_Period::factory($period, Piwik_Date::factory($date));
+ }
+
+ $startDate = $oPeriod->getDateStart();
+ $endDate = $oPeriod->getDateEnd();
+ }
+ else // if the range includes the last N periods
+ {
+ list($date, $lastN) =
+ Piwik_ViewDataTable_GenerateGraphHTML_ChartEvolution::getDateRangeAndLastN($period, $date, $lastN);
+ list($startDate, $endDate) = explode(',', $date);
+
+ $startDate = Piwik_Date::factory($startDate);
+ $endDate = Piwik_Date::factory($endDate);
+ }
+ return array($startDate, $endDate);
+ }
+}
diff --git a/plugins/Annotations/AnnotationList.php b/plugins/Annotations/AnnotationList.php
new file mode 100755
index 0000000000..8031d5c468
--- /dev/null
+++ b/plugins/Annotations/AnnotationList.php
@@ -0,0 +1,446 @@
+<?php
+/**
+ * Piwik - Open source web analytics
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ * @version $Id$
+ *
+ * @category Piwik_Plugins
+ * @package Piwik_Annotations
+ */
+
+/**
+ * This class can be used to query & modify annotations for multiple sites
+ * at once.
+ *
+ * Example use:
+ * $annotations = new Piwik_Annotations_AnnotationList($idSites = "1,2,5");
+ * $annotation = $annotations->get($idSite = 1, $idNote = 4);
+ * // do stuff w/ annotation
+ * $annotations->update($idSite = 2, $idNote = 4, $note = "This is the new text.");
+ * $annotations->save($idSite);
+ *
+ * Note: There is a concurrency issue w/ this code. If two users try to save
+ * an annotation for the same site, it's possible one of their changes will
+ * never get made (as it will be overwritten by the other's).
+ *
+ * @package Piwik_Annotations
+ */
+class Piwik_Annotations_AnnotationList
+{
+ const ANNOTATION_COLLECTION_OPTION_SUFFIX = '_annotations';
+
+ /**
+ * List of site IDs this instance holds annotations for.
+ *
+ * @var array
+ */
+ private $idSites;
+
+ /**
+ * Array that associates lists of annotations with site IDs.
+ *
+ * @var array
+ */
+ private $annotations;
+
+ /**
+ * Constructor. Loads annotations from the database.
+ *
+ * @param string|int $idSites The list of site IDs to load annotations for.
+ */
+ public function __construct( $idSites )
+ {
+ if ($idSites === 'all')
+ {
+ $this->idSites = Piwik_SitesManager_API::getInstance()->getSitesIdWithAtLeastViewAccess();
+ }
+ else
+ {
+ $this->idSites = Piwik_Site::getIdSitesFromIdSitesString($idSites);
+ }
+
+ $this->annotations = $this->getAnnotationsForSite();
+ }
+
+ /**
+ * Returns the list of site IDs this list contains annotations for.
+ *
+ * @return array
+ */
+ public function getIdSites()
+ {
+ return $this->idSites;
+ }
+
+ /**
+ * Creates a new annotation for a site. This method does not perist the result.
+ * To save the new annotation in the database, call $this->save.
+ *
+ * @param int $idSite The ID of the site to add an annotation to.
+ * @param string $date The date the annotation is in reference to.
+ * @param string $note The text of the new annotation.
+ * @param int $starred Either 1 or 0. If 1, the new annotation has been starred,
+ * otherwise it will start out unstarred.
+ * @return array The added annotation.
+ * @throws Exception if $idSite is not an ID that was supplied upon construction.
+ */
+ public function add($idSite, $date, $note, $starred = 0)
+ {
+ $this->checkIdSiteIsLoaded($idSite);
+
+ $this->annotations[$idSite][] = self::makeAnnotation($date, $note, $starred);
+
+ // get the id of the new annotation
+ end($this->annotations[$idSite]);
+ $newNoteId = key($this->annotations[$idSite]);
+
+ return $this->get($idSite, $newNoteId);
+ }
+
+ /**
+ * Persists the annotations list for a site, overwriting whatever exists.
+ *
+ * @param int $idSite The ID of the site to save annotations for.
+ * @throws Exception if $idSite is not an ID that was supplied upon construction.
+ */
+ public function save($idSite)
+ {
+ $this->checkIdSiteIsLoaded($idSite);
+
+ $optionName = self::getAnnotationCollectionOptionName($idSite);
+ Piwik_SetOption($optionName, serialize($this->annotations[$idSite]));
+ }
+
+ /**
+ * Modifies an annotation in this instance's collection of annotations.
+ *
+ * Note: This method does not perist the change in the DB. The save method must
+ * be called for that.
+ *
+ * @param int $idSite The ID of the site whose annotation will be updated.
+ * @param int $idNote The ID of the note.
+ * @param string|null $date The new date of the annotation, eg '2012-01-01'. If
+ * null, no change is made.
+ * @param string|null $note The new text of the annotation. If null, no change
+ * is made.
+ * @param int|null $starred Either 1 or 0, whether the annotation should be
+ * starred or not. If null, no change is made.
+ * @throws Exception if $idSite is not an ID that was supplied upon construction.
+ * @throws Exception if $idNote does not refer to valid note for the site.
+ */
+ public function update( $idSite, $idNote, $date = null, $note = null, $starred = null )
+ {
+ $this->checkIdSiteIsLoaded($idSite);
+ $this->checkNoteExists($idSite, $idNote);
+
+ $annotation =& $this->annotations[$idSite][$idNote];
+ if ($date !== null)
+ {
+ $annotation['date'] = $date;
+ }
+ if ($note !== null)
+ {
+ $annotation['note'] = $note;
+ }
+ if ($starred !== null)
+ {
+ $annotation['starred'] = $starred;
+ }
+ }
+
+ /**
+ * Removes a note from a site's collection of annotations.
+ *
+ * Note: This method does not perist the change in the DB. The save method must
+ * be called for that.
+ *
+ * @param int $idSite The ID of the site whose annotation will be updated.
+ * @param int $idNote The ID of the note.
+ * @throws Exception if $idSite is not an ID that was supplied upon construction.
+ * @throws Exception if $idNote does not refer to valid note for the site.
+ */
+ public function remove( $idSite, $idNote )
+ {
+ $this->checkIdSiteIsLoaded($idSite);
+ $this->checkNoteExists($idSite, $idNote);
+
+ unset($this->annotations[$idSite][$idNote]);
+ }
+
+ /**
+ * Retrieves an annotation by ID.
+ *
+ * This function returns an array with the following elements:
+ * - idNote: The ID of the annotation.
+ * - date: The date of the annotation.
+ * - note: The text of the annotation.
+ * - starred: 1 or 0, whether the annotation is stared;
+ * - user: (unless current user is anonymous) The user that created the annotation.
+ * - canEditOrDelete: True if the user can edit/delete the annotation.
+ *
+ * @param int $idSite The ID of the site to get an annotation for.
+ * @param int $idNote The ID of the note to get.
+ * @param array The annotation.
+ * @throws Exception if $idSite is not an ID that was supplied upon construction.
+ * @throws Exception if $idNote does not refer to valid note for the site.
+ */
+ public function get( $idSite, $idNote )
+ {
+ $this->checkIdSiteIsLoaded($idSite);
+ $this->checkNoteExists($idSite, $idNote);
+
+ $annotation = $this->annotations[$idSite][$idNote];
+ $this->augmentAnnotationData($idSite, $idNote, $annotation);
+ return $annotation;
+ }
+
+ /**
+ * Returns all annotations within a specific date range. The result is
+ * an array that maps site IDs with arrays of annotations within the range.
+ *
+ * Note: The date range is inclusive.
+ *
+ * @see self::get for info on what attributes stored within annotations.
+ *
+ * @param Piwik_Date $startDate The start of the date range.
+ * @param Piwik_Date $endDate THe end of the date range.
+ * @return array Array mapping site IDs with arrays of annotations, eg:
+ * array(
+ * '5' => array(
+ * array(...), // annotation
+ * array(...), // annotation
+ * ...
+ * ),
+ * '6' => array(
+ * array(...), // annotation
+ * array(...), // annotation
+ * ...
+ * ),
+ * )
+ */
+ public function search( $startDate, $endDate )
+ {
+ // collect annotations that are within the right date range & belong to the right
+ // report
+ $result = array();
+ foreach ($this->annotations as $idSite => $annotationForSite)
+ {
+ foreach ($annotationForSite as $idNote => $annotation)
+ {
+ if ($startDate !== false)
+ {
+ $annotationDate = Piwik_Date::factory($annotation['date']);
+ if ($annotationDate->getTimestamp() < $startDate->getTimestamp()
+ || $annotationDate->getTimestamp() > $endDate->getTimestamp())
+ {
+ continue;
+ }
+ }
+
+ $this->augmentAnnotationData($idSite, $idNote, $annotation);
+ $result[$idSite][] = $annotation;
+ }
+
+ // sort by annotation date
+ if (!empty($result[$idSite]))
+ {
+ uasort($result[$idSite], array($this, 'compareAnnotationDate'));
+ }
+ }
+ return $result;
+ }
+
+ /**
+ * Counts annotations & starred annotations within a date range and returns
+ * the counts. The date range includes the start date, but not the end date.
+ *
+ * @param int $idSite The ID of the site to count annotations for.
+ * @param string|false $startDate The start date of the range or false if no
+ * range check is desired.
+ * @param string|false $endDate The end date of the range or false if no
+ * range check is desired.
+ * @return array eg, array('count' => 5, 'starred' => 2)
+ */
+ public function count( $idSite, $startDate, $endDate )
+ {
+ $this->checkIdSiteIsLoaded($idSite);
+
+ // count the annotations
+ $count = $starred = 0;
+ foreach ($this->annotations[$idSite] as $annotation)
+ {
+ $annotationDate = Piwik_Date::factory($annotation['date']);
+
+ // if annotation start date is between start date & end date, increment count
+ if ($annotationDate->getTimestamp() >= $startDate->getTimestamp()
+ && $annotationDate->getTimestamp() < $endDate->getTimestamp())
+ {
+ ++$count;
+
+ if ($annotation['starred'])
+ {
+ ++$starred;
+ }
+ }
+ }
+
+ return array('count' => $count, 'starred' => $starred);
+ }
+
+ /**
+ * Utility function. Creates a new annotation.
+ *
+ * @param string $date
+ * @param string $note
+ * @param int $starred
+ */
+ private function makeAnnotation( $date, $note, $starred = 0 )
+ {
+ return array('date' => $date,
+ 'note' => $note,
+ 'starred' => (int)$starred,
+ 'user' => Piwik::getCurrentUserLogin());
+ }
+
+ /**
+ * Retrieves annotations from the database for the sites supplied to the
+ * constructor.
+ *
+ * @return array Lists of annotations mapped by site ID.
+ */
+ private function getAnnotationsForSite()
+ {
+ $result = array();
+ foreach ($this->idSites as $id)
+ {
+ $optionName = self::getAnnotationCollectionOptionName($id);
+ $serialized = Piwik_GetOption($optionName);
+
+ if ($serialized !== false)
+ {
+ $result[$id] = unserialize($serialized);
+ }
+ else
+ {
+ $result[$id] = array();
+ }
+ }
+ return $result;
+ }
+
+ /**
+ * Utility function that checks if a site ID was supplied and if not,
+ * throws an exception.
+ *
+ * We can only modify/read annotations for sites that we've actually
+ * loaded the annotations for.
+ *
+ * @param int $idSite
+ * @throws Exception
+ */
+ private function checkIdSiteIsLoaded( $idSite )
+ {
+ if (!in_array($idSite, $this->idSites))
+ {
+ throw new Exception("This AnnotationList was not initialized with idSite '$idSite'.");
+ }
+ }
+
+ /**
+ * Utility function that checks if a note exists for a site, and if not,
+ * throws an exception.
+ *
+ * @param int $idSite
+ * @param int $idNote
+ * @throws Exception
+ */
+ private function checkNoteExists( $idSite, $idNote )
+ {
+ if (empty($this->annotations[$idSite][$idNote]))
+ {
+ throw new Exception("There is no note with id '$idNote' for site with id '$idSite'.");
+ }
+ }
+
+ /**
+ * Returns true if the current user can modify or delete a specific annotation.
+ *
+ * A user can modify/delete a note if the user has admin access for the site OR
+ * the user has view access, is not the anonymous user and is the user that
+ * created the note in question.
+ *
+ * @param int $idSite The site ID the annotation belongs to.
+ * @param array $annotation The annotation.
+ * @return bool
+ */
+ public static function canUserModifyOrDelete( $idSite, $annotation )
+ {
+ // user can save if user is admin or if has view access, is not anonymous & is user who wrote note
+ $canEdit = Piwik::isUserHasAdminAccess($idSite)
+ || (!Piwik::isUserIsAnonymous()
+ && Piwik::getCurrentUserLogin() == $annotation['user']);
+ return $canEdit;
+ }
+
+ /**
+ * Adds extra data to an annotation, including the annotation's ID and whether
+ * the current user can edit or delete it.
+ *
+ * Also, if the current user is anonymous, the user attribute is removed.
+ *
+ * @param int $idSite
+ * @param int $idNote
+ * @param array $annotation
+ */
+ private function augmentAnnotationData( $idSite, $idNote, &$annotation )
+ {
+ $annotation['idNote'] = $idNote;
+ $annotation['canEditOrDelete'] = self::canUserModifyOrDelete($idSite, $annotation);
+
+ // we don't supply user info if the current user is anonymous
+ if (Piwik::isUserIsAnonymous())
+ {
+ unset($annotation['user']);
+ }
+ }
+
+ /**
+ * Utility function that compares two annotations.
+ *
+ * @param array $lhs An annotation.
+ * @param array $rhs An annotation.
+ * @return int -1, 0 or 1
+ */
+ public function compareAnnotationDate( $lhs, $rhs )
+ {
+ if ($lhs['date'] == $rhs['date'])
+ {
+ return $lhs['idNote'] <= $rhs['idNote'] ? -1 : 1;
+ }
+
+ return $lhs['date'] < $rhs['date'] ? -1 : 1; // string comparison works because date format should be YYYY-MM-DD
+ }
+
+ /**
+ * Returns true if the current user can add notes for a specific site.
+ *
+ * @param int $idSite The site to add notes to.
+ */
+ public static function canUserAddNotesFor( $idSite )
+ {
+ return Piwik::isUserHasViewAccess($idSite)
+ && !Piwik::isUserIsAnonymous($idSite);
+ }
+
+ /**
+ * Returns the option name used to store annotations for a site.
+ *
+ * @param int $idSite The site ID.
+ */
+ public static function getAnnotationCollectionOptionName( $idSite )
+ {
+ return $idSite.self::ANNOTATION_COLLECTION_OPTION_SUFFIX;
+ }
+}
diff --git a/plugins/Annotations/Annotations.php b/plugins/Annotations/Annotations.php
new file mode 100755
index 0000000000..ed339812e6
--- /dev/null
+++ b/plugins/Annotations/Annotations.php
@@ -0,0 +1,70 @@
+<?php
+/**
+ * Piwik - Open source web analytics
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ * @version $Id$
+ *
+ * @category Piwik_Plugins
+ * @package Piwik_Annotations
+ */
+
+/**
+ * Annotations plugins. Provides the ability to attach text notes to
+ * dates for each sites. Notes can be viewed, modified, deleted or starred.
+ *
+ * @package Piwik_Annotations
+ */
+class Piwik_Annotations extends Piwik_Plugin
+{
+ /**
+ * Returns information about this plugin.
+ *
+ * @return array
+ */
+ public function getInformation()
+ {
+ return array(
+ 'description' => Piwik_Translate('Annotations_PluginDescription'),
+ 'author' => 'Piwik',
+ 'author_homepage' => 'http://piwik.org/',
+ 'version' => Piwik_Version::VERSION,
+ );
+ }
+
+ /**
+ * Returns list of event hooks.
+ *
+ * @return array
+ */
+ public function getListHooksRegistered()
+ {
+ return array(
+ 'AssetManager.getCssFiles' => 'getCssFiles',
+ 'AssetManager.getJsFiles' => 'getJsFiles'
+ );
+ }
+
+ /**
+ * Adds css files for this plugin to the list in the event notification.
+ *
+ * @param Piwik_Event_Notification $notification notification object
+ */
+ function getCssFiles( $notification )
+ {
+ $cssFiles = &$notification->getNotificationObject();
+ $cssFiles[] = "plugins/Annotations/templates/styles.css";
+ }
+
+ /**
+ * Adds js files for this plugin to the list in the event notification.
+ *
+ * @param Piwik_Event_Notification $notification notification object
+ */
+ function getJsFiles( $notification )
+ {
+ $jsFiles = &$notification->getNotificationObject();
+ $jsFiles[] = "plugins/Annotations/templates/annotations.js";
+ }
+}
diff --git a/plugins/Annotations/Controller.php b/plugins/Annotations/Controller.php
new file mode 100755
index 0000000000..0912e9aac5
--- /dev/null
+++ b/plugins/Annotations/Controller.php
@@ -0,0 +1,226 @@
+<?php
+/**
+ * Piwik - Open source web analytics
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ * @version $Id$
+ *
+ * @category Piwik_Plugins
+ * @package Piwik_Annotations
+ */
+
+/**
+ * Controller for the Annotations plugin.
+ *
+ * @package Piwik_Annotations
+ */
+class Piwik_Annotations_Controller extends Piwik_Controller
+{
+ /**
+ * Controller action that returns HTML displaying annotations for a site and
+ * specific date range.
+ *
+ * Query Param Input:
+ * - idSite: The ID of the site to get annotations for. Only one allowed.
+ * - date: The date to get annotations for. If lastN is not supplied, this is the start date,
+ * otherwise the start date in the last period.
+ * - period: The period type.
+ * - lastN: If supplied, the last N # of periods will be included w/ the range specified
+ * by date + period.
+ *
+ * Output:
+ * - HTML displaying annotations for a specific range.
+ *
+ * @param bool $fetch True if the annotation manager should be returned as a string,
+ * false if it should be echo-ed.
+ * @param string $date Override for 'date' query parameter.
+ * @param string $period Override for 'period' query parameter.
+ * @param string $lastN Override for 'lastN' query parameter.
+ * @return string|void
+ */
+ public function getAnnotationManager( $fetch = false, $date = false, $period = false, $lastN = false )
+ {
+ $idSite = Piwik_Common::getRequestVar('idSite');
+
+ if ($date === false)
+ {
+ $date = Piwik_Common::getRequestVar('date', false);
+ }
+
+ if ($period === false)
+ {
+ $period = Piwik_Common::getRequestVar('period', 'day');
+ }
+
+ if ($lastN === false)
+ {
+ $lastN = Piwik_Common::getRequestVar('lastN', false);
+ }
+
+ // create & render the view
+ $view = Piwik_View::factory('annotationManager');
+
+ $allAnnotations = Piwik_API_Request::processRequest(
+ 'Annotations.getAll', array('date' => $date, 'period' => $period, 'lastN' => $lastN));
+ $view->annotations = empty($allAnnotations[$idSite]) ? array() : $allAnnotations[$idSite];
+
+ $view->period = $period;
+ $view->lastN = $lastN;
+
+ list($startDate, $endDate) = Piwik_Annotations_API::getDateRangeForPeriod($date, $period, $lastN);
+ $view->startDate = $startDate->toString();
+ $view->endDate = $endDate->toString();
+
+ $dateFormat = Piwik_Translate('CoreHome_ShortDateFormatWithYear');
+ $view->startDatePretty = $startDate->getLocalized($dateFormat);
+ $view->endDatePretty = $endDate->getLocalized($dateFormat);
+
+ $view->canUserAddNotes = Piwik_Annotations_AnnotationList::canUserAddNotesFor($idSite);
+
+ if ($fetch)
+ {
+ return $view->render();
+ }
+ else
+ {
+ echo $view->render();
+ }
+ }
+
+ /**
+ * Controller action that modifies an annotation and returns HTML displaying
+ * the modified annotation.
+ *
+ * Query Param Input:
+ * - idSite: The ID of the site the annotation belongs to. Only one ID is allowed.
+ * - idNote: The ID of the annotation.
+ * - date: The new date value for the annotation. (optional)
+ * - note: The new text for the annotation. (optional)
+ * - starred: Either 1 or 0. Whether the note should be starred or not. (optional)
+ *
+ * Output:
+ * - HTML displaying modified annotation.
+ *
+ * If an optional query param is not supplied, that part of the annotation is
+ * not modified.
+ */
+ public function saveAnnotation()
+ {
+ if ($_SERVER["REQUEST_METHOD"] == "POST")
+ {
+ $this->checkTokenInUrl();
+
+ $view = Piwik_View::factory('annotation');
+
+ // NOTE: permissions checked in API method
+ // save the annotation
+ $view->annotation = Piwik_API_Request::processRequest("Annotations.save");
+
+ echo $view->render();
+ }
+ }
+
+ /**
+ * Controller action that adds a new annotation for a site and returns new
+ * annotation manager HTML for the site and date range.
+ *
+ * Query Param Input:
+ * - idSite: The ID of the site to add an annotation to.
+ * - date: The date for the new annotation.
+ * - note: The text of the annotation.
+ * - starred: Either 1 or 0, whether the annotation should be starred or not.
+ * Defaults to 0.
+ * - managerDate: The date for the annotation manager. If a range is given, the start
+ * date is used for the new annotation.
+ * - managerPeriod: For rendering the annotation manager. @see self::getAnnotationManager
+ * for more info.
+ * - lastN: For rendering the annotation manager. @see self::getAnnotationManager
+ * for more info.
+ * Output:
+ * - @see self::getAnnotationManager
+ */
+ public function addAnnotation()
+ {
+ if ($_SERVER["REQUEST_METHOD"] == "POST")
+ {
+ $this->checkTokenInUrl();
+
+ // the date used is for the annotation manager HTML that gets echo'd. we
+ // use this date for the new annotation, unless it is a date range, in
+ // which case we use the first date of the range.
+ $date = Piwik_Common::getRequestVar('date');
+ if (strpos($date, ',') !== false)
+ {
+ $date = reset(explode(',', $date));
+ }
+
+ // add the annotation. NOTE: permissions checked in API method
+ Piwik_API_Request::processRequest("Annotations.add", array('date' => $date));
+
+ $managerDate = Piwik_Common::getRequestVar('managerDate', false);
+ $managerPeriod = Piwik_Common::getRequestVar('managerPeriod', false);
+ echo $this->getAnnotationManager($fetch = true, $managerDate, $managerPeriod);
+ }
+ }
+
+ /**
+ * Controller action that deletes an annotation and returns new annotation
+ * manager HTML for the site & date range.
+ *
+ * Query Param Input:
+ * - idSite: The ID of the site this annotation belongs to.
+ * - idNote: The ID of the annotation to delete.
+ * - date: For rendering the annotation manager. @see self::getAnnotationManager
+ * for more info.
+ * - period: For rendering the annotation manager. @see self::getAnnotationManager
+ * for more info.
+ * - lastN: For rendering the annotation manager. @see self::getAnnotationManager
+ * for more info.
+ *
+ * Output:
+ * - @see self::getAnnotationManager
+ */
+ public function deleteAnnotation()
+ {
+ if ($_SERVER["REQUEST_METHOD"] == "POST")
+ {
+ $this->checkTokenInUrl();
+
+ // delete annotation. NOTE: permissions checked in API method
+ Piwik_API_Request::processRequest("Annotations.delete");
+
+ echo $this->getAnnotationManager($fetch = true);
+ }
+ }
+
+ /**
+ * Controller action that echo's HTML that displays marker icons for an
+ * evolution graph's x-axis. The marker icons still need to be positioned
+ * by the JavaScript.
+ *
+ * Query Param Input:
+ * - idSite: The ID of the site this annotation belongs to. Only one is allowed.
+ * - date: The date to check for annotations. If lastN is not supplied, this is
+ * the start of the date range used to check for annotations. If supplied,
+ * this is the start of the last period in the date range.
+ * - period: The period type.
+ * - lastN: If supplied, the last N # of periods are included in the date range
+ * used to check for annotations.
+ *
+ * Output:
+ * - HTML that displays marker icons for an evolution graph based on the
+ * number of annotations & starred annotations in the graph's date range.
+ */
+ public function getEvolutionIcons()
+ {
+ // get annotation the count
+ $annotationCounts = Piwik_API_Request::processRequest("Annotations.getAnnotationCountForDates");
+
+ // create & render the view
+ $view = Piwik_View::factory('evolutionAnnotations');
+ $view->annotationCounts = reset($annotationCounts); // only one idSite allowed for this action
+
+ echo $view->render();
+ }
+}
diff --git a/plugins/Annotations/templates/annotation.tpl b/plugins/Annotations/templates/annotation.tpl
new file mode 100755
index 0000000000..1a94cf60d6
--- /dev/null
+++ b/plugins/Annotations/templates/annotation.tpl
@@ -0,0 +1,43 @@
+<tr class="annotation" data-id="{$annotation.idNote}" data-date="{$annotation.date}">
+ <td class="annotation-meta">
+ <div class="annotation-star{if $annotation.canEditOrDelete} annotation-star-changeable{/if}" data-starred="{$annotation.starred}" {if $annotation.canEditOrDelete}title="{'Annotations_ClickToStarOrUnstar'|translate}"{/if}>
+ {if $annotation.starred}
+ <img src="themes/default/images/star.png"/>
+ {else}
+ <img src="themes/default/images/star_empty.png"/>
+ {/if}
+ </div>
+ <div class="annotation-period {if $annotation.canEditOrDelete}annotation-enter-edit-mode{/if}">({$annotation.date})</div>
+ {if $annotation.canEditOrDelete}
+ <div class="annotation-period-edit" style="display:none">
+ <a href="#">{$annotation.date}</a>
+ <div class="datepicker" style="display:none"/>
+ </div>
+ {/if}
+ </td>
+ <td class="annotation-value">
+ <div class="annotation-view-mode">
+ <span {if $annotation.canEditOrDelete}title="{'Annotations_ClickToEdit'|translate}" class="annotation-enter-edit-mode"{/if}>{$annotation.note|unescape|escape:'html'}</span>
+ {if $annotation.canEditOrDelete}
+ <a href="#" class="edit-annotation annotation-enter-edit-mode" title="{'Annotations_ClickToEdit'|translate}">{'General_Edit'|translate}...</a>
+ {/if}
+ </div>
+ {if $annotation.canEditOrDelete}
+ <div class="annotation-edit-mode" style="display:none">
+ <input class="annotation-edit" type="text" value="{$annotation.note|unescape|escape:'html'}"/>
+ <br/>
+ <input class="annotation-save submit" type="button" value="{'General_Save'|translate}"/>
+ <input class="annotation-cancel submit" type="button" value="{'General_Cancel'|translate}"/>
+ </div>
+ {/if}
+ </td>
+ {if isset($annotation.user) && $userLogin != 'anonymous'}
+ <td class="annotation-user-cell">
+ <span class="annotation-user">{$annotation.user|unescape|escape:'html'}</span><br/>
+ {if $annotation.canEditOrDelete}
+ <a href="#" class="delete-annotation" style="display:none" title="{'Annotations_ClickToDelete'|translate}">Delete</a>
+ {/if}
+ </td>
+ {/if}
+</tr>
+
diff --git a/plugins/Annotations/templates/annotationManager.tpl b/plugins/Annotations/templates/annotationManager.tpl
new file mode 100755
index 0000000000..4afa631cfd
--- /dev/null
+++ b/plugins/Annotations/templates/annotationManager.tpl
@@ -0,0 +1,27 @@
+<div class="annotation-manager"
+ {if $startDate neq $endDate}data-date="{$startDate},{$endDate}" data-period="range"
+ {else}data-date="{$startDate}" data-period="{$period}"
+ {/if}>
+
+<div class="annotations-header">
+ <span>{'Annotations_Annotations'|translate}</span>
+</div>
+
+<div class="annotation-list-range">{$startDatePretty}{if $startDate neq $endDate} &mdash; {$endDatePretty}{/if}</div>
+
+<div class="annotation-list">
+{include file="Annotations/templates/annotations.tpl"}
+
+<span class="loadingPiwik" style="display:none"><img src="themes/default/images/loading-blue.gif"/>{'General_Loading_js'|translate}</span>
+
+</div>
+
+<div class="annotation-controls">
+ {if $canUserAddNotes}
+ <a href="#" class="add-annotation" title="{'Annotations_ClickToAdd'|translate}">{'Annotations_CreateNewAnnotation'|translate}</a>
+ {elseif $userLogin eq 'anonymous'}
+ <a href="index.php?module=Login">{'Annotations_LoginToAnnotate'|translate}</a>
+ {/if}
+</div>
+
+</div>
diff --git a/plugins/Annotations/templates/annotations.js b/plugins/Annotations/templates/annotations.js
new file mode 100755
index 0000000000..0a7ed20edd
--- /dev/null
+++ b/plugins/Annotations/templates/annotations.js
@@ -0,0 +1,583 @@
+/*!
+ * Piwik - Web Analytics
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+(function($, piwik) {
+
+var annotationsApi = {
+
+ // calls Annotations.getAnnotationManager
+ getAnnotationManager: function(idSite, date, period, lastN, callback)
+ {
+ var ajaxParams =
+ {
+ module: 'Annotations',
+ action: 'getAnnotationManager',
+ idSite: idSite,
+ date: date,
+ period: period,
+ };
+ if (lastN)
+ {
+ ajaxParams.lastN = lastN;
+ }
+
+ var ajaxRequest = new ajaxHelper();
+ ajaxRequest.addParams(ajaxParams, 'get');
+ ajaxRequest.setCallback(callback);
+ ajaxRequest.setFormat('html');
+ ajaxRequest.send(false);
+ },
+
+ // calls Annotations.addAnnotation
+ addAnnotation: function(idSite, managerDate, managerPeriod, date, note, callback)
+ {
+ var ajaxParams =
+ {
+ module: 'Annotations',
+ action: 'addAnnotation',
+ idSite: idSite,
+ date: date,
+ managerDate: managerDate,
+ managerPeriod: managerPeriod,
+ note: note
+ };
+
+ var ajaxRequest = new ajaxHelper();
+ ajaxRequest.addParams(ajaxParams, 'get');
+ ajaxRequest.addParams({token_auth: piwik.token_auth}, 'post');
+ ajaxRequest.setCallback(callback);
+ ajaxRequest.setFormat('html');
+ ajaxRequest.send(false);
+ },
+
+ // calls Annotations.saveAnnotation
+ saveAnnotation: function(idSite, idNote, date, noteData, callback)
+ {
+ var ajaxParams =
+ {
+ module: 'Annotations',
+ action: 'saveAnnotation',
+ idSite: idSite,
+ idNote: idNote,
+ date: date
+ };
+
+ for (var key in noteData)
+ {
+ ajaxParams[key] = noteData[key];
+ }
+
+ var ajaxRequest = new ajaxHelper();
+ ajaxRequest.addParams(ajaxParams, 'get');
+ ajaxRequest.addParams({token_auth: piwik.token_auth}, 'post');
+ ajaxRequest.setCallback(callback);
+ ajaxRequest.setFormat('html');
+ ajaxRequest.send(false);
+ },
+
+ // calls Annotations.deleteAnnotation
+ deleteAnnotation: function(idSite, idNote, managerDate, managerPeriod, callback)
+ {
+ var ajaxParams =
+ {
+ module: 'Annotations',
+ action: 'deleteAnnotation',
+ idSite: idSite,
+ idNote: idNote,
+ date: managerDate,
+ period: managerPeriod
+ };
+
+ var ajaxRequest = new ajaxHelper();
+ ajaxRequest.addParams(ajaxParams, 'get');
+ ajaxRequest.addParams({token_auth: piwik.token_auth}, 'post');
+ ajaxRequest.setCallback(callback);
+ ajaxRequest.setFormat('html');
+ ajaxRequest.send(false);
+ },
+
+ // calls Annotations.getEvolutionIcons
+ getEvolutionIcons: function(idSite, date, period, lastN, callback)
+ {
+ var ajaxParams =
+ {
+ module: 'Annotations',
+ action: 'getEvolutionIcons',
+ idSite: idSite,
+ date: date,
+ period: period,
+ };
+ if (lastN)
+ {
+ ajaxParams.lastN = lastN;
+ }
+
+ var ajaxRequest = new ajaxHelper();
+ ajaxRequest.addParams(ajaxParams, 'get');
+ ajaxRequest.setFormat('html');
+ ajaxRequest.setCallback(callback);
+ ajaxRequest.send(false);
+ },
+};
+
+var today = new Date();
+
+/**
+ * Returns options to configure an annotation's datepicker shown in edit mode.
+ *
+ * @param {Element} annotation The annotation element.
+ */
+var getDatePickerOptions = function(annotation)
+{
+ var annotationDateStr = annotation.attr('data-date'),
+ parts = annotationDateStr.split('-'),
+ annotationDate = new Date(parts[0], parts[1] - 1, parts[2]);
+
+ var result = piwik.getBaseDatePickerOptions(annotationDate);
+
+ // make sure days before site start & after today cannot be selected
+ var piwikMinDate = result.minDate;
+ result.beforeShowDay = function (date)
+ {
+ var valid = true;
+
+ // if date is after today or before date of site creation, it cannot be selected
+ if (date > today
+ || date < piwikMinDate)
+ {
+ valid = false;
+ }
+
+ return [valid, ''];
+ };
+
+ // on select a date, change the text of the edit date link
+ result.onSelect = function (dateText)
+ {
+ $('.annotation-period-edit>a', annotation).text(dateText);
+ };
+
+ return result;
+};
+
+/**
+ * Switches the current mode of an annotation between the view/edit modes.
+ *
+ * @param {Element} inAnnotationElement An element within the annotation to toggle the mode of.
+ * Should be two levels nested in the .annotation-value
+ * element.
+ * @return {Element} The .annotation-value element.
+ */
+var toggleAnnotationMode = function(inAnnotationElement)
+{
+ var annotation = $(inAnnotationElement).closest('.annotation');
+ $('.annotation-period,.annotation-period-edit,.delete-annotation,' +
+ '.annotation-edit-mode,.annotation-view-mode', annotation).toggle();
+
+ return $(inAnnotationElement).find('.annotation-value');
+};
+
+/**
+ * Creates the datepicker for an annotation element.
+ *
+ * @param {Element} annotation The annotation element.
+ */
+var createDatePicker = function ( annotation )
+{
+ $('.datepicker', annotation).datepicker(getDatePickerOptions(annotation)).hide();
+};
+
+/**
+ * Creates datepickers for every period edit in an annotation manager.
+ *
+ * @param {Element} manager The annotation manager element.
+ */
+var createDatePickers = function ( manager )
+{
+ $('.annotation-period-edit', manager).each(function() {
+ createDatePicker($(this).parent().parent());
+ });
+}
+
+/**
+ * Replaces the HTML of an annotation manager element, and resets date/period
+ * attributes.
+ *
+ * @param {Element} manager The annotation manager.
+ * @param {string} tml The HTML of the new annotation manager.
+ */
+var replaceAnnotationManager = function(manager, html)
+{
+ var newManager = $(html);
+ manager.html(newManager.html())
+ .attr('data-date', newManager.attr('data-date'))
+ .attr('data-period', newManager.attr('data-period'));
+ createDatePickers(manager);
+};
+
+/**
+ * Returns true if an annotation element is starred, false if otherwise.
+ *
+ * @param {Element} annotation The annotation element.
+ * @return {bool}
+ */
+var isAnnotationStarred = function(annotation)
+{
+ return +$('.annotation-star', annotation).attr('data-starred') == 1 ? true : false;
+};
+
+/**
+ * Replaces the HTML of an annotation element with HTML returned from Piwik, and
+ * makes sure the data attributes are correct.
+ *
+ * @param {Element} annotation The annotation element.
+ * @param {string} html The replacement HTML (or alternatively, the replacement
+ * element/jQuery object).
+ */
+var replaceAnnotationHtml = function ( annotation, html )
+{
+ var newHtml = $(html);
+ annotation.html(newHtml.html()).attr('data-date', newHtml.attr('data-date'));
+ createDatePicker(annotation);
+}
+
+/**
+ * Binds events to an annotation manager element.
+ *
+ * @param {Element} manager The annotation manager.
+ * @param {int} idSite The site ID the manager is showing annotations for.
+ * @param {function} onAnnotationCountChange Callback that is called when there is a change
+ * in the number of annotations and/or starred annotations,
+ * eg, when a user adds a new one or deletes an existing one.
+ */
+var bindAnnotationManagerEvents = function(manager, idSite, onAnnotationCountChange)
+{
+ if (!onAnnotationCountChange)
+ {
+ onAnnotationCountChange = function() {};
+ }
+
+ // show new annotation row if create new annotation link is clicked
+ manager.on('click', '.add-annotation', function(e) {
+ e.preventDefault();
+
+ $('.new-annotation-row', manager).show();
+ $(this).hide();
+
+ return false;
+ });
+
+ // hide new annotation row if cancel button clicked
+ manager.on('click', '.new-annotation-cancel', function() {
+ var newAnnotationRow = $(this).parent().parent();
+ newAnnotationRow.hide();
+
+ $('.add-annotation', newAnnotationRow.closest('.annotation-manager')).show();
+ });
+
+ // save new annotation when new annotation row save is clicked
+ manager.on('click', '.new-annotation-save', function() {
+ var addRow = $(this).parent().parent(),
+ addNoteInput = addRow.find('.new-annotation-edit'),
+ noteDate = addRow.find('.annotation-period-edit>a').text();
+
+ // do nothing if input is empty
+ if (!addNoteInput.val())
+ {
+ return;
+ }
+
+ // disable input & link
+ addNoteInput.attr('disabled', 'disabled');
+
+ // add a new annotation for the site, date & period
+ annotationsApi.addAnnotation(
+ idSite,
+ manager.attr('data-date'),
+ manager.attr('data-period'),
+ noteDate,
+ addNoteInput.val(),
+ function(response) {
+ replaceAnnotationManager(manager, response);
+
+ // increment annotation count for this date
+ onAnnotationCountChange(noteDate, 1, 0);
+ }
+ );
+ });
+
+ // add new annotation when enter key pressed on new annotation input
+ manager.on('keypress', '.new-annotation-edit', function(e) {
+ if (e.which == 13)
+ {
+ $(this).parent().find('.new-annotation-save').click();
+ }
+ });
+
+ // show annotation editor if edit link, annotation text or period text is clicked
+ manager.on('click', '.annotation-enter-edit-mode', function(e) {
+ e.preventDefault();
+
+ var annotationContent = toggleAnnotationMode(this);
+ annotationContent.find('.annotation-edit').focus();
+
+ return false;
+ });
+
+ // hide annotation editor if cancel button is clicked
+ manager.on('click', '.annotation-cancel', function() {
+ toggleAnnotationMode(this);
+ });
+
+ // save annotation if save button clicked
+ manager.on('click', '.annotation-edit-mode .annotation-save', function() {
+ var annotation = $(this).parent().parent().parent(),
+ input = $('.annotation-edit', annotation),
+ dateEditText = $('.annotation-period-edit>a', annotation).text();
+
+ // if annotation value/date has not changed, just show the view mode instead of edit
+ if (input[0].defaultValue == input.val()
+ && dateEditText == annotation.attr('data-date'))
+ {
+ toggleAnnotationMode(this);
+ return;
+ }
+
+ // disable input while ajax is happening
+ input.attr('disabled', 'disabled');
+
+ // save the note w/ the new note text & date
+ annotationsApi.saveAnnotation(
+ idSite,
+ annotation.attr('data-id'),
+ dateEditText,
+ {
+ note: input.val()
+ },
+ function(response) {
+ response = $(response);
+
+ var newDate = response.attr('data-date'),
+ isStarred = isAnnotationStarred(response),
+ originalDate = annotation.attr('data-date');
+
+ replaceAnnotationHtml(annotation, response);
+
+ // if the date has been changed, update the evolution icon counts to reflect the change
+ if (originalDate != newDate)
+ {
+ // reduce count for original date
+ onAnnotationCountChange(originalDate, -1, isStarred ? -1 : 0);
+
+ // increase count for new date
+ onAnnotationCountChange(newDate, 1, isStarred ? 1 : 0);
+ }
+ }
+ );
+ });
+
+ // save annotation if 'enter' pressed on input
+ manager.on('keypress', '.annotation-value input', function(e) {
+ if (e.which == 13)
+ {
+ $(this).parent().find('.annotation-save').click();
+ }
+ });
+
+ // delete annotation if delete link clicked
+ manager.on('click', '.delete-annotation', function(e) {
+ e.preventDefault();
+
+ var annotation = $(this).parent().parent();
+ $(this).attr('disabled', 'disabled');
+
+ // delete annotation by ajax
+ annotationsApi.deleteAnnotation(
+ idSite,
+ annotation.attr('data-id'),
+ manager.attr('data-date'),
+ manager.attr('data-period'),
+ function (response) {
+ manager.html($(response).html());
+
+ // update evolution icons
+ var isStarred = isAnnotationStarred(annotation);
+ onAnnotationCountChange(annotation.attr('data-date'), -1, isStarred ? -1 : 0);
+ }
+ );
+
+ return false;
+ });
+
+ // star/unstar annotation if star clicked
+ manager.on('click', '.annotation-star-changeable', function(e) {
+ var annotation = $(this).parent().parent(),
+ newStarredVal = $(this).attr('data-starred') == 0 ? 1 : 0 // flip existing 'starred' value
+ ;
+
+ // perform ajax request to star annotation
+ annotationsApi.saveAnnotation(
+ idSite,
+ annotation.attr('data-id'),
+ annotation.attr('data-date'),
+ {
+ starred: newStarredVal
+ },
+ function (response) {
+ replaceAnnotationHtml(annotation, response);
+
+ // change starred count for this annotation in evolution graph based on what we're
+ // changing the starred value to
+ onAnnotationCountChange(annotation.attr('data-date'), 0, newStarredVal == 0 ? -1 : 1);
+ }
+ );
+ });
+
+ // when period edit is clicked, show datepicker
+ manager.on('click', '.annotation-period-edit>a', function(e) {
+ e.preventDefault();
+ $('.datepicker', $(this).parent()).toggle();
+ return false;
+ });
+
+ // make sure datepicker popups are closed if someone clicks elsewhere
+ $('body').on('mouseup', function(e) {
+ var container = $('.annotation-period-edit>.datepicker:visible').parent();
+
+ if (!container.has(e.target).length)
+ {
+ container.find('.datepicker').hide();
+ }
+ });
+};
+
+/**
+ * Shows an annotation manager under a report for a specific site & date range.
+ *
+ * @param {Element} domElem The element of the report to show the annotation manger
+ * under.
+ * @param {int} idSite The ID of the site to show the annotations of.
+ * @param {string} date The start date of the period.
+ * @param {string} period The period type.
+ * @param {int} Whether to include the last N periods in the date range or not. Can
+ * be undefined.
+ */
+var showAnnotationViewer = function(domElem, idSite, date, period, lastN, callback)
+{
+ var addToAnnotationCount = function(date, amt, starAmt)
+ {
+ if (date.indexOf(',') != -1)
+ {
+ date = date.split(',')[0];
+ }
+
+ $('.evolution-annotations>span', domElem).each(function() {
+ if ($(this).attr('data-date') == date)
+ {
+ // get counts from attributes (and convert them to ints)
+ var starredCount = +$(this).attr('data-starred'),
+ annotationCount = +$(this).attr('data-count');
+
+ // modify the starred count & make sure the correct image is used
+ var newStarCount = starredCount + starAmt,
+ newImg = 'themes/default/images/' + (newStarCount > 0 ? 'yellow_marker.png' : 'grey_marker.png');
+ $(this).attr('data-starred', newStarCount).find('img').attr('src', newImg);
+
+ // modify the annotation count & hide/show based on new count
+ var newCount = annotationCount + amt;
+ $(this).attr('data-count', newCount).css('opacity', newCount > 0 ? 1 : 0);
+
+ return false;
+ }
+ });
+ };
+
+ var manager = $('.annotation-manager', domElem);
+ if (manager.length)
+ {
+ // if annotations for the requested date + period are already loaded, then just toggle the
+ // visibility of the annotation viewer. otherwise, we reload the annotations.
+ if (manager.attr('data-date') == date
+ && manager.attr('data-period') == period)
+ {
+ // toggle manager view
+ if (manager.is(':hidden'))
+ {
+ manager.slideDown('slow', function () { if (callback) callback(manager) });
+ }
+ else
+ {
+ manager.slideUp('slow', function () { if (callback) callback(manager) });
+ }
+ }
+ else
+ {
+ // show nothing but the loading gif
+ $('.annotations', manager).html('');
+ $('.loadingPiwik', manager).show();
+
+ // reload annotation manager for new date/period
+ annotationsApi.getAnnotationManager(idSite, date, period, lastN, function(response) {
+ replaceAnnotationManager(manager, response);
+
+ createDatePickers(manager);
+
+ // show if hidden
+ if (manager.is(':hidden'))
+ {
+ manager.slideDown('slow', function () { if (callback) callback(manager) });
+ }
+ else
+ {
+ if (callback)
+ {
+ callback(manager);
+ }
+ }
+ });
+ }
+ }
+ else
+ {
+ var loading = $('.loadingPiwikBelow', domElem).css({display: 'block'});
+
+ // the annotations for this report have not been retrieved yet, so do an ajax request
+ // & show the result
+ annotationsApi.getAnnotationManager(idSite, date, period, lastN, function(response) {
+ var manager = $(response).hide();
+
+ // if an error occurred (and response does not contain the annotation manager), do nothing
+ if (!manager.hasClass('annotation-manager'))
+ {
+ return;
+ }
+
+ // create datepickers for each shown annotation
+ createDatePickers(manager);
+
+ bindAnnotationManagerEvents(manager, idSite, addToAnnotationCount);
+
+ loading.css('visibility', 'hidden');
+
+ // add & show annotation manager
+ $('.dataTableFeatures', domElem).append(manager);
+ manager.slideDown('slow', function() {
+ loading.hide().css('visibility', 'visible');
+
+ if (callback) callback(manager)
+ });
+ });
+ }
+};
+
+// make showAnnotationViewer & annotationsApi globally accessible
+piwik.annotations = {
+ showAnnotationViewer: showAnnotationViewer,
+ api: annotationsApi
+};
+
+}(jQuery, piwik));
diff --git a/plugins/Annotations/templates/annotations.tpl b/plugins/Annotations/templates/annotations.tpl
new file mode 100755
index 0000000000..f15750e548
--- /dev/null
+++ b/plugins/Annotations/templates/annotations.tpl
@@ -0,0 +1,30 @@
+<div class="annotations">
+
+{if empty($annotations)}
+
+<div class="empty-annotation-list">{'Annotations_NoAnnotations'|translate}</div>
+
+{/if}
+
+<table>
+{foreach from=$annotations item=annotation}
+{include file="Annotations/templates/annotation.tpl"}
+{/foreach}
+<tr class="new-annotation-row" style="display:none" data-date="{$startDate}">
+ <td class="annotation-meta">
+ <div class="annotation-star">&nbsp;</div>
+ <div class="annotation-period-edit">
+ <a href="#">{$startDate}</a>
+ <div class="datepicker" style="display:none"/>
+ </div>
+ </td>
+ <td class="annotation-value">
+ <input type="text" value="" class="new-annotation-edit" placeholder="{'Annotations_EnterAnnotationText'|translate}"/><br/>
+ <input type="button" class="submit new-annotation-save" value="{'General_Save'|translate}"/>
+ <input type="button" class="submit new-annotation-cancel" value="{'General_Cancel'|translate}"/>
+ </td>
+ <td class="annotation-user-cell"><span class="annotation-user">{$userLogin}</span></td>
+</tr>
+</table>
+
+</div>
diff --git a/plugins/Annotations/templates/evolutionAnnotations.tpl b/plugins/Annotations/templates/evolutionAnnotations.tpl
new file mode 100755
index 0000000000..5248bddbf2
--- /dev/null
+++ b/plugins/Annotations/templates/evolutionAnnotations.tpl
@@ -0,0 +1,12 @@
+<div class="evolution-annotations">
+{foreach from=$annotationCounts item=dateCountPair}
+ {assign var=date value=$dateCountPair[0]}
+ {assign var=counts value=$dateCountPair[1]}
+ <span data-date="{$date}" data-count="{$counts.count}" data-starred="{$counts.starred}"
+ {if $counts.count eq 0}title="{'CoreHome_Annotations_AddAnnotationsFor_js'|translate:$date}"
+ {else}title="{'CoreHome_Annotations_ViewAndAddAnnotations_js'|translate:$date}"
+ {/if}>
+ <img src="themes/default/images/{if $counts.starred > 0}yellow_marker.png{else}grey_marker.png{/if}" width="16" height="16"/>
+ </span>
+{/foreach}
+</div>
diff --git a/plugins/Annotations/templates/styles.css b/plugins/Annotations/templates/styles.css
new file mode 100755
index 0000000000..4831bafcb1
--- /dev/null
+++ b/plugins/Annotations/templates/styles.css
@@ -0,0 +1,194 @@
+.evolution-annotations {
+ position: relative;
+ height: 16px;
+ width: 100%;
+ margin-top: 12px;
+ margin-bottom: -28px;
+ cursor: pointer;
+}
+
+.evolution-annotations > span {
+ position: absolute;
+}
+
+.annotation-manager {
+ text-align: left;
+ margin-top: -18px;
+}
+
+.annotations-header {
+ display: inline-block;
+ width: 128px;
+ text-align: right;
+ font-size: 12px;
+ font-style: italic;
+ margin-bottom: 8px;
+ vertical-align: top;
+ color: #666;
+}
+
+.annotation-controls {
+ display:inline-block;
+ margin-left: 132px;
+}
+
+.annotation-controls>a {
+ font-size: 11px;
+ font-style: italic;
+ color: #666;
+ cursor:pointer;
+ padding:3px 0 6px 0;
+ display:inline-block;
+}
+
+.annotation-controls>a:hover {
+ text-decoration:none;
+}
+
+.annotation-list {
+ margin-left: 8px;
+}
+
+.annotation-list table {
+ width: 100%;
+}
+
+.annotation-list-range {
+ display: inline-block;
+ font-size: 12px;
+ font-style: italic;
+ color: #666;
+ vertical-align: top;
+ margin: 0 0 8px 8px;
+}
+
+.empty-annotation-list,.annotation-list .loadingPiwik {
+ display: block;
+
+ font-style: italic;
+ color: #666;
+ margin: 0 0 12px 140px;
+}
+
+.annotation-meta {
+ width: 128px;
+ text-align: right;
+ vertical-align: top;
+ font-size:14px;
+}
+
+.annotation-user {
+ font-style: italic;
+ font-size: 11px;
+ color:#444;
+}
+
+.annotation-user-cell {
+ vertical-align: top;
+ width: 92px;
+}
+
+.annotation-period {
+ display:inline-block;
+ font-style: italic;
+ margin: 0 8px 8px 8px;
+ vertical-align: top;
+}
+
+.annotation-value {
+ margin: 0 12px 12px 8px;
+ vertical-align: top;
+ position: relative;
+ font-size:14px;
+}
+
+.annotation-enter-edit-mode {
+ cursor: pointer;
+}
+
+.annotation-edit,.new-annotation-edit {
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+ width:98%;
+}
+
+.annotation-star {
+ display: inline-block;
+ margin: 0 8px 8px 0;
+ width: 16px;
+}
+
+.annotation-star-changeable {
+ cursor: pointer;
+}
+
+.delete-annotation {
+ font-size:12px;
+ font-style: italic;
+ color: red;
+ text-decoration: none;
+ display: inline-block;
+}
+
+.delete-annotation:hover {
+ text-decoration: underline;
+}
+
+.annotation-manager .submit {
+ float:none;
+}
+
+.edit-annotation {
+ font-size:10px;
+ color:#666;
+ font-style:italic;
+}
+.edit-annotation:hover {
+ text-decoration:none;
+}
+
+.annotationView {
+ float: right;
+ margin-left: 5px;
+ position: relative;
+ cursor:pointer;
+}
+
+.annotationView > span {
+ font-style: italic;
+ display: inline-block;
+ margin: 4px 4px 0 4px;
+}
+
+.annotation-period-edit {
+ display:inline-block;
+ background:white;
+ color:#444;
+ font-size:12px;
+ border: 1px solid #e4e5e4;
+ padding:5px 5px 6px 3px;
+ border-radius:4px;
+ -moz-border-radius:4px;
+ -webkit-border-radius:4px;
+}
+.annotation-period-edit:hover {
+ background:#f1f0eb;
+ border-color:#a9a399;
+}
+.annotation-period-edit>a {
+ text-decoration:none;
+ cursor:pointer;
+ display:block;
+}
+.annotation-period-edit>.datepicker {
+ position:absolute;
+ margin-top:6px;
+ margin-left:-5px;
+ z-index:15;
+ background:white;
+ border: 1px solid #e4e5e4;
+ border-radius:4px;
+ -moz-border-radius:4px;
+ -webkit-border-radius:4px;
+}
diff --git a/plugins/CoreHome/templates/calendar.js b/plugins/CoreHome/templates/calendar.js
index 9588577b23..096728e0c6 100644
--- a/plugins/CoreHome/templates/calendar.js
+++ b/plugins/CoreHome/templates/calendar.js
@@ -121,11 +121,9 @@ function isDateInCurrentPeriod( date )
return [true, ''];
}
-var updateDate;
-function getDatePickerOptions()
+piwik.getBaseDatePickerOptions = function(defaultDate)
{
return {
- onSelect: function () { updateDate.apply(this, arguments); },
showOtherMonths: false,
dateFormat: 'yy-mm-dd',
firstDay: 1,
@@ -134,11 +132,10 @@ function getDatePickerOptions()
prevText: "",
nextText: "",
currentText: "",
- beforeShowDay: isDateInCurrentPeriod,
- defaultDate: currentDate,
+ defaultDate: defaultDate,
changeMonth: true,
changeYear: true,
- stepMonths: selectedPeriod == 'year' ? 12 : 1,
+ stepMonths: 1,
// jquery-ui-i18n 1.7.2 lacks some translations, so we use our own
dayNamesMin: [
_pk_translate('CoreHome_DaySu_js'),
@@ -190,7 +187,17 @@ function getDatePickerOptions()
_pk_translate('CoreHome_MonthOctober_js'),
_pk_translate('CoreHome_MonthNovember_js'),
_pk_translate('CoreHome_MonthDecember_js')]
- }
+ };
+};
+
+var updateDate;
+function getDatePickerOptions()
+{
+ var result = piwik.getBaseDatePickerOptions(currentDate);
+ result.beforeShowDay = isDateInCurrentPeriod;
+ result.stepMonths = selectedPeriod == 'year' ? 12 : 1;
+ result.onSelect = function () { updateDate.apply(this, arguments); };
+ return result;
};
$(document).ready(function() {
diff --git a/plugins/CoreHome/templates/datatable.css b/plugins/CoreHome/templates/datatable.css
index 126451750c..a19fdc0564 100644
--- a/plugins/CoreHome/templates/datatable.css
+++ b/plugins/CoreHome/templates/datatable.css
@@ -343,6 +343,11 @@ table thead div {
height: 0;
}
+.dataTable .loadingPiwikBelow {
+ padding-bottom:5px;
+ display:block;
+ text-align:center;
+}
.dataTableFooterIcons {
display:block;
@@ -623,6 +628,7 @@ body .piwik-tooltip.rowActionTooltip {
position: relative;
margin-left: 5px;
min-height: 20px;
+ z-index:1;
}
.limitSelection.hidden {
diff --git a/plugins/CoreHome/templates/datatable.js b/plugins/CoreHome/templates/datatable.js
index 03c1684e22..a2e57f098a 100644
--- a/plugins/CoreHome/templates/datatable.js
+++ b/plugins/CoreHome/templates/datatable.js
@@ -227,6 +227,8 @@ dataTable.prototype =
self.handleLimit(domElem);
self.handleSearchBox(domElem);
self.handleOffsetInformation(domElem);
+ self.handleAnnotationsButton(domElem);
+ self.handleEvolutionAnnotations(domElem);
self.handleExportBox(domElem);
self.applyCosmetics(domElem);
self.handleSubDataTable(domElem);
@@ -535,7 +537,156 @@ dataTable.prototype =
}
);
},
-
+
+ handleEvolutionAnnotations: function(domElem)
+ {
+ var self = this;
+ if (self.param.viewDataTable == 'graphEvolution'
+ && $('.annotationView', domElem).length > 0)
+ {
+ // get dates w/ annotations across evolution period (have to do it through AJAX since we
+ // determine placement using the elements created by jqplot)
+ piwik.annotations.api.getEvolutionIcons(
+ self.param.idSite,
+ self.param.date,
+ self.param.period,
+ self.param['evolution_' + self.param.period + '_last_n'],
+ function (response)
+ {
+ var canvases = $('.piwik-graph .jqplot-xaxis canvas', domElem),
+ datatableFeatures = $('.dataTableFeatures', domElem),
+ noteSize = 16,
+ annotationAxisHeight = 30 // css height + padding + margin
+ ;
+
+ // set position of evolution annotation icons
+ var annotations = $(response).css({
+ top: -datatableFeatures.height() - annotationAxisHeight + noteSize / 2,
+ left: 6 // padding-left of .jqplot-evolution element (in graph.tpl)
+ });
+
+ // set position of each individual icon
+ $('span', annotations).each(function(i) {
+ var canvas = $(canvases[i]),
+ canvasCenterX = canvas.position().left + (canvas.width() / 2);
+ $(this).css({
+ left: canvasCenterX - noteSize / 2,
+ // show if there are annotations for this x-axis tick
+ opacity: +$(this).attr('data-count') > 0 ? 1 : 0
+ });
+ });
+
+ // add new section under axis
+ datatableFeatures.append(annotations);
+
+ // on hover of x-axis, show note icon over correct part of x-axis
+ $('span', annotations).hover(
+ function() { $(this).css('opacity', 1); },
+ function() {
+ if ($(this).attr('data-count') == 0) // only hide if there are no annotations for this note
+ {
+ $(this).css('opacity', 0);
+ }
+ }
+ );
+
+ // when clicking an annotation, show the annotation viewer for that day
+ $('span', annotations).click(function() {
+ var spanSelf = $(this),
+ date = spanSelf.attr('data-date'),
+ oldDate = $('.annotation-manager', domElem).attr('data-date');
+ if (date)
+ {
+ piwik.annotations.showAnnotationViewer(
+ domElem,
+ self.param.idSite,
+ date,
+ self.param.period,
+ undefined, // lastN
+ function (manager) {
+ manager.attr('data-is-range', 0);
+ $('.annotationView img', domElem)
+ .attr('title', _pk_translate('CoreHome_Annotations_IconDesc_js'));
+
+ var viewAndAdd = _pk_translate('CoreHome_Annotations_ViewAndAddAnnotations_js'),
+ hideNotes = _pk_translate('CoreHome_Annotations_HideAnnotationsFor_js');
+
+ // change the tooltip of the previously clicked evolution icon (if any)
+ if (oldDate)
+ {
+ $('span', annotations).each(function() {
+ if ($(this).attr('data-date') == oldDate)
+ {
+ $(this).attr('title', viewAndAdd.replace("%s", oldDate));
+ return false;
+ }
+ });
+ }
+
+ // change the tooltip of the clicked evolution icon
+ if (manager.is(':hidden'))
+ {
+ spanSelf.attr('title', viewAndAdd.replace("%s", date));
+ }
+ else
+ {
+ spanSelf.attr('title', hideNotes.replace("%s", date));
+ }
+ }
+ );
+ }
+ });
+ }
+ );
+ }
+ },
+
+ handleAnnotationsButton: function(domElem)
+ {
+ var self = this;
+ if (self.param.idSubtable) // no annotations for subtables, just whole reports
+ {
+ return;
+ }
+
+ // show the annotations view on click
+ $('.annotationView', domElem).click(function() {
+ var annotationManager = $('.annotation-manager', domElem);
+
+ if (annotationManager.length > 0
+ && annotationManager.attr('data-is-range') == 1)
+ {
+ if (annotationManager.is(':hidden'))
+ {
+ annotationManager.slideDown('slow'); // showing
+ $('img', this).attr('title', _pk_translate('CoreHome_Annotations_IconDescHideNotes_js'));
+ }
+ else
+ {
+ annotationManager.slideUp('slow'); // hiding
+ $('img', this).attr('title', _pk_translate('CoreHome_Annotations_IconDesc_js'));
+ }
+ }
+ else
+ {
+ // show the annotation viewer for the whole date range
+ var lastN = self.param['evolution_' + self.param.period + '_last_n'];
+ piwik.annotations.showAnnotationViewer(
+ domElem,
+ self.param.idSite,
+ self.param.date,
+ self.param.period,
+ lastN,
+ function(manager) {
+ manager.attr('data-is-range', 1);
+ }
+ );
+
+ // change the tooltip of the view annotation icon
+ $('img', this).attr('title', _pk_translate('CoreHome_Annotations_IconDescHideNotes_js'));
+ }
+ });
+ },
// DataTable view box (simple table, all columns table, Goals table, pie graph, tag cloud, graph, ...)
handleExportBox: function(domElem)
@@ -1472,6 +1623,7 @@ actionDataTable.prototype =
reloadAjaxDataTable: dataTable.prototype.reloadAjaxDataTable,
handleConfigurationBox: dataTable.prototype.handleConfigurationBox,
handleSearchBox: dataTable.prototype.handleSearchBox,
+ handleAnnotationsButton: dataTable.prototype.handleAnnotationsButton,
handleExportBox: dataTable.prototype.handleExportBox,
handleSort: dataTable.prototype.handleSort,
handleColumnDocumentation: dataTable.prototype.handleColumnDocumentation,
@@ -1526,6 +1678,7 @@ actionDataTable.prototype =
self.applyCosmetics(domElem);
self.handleRowActions(domElem);
self.handleLimit(domElem);
+ self.handleAnnotationsButton(domElem);
self.handleExportBox(domElem);
self.handleSort(domElem);
self.handleOffsetInformation(domElem);
diff --git a/plugins/CoreHome/templates/datatable_footer.tpl b/plugins/CoreHome/templates/datatable_footer.tpl
index 4388a40c3c..11222fac2d 100644
--- a/plugins/CoreHome/templates/datatable_footer.tpl
+++ b/plugins/CoreHome/templates/datatable_footer.tpl
@@ -59,8 +59,7 @@
</span>
</div>
- {/if}
-
+ {/if}
<div class="tableIconsGroup">
<span class="exportToFormatIcons"><a class="tableIcon" var="export"><img width="16" height="16" src="themes/default/images/export.png" title="{'General_ExportThisReport'|translate}" /></a></span>
<span class="exportToFormatItems" style="display:none">
@@ -95,6 +94,12 @@
{/if}
</ul>
</div>
+ {if !$properties.hide_annotations_view}
+ <div class="annotationView">
+ <a class="tableIcon"><img width="16" height="16" src="themes/default/images/grey_marker.png" title="{'CoreHome_Annotations_IconDesc_js'|translate}"/></a>
+ <span>{'Annotations_Annotations'|translate}</span>
+ </div>
+ {/if}
</div>
{/if}
@@ -116,4 +121,6 @@
</div>
+<span class="loadingPiwikBelow" style='display:none'><img src="themes/default/images/loading-blue.gif" /> {'General_LoadingData'|translate}</span>
+
<div class="dataTableSpacer"></div>
diff --git a/tests/PHPUnit/Core/API/ResponseBuilderTest.php b/tests/PHPUnit/Core/API/ResponseBuilderTest.php
index ab98164197..fbfb80bb53 100644
--- a/tests/PHPUnit/Core/API/ResponseBuilderTest.php
+++ b/tests/PHPUnit/Core/API/ResponseBuilderTest.php
@@ -76,73 +76,4 @@ class API_ResponseBuilderTest extends PHPUnit_Framework_TestCase
$actual = Piwik_API_ResponseBuilder::convertMultiDimensionalArrayToJson($input);
$this->assertEquals($expected, $actual);
}
-
- /**
- * Two dimensions standard array
- *
- * @group Core
- * @group API
- * @group API_ResponseBuilder
- */
- public function testConvertMultiDimensionalStandardArrayToXML()
- {
- $input = array( "firstElement",
- array(
- "firstElement",
- "secondElement",
- ),
- "thirdElement");
-
- $expected = '<row>firstElement</row><row><row>firstElement</row><row>secondElement</row></row><row>thirdElement</row>';
- $actual = preg_replace("/[\t\n]+/", '', Piwik_API_ResponseBuilder::convertMultiDimensionalArrayToXml($input));
- $this->assertEquals($expected, $actual);
- }
-
- /**
- * Two dimensions associative array
- *
- * @group Core
- * @group API
- * @group API_ResponseBuilder
- */
- public function testConvertMultiDimensionalAssociativeArrayToXML()
- {
- $input = array(
- "firstElement" => "isFirst",
- "secondElement" => array(
- "firstElement" => "isFirst",
- "secondElement" => "isSecond",
- ),
- "thirdElement" => "isThird");
-
- $expected = '<firstElement>isFirst</firstElement><secondElement><firstElement>isFirst</firstElement><secondElement>isSecond</secondElement></secondElement><thirdElement>isThird</thirdElement>';
- $actual = preg_replace("/[\t\n]+/", '', Piwik_API_ResponseBuilder::convertMultiDimensionalArrayToXml($input));
- $this->assertEquals($expected, $actual);
- }
-
- /**
- * Two dimensions mixed array
- *
- * @group Core
- * @group API
- * @group API_ResponseBuilder
- */
- public function testConvertMultiDimensionalMixedArrayToXML()
- {
- $input = array(
- "firstElement" => "isFirst",
- array(
- "firstElement",
- "secondElement",
- ),
- "thirdElement" => array(
- "firstElement" => "isFirst",
- "secondElement" => "isSecond",
- )
- );
-
- $expected = '<firstElement>isFirst</firstElement><row><row>firstElement</row><row>secondElement</row></row><thirdElement><firstElement>isFirst</firstElement><secondElement>isSecond</secondElement></thirdElement>';
- $actual = preg_replace("/[\t\n]+/", '', Piwik_API_ResponseBuilder::convertMultiDimensionalArrayToXml($input));
- $this->assertEquals($expected, $actual);
- }
}
diff --git a/tests/PHPUnit/Core/DataTable/Renderer/JSONTest.php b/tests/PHPUnit/Core/DataTable/Renderer/JSONTest.php
index 4b2e921b41..c5970b3997 100644
--- a/tests/PHPUnit/Core/DataTable/Renderer/JSONTest.php
+++ b/tests/PHPUnit/Core/DataTable/Renderer/JSONTest.php
@@ -391,4 +391,72 @@ class DataTable_Renderer_JSONTest extends PHPUnit_Framework_TestCase
$this->assertEquals($expected, $rendered);
}
+ /**
+ * @group Core
+ * @group DataTable
+ * @group DataTable_Renderer
+ * @group DataTable_Renderer_XML
+ */
+ public function testRenderArray1()
+ {
+ $data = array();
+
+ $render = new Piwik_DataTable_Renderer_Json();
+ $render->setTable($data);
+ $expected = '[]';
+
+ $this->assertEquals($expected, $render->render());
+ }
+
+ /**
+ * @group Core
+ * @group DataTable
+ * @group DataTable_Renderer
+ * @group DataTable_Renderer_XML
+ */
+ public function testRenderArray2()
+ {
+ $data = array('a', 'b', 'c', array('a' => 'b'), array(1, 2));
+
+ $render = new Piwik_DataTable_Renderer_Json();
+ $render->setTable($data);
+ $expected = '["a","b","c",{"a":"b"},[1,2]]';
+
+ $this->assertEquals($expected, $render->render());
+ }
+
+ /**
+ * @group Core
+ * @group DataTable
+ * @group DataTable_Renderer
+ * @group DataTable_Renderer_XML
+ */
+ public function testRenderArray3()
+ {
+ $data = array('a' => 'b', 'c' => 'd', 'e' => 'f', 5 => 'g');
+
+ $render = new Piwik_DataTable_Renderer_Json();
+ $render->setTable($data);
+ $expected = '[{"a":"b","c":"d","e":"f","5":"g"}]';
+
+ $this->assertEquals($expected, $render->render());
+ }
+
+ /**
+ * @group Core
+ * @group DataTable
+ * @group DataTable_Renderer
+ * @group DataTable_Renderer_XML
+ */
+ public function testRenderArray4()
+ {
+ $data = array('a' => 'b', 'c' => array(1,2,3,4), 'e' => array('f' => 'g', 'h' => 'i', 'j' => 'k'));
+
+ $render = new Piwik_DataTable_Renderer_Json();
+ $render->setTable($data);
+ $expected = '{"a":"b","c":[1,2,3,4],"e":{"f":"g","h":"i","j":"k"}}';
+
+ $this->assertEquals($expected, $render->render());
+ }
+
}
diff --git a/tests/PHPUnit/Core/DataTable/Renderer/XMLTest.php b/tests/PHPUnit/Core/DataTable/Renderer/XMLTest.php
index c4798a9b12..0594c0a1da 100644
--- a/tests/PHPUnit/Core/DataTable/Renderer/XMLTest.php
+++ b/tests/PHPUnit/Core/DataTable/Renderer/XMLTest.php
@@ -547,4 +547,99 @@ class DataTable_Renderer_XMLTest extends PHPUnit_Framework_TestCase
$rendered = $render->render();
$this->assertEquals($expected, $rendered);
}
+
+ /**
+ * @group Core
+ * @group DataTable
+ * @group DataTable_Renderer
+ * @group DataTable_Renderer_XML
+ */
+ public function testRenderArray1()
+ {
+ $data = array();
+
+ $render = new Piwik_DataTable_Renderer_Xml();
+ $render->setTable($data);
+ $expected = '<?xml version="1.0" encoding="utf-8" ?>
+<result />';
+
+ $this->assertEquals($expected, $render->render());
+ }
+
+ /**
+ * @group Core
+ * @group DataTable
+ * @group DataTable_Renderer
+ * @group DataTable_Renderer_XML
+ */
+ public function testRenderArray2()
+ {
+ $data = array('a', 'b', 'c');
+
+ $render = new Piwik_DataTable_Renderer_Xml();
+ $render->setTable($data);
+ $expected = '<?xml version="1.0" encoding="utf-8" ?>
+<result>
+ <row>a</row>
+ <row>b</row>
+ <row>c</row>
+</result>';
+
+ $this->assertEquals($expected, $render->render());
+ }
+
+ /**
+ * @group Core
+ * @group DataTable
+ * @group DataTable_Renderer
+ * @group DataTable_Renderer_XML
+ */
+ public function testRenderArray3()
+ {
+ $data = array('a' => 'b', 'c' => 'd', 'e' => 'f', 5 => 'g');
+
+ $render = new Piwik_DataTable_Renderer_Xml();
+ $render->setTable($data);
+ $expected = '<?xml version="1.0" encoding="utf-8" ?>
+<result>
+ <row>
+ <a>b</a>
+ <c>d</c>
+ <e>f</e>
+ <row key="5">g</row>
+ </row>
+</result>';
+
+ $this->assertEquals($expected, $render->render());
+ }
+
+ /**
+ * @group Core
+ * @group DataTable
+ * @group DataTable_Renderer
+ * @group DataTable_Renderer_XML
+ */
+ public function testRenderArray4()
+ {
+ $data = array('c' => array(1,2,3,4), 'e' => array('f' => 'g', 'h' => 'i', 'j' => 'k'));
+
+ $render = new Piwik_DataTable_Renderer_Xml();
+ $render->setTable($data);
+ $expected = '<?xml version="1.0" encoding="utf-8" ?>
+<result>
+ <c>
+ <row>1</row>
+ <row>2</row>
+ <row>3</row>
+ <row>4</row>
+ </c>
+ <e>
+ <f>g</f>
+ <h>i</h>
+ <j>k</j>
+ </e>
+</result>';
+
+ $this->assertEquals($expected, $render->render());
+ }
}
diff --git a/tests/PHPUnit/Core/PiwikTest.php b/tests/PHPUnit/Core/PiwikTest.php
index e7077c17c3..ac43311e6d 100644
--- a/tests/PHPUnit/Core/PiwikTest.php
+++ b/tests/PHPUnit/Core/PiwikTest.php
@@ -204,4 +204,29 @@ class PiwikTest extends DatabaseTestCase
Piwik::getPrettyValue($idsite, $columnName, $value, false, false)
);
}
+
+ /**
+ * Data provider for testIsAssociativeArray.
+ */
+ public function getIsAssociativeArrayTestCases()
+ {
+ return array(
+ array(array(0 => 'a', 1 => 'b', 2 => 'c', 3 => 'd', 4 => 'e', 5 => 'f'), false),
+ array(array(-1 => 'a', 0 => 'a', 1 => 'a', 2 => 'a', 3 => 'a'), true),
+ array(array(4 => 'a', 5 => 'a', 6 => 'a', 7 => 'a', 8 => 'a'), true),
+ array(array(0 => 'a', 2 => 'a', 3 => 'a', 4 => 'a', 5 => 'a'), true),
+ array(array('abc' => 'a', 0 => 'b', 'sdfds' => 'd'), true),
+ array(array('abc' => 'def'), true)
+ );
+ }
+
+ /**
+ * @group Core
+ * @group Piwik
+ * @dataProvider getIsAssociativeArrayTestCases
+ */
+ public function testIsAssociativeArray( $array, $expected )
+ {
+ $this->assertEquals($expected, Piwik::isAssociativeArray($array));
+ }
}
diff --git a/tests/PHPUnit/Integration/AnnotationsTest.php b/tests/PHPUnit/Integration/AnnotationsTest.php
new file mode 100755
index 0000000000..01fb767d82
--- /dev/null
+++ b/tests/PHPUnit/Integration/AnnotationsTest.php
@@ -0,0 +1,415 @@
+<?php
+/**
+ * Piwik - Open source web analytics
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ * @version $Id$
+ */
+
+class AnnotationsTest extends IntegrationTestCase
+{
+ protected static $dateTime = '2011-01-01 00:11:42';
+ protected static $idSite1 = 1;
+ protected static $idSite2 = 2;
+
+ public static function setUpBeforeClass()
+ {
+ parent::setUpBeforeClass();
+ try {
+ self::setUpWebsitesAndGoals();
+ self::addAnnotations();
+ } catch(Exception $e) {
+ // Skip whole test suite if an error occurs while setup
+ throw new PHPUnit_Framework_SkippedTestSuiteError($e->getMessage());
+ }
+ }
+
+ public function getOutputPrefix()
+ {
+ return 'annotations';
+ }
+
+ public function getApiForTesting()
+ {
+ return array(
+
+ // get
+ array('Annotations.get', array('idSite' => self::$idSite1,
+ 'date' => '2012-01-01',
+ 'periods' => 'day',
+ 'otherRequestParameters' => array('idNote' => 1))),
+
+ // getAll
+ array('Annotations.getAll', array('idSite' => self::$idSite1,
+ 'date' => '2011-12-01',
+ 'periods' => array('day', 'week', 'month'))),
+ array('Annotations.getAll', array('idSite' => self::$idSite1,
+ 'date' => '2012-01-01',
+ 'periods' => array('year'))),
+ array('Annotations.getAll', array('idSite' => self::$idSite1,
+ 'date' => '2012-03-01',
+ 'periods' => array('week'),
+ 'otherRequestParameters' => array('lastN' => 6),
+ 'testSuffix' => '_lastN')),
+ array('Annotations.getAll', array('idSite' => self::$idSite1,
+ 'date' => '2012-01-15,2012-02-15',
+ 'periods' => array('range'),
+ 'otherRequestParameters' => array('lastN' => 6),
+ 'testSuffix' => '_range')),
+ array('Annotations.getAll', array('idSite' => 'all',
+ 'date' => '2012-01-01',
+ 'periods' => array('month'),
+ 'testSuffix' => '_multipleSites')),
+
+ // getAnnotationCountForDates
+ array('Annotations.getAnnotationCountForDates', array('idSite' => self::$idSite1,
+ 'date' => '2011-12-01',
+ 'periods' => array('day', 'week', 'month'))),
+ array('Annotations.getAnnotationCountForDates', array('idSite' => self::$idSite1,
+ 'date' => '2012-01-01',
+ 'periods' => array('year'))),
+ array('Annotations.getAnnotationCountForDates', array('idSite' => self::$idSite1,
+ 'date' => '2012-03-01',
+ 'periods' => array('week'),
+ 'otherRequestParameters' => array('lastN' => 6),
+ 'testSuffix' => '_lastN')),
+ array('Annotations.getAnnotationCountForDates', array('idSite' => self::$idSite1,
+ 'date' => '2012-01-15,2012-02-15',
+ 'periods' => array('range'),
+ 'otherRequestParameters' => array('lastN' => 6),
+ 'testSuffix' => '_range')),
+ array('Annotations.getAnnotationCountForDates', array('idSite' => 'all',
+ 'date' => '2012-01-01',
+ 'periods' => array('month'),
+ 'testSuffix' => '_multipleSites')),
+ );
+ }
+
+ /**
+ * @dataProvider getApiForTesting
+ * @group Integration
+ * @group Annotations
+ */
+ public function testApi($api, $params)
+ {
+ $this->runApiTests($api, $params);
+ }
+
+ /**
+ * @group Integration
+ * @group Annotations
+ */
+ public function testAddMultipleSitesFail()
+ {
+ try
+ {
+ Piwik_Annotations_API::getInstance()->add("1,2,3", "2012-01-01", "whatever");
+ $this->fail("add should fail when given multiple sites in idSite");
+ }
+ catch (Exception $ex)
+ {
+ // pass
+ }
+ }
+
+ /**
+ * @group Integration
+ * @group Annotations
+ */
+ public function testAddInvalidDateFail()
+ {
+ try
+ {
+ Piwik_Annotations_API::getInstance()->add(self::$idSite1, "invaliddate", "whatever");
+ $this->fail("add should fail when given invalid date");
+ }
+ catch (Exception $ex)
+ {
+ // pass
+ }
+ }
+
+ /**
+ * @group Integration
+ * @group Annotations
+ */
+ public function testSaveMultipleSitesFail()
+ {
+ try
+ {
+ Piwik_Annotations_API::getInstance()->save("1,2,3", 0);
+ $this->fail("save should fail when given multiple sites");
+ }
+ catch (Exception $ex)
+ {
+ // pass
+ }
+ }
+
+ /**
+ * @group Integration
+ * @group Annotations
+ */
+ public function testSaveInvalidDateFail()
+ {
+ try
+ {
+ Piwik_Annotations_API::getInstance()->save(self::$idSite1, 0, "invaliddate");
+ $this->fail("save should fail when given an invalid date");
+ }
+ catch (Exception $ex)
+ {
+ // pass
+ }
+ }
+
+ /**
+ * @group Integration
+ * @group Annotations
+ */
+ public function testSaveInvalidNoteIdFail()
+ {
+ try
+ {
+ Piwik_Annotations_API::getInstance()->save(self::$idSite1, -1);
+ $this->fail("save should fail when given an invalid note id");
+ }
+ catch (Exception $ex)
+ {
+ // pass
+ }
+ }
+
+ /**
+ * @group Integration
+ * @group Annotations
+ */
+ public function testDeleteMultipleSitesFail()
+ {
+ try
+ {
+ Piwik_Annotations_API::getInstance()->delete("1,2,3", 0);
+ $this->fail("delete should fail when given multiple site IDs");
+ }
+ catch (Exception $ex)
+ {
+ // pass
+ }
+ }
+
+ /**
+ * @group Integration
+ * @group Annotations
+ */
+ public function testDeleteInvalidNoteIdFail()
+ {
+ try
+ {
+ Piwik_Annotations_API::getInstance()->delete(self::$idSite1, -1);
+ $this->fail("delete should fail when given an invalid site ID");
+ }
+ catch (Exception $ex)
+ {
+ // pass
+ }
+ }
+
+ /**
+ * @group Integration
+ * @group Annotations
+ */
+ public function testGetMultipleSitesFail()
+ {
+ try
+ {
+ Piwik_Annotations_API::getInstance()->get("1,2,3", 0);
+ $this->fail("get should fail when given multiple site IDs");
+ }
+ catch (Exception $ex)
+ {
+ // pass
+ }
+ }
+
+ /**
+ * @group Integration
+ * @group Annotations
+ */
+ public function testGetInvalidNoteIdFail()
+ {
+ try
+ {
+ Piwik_Annotations_API::getInstance()->get(self::$idSite1, -1);
+ $this->fail("get should fail when given an invalid note ID");
+ }
+ catch (Exception $ex)
+ {
+ // pass
+ }
+ }
+
+ /**
+ * @group Integration
+ * @group Annotations
+ */
+ public function testSaveSuccess()
+ {
+ Piwik_Annotations_API::getInstance()->save(
+ self::$idSite1, 0, $date = '2011-04-01', $note = 'new note text', $starred = 1);
+
+ $expectedAnnotation = array(
+ 'date' => '2011-04-01',
+ 'note' => 'new note text',
+ 'starred' => 1,
+ 'user' => 'superUserLogin',
+ 'idNote' => 0,
+ 'canEditOrDelete' => true
+ );
+ $this->assertEquals($expectedAnnotation, Piwik_Annotations_API::getInstance()->get(self::$idSite1, 0));
+ }
+
+ /**
+ * @group Integration
+ * @group Annotations
+ */
+ public function testSaveNoChangesSuccess()
+ {
+ Piwik_Annotations_API::getInstance()->save(self::$idSite1, 1);
+
+ $expectedAnnotation = array(
+ 'date' => '2011-12-02',
+ 'note' => '1: Site 1 annotation for 2011-12-02',
+ 'starred' => 0,
+ 'user' => 'superUserLogin',
+ 'idNote' => 1,
+ 'canEditOrDelete' => true
+ );
+ $this->assertEquals($expectedAnnotation, Piwik_Annotations_API::getInstance()->get(self::$idSite1, 1));
+ }
+
+ /**
+ * @group Integration
+ * @group Annotations
+ */
+ public function testDeleteSuccess()
+ {
+ Piwik_Annotations_API::getInstance()->delete(self::$idSite1, 1);
+
+ try
+ {
+ Piwik_Annotations_API::getInstance()->get(self::$idSite1, 1);
+ $this->fail("failed to delete annotation");
+ }
+ catch (Exception $ex)
+ {
+ // pass
+ }
+ }
+
+ public function getPermissionsFailData()
+ {
+ return array(
+ // getAll
+ array(false, false, "module=API&method=Annotations.getAll&idSite=1&date=2012-01-01&period=year", true, "getAll should throw if user does not have view access"),
+
+ // get
+ array(false, false, "module=API&method=Annotations.get&idSite=1&idNote=0", true, "get should throw if user does not have view access"),
+
+ // getAnnotationCountForDates
+ array(false, false, "module=API&method=Annotations.getAnnotationCountForDates&idSite=1&date=2012-01-01&period=year", true, "getAnnotationCountForDates should throw if user does not have view access"),
+
+ // add
+ array(false, false, "module=API&method=Annotations.add&idSite=1&date=2011-02-01&note=whatever", true, "add should throw if user does not have view access"),
+ array(false, true, "module=API&method=Annotations.add&idSite=1&date=2011-02-01&note=whatever2", false, "add should not throw if user has view access"),
+ array(true, true, "module=API&method=Annotations.add&idSite=1&date=2011-02-01&note=whatever3", false, "add should not throw if user has admin access"),
+
+ // save
+ array(false, false, "module=API&method=Annotations.save&idSite=1&idNote=0&date=2011-03-01&note=newnote", true, "save should throw if user does not have view access"),
+ array(false, true, "module=API&method=Annotations.save&idSite=1&idNote=0&date=2011-03-01&note=newnote", true, "save should throw if user has view access but did not edit note"),
+ array(true, true, "module=API&method=Annotations.save&idSite=1&idNote=0&date=2011-03-01&note=newnote", false, "save should not throw if user has admin access"),
+
+ // delete
+ array(false, false, "module=API&method=Annotations.delete&idSite=1&idNote=0", true, "delete should throw if user does not have view access"),
+ array(false, true, "module=API&method=Annotations.delete&idSite=1&idNote=0", true, "delete should throw if user does not have view access"),
+ array(true, true, "module=API&method=Annotations.delete&idSite=1&idNote=0", false, "delete should not throw if user has admin access"),
+ );
+ }
+
+ /**
+ * @dataProvider getPermissionsFailData
+ * @group Integration
+ * @group Annotations
+ */
+ public function testMethodPermissions( $hasAdminAccess, $hasViewAccess, $request, $checkException, $failMessage )
+ {
+ // create fake access that denies user access
+ $access = new FakeAccess();
+ FakeAccess::$superUser = false;
+ FakeAccess::$idSitesAdmin = $hasAdminAccess ? array(self::$idSite1) : array();
+ FakeAccess::$idSitesView = $hasViewAccess ? array(self::$idSite1) : array();
+ Zend_Registry::set('access', $access);
+
+ if ($checkException)
+ {
+ try
+ {
+ $request = new Piwik_Api_Request($request);
+ $request->process();
+ $this->fail($failMessage);
+ }
+ catch (Exception $ex)
+ {
+ // pass
+ }
+ }
+ else
+ {
+ $request = new Piwik_Api_Request($request);
+ $request->process();
+ }
+ }
+
+ private static function addAnnotations()
+ {
+ // create fake access for fake username
+ $access = new FakeAccess();
+ FakeAccess::$superUser = true;
+ Zend_Registry::set('access', $access);
+
+ // add two annotations per week for three months, starring every third annotation
+ // first month in 2011, second two in 2012
+ $count = 0;
+ $dateStart = Piwik_Date::factory('2011-12-01');
+ $dateEnd = Piwik_Date::factory('2012-03-01');
+ while ($dateStart->getTimestamp() < $dateEnd->getTimestamp())
+ {
+ $starred = $count % 3 == 0 ? 1 : 0;
+ $site1Text = "$count: Site 1 annotation for ".$dateStart->toString();
+ $site2Text = "$count: Site 2 annotation for ".$dateStart->toString();
+
+ Piwik_Annotations_API::getInstance()->add(self::$idSite1, $dateStart->toString(), $site1Text, $starred);
+ Piwik_Annotations_API::getInstance()->add(self::$idSite2, $dateStart->toString(), $site2Text, $starred);
+
+ $nextDay = $dateStart->addDay(1);
+ ++$count;
+
+ $starred = $count % 3 == 0 ? 1 : 0;
+ $site1Text = "$count: Site 1 annotation for ".$nextDay->toString();
+ $site2Text = "$count: Site 2 annotation for ".$nextDay->toString();
+
+ Piwik_Annotations_API::getInstance()->add(self::$idSite1, $nextDay->toString(), $site1Text, $starred);
+ Piwik_Annotations_API::getInstance()->add(self::$idSite2, $nextDay->toString(), $site2Text, $starred);
+
+ $dateStart = $dateStart->addPeriod(1, 'WEEK');
+ ++$count;
+ }
+ }
+
+ private static function setUpWebsitesAndGoals()
+ {
+ // add two websites
+ self::createWebsite(self::$dateTime, $ecommerce = 1);
+ self::createWebsite(self::$dateTime, $ecommerce = 1);
+ }
+}
diff --git a/tests/PHPUnit/Integration/expected/test_ImportLogs__Goals.getGoals.xml b/tests/PHPUnit/Integration/expected/test_ImportLogs__Goals.getGoals.xml
index 14e6786d82..b33fbbd767 100755
--- a/tests/PHPUnit/Integration/expected/test_ImportLogs__Goals.getGoals.xml
+++ b/tests/PHPUnit/Integration/expected/test_ImportLogs__Goals.getGoals.xml
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8" ?>
<result>
- <row>
+ <row key="1">
<idsite>1</idsite>
<idgoal>1</idgoal>
<name>all</name>
diff --git a/tests/PHPUnit/Integration/expected/test_OneVisitorTwoVisits__Goals.getGoals.xml b/tests/PHPUnit/Integration/expected/test_OneVisitorTwoVisits__Goals.getGoals.xml
index 792ce9ebcd..c9cc82f044 100644
--- a/tests/PHPUnit/Integration/expected/test_OneVisitorTwoVisits__Goals.getGoals.xml
+++ b/tests/PHPUnit/Integration/expected/test_OneVisitorTwoVisits__Goals.getGoals.xml
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8" ?>
<result>
- <row>
+ <row key="1">
<idsite>1</idsite>
<idgoal>1</idgoal>
<name>triggered js</name>
@@ -9,7 +9,7 @@
<revenue>0</revenue>
<deleted>0</deleted>
</row>
- <row>
+ <row key="2">
<idsite>1</idsite>
<idgoal>2</idgoal>
<name>matching purchase.htm</name>
diff --git a/tests/PHPUnit/Integration/expected/test_OneVisitorTwoVisits_withCookieSupport__Goals.getGoals.xml b/tests/PHPUnit/Integration/expected/test_OneVisitorTwoVisits_withCookieSupport__Goals.getGoals.xml
index 792ce9ebcd..c9cc82f044 100644
--- a/tests/PHPUnit/Integration/expected/test_OneVisitorTwoVisits_withCookieSupport__Goals.getGoals.xml
+++ b/tests/PHPUnit/Integration/expected/test_OneVisitorTwoVisits_withCookieSupport__Goals.getGoals.xml
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8" ?>
<result>
- <row>
+ <row key="1">
<idsite>1</idsite>
<idgoal>1</idgoal>
<name>triggered js</name>
@@ -9,7 +9,7 @@
<revenue>0</revenue>
<deleted>0</deleted>
</row>
- <row>
+ <row key="2">
<idsite>1</idsite>
<idgoal>2</idgoal>
<name>matching purchase.htm</name>
diff --git a/tests/PHPUnit/Integration/expected/test_annotations__Annotations.get.xml b/tests/PHPUnit/Integration/expected/test_annotations__Annotations.get.xml
new file mode 100755
index 0000000000..8ac7b4520a
--- /dev/null
+++ b/tests/PHPUnit/Integration/expected/test_annotations__Annotations.get.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<result>
+ <row>
+ <date>2011-12-02</date>
+ <note>1: Site 1 annotation for 2011-12-02</note>
+ <starred>0</starred>
+ <user>superUserLogin</user>
+ <idNote>1</idNote>
+ <canEditOrDelete>1</canEditOrDelete>
+ </row>
+</result> \ No newline at end of file
diff --git a/tests/PHPUnit/Integration/expected/test_annotations__Annotations.getAll_day.xml b/tests/PHPUnit/Integration/expected/test_annotations__Annotations.getAll_day.xml
new file mode 100755
index 0000000000..c65b44912c
--- /dev/null
+++ b/tests/PHPUnit/Integration/expected/test_annotations__Annotations.getAll_day.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<result>
+ <row key="1">
+ <row>
+ <date>2011-12-01</date>
+ <note>0: Site 1 annotation for 2011-12-01</note>
+ <starred>1</starred>
+ <user>superUserLogin</user>
+ <idNote>0</idNote>
+ <canEditOrDelete>1</canEditOrDelete>
+ </row>
+ </row>
+</result> \ No newline at end of file
diff --git a/tests/PHPUnit/Integration/expected/test_annotations__Annotations.getAll_month.xml b/tests/PHPUnit/Integration/expected/test_annotations__Annotations.getAll_month.xml
new file mode 100755
index 0000000000..fa326bf6fd
--- /dev/null
+++ b/tests/PHPUnit/Integration/expected/test_annotations__Annotations.getAll_month.xml
@@ -0,0 +1,85 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<result>
+ <row key="1">
+ <row>
+ <date>2011-12-01</date>
+ <note>0: Site 1 annotation for 2011-12-01</note>
+ <starred>1</starred>
+ <user>superUserLogin</user>
+ <idNote>0</idNote>
+ <canEditOrDelete>1</canEditOrDelete>
+ </row>
+ <row>
+ <date>2011-12-02</date>
+ <note>1: Site 1 annotation for 2011-12-02</note>
+ <starred>0</starred>
+ <user>superUserLogin</user>
+ <idNote>1</idNote>
+ <canEditOrDelete>1</canEditOrDelete>
+ </row>
+ <row>
+ <date>2011-12-08</date>
+ <note>2: Site 1 annotation for 2011-12-08</note>
+ <starred>0</starred>
+ <user>superUserLogin</user>
+ <idNote>2</idNote>
+ <canEditOrDelete>1</canEditOrDelete>
+ </row>
+ <row>
+ <date>2011-12-09</date>
+ <note>3: Site 1 annotation for 2011-12-09</note>
+ <starred>1</starred>
+ <user>superUserLogin</user>
+ <idNote>3</idNote>
+ <canEditOrDelete>1</canEditOrDelete>
+ </row>
+ <row>
+ <date>2011-12-15</date>
+ <note>4: Site 1 annotation for 2011-12-15</note>
+ <starred>0</starred>
+ <user>superUserLogin</user>
+ <idNote>4</idNote>
+ <canEditOrDelete>1</canEditOrDelete>
+ </row>
+ <row>
+ <date>2011-12-16</date>
+ <note>5: Site 1 annotation for 2011-12-16</note>
+ <starred>0</starred>
+ <user>superUserLogin</user>
+ <idNote>5</idNote>
+ <canEditOrDelete>1</canEditOrDelete>
+ </row>
+ <row>
+ <date>2011-12-22</date>
+ <note>6: Site 1 annotation for 2011-12-22</note>
+ <starred>1</starred>
+ <user>superUserLogin</user>
+ <idNote>6</idNote>
+ <canEditOrDelete>1</canEditOrDelete>
+ </row>
+ <row>
+ <date>2011-12-23</date>
+ <note>7: Site 1 annotation for 2011-12-23</note>
+ <starred>0</starred>
+ <user>superUserLogin</user>
+ <idNote>7</idNote>
+ <canEditOrDelete>1</canEditOrDelete>
+ </row>
+ <row>
+ <date>2011-12-29</date>
+ <note>8: Site 1 annotation for 2011-12-29</note>
+ <starred>0</starred>
+ <user>superUserLogin</user>
+ <idNote>8</idNote>
+ <canEditOrDelete>1</canEditOrDelete>
+ </row>
+ <row>
+ <date>2011-12-30</date>
+ <note>9: Site 1 annotation for 2011-12-30</note>
+ <starred>1</starred>
+ <user>superUserLogin</user>
+ <idNote>9</idNote>
+ <canEditOrDelete>1</canEditOrDelete>
+ </row>
+ </row>
+</result> \ No newline at end of file
diff --git a/tests/PHPUnit/Integration/expected/test_annotations__Annotations.getAll_week.xml b/tests/PHPUnit/Integration/expected/test_annotations__Annotations.getAll_week.xml
new file mode 100755
index 0000000000..3d0078beac
--- /dev/null
+++ b/tests/PHPUnit/Integration/expected/test_annotations__Annotations.getAll_week.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<result>
+ <row key="1">
+ <row>
+ <date>2011-12-01</date>
+ <note>0: Site 1 annotation for 2011-12-01</note>
+ <starred>1</starred>
+ <user>superUserLogin</user>
+ <idNote>0</idNote>
+ <canEditOrDelete>1</canEditOrDelete>
+ </row>
+ <row>
+ <date>2011-12-02</date>
+ <note>1: Site 1 annotation for 2011-12-02</note>
+ <starred>0</starred>
+ <user>superUserLogin</user>
+ <idNote>1</idNote>
+ <canEditOrDelete>1</canEditOrDelete>
+ </row>
+ </row>
+</result> \ No newline at end of file
diff --git a/tests/PHPUnit/Integration/expected/test_annotations__Annotations.getAll_year.xml b/tests/PHPUnit/Integration/expected/test_annotations__Annotations.getAll_year.xml
new file mode 100755
index 0000000000..f2f2835d16
--- /dev/null
+++ b/tests/PHPUnit/Integration/expected/test_annotations__Annotations.getAll_year.xml
@@ -0,0 +1,133 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<result>
+ <row key="1">
+ <row>
+ <date>2012-01-05</date>
+ <note>10: Site 1 annotation for 2012-01-05</note>
+ <starred>0</starred>
+ <user>superUserLogin</user>
+ <idNote>10</idNote>
+ <canEditOrDelete>1</canEditOrDelete>
+ </row>
+ <row>
+ <date>2012-01-06</date>
+ <note>11: Site 1 annotation for 2012-01-06</note>
+ <starred>0</starred>
+ <user>superUserLogin</user>
+ <idNote>11</idNote>
+ <canEditOrDelete>1</canEditOrDelete>
+ </row>
+ <row>
+ <date>2012-01-12</date>
+ <note>12: Site 1 annotation for 2012-01-12</note>
+ <starred>1</starred>
+ <user>superUserLogin</user>
+ <idNote>12</idNote>
+ <canEditOrDelete>1</canEditOrDelete>
+ </row>
+ <row>
+ <date>2012-01-13</date>
+ <note>13: Site 1 annotation for 2012-01-13</note>
+ <starred>0</starred>
+ <user>superUserLogin</user>
+ <idNote>13</idNote>
+ <canEditOrDelete>1</canEditOrDelete>
+ </row>
+ <row>
+ <date>2012-01-19</date>
+ <note>14: Site 1 annotation for 2012-01-19</note>
+ <starred>0</starred>
+ <user>superUserLogin</user>
+ <idNote>14</idNote>
+ <canEditOrDelete>1</canEditOrDelete>
+ </row>
+ <row>
+ <date>2012-01-20</date>
+ <note>15: Site 1 annotation for 2012-01-20</note>
+ <starred>1</starred>
+ <user>superUserLogin</user>
+ <idNote>15</idNote>
+ <canEditOrDelete>1</canEditOrDelete>
+ </row>
+ <row>
+ <date>2012-01-26</date>
+ <note>16: Site 1 annotation for 2012-01-26</note>
+ <starred>0</starred>
+ <user>superUserLogin</user>
+ <idNote>16</idNote>
+ <canEditOrDelete>1</canEditOrDelete>
+ </row>
+ <row>
+ <date>2012-01-27</date>
+ <note>17: Site 1 annotation for 2012-01-27</note>
+ <starred>0</starred>
+ <user>superUserLogin</user>
+ <idNote>17</idNote>
+ <canEditOrDelete>1</canEditOrDelete>
+ </row>
+ <row>
+ <date>2012-02-02</date>
+ <note>18: Site 1 annotation for 2012-02-02</note>
+ <starred>1</starred>
+ <user>superUserLogin</user>
+ <idNote>18</idNote>
+ <canEditOrDelete>1</canEditOrDelete>
+ </row>
+ <row>
+ <date>2012-02-03</date>
+ <note>19: Site 1 annotation for 2012-02-03</note>
+ <starred>0</starred>
+ <user>superUserLogin</user>
+ <idNote>19</idNote>
+ <canEditOrDelete>1</canEditOrDelete>
+ </row>
+ <row>
+ <date>2012-02-09</date>
+ <note>20: Site 1 annotation for 2012-02-09</note>
+ <starred>0</starred>
+ <user>superUserLogin</user>
+ <idNote>20</idNote>
+ <canEditOrDelete>1</canEditOrDelete>
+ </row>
+ <row>
+ <date>2012-02-10</date>
+ <note>21: Site 1 annotation for 2012-02-10</note>
+ <starred>1</starred>
+ <user>superUserLogin</user>
+ <idNote>21</idNote>
+ <canEditOrDelete>1</canEditOrDelete>
+ </row>
+ <row>
+ <date>2012-02-16</date>
+ <note>22: Site 1 annotation for 2012-02-16</note>
+ <starred>0</starred>
+ <user>superUserLogin</user>
+ <idNote>22</idNote>
+ <canEditOrDelete>1</canEditOrDelete>
+ </row>
+ <row>
+ <date>2012-02-17</date>
+ <note>23: Site 1 annotation for 2012-02-17</note>
+ <starred>0</starred>
+ <user>superUserLogin</user>
+ <idNote>23</idNote>
+ <canEditOrDelete>1</canEditOrDelete>
+ </row>
+ <row>
+ <date>2012-02-23</date>
+ <note>24: Site 1 annotation for 2012-02-23</note>
+ <starred>1</starred>
+ <user>superUserLogin</user>
+ <idNote>24</idNote>
+ <canEditOrDelete>1</canEditOrDelete>
+ </row>
+ <row>
+ <date>2012-02-24</date>
+ <note>25: Site 1 annotation for 2012-02-24</note>
+ <starred>0</starred>
+ <user>superUserLogin</user>
+ <idNote>25</idNote>
+ <canEditOrDelete>1</canEditOrDelete>
+ </row>
+ </row>
+</result> \ No newline at end of file
diff --git a/tests/PHPUnit/Integration/expected/test_annotations__Annotations.getAnnotationCountForDates_day.xml b/tests/PHPUnit/Integration/expected/test_annotations__Annotations.getAnnotationCountForDates_day.xml
new file mode 100755
index 0000000000..7409bdf738
--- /dev/null
+++ b/tests/PHPUnit/Integration/expected/test_annotations__Annotations.getAnnotationCountForDates_day.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<result>
+ <row key="1">
+ <row>
+ <row>2011-12-01</row>
+ <row>
+ <count>1</count>
+ <starred>1</starred>
+ </row>
+ </row>
+ </row>
+</result> \ No newline at end of file
diff --git a/tests/PHPUnit/Integration/expected/test_annotations__Annotations.getAnnotationCountForDates_month.xml b/tests/PHPUnit/Integration/expected/test_annotations__Annotations.getAnnotationCountForDates_month.xml
new file mode 100755
index 0000000000..152be4e272
--- /dev/null
+++ b/tests/PHPUnit/Integration/expected/test_annotations__Annotations.getAnnotationCountForDates_month.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<result>
+ <row key="1">
+ <row>
+ <row>2011-12-01</row>
+ <row>
+ <count>10</count>
+ <starred>4</starred>
+ </row>
+ </row>
+ </row>
+</result> \ No newline at end of file
diff --git a/tests/PHPUnit/Integration/expected/test_annotations__Annotations.getAnnotationCountForDates_week.xml b/tests/PHPUnit/Integration/expected/test_annotations__Annotations.getAnnotationCountForDates_week.xml
new file mode 100755
index 0000000000..d428059285
--- /dev/null
+++ b/tests/PHPUnit/Integration/expected/test_annotations__Annotations.getAnnotationCountForDates_week.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<result>
+ <row key="1">
+ <row>
+ <row>2011-11-28</row>
+ <row>
+ <count>2</count>
+ <starred>1</starred>
+ </row>
+ </row>
+ </row>
+</result> \ No newline at end of file
diff --git a/tests/PHPUnit/Integration/expected/test_annotations__Annotations.getAnnotationCountForDates_year.xml b/tests/PHPUnit/Integration/expected/test_annotations__Annotations.getAnnotationCountForDates_year.xml
new file mode 100755
index 0000000000..3d501c35f1
--- /dev/null
+++ b/tests/PHPUnit/Integration/expected/test_annotations__Annotations.getAnnotationCountForDates_year.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<result>
+ <row key="1">
+ <row>
+ <row>2012-01-01</row>
+ <row>
+ <count>16</count>
+ <starred>5</starred>
+ </row>
+ </row>
+ </row>
+</result> \ No newline at end of file
diff --git a/tests/PHPUnit/Integration/expected/test_annotations_lastN__Annotations.getAll_week.xml b/tests/PHPUnit/Integration/expected/test_annotations_lastN__Annotations.getAll_week.xml
new file mode 100755
index 0000000000..d3aecc147b
--- /dev/null
+++ b/tests/PHPUnit/Integration/expected/test_annotations_lastN__Annotations.getAll_week.xml
@@ -0,0 +1,85 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<result>
+ <row key="1">
+ <row>
+ <date>2012-01-26</date>
+ <note>16: Site 1 annotation for 2012-01-26</note>
+ <starred>0</starred>
+ <user>superUserLogin</user>
+ <idNote>16</idNote>
+ <canEditOrDelete>1</canEditOrDelete>
+ </row>
+ <row>
+ <date>2012-01-27</date>
+ <note>17: Site 1 annotation for 2012-01-27</note>
+ <starred>0</starred>
+ <user>superUserLogin</user>
+ <idNote>17</idNote>
+ <canEditOrDelete>1</canEditOrDelete>
+ </row>
+ <row>
+ <date>2012-02-02</date>
+ <note>18: Site 1 annotation for 2012-02-02</note>
+ <starred>1</starred>
+ <user>superUserLogin</user>
+ <idNote>18</idNote>
+ <canEditOrDelete>1</canEditOrDelete>
+ </row>
+ <row>
+ <date>2012-02-03</date>
+ <note>19: Site 1 annotation for 2012-02-03</note>
+ <starred>0</starred>
+ <user>superUserLogin</user>
+ <idNote>19</idNote>
+ <canEditOrDelete>1</canEditOrDelete>
+ </row>
+ <row>
+ <date>2012-02-09</date>
+ <note>20: Site 1 annotation for 2012-02-09</note>
+ <starred>0</starred>
+ <user>superUserLogin</user>
+ <idNote>20</idNote>
+ <canEditOrDelete>1</canEditOrDelete>
+ </row>
+ <row>
+ <date>2012-02-10</date>
+ <note>21: Site 1 annotation for 2012-02-10</note>
+ <starred>1</starred>
+ <user>superUserLogin</user>
+ <idNote>21</idNote>
+ <canEditOrDelete>1</canEditOrDelete>
+ </row>
+ <row>
+ <date>2012-02-16</date>
+ <note>22: Site 1 annotation for 2012-02-16</note>
+ <starred>0</starred>
+ <user>superUserLogin</user>
+ <idNote>22</idNote>
+ <canEditOrDelete>1</canEditOrDelete>
+ </row>
+ <row>
+ <date>2012-02-17</date>
+ <note>23: Site 1 annotation for 2012-02-17</note>
+ <starred>0</starred>
+ <user>superUserLogin</user>
+ <idNote>23</idNote>
+ <canEditOrDelete>1</canEditOrDelete>
+ </row>
+ <row>
+ <date>2012-02-23</date>
+ <note>24: Site 1 annotation for 2012-02-23</note>
+ <starred>1</starred>
+ <user>superUserLogin</user>
+ <idNote>24</idNote>
+ <canEditOrDelete>1</canEditOrDelete>
+ </row>
+ <row>
+ <date>2012-02-24</date>
+ <note>25: Site 1 annotation for 2012-02-24</note>
+ <starred>0</starred>
+ <user>superUserLogin</user>
+ <idNote>25</idNote>
+ <canEditOrDelete>1</canEditOrDelete>
+ </row>
+ </row>
+</result> \ No newline at end of file
diff --git a/tests/PHPUnit/Integration/expected/test_annotations_lastN__Annotations.getAnnotationCountForDates_week.xml b/tests/PHPUnit/Integration/expected/test_annotations_lastN__Annotations.getAnnotationCountForDates_week.xml
new file mode 100755
index 0000000000..2abbab5b3f
--- /dev/null
+++ b/tests/PHPUnit/Integration/expected/test_annotations_lastN__Annotations.getAnnotationCountForDates_week.xml
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<result>
+ <row key="1">
+ <row>
+ <row>2012-01-23</row>
+ <row>
+ <count>2</count>
+ <starred>0</starred>
+ </row>
+ </row>
+ <row>
+ <row>2012-01-30</row>
+ <row>
+ <count>2</count>
+ <starred>1</starred>
+ </row>
+ </row>
+ <row>
+ <row>2012-02-06</row>
+ <row>
+ <count>2</count>
+ <starred>1</starred>
+ </row>
+ </row>
+ <row>
+ <row>2012-02-13</row>
+ <row>
+ <count>2</count>
+ <starred>0</starred>
+ </row>
+ </row>
+ <row>
+ <row>2012-02-20</row>
+ <row>
+ <count>2</count>
+ <starred>1</starred>
+ </row>
+ </row>
+ <row>
+ <row>2012-02-27</row>
+ <row>
+ <count>0</count>
+ <starred>0</starred>
+ </row>
+ </row>
+ </row>
+</result> \ No newline at end of file
diff --git a/tests/PHPUnit/Integration/expected/test_annotations_multipleSites__Annotations.getAll_month.xml b/tests/PHPUnit/Integration/expected/test_annotations_multipleSites__Annotations.getAll_month.xml
new file mode 100755
index 0000000000..0d3c8ae176
--- /dev/null
+++ b/tests/PHPUnit/Integration/expected/test_annotations_multipleSites__Annotations.getAll_month.xml
@@ -0,0 +1,135 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<result>
+ <row key="1">
+ <row>
+ <date>2012-01-05</date>
+ <note>10: Site 1 annotation for 2012-01-05</note>
+ <starred>0</starred>
+ <user>superUserLogin</user>
+ <idNote>10</idNote>
+ <canEditOrDelete>1</canEditOrDelete>
+ </row>
+ <row>
+ <date>2012-01-06</date>
+ <note>11: Site 1 annotation for 2012-01-06</note>
+ <starred>0</starred>
+ <user>superUserLogin</user>
+ <idNote>11</idNote>
+ <canEditOrDelete>1</canEditOrDelete>
+ </row>
+ <row>
+ <date>2012-01-12</date>
+ <note>12: Site 1 annotation for 2012-01-12</note>
+ <starred>1</starred>
+ <user>superUserLogin</user>
+ <idNote>12</idNote>
+ <canEditOrDelete>1</canEditOrDelete>
+ </row>
+ <row>
+ <date>2012-01-13</date>
+ <note>13: Site 1 annotation for 2012-01-13</note>
+ <starred>0</starred>
+ <user>superUserLogin</user>
+ <idNote>13</idNote>
+ <canEditOrDelete>1</canEditOrDelete>
+ </row>
+ <row>
+ <date>2012-01-19</date>
+ <note>14: Site 1 annotation for 2012-01-19</note>
+ <starred>0</starred>
+ <user>superUserLogin</user>
+ <idNote>14</idNote>
+ <canEditOrDelete>1</canEditOrDelete>
+ </row>
+ <row>
+ <date>2012-01-20</date>
+ <note>15: Site 1 annotation for 2012-01-20</note>
+ <starred>1</starred>
+ <user>superUserLogin</user>
+ <idNote>15</idNote>
+ <canEditOrDelete>1</canEditOrDelete>
+ </row>
+ <row>
+ <date>2012-01-26</date>
+ <note>16: Site 1 annotation for 2012-01-26</note>
+ <starred>0</starred>
+ <user>superUserLogin</user>
+ <idNote>16</idNote>
+ <canEditOrDelete>1</canEditOrDelete>
+ </row>
+ <row>
+ <date>2012-01-27</date>
+ <note>17: Site 1 annotation for 2012-01-27</note>
+ <starred>0</starred>
+ <user>superUserLogin</user>
+ <idNote>17</idNote>
+ <canEditOrDelete>1</canEditOrDelete>
+ </row>
+ </row>
+ <row key="2">
+ <row>
+ <date>2012-01-05</date>
+ <note>10: Site 2 annotation for 2012-01-05</note>
+ <starred>0</starred>
+ <user>superUserLogin</user>
+ <idNote>10</idNote>
+ <canEditOrDelete>1</canEditOrDelete>
+ </row>
+ <row>
+ <date>2012-01-06</date>
+ <note>11: Site 2 annotation for 2012-01-06</note>
+ <starred>0</starred>
+ <user>superUserLogin</user>
+ <idNote>11</idNote>
+ <canEditOrDelete>1</canEditOrDelete>
+ </row>
+ <row>
+ <date>2012-01-12</date>
+ <note>12: Site 2 annotation for 2012-01-12</note>
+ <starred>1</starred>
+ <user>superUserLogin</user>
+ <idNote>12</idNote>
+ <canEditOrDelete>1</canEditOrDelete>
+ </row>
+ <row>
+ <date>2012-01-13</date>
+ <note>13: Site 2 annotation for 2012-01-13</note>
+ <starred>0</starred>
+ <user>superUserLogin</user>
+ <idNote>13</idNote>
+ <canEditOrDelete>1</canEditOrDelete>
+ </row>
+ <row>
+ <date>2012-01-19</date>
+ <note>14: Site 2 annotation for 2012-01-19</note>
+ <starred>0</starred>
+ <user>superUserLogin</user>
+ <idNote>14</idNote>
+ <canEditOrDelete>1</canEditOrDelete>
+ </row>
+ <row>
+ <date>2012-01-20</date>
+ <note>15: Site 2 annotation for 2012-01-20</note>
+ <starred>1</starred>
+ <user>superUserLogin</user>
+ <idNote>15</idNote>
+ <canEditOrDelete>1</canEditOrDelete>
+ </row>
+ <row>
+ <date>2012-01-26</date>
+ <note>16: Site 2 annotation for 2012-01-26</note>
+ <starred>0</starred>
+ <user>superUserLogin</user>
+ <idNote>16</idNote>
+ <canEditOrDelete>1</canEditOrDelete>
+ </row>
+ <row>
+ <date>2012-01-27</date>
+ <note>17: Site 2 annotation for 2012-01-27</note>
+ <starred>0</starred>
+ <user>superUserLogin</user>
+ <idNote>17</idNote>
+ <canEditOrDelete>1</canEditOrDelete>
+ </row>
+ </row>
+</result> \ No newline at end of file
diff --git a/tests/PHPUnit/Integration/expected/test_annotations_multipleSites__Annotations.getAnnotationCountForDates_month.xml b/tests/PHPUnit/Integration/expected/test_annotations_multipleSites__Annotations.getAnnotationCountForDates_month.xml
new file mode 100755
index 0000000000..8549b62364
--- /dev/null
+++ b/tests/PHPUnit/Integration/expected/test_annotations_multipleSites__Annotations.getAnnotationCountForDates_month.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<result>
+ <row key="1">
+ <row>
+ <row>2012-01-01</row>
+ <row>
+ <count>8</count>
+ <starred>2</starred>
+ </row>
+ </row>
+ </row>
+ <row key="2">
+ <row>
+ <row>2012-01-01</row>
+ <row>
+ <count>8</count>
+ <starred>2</starred>
+ </row>
+ </row>
+ </row>
+</result> \ No newline at end of file
diff --git a/tests/PHPUnit/Integration/expected/test_annotations_range__Annotations.getAll_range.xml b/tests/PHPUnit/Integration/expected/test_annotations_range__Annotations.getAll_range.xml
new file mode 100755
index 0000000000..2e351524dc
--- /dev/null
+++ b/tests/PHPUnit/Integration/expected/test_annotations_range__Annotations.getAll_range.xml
@@ -0,0 +1,69 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<result>
+ <row key="1">
+ <row>
+ <date>2012-01-19</date>
+ <note>14: Site 1 annotation for 2012-01-19</note>
+ <starred>0</starred>
+ <user>superUserLogin</user>
+ <idNote>14</idNote>
+ <canEditOrDelete>1</canEditOrDelete>
+ </row>
+ <row>
+ <date>2012-01-20</date>
+ <note>15: Site 1 annotation for 2012-01-20</note>
+ <starred>1</starred>
+ <user>superUserLogin</user>
+ <idNote>15</idNote>
+ <canEditOrDelete>1</canEditOrDelete>
+ </row>
+ <row>
+ <date>2012-01-26</date>
+ <note>16: Site 1 annotation for 2012-01-26</note>
+ <starred>0</starred>
+ <user>superUserLogin</user>
+ <idNote>16</idNote>
+ <canEditOrDelete>1</canEditOrDelete>
+ </row>
+ <row>
+ <date>2012-01-27</date>
+ <note>17: Site 1 annotation for 2012-01-27</note>
+ <starred>0</starred>
+ <user>superUserLogin</user>
+ <idNote>17</idNote>
+ <canEditOrDelete>1</canEditOrDelete>
+ </row>
+ <row>
+ <date>2012-02-02</date>
+ <note>18: Site 1 annotation for 2012-02-02</note>
+ <starred>1</starred>
+ <user>superUserLogin</user>
+ <idNote>18</idNote>
+ <canEditOrDelete>1</canEditOrDelete>
+ </row>
+ <row>
+ <date>2012-02-03</date>
+ <note>19: Site 1 annotation for 2012-02-03</note>
+ <starred>0</starred>
+ <user>superUserLogin</user>
+ <idNote>19</idNote>
+ <canEditOrDelete>1</canEditOrDelete>
+ </row>
+ <row>
+ <date>2012-02-09</date>
+ <note>20: Site 1 annotation for 2012-02-09</note>
+ <starred>0</starred>
+ <user>superUserLogin</user>
+ <idNote>20</idNote>
+ <canEditOrDelete>1</canEditOrDelete>
+ </row>
+ <row>
+ <date>2012-02-10</date>
+ <note>21: Site 1 annotation for 2012-02-10</note>
+ <starred>1</starred>
+ <user>superUserLogin</user>
+ <idNote>21</idNote>
+ <canEditOrDelete>1</canEditOrDelete>
+ </row>
+ </row>
+</result> \ No newline at end of file
diff --git a/tests/PHPUnit/Integration/expected/test_annotations_range__Annotations.getAnnotationCountForDates_range.xml b/tests/PHPUnit/Integration/expected/test_annotations_range__Annotations.getAnnotationCountForDates_range.xml
new file mode 100755
index 0000000000..77c83c9c84
--- /dev/null
+++ b/tests/PHPUnit/Integration/expected/test_annotations_range__Annotations.getAnnotationCountForDates_range.xml
@@ -0,0 +1,229 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<result>
+ <row key="1">
+ <row>
+ <row>2012-01-15</row>
+ <row>
+ <count>0</count>
+ <starred>0</starred>
+ </row>
+ </row>
+ <row>
+ <row>2012-01-16</row>
+ <row>
+ <count>0</count>
+ <starred>0</starred>
+ </row>
+ </row>
+ <row>
+ <row>2012-01-17</row>
+ <row>
+ <count>0</count>
+ <starred>0</starred>
+ </row>
+ </row>
+ <row>
+ <row>2012-01-18</row>
+ <row>
+ <count>0</count>
+ <starred>0</starred>
+ </row>
+ </row>
+ <row>
+ <row>2012-01-19</row>
+ <row>
+ <count>1</count>
+ <starred>0</starred>
+ </row>
+ </row>
+ <row>
+ <row>2012-01-20</row>
+ <row>
+ <count>1</count>
+ <starred>1</starred>
+ </row>
+ </row>
+ <row>
+ <row>2012-01-21</row>
+ <row>
+ <count>0</count>
+ <starred>0</starred>
+ </row>
+ </row>
+ <row>
+ <row>2012-01-22</row>
+ <row>
+ <count>0</count>
+ <starred>0</starred>
+ </row>
+ </row>
+ <row>
+ <row>2012-01-23</row>
+ <row>
+ <count>0</count>
+ <starred>0</starred>
+ </row>
+ </row>
+ <row>
+ <row>2012-01-24</row>
+ <row>
+ <count>0</count>
+ <starred>0</starred>
+ </row>
+ </row>
+ <row>
+ <row>2012-01-25</row>
+ <row>
+ <count>0</count>
+ <starred>0</starred>
+ </row>
+ </row>
+ <row>
+ <row>2012-01-26</row>
+ <row>
+ <count>1</count>
+ <starred>0</starred>
+ </row>
+ </row>
+ <row>
+ <row>2012-01-27</row>
+ <row>
+ <count>1</count>
+ <starred>0</starred>
+ </row>
+ </row>
+ <row>
+ <row>2012-01-28</row>
+ <row>
+ <count>0</count>
+ <starred>0</starred>
+ </row>
+ </row>
+ <row>
+ <row>2012-01-29</row>
+ <row>
+ <count>0</count>
+ <starred>0</starred>
+ </row>
+ </row>
+ <row>
+ <row>2012-01-30</row>
+ <row>
+ <count>0</count>
+ <starred>0</starred>
+ </row>
+ </row>
+ <row>
+ <row>2012-01-31</row>
+ <row>
+ <count>0</count>
+ <starred>0</starred>
+ </row>
+ </row>
+ <row>
+ <row>2012-02-01</row>
+ <row>
+ <count>0</count>
+ <starred>0</starred>
+ </row>
+ </row>
+ <row>
+ <row>2012-02-02</row>
+ <row>
+ <count>1</count>
+ <starred>1</starred>
+ </row>
+ </row>
+ <row>
+ <row>2012-02-03</row>
+ <row>
+ <count>1</count>
+ <starred>0</starred>
+ </row>
+ </row>
+ <row>
+ <row>2012-02-04</row>
+ <row>
+ <count>0</count>
+ <starred>0</starred>
+ </row>
+ </row>
+ <row>
+ <row>2012-02-05</row>
+ <row>
+ <count>0</count>
+ <starred>0</starred>
+ </row>
+ </row>
+ <row>
+ <row>2012-02-06</row>
+ <row>
+ <count>0</count>
+ <starred>0</starred>
+ </row>
+ </row>
+ <row>
+ <row>2012-02-07</row>
+ <row>
+ <count>0</count>
+ <starred>0</starred>
+ </row>
+ </row>
+ <row>
+ <row>2012-02-08</row>
+ <row>
+ <count>0</count>
+ <starred>0</starred>
+ </row>
+ </row>
+ <row>
+ <row>2012-02-09</row>
+ <row>
+ <count>1</count>
+ <starred>0</starred>
+ </row>
+ </row>
+ <row>
+ <row>2012-02-10</row>
+ <row>
+ <count>1</count>
+ <starred>1</starred>
+ </row>
+ </row>
+ <row>
+ <row>2012-02-11</row>
+ <row>
+ <count>0</count>
+ <starred>0</starred>
+ </row>
+ </row>
+ <row>
+ <row>2012-02-12</row>
+ <row>
+ <count>0</count>
+ <starred>0</starred>
+ </row>
+ </row>
+ <row>
+ <row>2012-02-13</row>
+ <row>
+ <count>0</count>
+ <starred>0</starred>
+ </row>
+ </row>
+ <row>
+ <row>2012-02-14</row>
+ <row>
+ <count>0</count>
+ <starred>0</starred>
+ </row>
+ </row>
+ <row>
+ <row>2012-02-15</row>
+ <row>
+ <count>0</count>
+ <starred>0</starred>
+ </row>
+ </row>
+ </row>
+</result> \ No newline at end of file
diff --git a/tests/PHPUnit/IntegrationTestCase.php b/tests/PHPUnit/IntegrationTestCase.php
index 8a03840dd8..bf5f27cd7a 100755
--- a/tests/PHPUnit/IntegrationTestCase.php
+++ b/tests/PHPUnit/IntegrationTestCase.php
@@ -174,6 +174,7 @@ abstract class IntegrationTestCase extends PHPUnit_Framework_TestCase
'Transitions',
'API',
'ImageGraph',
+ 'Annotations',
);
const DEFAULT_USER_PASSWORD = 'nopass';
diff --git a/themes/default/images/grey_marker.png b/themes/default/images/grey_marker.png
new file mode 100755
index 0000000000..b0e3e3e6ae
--- /dev/null
+++ b/themes/default/images/grey_marker.png
Binary files differ
diff --git a/themes/default/images/star.png b/themes/default/images/star.png
new file mode 100755
index 0000000000..52d161eab4
--- /dev/null
+++ b/themes/default/images/star.png
Binary files differ
diff --git a/themes/default/images/star_empty.png b/themes/default/images/star_empty.png
new file mode 100755
index 0000000000..965f4bacb8
--- /dev/null
+++ b/themes/default/images/star_empty.png
Binary files differ
diff --git a/themes/default/images/yellow_marker.png b/themes/default/images/yellow_marker.png
new file mode 100755
index 0000000000..a2506a2a7d
--- /dev/null
+++ b/themes/default/images/yellow_marker.png
Binary files differ