Index: main/plugin-odf-web/i18n/messages_en.xml
===================================================================
--- main/plugin-odf-web/i18n/messages_en.xml	(revision 40731)
+++ main/plugin-odf-web/i18n/messages_en.xml	(working copy)
@@ -320,7 +320,6 @@
     <message key="PLUGINS_ODFWEB_CART_COURSES">courses</message>
     <message key="PLUGINS_ODFWEB_CART_BO_UNAVAILABLE_TITLE">Unavailable in the CMS</message>
     <message key="PLUGINS_ODFWEB_CART_BO_UNAVAILABLE_DESC">This feature is not available inside the CMS.&lt;br&gt;Please use the preview or live mode, or directly the site.</message>
-    
                 
     <message key="PLUGINS_ODFWEB_SERVICE_SEARCH_SCHEMA_BO_UNAVAILABLE_TITLE">Unavailable inside CMS</message>
     <message key="PLUGINS_ODFWEB_SERVICE_SEARCH_SCHEMA_BO_UNAVAILABLE_DESC">This feature is not available inside the CMS.&lt;br&gt;You could test it in preview mode or live mode.</message>
@@ -329,6 +328,19 @@
    
     <message key="PLUGINS_ODFWEB_PDF_EXPORT_MAIL_SUBJECT">[{0}] Programs' catalog</message>
     
+    <message key="PLUGINS_ODFWEB_CART_ERROR_GET_USER_PREF">An error occurred getting users preferences</message>
+    <message key="PLUGINS_ODFWEB_CART_ERROR_SET_USER_PREF">An error occurred saving users preferences</message>
+    <message key="PLUGINS_ODFWEB_CART_ERROR_CONTENT_DELETED_INFO">Following contents have been deleted from your cart because they don't exist anymore :</message>
+    
+    <message key="PLUGINS_ODFWEB_SERVICE_CART_SAVE_INFO">Please, connect to save your cart</message>
+    <message key="PLUGINS_ODFWEB_SERVICE_CART_SAVE_LINK_TITLE">Here</message>
+    
+    <message key="PLUGINS_ODFWEB_CART_USERPREF_PROGRAM">Programs</message>
+    <message key="PLUGINS_ODFWEB_CART_USERPREF_PROGRAM_DESC">User preferences for cart programs</message>
+    <message key="PLUGINS_ODFWEB_CART_USERPREF_COURSE">Courses</message>
+    <message key="PLUGINS_ODFWEB_CART_USERPREF_COURSE_DESC">User preferences for cart courses</message>
+    <message key="PLUGINS_ODFWEB_CART_USERPREF_GROUP">ODF cart</message>
+    
     <!-- Download InDesign template -->
     <message key="PLUGINS_ODFWEB_EXPORT_INDESIGN_DOWNLOAD_INDT_BUTTON_LABEL">Download InDesign template file (.indt)</message>
     <message key="PLUGINS_ODFWEB_EXPORT_INDESIGN_DOWNLOAD_INDT_BUTTON_DESCRIPTION">Select and download the InDesign template file among the available templates.</message>
Index: main/plugin-odf-web/i18n/messages_fr.xml
===================================================================
--- main/plugin-odf-web/i18n/messages_fr.xml	(revision 40731)
+++ main/plugin-odf-web/i18n/messages_fr.xml	(working copy)
@@ -328,6 +328,19 @@
     
     <message key="PLUGINS_ODFWEB_PDF_EXPORT_MAIL_SUBJECT">[{0}] Catalogue des formations</message>
 
+    <message key="PLUGINS_ODFWEB_CART_ERROR_GET_USER_PREF">Une erreur est survenue durant la récupération des préférences utilisateurs</message>
+    <message key="PLUGINS_ODFWEB_CART_ERROR_SET_USER_PREF">Une erreur est survenue durant la sauvegarde des préférences utilisateurs</message>
+    <message key="PLUGINS_ODFWEB_CART_ERROR_CONTENT_DELETED_INFO">Les contenus suivants ont été supprimés de votre panier car ils n'existent plus :</message>
+
+    <message key="PLUGINS_ODFWEB_SERVICE_CART_SAVE_INFO">Veuillez vous connecter pour que votre panier soit sauvegardé</message>
+    <message key="PLUGINS_ODFWEB_SERVICE_CART_SAVE_LINK_TITLE">Ici</message>
+
+    <message key="PLUGINS_ODFWEB_CART_USERPREF_PROGRAM">Formations</message>
+    <message key="PLUGINS_ODFWEB_CART_USERPREF_PROGRAM_DESC">La préférence utilisateur des formations du panier</message>
+    <message key="PLUGINS_ODFWEB_CART_USERPREF_COURSE">ELP</message>
+    <message key="PLUGINS_ODFWEB_CART_USERPREF_COURSE_DESC">La préférence utilisateur des ELP du panier</message>
+    <message key="PLUGINS_ODFWEB_CART_USERPREF_GROUP">Panier ODF</message>
+
     <!-- Download InDesign template -->
     <message key="PLUGINS_ODFWEB_EXPORT_INDESIGN_DOWNLOAD_INDT_BUTTON_LABEL">Télécharger le modèle InDesign (.indt)</message>
     <message key="PLUGINS_ODFWEB_EXPORT_INDESIGN_DOWNLOAD_INDT_BUTTON_DESCRIPTION">Sélection et téléchargement du modèle InDesign parmi les modèles existants dans la charte graphique.</message>
Index: main/plugin-odf-web/pages/services/cart/cart.xsl
===================================================================
--- main/plugin-odf-web/pages/services/cart/cart.xsl	(revision 40731)
+++ main/plugin-odf-web/pages/services/cart/cart.xsl	(working copy)
@@ -18,8 +18,9 @@
                 xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
                 xmlns:i18n="http://apache.org/cocoon/i18n/2.1"
                 xmlns:ametys="org.ametys.web.transformation.xslt.AmetysXSLTHelper"
+                xmlns:resolver="org.ametys.cms.transformation.xslt.ResolveURIComponent"
                 xmlns:math="http://exslt.org/math"
-                extension-element-prefixes="ametys math">
+                extension-element-prefixes="ametys math resolver">
     
     <xsl:import href="service:web://pages/services/common.xsl"/>
 
@@ -49,19 +50,20 @@
     
     <!-- Used in common.xsl -->
     <xsl:template name="common-service-head-js">
-        <script type="text/javascript" src="{ametys:pluginResourceURL('odf-web', 'js/org/helper/cart-helper.js')}"></script>
+        <script type="text/javascript" src="{ametys:pluginResourceURL('odf-web', 'js/org/helper/cart-helper.i18n.js')}"></script>
         <script type="text/javascript">
-        
+            odfcart_getUserPref("<xsl:value-of select="ametys:siteUriPrefix()"/>");
+            
             function clearCart_<xsl:value-of select="$uniqueId"/> ()
             {
                 odfcart_removeAll();
-                document.location.reload(false);
+                odfcart_save("<xsl:value-of select="ametys:siteUriPrefix()"/>");
             }
             
             function removeFromCart_<xsl:value-of select="$uniqueId"/> (id)
             {
                 odfcart_remove(id);
-                document.location.reload(false);
+                odfcart_save("<xsl:value-of select="ametys:siteUriPrefix()"/>");
             }
             
             function displayCartElements_<xsl:value-of select="$uniqueId"/> (type, targetId)
@@ -107,6 +109,7 @@
             
             <xsl:otherwise>
                 <xsl:call-template name="clear-cart"/>
+                <xsl:call-template name="before-cart" />
         
                 <div class="cart-content" id="cart-content-{$uniqueId}" style="display: none">
                     <div class="empty-cart" id="cart-empty-{$uniqueId}" style="display: none"><i18n:text i18n:key="PLUGINS_ODFWEB_SERVICE_CART_EMPTY"/></div>
@@ -123,6 +126,7 @@
                 
                 <xsl:call-template name="display-cart"/>
                 
+                <xsl:call-template name="after-cart" />
                 <xsl:call-template name="cart-footer"/>
             </xsl:otherwise>
         </xsl:choose>
@@ -180,6 +184,26 @@
         </script>
     </xsl:template>
     
+    <xsl:template name="save-cart">
+        <xsl:if test="$rendering-context = 'front' and not(ametys:user())">
+	        <div class="save-cart" id="save-cart-{$uniqueId}">
+	            <i18n:text i18n:key="PLUGINS_ODFWEB_SERVICE_CART_SAVE_INFO"/>
+	            <xsl:text> </xsl:text>
+	            
+	            <xsl:variable name="pagePath" select="concat(ametys:absoluteSiteUriPrefix(), '/_authenticate?requestedURL=', '/', ametys:lang(), '/', ametys:pagePath(), '.html')" />
+	            <a class="cart-save" href="{$pagePath}">
+	                <i18n:text i18n:key="PLUGINS_ODFWEB_SERVICE_CART_SAVE_LINK_TITLE"/>
+	            </a>
+            </div>
+        </xsl:if>
+    </xsl:template>
+    
+    <xsl:template name="before-cart" />
+        
+    <xsl:template name="after-cart" >
+        <xsl:call-template name="save-cart" />
+    </xsl:template>
+    
     <xsl:template name="cart-footer">
     </xsl:template>
 </xsl:stylesheet>
Index: main/plugin-odf-web/plugin.xml
===================================================================
--- main/plugin-odf-web/plugin.xml	(revision 40731)
+++ main/plugin-odf-web/plugin.xml	(working copy)
@@ -1173,4 +1173,52 @@
         </extensions>
     </feature>
     
+    <feature name="odf.cart.user.preference">
+        <extensions>
+            <extension point="org.ametys.runtime.plugins.core.userpref.UserPreferencesExtensionPoint.FO"
+                       class="org.ametys.runtime.plugins.core.userpref.StaticUserPreferenceProvider"
+                       id="org.ametys.plugins.odfweb.cart.user.preference">
+                 <param id="cartProgramIds" type="string">
+                     <label i18n="true">PLUGINS_ODFWEB_CART_USERPREF_PROGRAM</label>
+                     <description i18n="true">PLUGINS_ODFWEB_CART_USERPREF_PROGRAM_DESC</description>
+                     <group i18n="true">PLUGINS_ODFWEB_CART_USERPREF_GROUP</group>
+                     <order>10</order>
+                     <manager-role>org.ametys.plugins.odfweb.userpref.ODFCartUserPreferencesStorage</manager-role>
+                 </param>
+                 <param id="cartCourseIds" type="string">
+                     <label i18n="true">PLUGINS_ODFWEB_CART_USERPREF_COURSE</label>
+                     <description i18n="true">PLUGINS_ODFWEB_CART_USERPREF_COURSE_DESC</description>
+                     <group i18n="true">PLUGINS_ODFWEB_CART_USERPREF_GROUP</group>
+                     <order>20</order>
+                     <manager-role>org.ametys.plugins.odfweb.userpref.ODFCartUserPreferencesStorage</manager-role>
+                 </param>
+            </extension>
+        </extensions>
+    </feature>
+    
+    <feature name="odf.cart.user.preference.observation">
+        <extensions>
+            <extension point="org.ametys.cms.observation.ObserverExtensionPoint" 
+                       id="org.ametys.plugins.odfweb.observation.ODFCartUserPreferencesObserver" 
+                       class="org.ametys.plugins.odfweb.observation.ODFCartUserPreferencesObserver">
+                <mappings>
+                    <mapping prefId="cartProgramIds" table="ODFCartProgramsUserPref"/>
+                    <mapping prefId="cartCourseIds" table="ODFCartCoursesUserPref"/>
+                </mappings>
+            </extension>
+        </extensions>
+    </feature>
+    
+    <feature name="odf.cart.user.preference.storage">
+        <components>
+            <component role="org.ametys.plugins.odfweb.userpref.ODFCartUserPreferencesStorage"
+                       class="org.ametys.plugins.odfweb.userpref.ODFCartUserPreferencesStorage">
+                <mappings>
+                    <mapping prefId="cartProgramIds" table="ODFCartProgramsUserPref"/>
+                    <mapping prefId="cartCourseIds" table="ODFCartCoursesUserPref"/>
+                </mappings>
+            </component>
+        </components>
+    </feature>
+    
 </plugin>
Index: main/plugin-odf-web/resources/js/org/helper/cart-helper.i18n.js
===================================================================
--- main/plugin-odf-web/resources/js/org/helper/cart-helper.i18n.js	(revision 0)
+++ main/plugin-odf-web/resources/js/org/helper/cart-helper.i18n.js	(revision 0)
@@ -0,0 +1,324 @@
+/*
+ *  Copyright 2015 Anyware Services
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+/**
+ * Add a program into the cart
+ * @param {String} id the id of the program to add
+ * @param {String} pageId the id of the corresponding page
+ * @param {String} pageURL the URL of the corresponding page
+ * @param {String} title the title of the program
+ */
+function odfcart_addProgram (id, pageId, pageURL, title)
+{
+	odfcart_addInternal(id, pageId, pageURL, title, 'programs');
+}
+
+/**
+ * Add a course to the cart
+ * @param {String} id the id of the program to add
+ * @param {String} pageId the id of the corresponding page
+ * @param {String} pageURL the URL of the corresponding page
+ * @param {String} title the title of the course
+ */
+function odfcart_addCourse (id, pageId, pageURL, title)
+{
+	odfcart_addInternal(id, pageId, pageURL, title, 'courses');
+}
+
+/**
+ * Get the elements stored into the session cart
+ * @param {String} type the type of elements
+ * @return {Object} elements stored into the session cart
+ */
+function odfcart_getElements (type)
+{
+	var elmts = localStorage.getItem(type);
+	return elmts ? JSON.parse(elmts) : {}; 
+}
+
+
+/**
+ * Get the programs stored into the session cart
+ * @return {Object} the list of programs store into the session cart
+ */
+function odfcart_getPrograms ()
+{
+	return odfcart_getElements('programs');
+}
+
+/**
+ * Get the courses stored into the session cart
+ * @return {Object} the list of courses store into the session cart
+ */
+function odfcart_getCourses ()
+{
+	return odfcart_getElements('courses');
+}
+
+function odfcart_removeAll ()
+{
+	localStorage.setItem('programs', '{}');
+	localStorage.setItem('courses', '{}');
+}
+
+/**
+ * Remove the element corresponding to the given id
+ * @param {String} id the id of the program/course to remove from the cart  
+ */
+function odfcart_remove (id)
+{
+	if (localStorage.programs)
+	{
+		var storedPrograms = JSON.parse(localStorage.programs);
+		delete storedPrograms[id];
+		localStorage.programs = JSON.stringify(storedPrograms);
+	}
+	
+	if (localStorage.courses)
+	{
+		var storedCourses = JSON.parse(localStorage.courses);
+		delete storedCourses[id];
+		localStorage.courses = JSON.stringify(storedCourses);
+	}
+}
+
+function odfcart_getElement (id)
+{
+	if (localStorage.programs)
+	{
+		var storedPrograms = JSON.parse(localStorage.programs);
+        if (typeof(storedPrograms[id]) != "undefined")
+        {
+        	return storedPrograms[id];
+        }
+	}
+	
+	if (localStorage.courses)
+	{
+		var storedCourses = JSON.parse(localStorage.courses);
+        if (typeof(storedCourses[id]) != "undefined")
+        {
+        	return storedCourses[id];
+        }
+	}
+	
+	return null;
+}
+
+function odfcart_contains (id)
+{
+	if (localStorage.programs)
+	{
+		var storedPrograms = JSON.parse(localStorage.programs);
+        if (typeof(storedPrograms[id]) != "undefined")
+        {
+        	return true;
+        }
+	}
+	
+	if (localStorage.courses)
+	{
+		var storedCourses = JSON.parse(localStorage.courses);
+        if (typeof(storedCourses[id]) != "undefined")
+        {
+        	return true;
+        }
+	}
+	
+	return false;
+}
+
+function odfcart_addInternal (id, pageId, pageURL, title, type)
+{
+	if (localStorage.getItem(type) == null)
+	{
+		localStorage.setItem(type, '{}');
+	}
+	
+	var storedEntities = JSON.parse(localStorage.getItem(type));
+	storedEntities[id] = {
+		pageId: pageId, 
+		url: pageURL, 
+		title: title
+	};
+	
+	localStorage.setItem(type, JSON.stringify(storedEntities));
+}
+
+function odfcart_save (contextPath)
+{
+    var cartProgramIds = "";
+    var i = 0;
+    for (var id in odfcart_getPrograms())
+    {
+        if (i != 0)
+        {
+            cartProgramIds += ",";
+        }
+        cartProgramIds += id;
+        i++;
+    }
+    
+    var cartCourseIds = "";
+    var j = 0;
+    for (var id in odfcart_getCourses())
+    {
+        if (j != 0)
+        {
+            cartCourseIds += ",";
+        }
+        cartCourseIds += id;
+        j++;
+    }
+        
+    var data = {
+        "cartProgramIds": cartProgramIds,
+        "cartCourseIds": cartCourseIds,
+        "preferences-ids": "cartProgramIds,cartCourseIds"
+    };
+
+    function result(data)
+    {
+    	var contentError = $j('contentError', data).text();
+    	var contentIds = contentError.split(",");
+    	var contentNames = [];
+    	for (var i in contentIds)
+		{
+    		var content = odfcart_getElement(contentIds[i]);
+    		if (content != null)
+			{
+    			contentNames.push(content.title);
+    			odfcart_remove(contentIds[i]);
+			}
+    		
+		}
+
+    	if (contentNames.length > 0)
+		{
+    		var message = "<i18n:text i18n:key='PLUGINS_ODFWEB_CART_ERROR_CONTENT_DELETED_INFO' />\n";
+    		for (var j in contentNames)
+			{
+    			message += "\n* " + contentNames[j];
+			}
+    		alert(message);
+		}
+        document.location.reload(false);
+    }
+    
+    function handleError(jqXHR, textStatus, errorThrown)
+    {
+    	alert("<i18n:text i18n:key='PLUGINS_ODFWEB_CART_ERROR_SET_USER_PREF' />");
+    }
+
+    $j.ajax({
+        url: contextPath + "/_plugins/odf-web/set-odf-cart-userpref",
+        data: data,
+        type: 'post',
+        success: result,
+        error: handleError
+    });
+}
+
+function odfcart_getUserPref (contextPath)
+{
+	var programs = [];
+	var courses = [];
+	
+    var data = {
+        "preferences-ids": "cartProgramIds,cartCourseIds"
+    };
+
+    function result(data)
+    {
+    	if ($j('UserPreferences', data).attr("username") != null)
+    	{
+	    	$j('preferences preference', data).each(function () {
+	    		var preference = $j(this)[0];
+	    		if (preference.getAttribute("id") == "cartProgramIds")
+				{
+	    			$j('contents content', preference).each(function () {
+	    				var content = $j(this);
+	    				programs.push({
+	    					id: content.text(), 
+	    					pageId: content[0].getAttribute("pageId"),
+	    					pagePath: content[0].getAttribute("pagePath"),
+	    					title: content[0].getAttribute("title")
+						})
+	    			});
+				}
+	    		else if (preference.getAttribute("id") == "cartCourseIds")
+	    		{
+	    			$j('contents content', preference).each(function () {
+	    				var content = $j(this);
+	    				courses.push({
+	    					id: content.text(), 
+	    					pageId: content[0].getAttribute("pageId"),
+	    					pagePath: content[0].getAttribute("pagePath"),
+	    					title: content[0].getAttribute("title")
+						})
+	    			});
+	    		}
+			});
+		
+    		odfcart_mergeUserPref(contextPath, programs, courses);
+    	}
+    }
+    
+    function handleError(jqXHR, textStatus, errorThrown)
+    {
+        alert("<i18n:text i18n:key='PLUGINS_ODFWEB_CART_ERROR_GET_USER_PREF' />");
+    }
+
+    $j.ajax({
+        url: contextPath + "/_plugins/odf-web/user/preferences.xml",
+        data: data,
+        type: 'post',
+        success: result,
+        error: handleError
+    });
+}
+
+function odfcart_mergeUserPref (programs, courses)
+{
+	var localPrograms = odfcart_getPrograms();
+	var localCourse = odfcart_getCourses();
+	var size = Object.keys(localPrograms).length + Object.keys(localCourse).length;
+	
+	for (var i in programs)
+	{
+		var program = programs[i];
+		if (odfcart_contains(program.id))
+		{
+			size--;
+		}
+		odfcart_addProgram(program.id, program.pageId, program.pagePath, program.title)
+	}
+	
+	for (var i in courses)
+	{
+		var course = courses[i];
+		if (odfcart_contains(course.id))
+		{
+			size--;
+		}
+		odfcart_addCourse(course.id, course.pageId, course.pagePath, course.title)
+	}
+	
+	if (size > 0)
+	{
+		odfcart_save(contextPath);
+	}
+}
Index: main/plugin-odf-web/sitemap.xmap
===================================================================
--- main/plugin-odf-web/sitemap.xmap	(revision 40731)
+++ main/plugin-odf-web/sitemap.xmap	(working copy)
@@ -41,6 +41,8 @@
 			<map:generator name="indesign-catalogue" src="org.ametys.plugins.odfweb.generators.IndesignCatalogueGenerator"/>
 			
 			<map:generator name="content-html" src="org.ametys.web.content.HtmlContentGenerator" logger="org.ametys.web.content.HtmlContentGenerator"/>
+			
+			<map:generator name="user-preferences" src="org.ametys.plugins.odfweb.userpref.ODFCartUserPreferencesGenerator"/>
 		</map:generators>	
 		
 		<map:actions>
@@ -58,6 +60,8 @@
             
             <map:action name="set-filename" src="org.ametys.odf.program.actions.SetFileNameAction"/>
             <map:action name="set-header" src="org.apache.cocoon.acting.HttpHeaderAction"/>
+            
+            <map:action name="set-odf-cart-userpref" src="org.ametys.plugins.odfweb.userpref.SetODFCartUserPreferencesAction"/>
  		</map:actions>
         
 	</map:components>
@@ -219,6 +223,23 @@
                 <map:serialize type="xhtml"/>
            </map:match>
            
+           <map:match pattern="set-odf-cart-userpref">
+                <map:act type="set-odf-cart-userpref">
+                    <map:parameter name="prefContext" value="/sites/{request-attr:site}"/>
+                    <map:generate type="action-result" >
+                        <map:parameter name="contentError" value="{contentError}" />
+                    </map:generate>
+                    <map:serialize type="xml"/>
+                </map:act>
+           </map:match>
+           
+           <map:match pattern="user/preferences.xml">
+                <map:generate type="user-preferences">
+                    <map:parameter name="prefContext" value="/sites/{request-attr:site}"/>
+                </map:generate>
+                <map:serialize type="xml"/>
+            </map:match>
+           
            <!-- +
                  | INSERT PROGRAM Service
                  + -->
Index: main/plugin-odf-web/src/org/ametys/plugins/odfweb/observation/ODFCartUserPreferencesObserver.java
===================================================================
--- main/plugin-odf-web/src/org/ametys/plugins/odfweb/observation/ODFCartUserPreferencesObserver.java	(revision 0)
+++ main/plugin-odf-web/src/org/ametys/plugins/odfweb/observation/ODFCartUserPreferencesObserver.java	(revision 0)
@@ -0,0 +1,139 @@
+/*
+ *  Copyright 2016 Anyware Services
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package org.ametys.plugins.odfweb.observation;
+
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.SQLException;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.avalon.framework.configuration.Configurable;
+import org.apache.avalon.framework.configuration.Configuration;
+import org.apache.avalon.framework.configuration.ConfigurationException;
+import org.apache.avalon.framework.logger.AbstractLogEnabled;
+
+import org.ametys.cms.observation.Event;
+import org.ametys.cms.observation.ObservationConstants;
+import org.ametys.cms.observation.Observer;
+import org.ametys.cms.repository.Content;
+import org.ametys.odf.course.Course;
+import org.ametys.odf.program.Program;
+import org.ametys.runtime.datasource.ConnectionHelper;
+
+/**
+ * Delete specific user pref when program or course are deleted 
+ */
+public class ODFCartUserPreferencesObserver extends AbstractLogEnabled implements Observer, Configurable
+{
+    /** The content id column */
+    protected String _contentIdColumn;
+    
+    /** Connection pool name. */
+    protected String _poolName;
+    
+    /** Mapping from preference id to table name. */
+    protected Map<String, String> _prefIdToTable;
+    
+    @Override
+    public boolean supports(Event event)
+    {
+        return event.getId().equals(ObservationConstants.CONTENT_DELETING);
+    }
+
+    @Override
+    public int getPriority(Event event)
+    {
+        return MAX_PRIORITY;
+    }
+
+    @Override
+    public void configure(Configuration configuration) throws ConfigurationException
+    {
+        // Default to the core pool.
+        _poolName = configuration.getChild("pool").getValue(ConnectionHelper.CORE_POOL_NAME);
+        
+        // Default to "contentId".
+        _contentIdColumn = configuration.getChild("content-id").getValue("contentId");
+        
+        // Configure the preference-table mappings.
+        configureMappings(configuration.getChild("mappings"));
+    }
+    
+    /**
+     * Configure the mappings from preference ID to table name.
+     * @param configuration the mapping configuration root.
+     * @throws ConfigurationException if an error occurs.
+     */
+    public void configureMappings(Configuration configuration) throws ConfigurationException
+    {
+        _prefIdToTable = new HashMap<String, String>();
+        
+        for (Configuration mappingConf : configuration.getChildren("mapping"))
+        {
+            String prefId = mappingConf.getAttribute("prefId");
+            String table = mappingConf.getAttribute("table");
+            
+            _prefIdToTable.put(prefId, table);
+        }
+    }
+
+    @Override
+    public void observe(Event event)
+    {
+        Content content = (Content) event.getTarget();
+        if (content instanceof Program)
+        {
+            _removeODFContentFromUserPref(_prefIdToTable.get("cartProgramIds"), content.getId());
+        }
+        else if (content instanceof Course)
+        {
+            _removeODFContentFromUserPref(_prefIdToTable.get("cartCourseIds"), content.getId());
+        }
+    }
+    
+    private void _removeODFContentFromUserPref(String table, String idContent)
+    {
+        Connection connection = null;
+        PreparedStatement stmt = null;
+        
+        try
+        {
+            connection = ConnectionHelper.getConnection(_poolName);
+            
+            StringBuilder query = new StringBuilder();
+            query.append("DELETE FROM ").append(table).append(" WHERE ").append(_contentIdColumn).append(" = ?");
+            
+            stmt = connection.prepareStatement(query.toString());
+            
+            stmt.setString(1, idContent);
+            stmt.executeUpdate();
+        }
+        catch (SQLException e)
+        {
+            String message = "Database error trying to remove content with id " + idContent + " to all the user preference.";
+            getLogger().error(message, e);
+        }
+        finally
+        {
+            ConnectionHelper.cleanup(stmt);
+            ConnectionHelper.cleanup(connection);
+        }
+    }
+
+    
+
+}
Index: main/plugin-odf-web/src/org/ametys/plugins/odfweb/userpref/ODFCartUserPreferencesGenerator.java
===================================================================
--- main/plugin-odf-web/src/org/ametys/plugins/odfweb/userpref/ODFCartUserPreferencesGenerator.java	(revision 0)
+++ main/plugin-odf-web/src/org/ametys/plugins/odfweb/userpref/ODFCartUserPreferencesGenerator.java	(revision 0)
@@ -0,0 +1,156 @@
+/*
+ *  Copyright 2016 Anyware Services
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package org.ametys.plugins.odfweb.userpref;
+
+import java.io.IOException;
+import java.util.Map;
+
+import org.apache.avalon.framework.service.ServiceException;
+import org.apache.avalon.framework.service.ServiceManager;
+import org.apache.cocoon.ProcessingException;
+import org.apache.cocoon.environment.ObjectModelHelper;
+import org.apache.cocoon.environment.Request;
+import org.apache.cocoon.xml.AttributesImpl;
+import org.apache.cocoon.xml.XMLUtils;
+import org.apache.commons.lang.StringUtils;
+import org.xml.sax.SAXException;
+
+import org.ametys.cms.repository.ModifiableContent;
+import org.ametys.cms.transformation.xslt.ResolveURIComponent;
+import org.ametys.odf.course.Course;
+import org.ametys.odf.program.Program;
+import org.ametys.plugins.odfweb.repository.OdfPageResolver;
+import org.ametys.plugins.repository.AmetysObjectResolver;
+import org.ametys.plugins.repository.UnknownAmetysObjectException;
+import org.ametys.runtime.plugins.core.userpref.UserPreference;
+import org.ametys.runtime.plugins.core.userpref.UserPreferencesException;
+import org.ametys.runtime.util.parameter.ParameterHelper;
+import org.ametys.web.repository.page.Page;
+import org.ametys.web.userpref.FOUserPreferencesGenerator;
+
+/**
+ * Get user preferences for ODF cart
+ */
+public class ODFCartUserPreferencesGenerator extends FOUserPreferencesGenerator
+{
+    /** The ametys object resolver */
+    protected AmetysObjectResolver _resolver;
+    
+    /** The odf page resolver */
+    protected OdfPageResolver _odfPageResolver;
+    
+    @Override
+    public void service(ServiceManager serviceManager) throws ServiceException
+    {
+        super.service(serviceManager);
+        _resolver = (AmetysObjectResolver) serviceManager.lookup(AmetysObjectResolver.ROLE);
+        _odfPageResolver = (OdfPageResolver) serviceManager.lookup(OdfPageResolver.ROLE);
+    }
+    
+    @Override
+    public void generate() throws IOException, SAXException, ProcessingException
+    {
+        Request request = ObjectModelHelper.getRequest(objectModel);
+        String storageContext = parameters.getParameter("prefContext", request.getParameter("prefContext"));
+        Map<String, String> contextVars = getContextVars(request);
+        
+        String username = getUsername();
+        if (StringUtils.isNotBlank(username))
+        {
+            if (StringUtils.isBlank(storageContext))
+            {
+                throw new ProcessingException("Preferences context can't be blank");
+            }
+            
+            try
+            {
+                contentHandler.startDocument();
+                
+                AttributesImpl atts = new AttributesImpl();
+                atts.addCDATAAttribute("username", username);
+                atts.addCDATAAttribute("context", storageContext);
+                XMLUtils.startElement(contentHandler, "UserPreferences", atts);
+                
+                _saxPreferences(storageContext, contextVars, username);
+                
+                XMLUtils.endElement(contentHandler, "UserPreferences");
+                
+                contentHandler.endDocument();
+            }
+            catch (UserPreferencesException e)
+            {
+                getLogger().error("Cannot get user preferences.", e);
+                throw new ProcessingException("Cannot get user preferences.", e);
+            }
+        }
+    }
+    
+    @Override
+    protected void _saxPreference(UserPreference preference, Object value) throws SAXException, ProcessingException
+    {
+        AttributesImpl attr = new AttributesImpl();
+        attr.addCDATAAttribute("id", preference.getId());
+        XMLUtils.startElement(contentHandler, "preference", attr);
+        
+        String idContents = ParameterHelper.valueToString(value);
+        XMLUtils.startElement(contentHandler, "contents");
+        if (StringUtils.isNotBlank(idContents))
+        {
+            for (String idContent : StringUtils.split(idContents, ","))
+            {
+                try
+                {
+                    ModifiableContent content = _resolver.resolveById(idContent);
+                    Page page = null;
+                    if (content instanceof Course)
+                    {
+                        page = _odfPageResolver.getCoursePage((Course) content);
+                    }
+                    else if (content instanceof Program)
+                    {
+                        page = _odfPageResolver.getProgramPage((Program) content);
+                    }
+                    
+                    if (page != null)
+                    {
+                        String pageId = page.getId();
+                        AttributesImpl attrContent = new AttributesImpl();
+                        String pagePath = ResolveURIComponent.resolve("page", pageId, false);
+                        
+                        attrContent.addCDATAAttribute("pagePath", pagePath);
+                        
+                        attrContent.addCDATAAttribute("pageId", pageId);
+                        attrContent.addCDATAAttribute("title", content.getTitle());
+                        XMLUtils.createElement(contentHandler, "content", attrContent, idContent);
+                    }
+                    else
+                    {
+                        getLogger().info("Cannot get the page linked to the content with id: " + idContent + ". It will be ignored.");
+                    }
+                    
+                }
+                catch (UnknownAmetysObjectException e)
+                {
+                    getLogger().info("The content with id: " + idContent + " doesn't exist. It will be ignored.", e);
+                }
+            }
+        }
+        XMLUtils.endElement(contentHandler, "contents");
+        
+        XMLUtils.endElement(contentHandler, "preference");
+    }
+    
+}
Index: main/plugin-odf-web/src/org/ametys/plugins/odfweb/userpref/ODFCartUserPreferencesStorage.java
===================================================================
--- main/plugin-odf-web/src/org/ametys/plugins/odfweb/userpref/ODFCartUserPreferencesStorage.java	(revision 0)
+++ main/plugin-odf-web/src/org/ametys/plugins/odfweb/userpref/ODFCartUserPreferencesStorage.java	(revision 0)
@@ -0,0 +1,292 @@
+/*
+ *  Copyright 2016 Anyware Services
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package org.ametys.plugins.odfweb.userpref;
+
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.avalon.framework.configuration.Configurable;
+import org.apache.avalon.framework.configuration.Configuration;
+import org.apache.avalon.framework.configuration.ConfigurationException;
+import org.apache.avalon.framework.logger.AbstractLogEnabled;
+import org.apache.commons.lang.StringUtils;
+
+import org.ametys.runtime.datasource.ConnectionHelper;
+import org.ametys.runtime.plugins.core.userpref.UserPreferencesException;
+import org.ametys.runtime.plugins.core.userpref.UserPreferencesStorage;
+
+
+/**
+ * Specific storage for odf cart user preferences
+ */
+public class ODFCartUserPreferencesStorage extends AbstractLogEnabled implements UserPreferencesStorage, Configurable
+{
+    /** The login column */
+    protected String _loginColumn;
+    
+    /** The context column */
+    protected String _contextColumn;
+    
+    /** The content id column */
+    protected String _contentIdColumn;
+    
+    /** Connection pool name. */
+    protected String _poolName;
+    
+    /** Mapping from preference id to table name. */
+    protected Map<String, String> _prefIdToTable;
+
+    @Override
+    public void configure(Configuration configuration) throws ConfigurationException
+    {
+        // Default to the core pool.
+        _poolName = configuration.getChild("pool").getValue(ConnectionHelper.CORE_POOL_NAME);
+        
+        // Default to "contentId".
+        _contentIdColumn = configuration.getChild("content-id").getValue("contentId");
+        
+        // Default to "context".
+        _contextColumn = configuration.getChild("context").getValue("context");
+        
+        // Default to "login".
+        _loginColumn = configuration.getChild("login").getValue("login");
+        
+        // Configure the preference-table mappings.
+        configureMappings(configuration.getChild("mappings"));
+    }
+    
+    /**
+     * Configure the mappings from preference ID to table name.
+     * @param configuration the mapping configuration root.
+     * @throws ConfigurationException if an error occurs.
+     */
+    public void configureMappings(Configuration configuration) throws ConfigurationException
+    {
+        _prefIdToTable = new HashMap<String, String>();
+        
+        for (Configuration mappingConf : configuration.getChildren("mapping"))
+        {
+            String prefId = mappingConf.getAttribute("prefId");
+            String table = mappingConf.getAttribute("table");
+            
+            _prefIdToTable.put(prefId, table);
+        }
+    }
+    
+    public Map<String, String> getUnTypedUserPrefs(String login, String storageContext, Map<String, String> contextVars) throws UserPreferencesException
+    {
+        Map<String, String> prefs = new HashMap<String, String>();
+        
+        for (String id : _prefIdToTable.keySet())
+        {
+            prefs.put(id, getUserPreferenceAsString(login, storageContext, contextVars, id));
+        }
+        
+        return prefs;
+    }
+
+    public void removeUserPreferences(String login, String storageContext, Map<String, String> contextVars) throws UserPreferencesException
+    {
+        Connection connection = null;
+        try
+        {
+            connection = ConnectionHelper.getConnection(_poolName);
+            for (String id : _prefIdToTable.keySet())
+            {
+                _removeUserPreferencesFromTable(connection, login, storageContext, _prefIdToTable.get(id));
+            }
+        }
+        catch (SQLException e)
+        {
+            String message = "Database error trying to remove preferences for login '" + login + "' in context '" + storageContext + "'.";
+            getLogger().error(message, e);
+            throw new UserPreferencesException(message, e);
+        }
+        finally
+        {
+            ConnectionHelper.cleanup(connection);
+        }
+        
+    }
+    
+    private void _removeUserPreferencesFromTable(Connection connection, String login, String storageContext, String table) throws SQLException
+    {
+        PreparedStatement stmt = null;
+        try
+        {
+            StringBuilder query = new StringBuilder();
+            query.append("DELETE FROM ").append(table).append(" WHERE ").append(_loginColumn).append(" = ?");
+            query.append(" AND ").append(_contextColumn).append(" = ?");
+            
+            stmt = connection.prepareStatement(query.toString());
+            
+            stmt.setString(1, login);
+            stmt.setString(2, storageContext);
+            
+            stmt.executeUpdate();
+        }
+        finally
+        {
+            ConnectionHelper.cleanup(stmt);
+        }
+        
+    }
+
+    public void setUserPreferences(String login, String storageContext, Map<String, String> contextVars, Map<String, String> preferences) throws UserPreferencesException
+    {
+        removeUserPreferences(login, storageContext, contextVars);
+        Connection connection = null;
+        try
+        {
+            connection = ConnectionHelper.getConnection(_poolName);
+            _insertPreferences(connection, preferences, login, storageContext);
+        }
+        catch (SQLException e)
+        {
+            String message = "Database error trying to set the preferences of user '" + login + "' in context '" + storageContext + "'.";
+            getLogger().error(message, e);
+            throw new UserPreferencesException(message, e);
+        }
+        finally
+        {
+            ConnectionHelper.cleanup(connection);
+        }
+    }
+    
+    private void _insertPreferences(Connection connection, Map<String, String> preferences, String login, String storageContext) throws SQLException
+    {
+        for (String id : preferences.keySet())
+        {
+            String table = _prefIdToTable.get(id);
+            String[] contentIdsTab = StringUtils.split(preferences.get(id), ",");
+            
+            if (contentIdsTab.length > 0)
+            {
+                PreparedStatement stmt = null;
+                try
+                {
+                    StringBuilder query = new StringBuilder();
+                    query.append("INSERT INTO ").append(table).append(" (").append(_loginColumn);
+                    query.append(", ").append(_contextColumn);
+                    query.append(", ").append(_contentIdColumn);
+    
+                    StringBuilder values = new StringBuilder();
+                    for (int i = 0; i < contentIdsTab.length; i++)
+                    {
+                        if (i != 0)
+                        {
+                            values.append(",");
+                        }
+                        values.append("(?, ?, ?)");
+                    }
+                    
+                    query.append(") VALUES ").append(values);
+                    
+                    int i = 1;
+                    stmt = connection.prepareStatement(query.toString());
+                    
+                    for (String idContent : contentIdsTab)
+                    {
+                        stmt.setString(i++, login);
+                        stmt.setString(i++, storageContext);
+                        stmt.setString(i++, idContent);
+                    }
+                    
+                    stmt.executeUpdate();
+                }
+                finally
+                {
+                    ConnectionHelper.cleanup(stmt);
+                }
+            }
+        }
+        
+    }
+
+    public String getUserPreferenceAsString(String login, String storageContext, Map<String, String> contextVars, String id) throws UserPreferencesException
+    {
+        Connection connection = null;
+        PreparedStatement statement = null;
+        ResultSet rs = null;
+        String value = null;
+        String table = _prefIdToTable.get(id);
+        
+        try
+        {
+            StringBuilder query = new StringBuilder();
+            query.append("SELECT ").append(_contentIdColumn).append(" FROM ").append(table).append(" WHERE ").append(_loginColumn).append(" = ?");
+            query.append(" AND ").append(_contextColumn).append(" = ?");
+            
+            connection = ConnectionHelper.getConnection(_poolName);
+            
+            statement = connection.prepareStatement(query.toString());
+            statement.setString(1, login);
+            statement.setString(2, storageContext);
+            
+            rs = statement.executeQuery();
+            
+            List<String> results = new ArrayList<String>();
+            while (rs.next())
+            {
+                results.add(rs.getString(1));
+            }
+            
+            value = StringUtils.join(results, ",");
+        }
+        catch (SQLException e)
+        {
+            String message = "Database error trying to get the preferences of user '" + login + "' in context '" + storageContext + "'.";
+            getLogger().error(message, e);
+            throw new UserPreferencesException(message, e);
+        }
+        finally
+        {
+            ConnectionHelper.cleanup(rs);
+            ConnectionHelper.cleanup(statement);
+            ConnectionHelper.cleanup(connection);
+        }
+        
+        return value;
+    }
+    
+    public Long getUserPreferenceAsLong(String login, String storageContext, Map<String, String> contextVars, String id) throws UserPreferencesException
+    {
+        return null;
+    }
+
+    public Date getUserPreferenceAsDate(String login, String storageContext, Map<String, String> contextVars, String id) throws UserPreferencesException
+    {
+        return null;
+    }
+
+    public Boolean getUserPreferenceAsBoolean(String login, String storageContext, Map<String, String> contextVars, String id) throws UserPreferencesException
+    {
+        return null;
+    }
+
+    public Double getUserPreferenceAsDouble(String login, String storageContext, Map<String, String> contextVars, String id) throws UserPreferencesException
+    {
+        return null;
+    }
+
+}
Index: main/plugin-odf-web/src/org/ametys/plugins/odfweb/userpref/SetODFCartUserPreferencesAction.java
===================================================================
--- main/plugin-odf-web/src/org/ametys/plugins/odfweb/userpref/SetODFCartUserPreferencesAction.java	(revision 0)
+++ main/plugin-odf-web/src/org/ametys/plugins/odfweb/userpref/SetODFCartUserPreferencesAction.java	(revision 0)
@@ -0,0 +1,171 @@
+/*
+ *  Copyright 2016 Anyware Services
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package org.ametys.plugins.odfweb.userpref;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.avalon.framework.parameters.Parameters;
+import org.apache.avalon.framework.service.ServiceException;
+import org.apache.avalon.framework.service.ServiceManager;
+import org.apache.cocoon.environment.ObjectModelHelper;
+import org.apache.cocoon.environment.Redirector;
+import org.apache.cocoon.environment.Request;
+import org.apache.cocoon.environment.SourceResolver;
+import org.apache.commons.lang.StringUtils;
+
+import org.ametys.cms.observation.Event;
+import org.ametys.cms.repository.Content;
+import org.ametys.odf.course.Course;
+import org.ametys.odf.program.Program;
+import org.ametys.plugins.odfweb.repository.OdfPageResolver;
+import org.ametys.plugins.repository.AmetysObjectResolver;
+import org.ametys.plugins.repository.UnknownAmetysObjectException;
+import org.ametys.runtime.plugins.core.userpref.UserPreference;
+import org.ametys.runtime.plugins.core.userpref.UserPreferencesErrors;
+import org.ametys.web.ObservationConstants;
+import org.ametys.web.WebConstants;
+import org.ametys.web.repository.page.Page;
+import org.ametys.web.repository.site.Site;
+import org.ametys.web.userpref.SetFOUserPreferencesAction;
+
+/**
+ *  Set user preference for ODF cart 
+ */
+public class SetODFCartUserPreferencesAction extends SetFOUserPreferencesAction
+{
+    /** The ametys object resolver */
+    protected AmetysObjectResolver _resolver;
+    
+    /** The odf page resolver */
+    protected OdfPageResolver _odfPageResolver;
+    
+    private Map<String, String> _preferences;
+    
+    private List<String> _contentIdsError;
+    
+    @Override
+    public void service(ServiceManager serviceManager) throws ServiceException
+    {
+        super.service(serviceManager);
+        _resolver = (AmetysObjectResolver) serviceManager.lookup(AmetysObjectResolver.ROLE);
+        _odfPageResolver = (OdfPageResolver) serviceManager.lookup(OdfPageResolver.ROLE);
+    }
+    
+    @Override
+    public Map act(Redirector redirector, SourceResolver resolver, Map objectModel, String source, Parameters parameters) throws Exception
+    {
+        _preferences = new HashMap<String, String>();
+        _contentIdsError = new ArrayList<String>();
+        
+        Request request = ObjectModelHelper.getRequest(objectModel);
+        Map<String, Object> parentContext = (Map<String, Object>) objectModel.get(ObjectModelHelper.PARENT_CONTEXT);
+        
+        String context = parameters.getParameter("prefContext", request.getParameter("prefContext"));
+        String username = parameters.getParameter("username", (String) request.getAttribute(WebConstants.FO_LOGIN));
+        
+        Map<String, String> results = new HashMap<String, String>();
+        Map<String, String> contextVars = getContextVars(request);
+        
+        _handleValues(request, contextVars);
+        if (StringUtils.isNotBlank(username))
+        {
+            Collection<String> preferenceIds = getPreferenceIds(request, parentContext);
+            results = setUserPreferences(request, context, contextVars, username, preferenceIds);
+            
+            String siteName = (String) request.getAttribute("site");
+            
+            // Send an event indicating that the user preferences have changed.
+            // The target is the username, the context is provided as the single parameter.
+            Site site = _siteManager.getSite(siteName);
+            Event event = new Event(username, ObservationConstants.USER_PREFERENCES_CHANGED, site, new Object[] {context});
+            _observationManager.notify(event);
+        }
+        
+        results.put("contentError", StringUtils.join(_contentIdsError, ','));
+        return results;
+    }
+    
+    private void _handleValues(Request request, Map<String, String> contextVars)
+    {
+        for (UserPreference preference : _userPrefEP.getUserPreferences(contextVars).values())
+        {
+            String id = preference.getId();
+            String contentIdsAsString = request.getParameter(id);
+            if (contentIdsAsString != null)
+            {
+                List<String> contentIds = new ArrayList<String>();
+                for (String contentId : StringUtils.split(contentIdsAsString, ","))
+                {
+                    try
+                    {
+                        Content content = _resolver.resolveById(contentId);
+                        Page page = null;
+                        if (id.equals("cartProgramIds"))
+                        {
+                            page = _odfPageResolver.getProgramPage((Program) content);
+                        }
+                        else if (id.equals("cartCourseIds"))
+                        {
+                            page = _odfPageResolver.getCoursePage((Course) content);
+                        }
+                        
+                        if (page != null)
+                        {
+                            contentIds.add(contentId);
+                        }
+                        else
+                        {
+                            _contentIdsError.add(contentId);
+                        }
+                    }
+                    catch (UnknownAmetysObjectException e)
+                    {
+                        _contentIdsError.add(contentId);
+                    }
+                }
+                String valuesStr = StringUtils.join(contentIds, ',');
+                _preferences.put(id, valuesStr);
+            }
+        }
+    }
+    
+    @Override
+    protected Map<String, String> _getValues(Request request, Map<String, String> contextVars, Collection<String> preferenceIds, UserPreferencesErrors errors)
+    {
+        return _preferences;
+    }
+    
+    @Override
+    protected Collection<String> getPreferenceIds(Request request, Map<String, Object> parentContext)
+    {
+        Set<String> preferenceIdSet = new HashSet<String>();
+        
+        String prefIdStr = (String) request.get("preferences-ids");
+        if (StringUtils.isNotBlank(prefIdStr))
+        {
+            Collections.addAll(preferenceIdSet, StringUtils.split(prefIdStr, ','));
+        }
+        
+        return preferenceIdSet;
+    }
+}
Index: main/plugin-odf-web/stylesheets/helper/cart.xsl
===================================================================
--- main/plugin-odf-web/stylesheets/helper/cart.xsl	(revision 40731)
+++ main/plugin-odf-web/stylesheets/helper/cart.xsl	(working copy)
@@ -153,7 +153,11 @@
     
     <xsl:template name="add-to-cart-js">
         <xsl:param name="type"/>
-       
+        
+        <xsl:variable name="uri-prefix">
+            <xsl:call-template name="get-uri-prefix"/>
+        </xsl:variable>
+        
         <script type="text/javascript">
             if (odfcart_contains('<xsl:value-of select="@id"/>'))
             {
@@ -168,13 +172,13 @@
             {
                 var pageUrl = "<xsl:value-of select="resolver:resolve('page', ametys:pageId(), false)"/>";
                 odfcart_add<xsl:value-of select="$type"/> ('<xsl:value-of select="@id"/>', '<xsl:value-of select="ametys:pageId()"/>', pageUrl, '<xsl:value-of select="@title"/>');
-                document.location.reload(false);
+                odfcart_save("<xsl:value-of select="ametys:siteUriPrefix()"/>");
             }
             
             function removeFromCart_<xsl:value-of select="$uniqueId"/> (id)
             {
                 odfcart_remove('<xsl:value-of select="@id"/>');
-                document.location.reload(false);
+                odfcart_save("<xsl:value-of select="ametys:siteUriPrefix()"/>");
             }
         </script>
         
@@ -199,4 +203,10 @@
         </script>
     </xsl:template>
     
+    <xsl:template name="handle-userpref-cart-js">
+        <script type="text/javascript">
+            odfcart_getUserPref("<xsl:value-of select="ametys:siteUriPrefix()"/>");
+        </script>
+    </xsl:template>
+    
 </xsl:stylesheet>
\ No newline at end of file
