From 043fc882559e3ed31c1387fbd5ef158510c5668f Mon Sep 17 00:00:00 2001
From: benakamoorthi <benaka.moorthi@gmail.com>
Date: Fri, 14 Dec 2012 08:56:21 +0000
Subject: [PATCH] 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
---
 config/global.ini.php                         |   1 +
 core/API/Request.php                          |  22 +
 core/API/ResponseBuilder.php                  |  94 +--
 core/DataTable/Renderer.php                   |  70 ++-
 core/DataTable/Renderer/Json.php              |  23 +-
 core/DataTable/Renderer/Xml.php               | 119 +++-
 core/Date.php                                 |   2 +-
 core/FrontController.php                      |   2 +-
 core/Piwik.php                                |  40 ++
 core/Updates/1.9.3-b10.php                    |  34 +
 core/Version.php                              |   2 +-
 core/ViewDataTable.php                        |  15 +
 .../GenerateGraphHTML/ChartEvolution.php      |   1 +
 lang/en.php                                   |  16 +
 plugins/API/API.php                           |   2 +-
 plugins/Annotations/API.php                   | 364 +++++++++++
 plugins/Annotations/AnnotationList.php        | 446 ++++++++++++++
 plugins/Annotations/Annotations.php           |  70 +++
 plugins/Annotations/Controller.php            | 226 +++++++
 plugins/Annotations/templates/annotation.tpl  |  43 ++
 .../templates/annotationManager.tpl           |  27 +
 plugins/Annotations/templates/annotations.js  | 583 ++++++++++++++++++
 plugins/Annotations/templates/annotations.tpl |  30 +
 .../templates/evolutionAnnotations.tpl        |  12 +
 plugins/Annotations/templates/styles.css      | 194 ++++++
 plugins/CoreHome/templates/calendar.js        |  21 +-
 plugins/CoreHome/templates/datatable.css      |   6 +
 plugins/CoreHome/templates/datatable.js       | 155 ++++-
 .../CoreHome/templates/datatable_footer.tpl   |  11 +-
 .../PHPUnit/Core/API/ResponseBuilderTest.php  |  69 ---
 .../Core/DataTable/Renderer/JSONTest.php      |  68 ++
 .../Core/DataTable/Renderer/XMLTest.php       |  95 +++
 tests/PHPUnit/Core/PiwikTest.php              |  25 +
 tests/PHPUnit/Integration/AnnotationsTest.php | 415 +++++++++++++
 .../test_ImportLogs__Goals.getGoals.xml       |   2 +-
 ...st_OneVisitorTwoVisits__Goals.getGoals.xml |   4 +-
 ...sits_withCookieSupport__Goals.getGoals.xml |   4 +-
 .../test_annotations__Annotations.get.xml     |  11 +
 ...st_annotations__Annotations.getAll_day.xml |  13 +
 ..._annotations__Annotations.getAll_month.xml |  85 +++
 ...t_annotations__Annotations.getAll_week.xml |  21 +
 ...t_annotations__Annotations.getAll_year.xml | 133 ++++
 ...tations.getAnnotationCountForDates_day.xml |  12 +
 ...tions.getAnnotationCountForDates_month.xml |  12 +
 ...ations.getAnnotationCountForDates_week.xml |  12 +
 ...ations.getAnnotationCountForDates_year.xml |  12 +
 ...tations_lastN__Annotations.getAll_week.xml |  85 +++
 ...ations.getAnnotationCountForDates_week.xml |  47 ++
 ...ultipleSites__Annotations.getAll_month.xml | 135 ++++
 ...tions.getAnnotationCountForDates_month.xml |  21 +
 ...ations_range__Annotations.getAll_range.xml |  69 +++
 ...tions.getAnnotationCountForDates_range.xml | 229 +++++++
 tests/PHPUnit/IntegrationTestCase.php         |   1 +
 themes/default/images/grey_marker.png         | Bin 0 -> 1627 bytes
 themes/default/images/star.png                | Bin 0 -> 757 bytes
 themes/default/images/star_empty.png          | Bin 0 -> 658 bytes
 themes/default/images/yellow_marker.png       | Bin 0 -> 1618 bytes
 57 files changed, 4024 insertions(+), 187 deletions(-)
 create mode 100755 core/Updates/1.9.3-b10.php
 create mode 100755 plugins/Annotations/API.php
 create mode 100755 plugins/Annotations/AnnotationList.php
 create mode 100755 plugins/Annotations/Annotations.php
 create mode 100755 plugins/Annotations/Controller.php
 create mode 100755 plugins/Annotations/templates/annotation.tpl
 create mode 100755 plugins/Annotations/templates/annotationManager.tpl
 create mode 100755 plugins/Annotations/templates/annotations.js
 create mode 100755 plugins/Annotations/templates/annotations.tpl
 create mode 100755 plugins/Annotations/templates/evolutionAnnotations.tpl
 create mode 100755 plugins/Annotations/templates/styles.css
 create mode 100755 tests/PHPUnit/Integration/AnnotationsTest.php
 create mode 100755 tests/PHPUnit/Integration/expected/test_annotations__Annotations.get.xml
 create mode 100755 tests/PHPUnit/Integration/expected/test_annotations__Annotations.getAll_day.xml
 create mode 100755 tests/PHPUnit/Integration/expected/test_annotations__Annotations.getAll_month.xml
 create mode 100755 tests/PHPUnit/Integration/expected/test_annotations__Annotations.getAll_week.xml
 create mode 100755 tests/PHPUnit/Integration/expected/test_annotations__Annotations.getAll_year.xml
 create mode 100755 tests/PHPUnit/Integration/expected/test_annotations__Annotations.getAnnotationCountForDates_day.xml
 create mode 100755 tests/PHPUnit/Integration/expected/test_annotations__Annotations.getAnnotationCountForDates_month.xml
 create mode 100755 tests/PHPUnit/Integration/expected/test_annotations__Annotations.getAnnotationCountForDates_week.xml
 create mode 100755 tests/PHPUnit/Integration/expected/test_annotations__Annotations.getAnnotationCountForDates_year.xml
 create mode 100755 tests/PHPUnit/Integration/expected/test_annotations_lastN__Annotations.getAll_week.xml
 create mode 100755 tests/PHPUnit/Integration/expected/test_annotations_lastN__Annotations.getAnnotationCountForDates_week.xml
 create mode 100755 tests/PHPUnit/Integration/expected/test_annotations_multipleSites__Annotations.getAll_month.xml
 create mode 100755 tests/PHPUnit/Integration/expected/test_annotations_multipleSites__Annotations.getAnnotationCountForDates_month.xml
 create mode 100755 tests/PHPUnit/Integration/expected/test_annotations_range__Annotations.getAll_range.xml
 create mode 100755 tests/PHPUnit/Integration/expected/test_annotations_range__Annotations.getAnnotationCountForDates_range.xml
 create mode 100755 themes/default/images/grey_marker.png
 create mode 100755 themes/default/images/star.png
 create mode 100755 themes/default/images/star_empty.png
 create mode 100755 themes/default/images/yellow_marker.png

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;
 						}
@@ -419,82 +411,6 @@ class Piwik_API_ResponseBuilder
 		return false;
 	}
 
-	/**
-	 * 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:
@@ -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);
@@ -958,6 +959,20 @@ abstract class Piwik_ViewDataTable
 		$this->viewProperties['hide_all_views_icons'] = true;
 	}
 	
+	/**
+	 * 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.
 	 */
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 0000000000000000000000000000000000000000..b0e3e3e6aecd6e59cac279667c71c4fee45aa394
GIT binary patch
literal 1627
zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`EX7WqAsj$Z!;#Vf<Z~8yL>4nJ
za0`Jj<E6WGe}IBAC9V-A!TD(=<%vb942~)JNvR5MnMJAP`9;~q3eLgCGp@NY1J#Lw
z)HxTWCYEI8=P86_=B6?j=^L8s8(1jJSrq}aOc10X!q>+tIX_n~5oC^DMQ#CujeSKy
zVsdtBi9%9pdS;%jl7fPQl0s&Rtx~wDuYqrYb81GWM^#a3aFt(3a#eP+Wr~u$9hXgo
z6;N|-YDuC(MQ%=Bu~mhw64*>DAR8pCucQE0Qj%?}1aWkPZ-9bxeo?A|iJqZuvVpOQ
zf{B@)k-3qjxtWeaaAJvqS7M%mk-37AfdP;(vNANZGBE@?1`L$!xPY`xQA(Oskc%7C
zP9V=#DWjyMz)D}gyu4hm+*mKaC|%#s($Z4jz)0W7NEfI=x41H|B(Xv_uUHvk2+SOp
z)Z*l#%mQ$5fy_-z$}cUkRZ;?31P4&hB^JOf$}5Hj9xxd7D-sLz4fPE4;U)t$+5iQu
zz!8yO6q28xV}~WqY(P3u6d`Oy=udS?EJ?KkhKGf&fswAEd5D3Lm9d$XiD?v)euyG8
z?Y{XbnQ4_s+KqLMOhODTtqcsTOpKt~krY9-+vtM=0x4j?p$_sBnz#ai082@RhgU&q
zQ4Tm-Qj+ykb5e6t^Gb?=VP=RLW+};5Y57IDi6wTKxryni`UQFEHu?xbyzYaz8kj7A
z$x<JlE@4iGM<q=dh;XNg@eo64X^E+9PznPB<8)6K#}JF&sS`JL-F6Uh?RV;lk=PdS
z@YLLB@mDb%kET4XvvcTO)q0j)XU+b@^%LFnOeU~w3sC*Ucw4e%0@vF`FIlQGbrxQ|
zrMT0;;BfW#bCt$@9T!d)$}mg`vVOo&vvW!vW4lFPz0uSTp4Uls8EaKNg<eio>C9N`
zs#z-Wy_@6QjD-F7--~r0?bqnaxLw9kD${;5=h(-JBT>IgeLiI}eYrB*_PjC&Q<K7h
z&o;NVsdgzfEZ>+Ywc&P-8N-i#{jYcJSl9jjH$zO;?)|JAV)WW;ua}m@mTxW7esDC3
z+hJO2ME1sOlTsxarr$b!<3pv;hn$|s_1Ck{JvXkL$80eFeB|0NiDZU(_brdbFPgIJ
za@2k^Uu7kw(pc}<>q<(OOnNic#!n3@Pd>j}&~N$W*;~IU`d;SGpRi2x+AW~3?&k@8
z;X3?5M=5C9(kZE$N=k_m^OpaW=cp2O{g}}s$<bpd;`*^-?RPCl|B26cKB=_3^Y1*1
zc-QY$t5&S)TBPwV|Eur*!V51;e(k%PSO5L@e?A6}%U^E)u#pvhz(3X6D16BppWna&
Op25@A&t;ucLK6ThY4}|L

literal 0
HcmV?d00001

diff --git a/themes/default/images/star.png b/themes/default/images/star.png
new file mode 100755
index 0000000000000000000000000000000000000000..52d161eab4370b9d2d3350b97c0c653cbbe9f6ed
GIT binary patch
literal 757
zcmV<R0t)?!P)<h;3K|Lk000e1NJLTq000mG000mO1^@s6AM^iV0000PbVXQnQ*UN;
zcVTj606}DLVr3vnZDD6+Qe|Oed2z{QJOBU!mq|oHRCwBKQcY+RQ4pTD*`%8$Y0@OL
z*oIb%putvKltK^nXb&nv5y6A?qImWu2p&X4FN$7@B8V0}iQr!arGHWr5!6d-#MW9g
z!IswkC7b=r+viM@wKjF&<IR3=W_D)2$DnL`+Zf!s43*YbKJPoknKu%A$VmYG7WDNp
zV6O*Q@xm<w`2Cv-TrZzz|0Ty}8SD;;qAxq7RX|us{AHlY>)*DM94+^~36CG-7ckCA
z-cW!nrvmtVC7&_q3fAwB0SL&he(w^ZR6tcXwkTEhtm~z}AiBf#ow0CTXN-K}=&sT>
z72q6EfM$;pi_4M-$&wxND2;LA=&sUaY;CZTq<z#x+Z%Ls7AFu82)8z@E@%4TKgM1S
zxNuH5S6kQCwWH&7T$Uv-H<vKc1}mQ~{Y9xOelu4Un1-r-`}{by@_QzUS68TjGs;ny
ztek3z_r}A~=9sQ!K(lh7n;P!JLUjUQtS+V(GVfA0x9(v4BZTb~aE!XS*s`bhhyin9
zA=c#*rfccfm+>d)UO=Qb$*8IV-+@RYvW@y2^ZGaHmCJLsu)wo!H%lW>?$QI44xHeS
z$#7U%v8jlMbE!R2YX+;`8u&am_}ms$%Nj&$tsv*Donk1KawxYb<JH?``8c!&L%v9r
zoRhd|n~9;R$;9yVBsVovaT`)iP>E0s#ZnGgaRH0VxD6b4dovtr+8XG|tA;T?@-vmq
znm1LokeHcXiz$k$3Kf#)=k=+l!|*ZjX}th-;r%)b!Jck7spSiiiHWuMFUQ3BYj@%8
z^ASjKE|RF}7SrGXO|#*~JxDt5j+RK<s76F31|6eG>%qfkFld;NN3Mnhcrgk~LrJ)F
n{IGR!-!3>tvD7%i_#?moaXc(Dvb(%Q00000NkvXXu0mjf4+do@

literal 0
HcmV?d00001

diff --git a/themes/default/images/star_empty.png b/themes/default/images/star_empty.png
new file mode 100755
index 0000000000000000000000000000000000000000..965f4bacb8248523d65afbd577346cfd08f28b01
GIT binary patch
literal 658
zcmV;D0&V??P)<h;3K|Lk000e1NJLTq000mG000mO1^@s6AM^iV0000PbVXQnQ*UN;
zcVTj606}DLVr3vnZDD6+Qe|Oed2z{QJOBU!G)Y83RCwBKQ@f7ZKoqsX2=Pk5AcBYh
zZA3s3si+nyO*%dx`~XF0_z3=iiiVN~DIG;BloTixK>@S^K?6dd4H$=)*I;n&v23z(
z80mVh&OP@rbH;43SY$?{5tGm7nH%d~-+!0`)oPWA$Kwo#4{WtsF@C?_%!}MA0B$y$
z)qKtXWm(qDX0sU(?{c}^aQ6E>Pn!V$bUJA~V_D3VjY7xc@h%(=%P{x`4$kvsG0(i+
zZjp>mZ=%uY76u;}V&=t}0Yy<5mSuU5$KwhF0*}DxSbj?8DJJ|^;BvWqHiH;7l}e$V
z){>@>+wJzJTQae^T<#@x_?So}JcUAG6$*t8giCr{)OWAGI~I$Dvr?(}hr=O`*0fS5
z+3WQVD5z4Gw|2XojYJ|M9b*klH%V1hd#lxYRjbtkhr{6~#lK6E^kiJ3%5mJUU@-Wl
z-|usi$po?~&kY!2;DD<a{-*-;oy}%{dcB^sUa!yQ0>BUh2i&WS^?IE-pU=!@vta~5
zuu?FXOeO`r!wcP1V2FVOE^?5oP0(mGH2KJaKA-Q&;cy)1^LeyXDy3oYfguJCxX8h>
z;<MZBw2GvAGLy+TrqijbTrS@&m&-17{M~Fe-_yA*Znyg~7!1@-r!yaq$Jd9%;;nSM
zU5>_lO=K@69q14JN|&Y`g_K%|qWFR5c_+O{mADW3-JGrkpfO1kqNE3sb4X+2rtBy{
sm_(hwrPJwTEEao5m{Wf%%3lEn0O815@iXI*8~^|S07*qoM6N<$f<rhiApigX

literal 0
HcmV?d00001

diff --git a/themes/default/images/yellow_marker.png b/themes/default/images/yellow_marker.png
new file mode 100755
index 0000000000000000000000000000000000000000..a2506a2a7d7d2da2d36133becc98637444f05f0b
GIT binary patch
literal 1618
zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`EX7WqAsj$Z!;#Vf<Z~8yL>4nJ
za0`Jj<E6WGe}IBAC9V-A!TD(=<%vb942~)JNvR5MnMJAP`9;~q3eLgCGp@NY1J#Lw
z)HxTWCYEI8=P86_=B6?j=^L8s8(1jJSrq}aOc10X!q>+tIX_n~5oC^DMQ#CujeSKy
zVsdtBi9%9pdS;%jl7fPQl0s&Rtx~wDuYqrYb81GWM^#a3aFt(3a#eP+Wr~u$9hXgo
z6;N|-YDuC(MQ%=Bu~mhw64*>DAR8pCucQE0Qj%?}1aWkPZ-9bxeo?A|iJqZuvVpOQ
zf{B@)k-3qjxtWeaaAJvqS7M%mk-37AfdP;(vNANZGBE@?1`L$!xPY`xQA(Oskc%7C
zP9V=#DWjyMz)D}gyu4hm+*mKaC|%#s($Z4jz)0W7NEfI=x41H|B(Xv_uUHvk2+SOp
z)Z*l#%mQ$5fy_-z$}cUkRZ;?31P4&hB^JOf$}5Hj9xxd7D-sLz4fPE4;U)t$+5iQu
zz!8yO6q28xV}~WqY(P3u6d`Oy=udS?EJ?KkhKGf&fswAEd5D3Lm9d$XiD?v)euyG8
z?Y{XbnQ4_s+KqLMOhODTtqcsTOpKt~krY9-+vtM=0x4j?p$_sBnz#ai082@RhgU&q
zQ4Tm-Qj+ykb5e6t^Gb?=VP=RLW+};5Y57IDi6wTKxryni`UQFEHu?xbyzYaz8kj7A
z$x<JlE@4iGM<q=dh;XNg@eo64X^E+9PznPBW1pvsV~9m>?WFzQPJtrF?Zch5R%UcA
zTHdlH-7inlrM{usyLeV`BipTnfaJH0U-DT*Ew+5g7VPBkJh@1LhjZu4-Mrz!`L_Hr
z7IQw^&j0>&uJ)9m5+@GE1Buf(CNNIBwU}E$*n3rRkAT_(1Mg#tJ=k?;%)a<HNHkAd
zfpz!BC8sQy`o6gwS!}@_m2mrSe9_K2#nXHJV%ip$q~DZ}P!QRxB(nFAe8homiW!-{
z@;4Y<mZ_B*%Gw>ZeRSCNQL<GHmqWFfBuhfcj_BKK_1|2(zTa5=K=Yq#M^8WH+EmxS
z=KklRolFkZ-!AMnNMZQ2RO|EP#~;2_xG&zpbzlSgu|2<}9O@=CyF8qFt3{k?bAs<n
zo->D9#JSdAX=qVan72rcb@uu9Z|f7v&;R_x7qmgrd(|xk_9MYhy7EI9a+V4&_~E8H
zPbvM?e+C9SRqLf%Cec>jp?81#|9-An{-|Pp(TR4}g4ku7_Vw~F2!H>AVZptW#fl3R
zOW!UjE@3DrI>3A2mRR@Pc`pytu-AV4c(>lDSn>h8;NC^+|L;2ksuUPJUHx3vIVCg!
E0OvaEZ2$lO

literal 0
HcmV?d00001

-- 
GitLab