Index: main/plugin-explorer/i18n/messages_en.xml
===================================================================
--- main/plugin-explorer/i18n/messages_en.xml	(revision 37412)
+++ main/plugin-explorer/i18n/messages_en.xml	(working copy)
@@ -108,6 +108,8 @@
     <message key="PLUGINS_EXPLORER_CALENDAR_TOOLTIP_EVENT_FROM_2">From</message>
     <message key="PLUGINS_EXPLORER_CALENDAR_TOOLTIP_EVENT_THE">The</message>
     <message key="PLUGINS_EXPLORER_CALENDAR_TOOLTIP_EVENT_TO">to</message>
+    <message key="PLUGINS_EXPLORER_CALENDAR_TOOLTIP_EVENT_UNTIL">until</message>
+    <message key="PLUGINS_EXPLORER_CALENDAR_TOOLTIP_EVENT_RECURRENT">Recurrent</message>
     
     <!-- Format -->
     <message key="PLUGINS_EXPLORER_CALENDAR_FORMAT_TOOLTIP_FULL_DAY">m/d/Y</message>
@@ -199,6 +201,44 @@
     <message key="PLUGINS_EXPLORER_EVENT_LIST_CALENDAR_DESC">Choice of the calendar linked to the event.</message>
     <message key="PLUGINS_EXPLORER_EVENT_INTERVAL_ERROR">The end date can not be less than the start date</message>
    
+    <message key="PLUGINS_EXPLORER_EVENT_LIST_PERIOD_LABEL">Frequency</message>
+    <message key="PLUGINS_EXPLORER_EVENT_LIST_PERIOD_DESC">Choose the event frequency</message>
+    <message key="PLUGINS_EXPLORER_EVENT_LIST_PERIOD_NEVER">Never</message>
+    <message key="PLUGINS_EXPLORER_EVENT_LIST_PERIOD_ALL_DAY">All day</message>
+    <message key="PLUGINS_EXPLORER_EVENT_LIST_PERIOD_WORKING_DAY">All working day</message>
+    <message key="PLUGINS_EXPLORER_EVENT_LIST_PERIOD_WEEKLY">Weekly</message>
+    <message key="PLUGINS_EXPLORER_EVENT_LIST_PERIOD_TWO_WEEK">Biweekly</message>
+    <message key="PLUGINS_EXPLORER_EVENT_LIST_PERIOD_MONTHLY">Monthly</message>
+    
+    <message key="PLUGINS_EXPLORER_EVENT_UNTIL_DATE_LABEL">Until</message>
+    <message key="PLUGINS_EXPLORER_EVENT_UNTIL_DATE_DESC">Date where the event frequency stops. Empty for all the time.</message>
+    <message key="PLUGINS_EXPLORER_EVENT_UNTIL_DATE_EMPTY_TEXT">All the time</message>
+    
+    <message key="PLUGINS_EXPLORER_EVENT_RECURRENT_BOX_DELETE_TITLE">Delete recurrent event</message>
+    <message key="PLUGINS_EXPLORER_EVENT_RECURRENT_BOX_DELETE_HINT">Do you want to delete this occurrence or all event occurrences ?</message>
+    <message key="PLUGINS_EXPLORER_EVENT_RECURRENT_BOX_MOVE_TITLE">Move recurrent event</message>
+    <message key="PLUGINS_EXPLORER_EVENT_RECURRENT_BOX_MOVE_HINT">Do you want to move this occurrence or all event occurrences ?</message>
+    <message key="PLUGINS_EXPLORER_EVENT_RECURRENT_BOX_EDIT_TITLE">Edit recurrent event</message>
+    <message key="PLUGINS_EXPLORER_EVENT_RECURRENT_BOX_EDIT_HINT">Do you want to edit this occurrence or all event occurrences ?</message>
+
+    <message key="PLUGINS_EXPLORER_EVENT_RECURRENT_RADIO_CHOICE_DESC_1">Others event occurrences will not be changed.</message>
+    <message key="PLUGINS_EXPLORER_EVENT_RECURRENT_RADIO_CHOICE_DESC_2">All event occurrences will be changed.</message>
+    <message key="PLUGINS_EXPLORER_EVENT_RECURRENT_RADIO_CHOICE_1">Only this event</message>
+    <message key="PLUGINS_EXPLORER_EVENT_RECURRENT_RADIO_CHOICE_2">All event occurrences</message>
+    
+    <!-- Config -->
+    <message key="PLUGINS_EXPLORER_CALENDAR_EVENT_PARAM_WORKING_DAY_GROUP">Events</message>
+    <message key="PLUGINS_EXPLORER_CALENDAR_EVENT_PARAM_WORKING_DAY_LABEL">Working day</message>
+    <message key="PLUGINS_EXPLORER_CALENDAR_EVENT_PARAM_WORKING_DAY_DESC">List of working day.</message>
+    
+    <message key="PLUGINS_EXPLORER_CALENDAR_EVENT_PARAM_WORKING_DAY_MONDAY">Monday</message>
+    <message key="PLUGINS_EXPLORER_CALENDAR_EVENT_PARAM_WORKING_DAY_TUESDAY">Tuesday</message>
+    <message key="PLUGINS_EXPLORER_CALENDAR_EVENT_PARAM_WORKING_DAY_WEDNESDAY">Wednesday</message>
+    <message key="PLUGINS_EXPLORER_CALENDAR_EVENT_PARAM_WORKING_DAY_THURSDAY">Thursday</message>
+    <message key="PLUGINS_EXPLORER_CALENDAR_EVENT_PARAM_WORKING_DAY_FRIDAY">Friday</message>
+    <message key="PLUGINS_EXPLORER_CALENDAR_EVENT_PARAM_WORKING_DAY_SATURDAY">Saturday</message>
+    <message key="PLUGINS_EXPLORER_CALENDAR_EVENT_PARAM_WORKING_DAY_SUNDAY">Sunday</message>
+	
 	<!-- + 
 		 | RESOURCES 
 		 +-->
Index: main/plugin-explorer/i18n/messages_fr.xml
===================================================================
--- main/plugin-explorer/i18n/messages_fr.xml	(revision 37412)
+++ main/plugin-explorer/i18n/messages_fr.xml	(working copy)
@@ -108,6 +108,8 @@
     <message key="PLUGINS_EXPLORER_CALENDAR_TOOLTIP_EVENT_FROM_2">Du</message>
     <message key="PLUGINS_EXPLORER_CALENDAR_TOOLTIP_EVENT_THE">Le</message>
     <message key="PLUGINS_EXPLORER_CALENDAR_TOOLTIP_EVENT_TO">au</message>
+    <message key="PLUGINS_EXPLORER_CALENDAR_TOOLTIP_EVENT_UNTIL">jusqu'au</message>
+    <message key="PLUGINS_EXPLORER_CALENDAR_TOOLTIP_EVENT_RECURRENT">Récursif</message>
     
     <!-- Format -->
     <message key="PLUGINS_EXPLORER_CALENDAR_FORMAT_TOOLTIP_FULL_DAY">d/m/y</message>
@@ -199,6 +201,44 @@
     <message key="PLUGINS_EXPLORER_EVENT_LIST_CALENDAR_DESC">Choix du calendrier auquel est associé l'événement</message>
     <message key="PLUGINS_EXPLORER_EVENT_INTERVAL_ERROR">La date de fin ne peut pas être inférieure à la date de début</message>
     
+    <message key="PLUGINS_EXPLORER_EVENT_LIST_PERIOD_LABEL">Répétition</message>
+    <message key="PLUGINS_EXPLORER_EVENT_LIST_PERIOD_DESC">Choisir la périodicité de l'événement</message>
+    <message key="PLUGINS_EXPLORER_EVENT_LIST_PERIOD_NEVER">Jamais</message>
+    <message key="PLUGINS_EXPLORER_EVENT_LIST_PERIOD_ALL_DAY">Tous les jours</message>
+    <message key="PLUGINS_EXPLORER_EVENT_LIST_PERIOD_WORKING_DAY">Tous les jours ouvrables</message>
+    <message key="PLUGINS_EXPLORER_EVENT_LIST_PERIOD_WEEKLY">Hebdomadaire</message>
+    <message key="PLUGINS_EXPLORER_EVENT_LIST_PERIOD_TWO_WEEK">Toutes les deux semaines</message>
+    <message key="PLUGINS_EXPLORER_EVENT_LIST_PERIOD_MONTHLY">Mensuelle</message>
+    
+    <message key="PLUGINS_EXPLORER_EVENT_UNTIL_DATE_LABEL">Jusqu'à</message>
+    <message key="PLUGINS_EXPLORER_EVENT_UNTIL_DATE_DESC">La date de fin de la répétition. Laissez vide pour tout le temps.</message>
+    <message key="PLUGINS_EXPLORER_EVENT_UNTIL_DATE_EMPTY_TEXT">Tout le temps</message>
+    
+    <message key="PLUGINS_EXPLORER_EVENT_RECURRENT_BOX_DELETE_TITLE">Supprimer l'événement périodique</message>
+    <message key="PLUGINS_EXPLORER_EVENT_RECURRENT_BOX_DELETE_HINT">Souhaitez-vous supprimer uniquement cet événement ou tous les événéments de la série ?</message>
+    <message key="PLUGINS_EXPLORER_EVENT_RECURRENT_BOX_MOVE_TITLE">Déplacer l'événement périodique</message>
+    <message key="PLUGINS_EXPLORER_EVENT_RECURRENT_BOX_MOVE_HINT">Souhaitez-vous déplacer uniquement cet événement ou tous les événéments de la série ?</message>
+    <message key="PLUGINS_EXPLORER_EVENT_RECURRENT_BOX_EDIT_TITLE">Modifier l'événement périodique</message>
+    <message key="PLUGINS_EXPLORER_EVENT_RECURRENT_BOX_EDIT_HINT">Souhaitez-vous modifier uniquement cet événement ou tous les événéments de la série ?</message>
+
+    <message key="PLUGINS_EXPLORER_EVENT_RECURRENT_RADIO_CHOICE_DESC_1">Les autres événéments de la série resteront identiques.</message>
+    <message key="PLUGINS_EXPLORER_EVENT_RECURRENT_RADIO_CHOICE_DESC_2">Tous les événements de la série seront impactés.</message>
+    <message key="PLUGINS_EXPLORER_EVENT_RECURRENT_RADIO_CHOICE_1">Uniquement cet événement</message>
+    <message key="PLUGINS_EXPLORER_EVENT_RECURRENT_RADIO_CHOICE_2">Tous les événements</message>
+    
+    <!-- Config -->
+    <message key="PLUGINS_EXPLORER_CALENDAR_EVENT_PARAM_WORKING_DAY_GROUP">Événements</message>
+    <message key="PLUGINS_EXPLORER_CALENDAR_EVENT_PARAM_WORKING_DAY_LABEL">Jours ouvrés</message>
+    <message key="PLUGINS_EXPLORER_CALENDAR_EVENT_PARAM_WORKING_DAY_DESC">Liste des jours ouvrés de la semaine.</message>
+    
+    <message key="PLUGINS_EXPLORER_CALENDAR_EVENT_PARAM_WORKING_DAY_MONDAY">Lundi</message>
+    <message key="PLUGINS_EXPLORER_CALENDAR_EVENT_PARAM_WORKING_DAY_TUESDAY">Mardi</message>
+    <message key="PLUGINS_EXPLORER_CALENDAR_EVENT_PARAM_WORKING_DAY_WEDNESDAY">Mercredi</message>
+    <message key="PLUGINS_EXPLORER_CALENDAR_EVENT_PARAM_WORKING_DAY_THURSDAY">Jeudi</message>
+    <message key="PLUGINS_EXPLORER_CALENDAR_EVENT_PARAM_WORKING_DAY_FRIDAY">Vendredi</message>
+    <message key="PLUGINS_EXPLORER_CALENDAR_EVENT_PARAM_WORKING_DAY_SATURDAY">Samedi</message>
+    <message key="PLUGINS_EXPLORER_CALENDAR_EVENT_PARAM_WORKING_DAY_SUNDAY">Dimanche</message>
+    
 	<!-- + 
 		 | RESOURCES 
 		 +-->
Index: main/plugin-explorer/nodetypes.xml
===================================================================
--- main/plugin-explorer/nodetypes.xml	(revision 37412)
+++ main/plugin-explorer/nodetypes.xml	(working copy)
@@ -70,6 +70,9 @@
         <propertyDefinition name="ametys:contributor" requiredType="String" autoCreated="false" mandatory="true" onParentVersion="COPY" protected="false" multiple="false"/>
         <propertyDefinition name="ametys:creationDate" requiredType="Date" autoCreated="false" mandatory="true" onParentVersion="COPY" protected="false" multiple="false"/>
         <propertyDefinition name="ametys:lastModified" requiredType="Date" autoCreated="false" mandatory="true" onParentVersion="COPY" protected="false" multiple="false"/>
+        <propertyDefinition name="ametys:recurrenceType" requiredType="String" autoCreated="false" mandatory="false" onParentVersion="COPY" protected="false" multiple="false"/>
+        <propertyDefinition name="ametys:untilDate" requiredType="Date" autoCreated="false" mandatory="false" onParentVersion="COPY" protected="false" multiple="false"/>
+        <propertyDefinition name="ametys:excludedDate" requiredType="Date" autoCreated="false" mandatory="false" onParentVersion="COPY" protected="false" multiple="true"/>
     </nodeType>
     <nodeType name="ametys:thread" isMixin="false" hasOrderableChildNodes="false" primaryItemName="">
          <supertypes>
Index: main/plugin-explorer/plugin.xml
===================================================================
--- main/plugin-explorer/plugin.xml	(revision 37412)
+++ main/plugin-explorer/plugin.xml	(working copy)
@@ -45,6 +45,49 @@
             <group i18n="true">PLUGINS_EXPLORER_CALENDAR_PARAM_VIEW_GROUP</group>
             <order>10</order>
         </param>
+        
+        <param id="org.ametys.plugins.explorer.calendar.event.working.day" type="string">
+            <label i18n="true">PLUGINS_EXPLORER_CALENDAR_EVENT_PARAM_WORKING_DAY_LABEL</label>
+            <description i18n="true">PLUGINS_EXPLORER_CALENDAR_EVENT_PARAM_WORKING_DAY_DESC</description>
+            <enumeration>
+                <entry>
+                    <label i18n="true">PLUGINS_EXPLORER_CALENDAR_EVENT_PARAM_WORKING_DAY_MONDAY</label>
+                    <value>2</value>
+                </entry>
+                <entry>
+                    <label i18n="true">PLUGINS_EXPLORER_CALENDAR_EVENT_PARAM_WORKING_DAY_TUESDAY</label>
+                    <value>3</value>
+                </entry>
+                <entry>
+                    <label i18n="true">PLUGINS_EXPLORER_CALENDAR_EVENT_PARAM_WORKING_DAY_WEDNESDAY</label>
+                    <value>4</value>
+                </entry>
+                <entry>
+                    <label i18n="true">PLUGINS_EXPLORER_CALENDAR_EVENT_PARAM_WORKING_DAY_THURSDAY</label>
+                    <value>5</value>
+                </entry>
+                <entry>
+                    <label i18n="true">PLUGINS_EXPLORER_CALENDAR_EVENT_PARAM_WORKING_DAY_FRIDAY</label>
+                    <value>6</value>
+                </entry>
+                <entry>
+                    <label i18n="true">PLUGINS_EXPLORER_CALENDAR_EVENT_PARAM_WORKING_DAY_SATURDAY</label>
+                    <value>7</value>
+                </entry>
+                <entry>
+                    <label i18n="true">PLUGINS_EXPLORER_CALENDAR_EVENT_PARAM_WORKING_DAY_SUNDAY</label>
+                    <value>1</value>
+                </entry>
+            </enumeration>
+            <validation>
+                <mandatory/>
+            </validation>
+            <default-value>0</default-value>
+            <category i18n="true">PLUGINS_EXPLORER_CALENDAR_PARAM_VIEW_CATEGORY</category>
+            <group i18n="true">PLUGINS_EXPLORER_CALENDAR_EVENT_PARAM_WORKING_DAY_GROUP</group>
+            <order>20</order>
+            <widget>multiselect</widget>
+        </param>
     </config>
     
     <extension-points>
@@ -151,6 +194,7 @@
 	 <feature name="userinterface.explorer.applications">
 	    <config>
 	       <param-ref id="org.ametys.plugins.explorer.calendar.view" />
+	       <param-ref id="org.ametys.plugins.explorer.calendar.event.working.day" />
 	    </config>
         <components>
 	       <component role="org.ametys.plugins.explorer.calendar.workflow.AddEventFunction"
@@ -223,7 +267,9 @@
                     <file>js/org/ametys/explorer/applications/calendar/CalendarActions.i18n.js</file>
                     <file>js/org/ametys/explorer/applications/calendar/CalendarApplication.i18n.js</file>
                     <file>js/org/ametys/explorer/applications/calendar/toolbar/ToolbarActions.i18n.js</file>
+                    <file>js/org/ametys/explorer/applications/calendar/helper/EditRecurrentEventHelper.i18n.js</file>
                     <file>js/org/ametys/explorer/applications/calendar/EventActions.i18n.js</file>
+                    <file>js/org/ametys/explorer/applications/calendar/widget/UntilDateField.js</file>
                  </scripts>
                  <css>
                     <file>../calendar/colors.css</file>
Index: main/plugin-explorer/resources/css/calendar-ametys.css
===================================================================
--- main/plugin-explorer/resources/css/calendar-ametys.css	(revision 37412)
+++ main/plugin-explorer/resources/css/calendar-ametys.css	(working copy)
@@ -53,3 +53,24 @@
 	font-style: italic;
 }
 
+div.x-form-date-until-wrap {
+    float:left;
+}
+
+.x-form-date-until-wrap .x-form-empty-field {
+    color: #000;
+}
+
+.calendar-event-icon-recurrent
+{
+    float: right;
+    width: 16px;
+    height: 16px;
+    margin-right: 2px;
+}
+
+.ametys .x-form-check-wrap label
+{
+	font-size: 0.9em;
+}
+
Index: main/plugin-explorer/resources/js/Ext/calendar-all-debug.js
===================================================================
--- main/plugin-explorer/resources/js/Ext/calendar-all-debug.js	(revision 37412)
+++ main/plugin-explorer/resources/js/Ext/calendar-all-debug.js	(working copy)
@@ -3003,8 +3003,12 @@
             this.initClock();
         }
         
-        /** Ametys set current day*/
-        Ext.get(this.getDayId(this.startDate)).addClass("ext-cal-day-curday")
+        /** Ametys set current day */
+        var ele = Ext.get(this.getDayId(this.startDate));
+        if (ele != null)
+    	{
+        	ele.addClass("ext-cal-day-curday");
+    	}
     },
 
     // private
Index: main/plugin-explorer/resources/js/org/ametys/explorer/applications/calendar/CalendarApplication.i18n.js
===================================================================
--- main/plugin-explorer/resources/js/org/ametys/explorer/applications/calendar/CalendarApplication.i18n.js	(revision 37412)
+++ main/plugin-explorer/resources/js/org/ametys/explorer/applications/calendar/CalendarApplication.i18n.js	(working copy)
@@ -76,12 +76,16 @@
 	                	}
 	                	else
 	                	{
-	                		var imgPath = context.contextPath + "/plugins/explorer/resources_workflow/WORKFLOW_CALENDAR_STATE_DRAFT-small.png";
-	                		var text = "&lt;span class='calendar-event-icon-workflow-state'&gt;&lt;span style='background-image: url(" + imgPath + ");' &gt;&lt;/span&gt;&lt;/span&gt;&lt;span class='calendar-event-icon-workflow-state-placeholder'&gt;&lt;/span&gt;";
-	                		titleConverted = text + "&lt;span &gt;" + v + "&lt;/span&gt;";
-	                		
-	                		//titleConverted = "&lt;span &gt;" + v + "&lt;/span&gt;";
+	                		titleConverted = "&lt;span &gt;" + v + "&lt;/span&gt;";
 	                	}
+                		
+	                	var recurrenceType = record.selectSingleNode("recurrenceType")[org.ametys.servercomm.ServerComm.xmlTextContent];
+	                	if (recurrenceType != "NEVER")
+	                	{
+	                		var imgPathRecurrent = context.contextPath + "/plugins/explorer/resources/img/event/recurrent.png";
+                			var textRecurrent = "&lt;div class='calendar-event-icon-recurrent' style='background-image: url(" + imgPathRecurrent + ");' &gt;&lt;/div&gt;";
+                			titleConverted = textRecurrent + titleConverted;
+                		}
 	                	
 	                	return titleConverted;
                 	}
@@ -97,6 +101,8 @@
                 {name:'ContributorLastname', mapping: 'contributor/user/lastname'},
                 {name:'ContributorLogin', mapping: 'contributor/@login'},
                 {name:'Notes', mapping: 'desc'},
+                {name:'RecurrenceType', mapping: 'recurrenceType'},
+                {name:'UntilDate', mapping: 'untilDate', type: 'date', dateFormat: Date.patterns.ISO8601Long},
                 {name:'CalendarId', mapping: '@color'},
                 {name:'TrueCalendarId', mapping: '@calendarId'},
                 {name:'IsAllDay', mapping: 'fullDay', type: 'boolean'},
@@ -423,13 +429,15 @@
     		},
             'eventmove': {
             	fn: function(calendarView, rc) {
-            		org.ametys.explorer.applications.calendar.Event.moveEvent(this, rc, this._selectedNode);
+            		var isRecurrent = rc.get("RecurrenceType") != "NEVER"; //FIXME multiselect
+            		org.ametys.explorer.applications.calendar.Event.moveEvent(this, rc, this._selectedNode, isRecurrent);
             	},
             	scope : this
             },
             'eventresize' : {
             	fn: function(calendarView, rc) {
-            		org.ametys.explorer.applications.calendar.Event.moveEvent(this, rc, this._selectedNode);
+            		var isRecurrent = rc.get("RecurrenceType") != "NEVER"; //FIXME multiselect
+            		org.ametys.explorer.applications.calendar.Event.moveEvent(this, rc, this._selectedNode, isRecurrent);
             	},
             	scope : this
             },
@@ -572,6 +580,9 @@
 		'&lt;div class="event-tooltip"&gt;',
 			'&lt;div class="event-tooltip-title"&gt;{title}&lt;/div&gt;',
 			'&lt;div class="event-tooltip-date"&gt;{date}&lt;/div&gt;',
+			'&lt;tpl if="recurrentDesc != \'\'"&gt;',
+			'&lt;div class="event-tooltip-recurrence-desc"&gt;{recurrentDesc}&lt;/div&gt;',
+			'&lt;/tpl&gt;',
 			'&lt;tpl if="desc != \'\'"&gt;',
 				'&lt;div class="event-tooltip-desc"&gt;{desc}&lt;/div&gt;',
 			'&lt;/tpl&gt;',
@@ -649,10 +660,19 @@
 		}
 	}
 	
+	var recurrenceDesc = "";
+	var recurrenceType = record.get("RecurrenceType");
+	var untilDate = record.get("UntilDate") == null ? "" : " <i18n:text i18n:key="PLUGINS_EXPLORER_CALENDAR_TOOLTIP_EVENT_UNTIL"/> " + record.get("UntilDate").format(formatDay);
+	if (recurrenceType != "NEVER")
+	{
+		recurrenceDesc = "<i18n:text i18n:key="PLUGINS_EXPLORER_CALENDAR_TOOLTIP_EVENT_RECURRENT"/>" + untilDate;
+	}
+	
 	return this._tooltipTpl.apply({
 		title: record.get("Title"),
 		date: fullDateAsStr,
 		desc: Utils.textareaToHTML(record.get("Notes")),
+		recurrentDesc: recurrenceDesc,
 		creator: creator,
 		contributor: contributor
 	});
@@ -955,15 +975,7 @@
 			    		   height: org.ametys.explorer.ResourcesExplorer.TOOLBAR_GROUP_BUTTON_SMALL_HEIGHT,
 			    		   application: this,
 			    		   handler: function () {
-			    			   var application = this.application;
-			    			   Ext.MessageBox.confirm("<i18n:text i18n:key="PLUGINS_EXPLORER_EVENT_DELETE_CONFIRM_TITLE"/>",
-			    					   "<i18n:text i18n:key="PLUGINS_EXPLORER_EVENT_DELETE_CONFIRM_DESC"/>",
-			    					   function(btn) {
-			    				   if (btn === 'yes') {
-			    					   org.ametys.explorer.applications.calendar.toolbar.Event.deleteEvent (application);
-			    				   }
-			    			   }
-			    			   )
+			    			  org.ametys.explorer.applications.calendar.toolbar.Event.deleteEvent (this.application);
 			    		   },
 			    		   rightId: 'Plugin_Explorer_Event_Delete',
 			    		   deleteOwnedEventRightId: 'Plugin_Explorer_Owned_Event_Delete',
@@ -1164,7 +1176,7 @@
 {
 	params = {};
 	params.actionId = actionId;
-	params.id = this.getSelectedEventIds()[0]; //FIXME multiselection
+	params.id = this.getSelectedEventIds()[0].split("$")[0]; //FIXME multiselection
 	params.parentId = this._selectedNode;
 	
 	var serverMessage = new org.ametys.servercomm.ServerMessage('explorer', '/workflow', params, org.ametys.servercomm.ServerComm.PRIORITY_SYNCHRONOUS, null, this, null);
@@ -1440,7 +1452,7 @@
 		var params = {};
 		for (var i = 0; i &lt; events.length; i++)
 		{
-			eventsIds.push(events[i].get('TrueEventId'));
+			eventsIds.push(events[i].get('TrueEventId').split("$")[0]);
 		}
 
 		params.eventsIds = eventsIds;
Index: main/plugin-explorer/resources/js/org/ametys/explorer/applications/calendar/EventActions.i18n.js
===================================================================
--- main/plugin-explorer/resources/js/org/ametys/explorer/applications/calendar/EventActions.i18n.js	(revision 37412)
+++ main/plugin-explorer/resources/js/org/ametys/explorer/applications/calendar/EventActions.i18n.js	(working copy)
@@ -16,7 +16,6 @@
 
 Ext.namespace('org.ametys.explorer.applications.calendar.Event');
 Ext.namespace('org.ametys.explorer.applications.calendar.Event'); 
-
 /*------------------------------------------------------------------------*/
 
 
@@ -39,6 +38,10 @@
 	org.ametys.explorer.applications.calendar.Event._desc.setValue(templateDesc);
 	
 	org.ametys.explorer.applications.calendar.Event._fullDay.setValue(fullDay);
+	org.ametys.explorer.applications.calendar.Event._comboListPeriod.setValue("NEVER");
+	org.ametys.explorer.applications.calendar.Event._untilDate.setValue("");
+	org.ametys.explorer.applications.calendar.Event._untilDate.hide();
+	
 	org.ametys.explorer.applications.calendar.Event._comboListCalendar.getStore().setBaseParam ("calendarId", org.ametys.explorer.applications.calendar.Event._parentId);
 	org.ametys.explorer.applications.calendar.Event._comboListCalendar.getStore().setBaseParam ("projectName", context.parameters.projectName);
 	org.ametys.explorer.applications.calendar.Event._comboListCalendar.setValue(null);
@@ -172,6 +175,8 @@
     	    		
     	    		org.ametys.explorer.applications.calendar.Event._endDateTime.setValue(newEndDate);
     	    		field.startValue = field.getValue();
+    	    		
+    	    		org.ametys.explorer.applications.calendar.Event._untilDate.setMinValue(value);
     	    	}
     	    }
     	    
@@ -219,6 +224,7 @@
     	    		
     	    		org.ametys.explorer.applications.calendar.Event._endDate.setValue(newEndDate);
     	    		field.startValue = value;
+    	    		org.ametys.explorer.applications.calendar.Event._untilDate.setMinValue(value);
     	    	}
     	    }
         });
@@ -241,6 +247,63 @@
     	    }
         });
 		
+		org.ametys.explorer.applications.calendar.Event._comboListPeriod = new org.ametys.form.ComboField({
+    	    id: 'explorer-event-list-period',
+    	    fieldLabel: "<i18n:text i18n:key='PLUGINS_EXPLORER_EVENT_LIST_PERIOD_LABEL'/>",
+    	    desc: "<i18n:text i18n:key='PLUGINS_EXPLORER_EVENT_LIST_PERIOD_DESC'/>",
+    	    
+    	    displayField: 'label_Period',
+    	    valueField: 'id_Period',
+    	    width: 220,
+    	    store: new Ext.data.ArrayStore({
+    	        fields: [
+    	            'id_Period',
+    	            'label_Period'
+    	        ],
+    	        data: [
+    	               ["NEVER", "<i18n:text i18n:key='PLUGINS_EXPLORER_EVENT_LIST_PERIOD_NEVER'/>"], 
+    	               ["ALL_DAY", "<i18n:text i18n:key='PLUGINS_EXPLORER_EVENT_LIST_PERIOD_ALL_DAY'/>"],
+    	               ["ALL_WORKING_DAY", "<i18n:text i18n:key='PLUGINS_EXPLORER_EVENT_LIST_PERIOD_WORKING_DAY'/>"],
+    	               ["WEEKLY", "<i18n:text i18n:key='PLUGINS_EXPLORER_EVENT_LIST_PERIOD_WEEKLY'/>"],
+    	               ["BIWEEKLY", "<i18n:text i18n:key='PLUGINS_EXPLORER_EVENT_LIST_PERIOD_TWO_WEEK'/>"],
+    	               ["MONTHLY", "<i18n:text i18n:key='PLUGINS_EXPLORER_EVENT_LIST_PERIOD_MONTHLY'/>"]
+     	              ]
+    	    }),
+    	    mode: 'local',
+    	    allowBlank: false,
+    	    forceSelection: true,
+    	    triggerAction: 'all',
+    	    minChars: 1,
+    	    listeners: {
+    	    	'select': function(combo, record, index)
+    	    	{
+    	    		if (record.get("id_Period") == "NEVER")
+	    			{
+    	    			org.ametys.explorer.applications.calendar.Event._untilDate.hide();
+	    			}
+    	    		else
+    	    		{
+    	    			org.ametys.explorer.applications.calendar.Event._untilDate.show();
+    	    		}
+    	    	}
+    	    }
+    	});
+		
+		org.ametys.explorer.applications.calendar.Event._untilDate = new org.ametys.form.UntilDateField({
+            id: 'explorer-event-until-date',
+            fieldLabel: "<i18n:text i18n:key='PLUGINS_EXPLORER_EVENT_UNTIL_DATE_LABEL'/>",
+            desc: "<i18n:text i18n:key='PLUGINS_EXPLORER_EVENT_UNTIL_DATE_DESC'/>",
+            width: 198,
+            height: 100,
+            listeners:
+            	{
+//            		"focus" : function(thiss){debugger}
+            	},
+    	    emptyText: "<i18n:text i18n:key='PLUGINS_EXPLORER_EVENT_UNTIL_DATE_EMPTY_TEXT'/>",
+    	    showToday: false,
+    	    hidden: true,
+    	    value: 'Thu Nov 20 2015 00:00:00 GMT+0100 (CET)'
+        });
 		
 		function adaptHelp()
 		{
@@ -320,6 +383,8 @@
 	            org.ametys.explorer.applications.calendar.Event._endDate,
 	            org.ametys.explorer.applications.calendar.Event._startDateTime,
 	            org.ametys.explorer.applications.calendar.Event._endDateTime,
+	            org.ametys.explorer.applications.calendar.Event._comboListPeriod,
+	            org.ametys.explorer.applications.calendar.Event._untilDate,
 	            org.ametys.explorer.applications.calendar.Event._comboListCalendar,
 	            org.ametys.explorer.applications.calendar.Event._comboListCalendarHelp
 	        ],
@@ -333,7 +398,7 @@
 	        title: "<i18n:text i18n:key='PLUGINS_EXPLORER_EVENT_TITLE_ADD_LABEL'/>",
 	        icon: getPluginResourcesUrl ('explorer') + '/img/event/event_new_16.png',        
 	        width: 450,
-	        height: 460,
+	        height: 520,
 	        autoScroll: false,	        
 	        items: [ org.ametys.explorer.applications.calendar.Event._formPanel ],        
 	        closeAction: 'hide',
@@ -420,7 +485,19 @@
 			org.ametys.explorer.applications.calendar.Event._endDateTime.setValue(endDate);
 		}
 	}
-		
+	
+	
+	org.ametys.explorer.applications.calendar.Event._untilDate.setMinValue(org.ametys.explorer.applications.calendar.Event._startDate.getValue());
+	org.ametys.explorer.applications.calendar.Event._untilDate.trigger.addListener('click', 
+			function()
+			{
+				if (org.ametys.explorer.applications.calendar.Event._untilDate.getValue() == "")
+				{
+					var newStartDate = org.ametys.explorer.applications.calendar.Event._startDate.getValue();
+					org.ametys.explorer.applications.calendar.Event._untilDate.setValue(newStartDate);
+					org.ametys.explorer.applications.calendar.Event._untilDate.onTriggerClick( );
+				}
+			});
 }
 
 org.ametys.explorer.applications.calendar.Event._cancel = function()
@@ -428,7 +505,48 @@
 	org.ametys.explorer.applications.calendar.Event._box.hide();
 }
 
-org.ametys.explorer.applications.calendar.Event._addOrEdit = function ()
+
+org.ametys.explorer.applications.calendar.Event._addOrEdit = function()
+{
+	// Add an event
+	if (org.ametys.explorer.applications.calendar.Event._eventId == null)
+	{
+		org.ametys.explorer.applications.calendar.Event._addOrEditAction();
+	}
+	else // Edit an event
+	{
+		if (org.ametys.explorer.applications.calendar.Event._isRecurrent)
+		{
+			var newRecurrenceType = org.ametys.explorer.applications.calendar.Event._comboListPeriod.getValue();
+			var newUntilDate = org.ametys.explorer.applications.calendar.Event._untilDate.getValue();
+			
+			if(newUntilDate != "" &amp;&amp; org.ametys.explorer.applications.calendar.Event._untilDateValue != newUntilDate.format(Date.patterns.ISO8601Long) 
+			|| newUntilDate == "" &amp;&amp; org.ametys.explorer.applications.calendar.Event._untilDateValue != newUntilDate
+			|| org.ametys.explorer.applications.calendar.Event._recurrenceTypeValue != newRecurrenceType)
+			{
+				org.ametys.explorer.applications.calendar.Event._addOrEditAction("all");
+			}
+			else
+			{
+				org.ametys.explorer.applications.calendar.Event.Recurrent.Helper._openBox(
+						"<i18n:text i18n:key="PLUGINS_EXPLORER_EVENT_RECURRENT_BOX_EDIT_TITLE"/>",
+						"<i18n:text i18n:key="PLUGINS_EXPLORER_EVENT_RECURRENT_BOX_EDIT_HINT"/>",
+						getPluginResourcesUrl ('explorer') + '/img/event/event_edit_16.png',
+						function(choice) {
+							org.ametys.explorer.applications.calendar.Event._addOrEditAction(choice);
+						}
+				);
+			}
+			
+		}
+		else
+		{
+			org.ametys.explorer.applications.calendar.Event._addOrEditAction();
+		}
+	}
+}
+
+org.ametys.explorer.applications.calendar.Event._addOrEditAction = function (choice)
 {
 	if (!org.ametys.explorer.applications.calendar.Event._formPanel.getForm().isValid())
 	{
@@ -478,17 +596,33 @@
 	var calendarId = org.ametys.explorer.applications.calendar.Event._comboListCalendar.getValue();
 	params.parentId = calendarId;
 	params.id = org.ametys.explorer.applications.calendar.Event._eventId;
+	params.choice = choice;
+	params.occurrenceDate = org.ametys.explorer.applications.calendar.Event._occurrenceDate;
 	params.fullDay = fullDay;
 	params.renameIfExists = true;
 
+	var recurrenceType = org.ametys.explorer.applications.calendar.Event._comboListPeriod.getValue();
+	params.recurrenceType = recurrenceType;
+	if (recurrenceType != "NEVER")
+	{
+		var untilDate = org.ametys.explorer.applications.calendar.Event._untilDate.getValue();
+		if (untilDate != "")
+		{
+			untilDate = untilDate.format(Date.patterns.ISO8601Long);
+		}
+		params.untilDate = untilDate;
+	}
+	
 	if (org.ametys.explorer.applications.calendar.Event._initialParentCalendar != null &amp;&amp; org.ametys.explorer.applications.calendar.Event._initialParentCalendar != calendarId)
 	{
+		var eventId = [org.ametys.explorer.applications.calendar.Event._eventId + "$" + org.ametys.explorer.applications.calendar.Event._occurrenceDate];
 		// Calendar was changed, first delete event
-		org.ametys.explorer.applications.calendar.Event.deleteEvent(
+		org.ametys.explorer.applications.calendar.Event._deleteEventAction(
 				org.ametys.explorer.applications.calendar.Event._initialParentCalendar,
-				org.ametys.explorer.applications.calendar.Event._eventId
-		);
-		
+				eventId, 
+				choice, 
+				org.ametys.explorer.applications.calendar.Event._callback);
+
 		// Then create event in selected calendar
 		params.actionId = 1;
 		params.id = null;
@@ -498,7 +632,7 @@
 			params, 
 			org.ametys.servercomm.ServerComm.PRIORITY_MAJOR, 
 			function (response, args) {
-				org.ametys.explorer.applications.calendar.Event._addOrEditCb (response, args, org.ametys.explorer.applications.calendar.Event._callback);
+				org.ametys.explorer.applications.calendar.Event._addOrEditActionCb (response, args, org.ametys.explorer.applications.calendar.Event._callback);
 			},
 			this, 
 			[]);
@@ -506,7 +640,7 @@
 	org.ametys.servercomm.ServerComm.getInstance().send(serverMessage);
 }
 
-org.ametys.explorer.applications.calendar.Event._addOrEditCb = function (response, args, callback)
+org.ametys.explorer.applications.calendar.Event._addOrEditActionCb = function (response, args, callback)
 {
 	org.ametys.explorer.applications.calendar.Event._box.hideWaitMsg();
 	if (org.ametys.servercomm.ServerComm.handleBadResponse("<i18n:text i18n:key="PLUGINS_EXPLORER_EVENT_HANDLE_ADD_EDIT_ERROR"/>", response, "org.ametys.explorer.applications.calendar.Event"))
@@ -530,24 +664,51 @@
  * Delete a event
  * @param {String} eventId of the event
  */
-org.ametys.explorer.applications.calendar.Event.deleteEvent = function(calendarId, eventId, callback)
+org.ametys.explorer.applications.calendar.Event.deleteEvent = function(calendarId, eventId, isRecurrent, callback)
+{
+	if (isRecurrent)
+	{
+		org.ametys.explorer.applications.calendar.Event.Recurrent.Helper._openBox(
+				"<i18n:text i18n:key="PLUGINS_EXPLORER_EVENT_RECURRENT_BOX_DELETE_TITLE"/>",
+				"<i18n:text i18n:key="PLUGINS_EXPLORER_EVENT_RECURRENT_BOX_DELETE_HINT"/>",
+				getPluginResourcesUrl ('explorer') + '/img/event/event_delete_16.png',
+				function(choice) {
+				   org.ametys.explorer.applications.calendar.Event._deleteEventAction(calendarId, eventId, choice, callback);
+     		    });
+	}
+	else
+	{
+		Ext.MessageBox.confirm("<i18n:text i18n:key="PLUGINS_EXPLORER_EVENT_DELETE_CONFIRM_TITLE"/>",
+		   "<i18n:text i18n:key="PLUGINS_EXPLORER_EVENT_DELETE_CONFIRM_DESC"/>",
+		   function(btn) {
+         		   if (btn === 'yes') {
+         			   org.ametys.explorer.applications.calendar.Event._deleteEventAction(calendarId, eventId, null, callback);
+         		   }
+			}
+		);
+	}
+}
+
+org.ametys.explorer.applications.calendar.Event._deleteEventAction = function(calendarId, eventId, choice, callback)
 {
 	var params = {};
-	params.ids = eventId;
+	params.ids = eventId[0].split("$")[0]; //FIXME Multiselect
 	params.calendarId = calendarId;
-		
-	var serverMessage = new org.ametys.servercomm.ServerMessage('explorer', '/delete-event', 
+	params.choice = choice; //FIXME Multiselect
+	params.occurrencesDate = eventId[0].split("$")[1]; //FIXME Multiselect
+	
+	var serverMessage = new org.ametys.servercomm.ServerMessage('explorer', '/delete-event',
 			params, 
 			org.ametys.servercomm.ServerComm.PRIORITY_MAJOR, 
 			function (response, args) {
-				org.ametys.explorer.applications.calendar.Event.deleteEventCb (response, args, callback);
+				org.ametys.explorer.applications.calendar.Event.deleteEventActionCb (response, args, callback);
 			},
 			this, 
 			[]);
 	org.ametys.servercomm.ServerComm.getInstance().send(serverMessage);
 }
 
-org.ametys.explorer.applications.calendar.Event.deleteEventCb = function(response, args, callback)
+org.ametys.explorer.applications.calendar.Event.deleteEventActionCb = function(response, args, callback)
 {
 	if (org.ametys.servercomm.ServerComm.handleBadResponse("<i18n:text i18n:key="PLUGINS_EXPLORER_EVENT_HANDLE_DELETE_ERROR"/>", response, "org.ametys.explorer.applications.calendar.Event"))
 	{
@@ -570,14 +731,15 @@
  */
 org.ametys.explorer.applications.calendar.Event.editEvent = function(parentId, eventId, callback)
 {
-	org.ametys.explorer.applications.calendar.Event._eventId = eventId[0]; //FIXME multiselect
+	org.ametys.explorer.applications.calendar.Event._eventId = eventId[0].split("$")[0]; //FIXME multiselect
+	org.ametys.explorer.applications.calendar.Event._occurrenceDate = eventId[0].split("$")[1]; //FIXME multiselect
 	org.ametys.explorer.applications.calendar.Event._parentId = parentId;
 	org.ametys.explorer.applications.calendar.Event._callback = callback;
 	org.ametys.explorer.applications.calendar.Event._actionId = 2;
-
+	
 	var params = {};
-
-	params.ids = eventId[0] //FIXME multiselect
+	params.ids = org.ametys.explorer.applications.calendar.Event._eventId; //FIXME multiselect
+	params.occurrencesDate = org.ametys.explorer.applications.calendar.Event._occurrenceDate; //FIXME multiselect
 	params.recursive = true;
 	params["sax-events"] = true;
 
@@ -606,13 +768,18 @@
 			eventNode.selectSingleNode("fullDay")[org.ametys.servercomm.ServerComm.xmlTextContent], 
 			eventNode.selectSingleNode("startDate")[org.ametys.servercomm.ServerComm.xmlTextContent], 
 			eventNode.selectSingleNode("endDate")[org.ametys.servercomm.ServerComm.xmlTextContent],
+			eventNode.selectSingleNode("recurrenceType")[org.ametys.servercomm.ServerComm.xmlTextContent],
+			eventNode.selectSingleNode("untilDate")[org.ametys.servercomm.ServerComm.xmlTextContent],
 			eventNode.getAttribute("calendarId")
 	);
 	
+	org.ametys.explorer.applications.calendar.Event._recurrenceTypeValue = eventNode.selectSingleNode("recurrenceType")[org.ametys.servercomm.ServerComm.xmlTextContent];
+	org.ametys.explorer.applications.calendar.Event._untilDateValue = eventNode.selectSingleNode("untilDate")[org.ametys.servercomm.ServerComm.xmlTextContent];
+	org.ametys.explorer.applications.calendar.Event._isRecurrent =  eventNode.selectSingleNode("recurrenceType")[org.ametys.servercomm.ServerComm.xmlTextContent] != "NEVER";
 	org.ametys.explorer.applications.calendar.Event._workflowId = eventNode.getAttribute("workflowId");
 }
 
-org.ametys.explorer.applications.calendar.Event._showBoxWithInfo = function (title, desc, fullDay, startDateAsString, endDateAsString, calendarId)
+org.ametys.explorer.applications.calendar.Event._showBoxWithInfo = function (title, desc, fullDay, startDateAsString, endDateAsString, recurrenceType, untilDateAsString, calendarId)
 {
 	org.ametys.explorer.applications.calendar.Event._delayedInitialized();
 
@@ -654,6 +821,21 @@
 	org.ametys.explorer.applications.calendar.Event._initialParentCalendar = calendarId;
 	// org.ametys.explorer.applications.calendar.Event._comboListCalendar.disable();
 	
+	org.ametys.explorer.applications.calendar.Event._untilDate.setMinValue(startDate);
+	org.ametys.explorer.applications.calendar.Event._untilDate.setValue("");
+	org.ametys.explorer.applications.calendar.Event._comboListPeriod.setValue(recurrenceType);
+	if (recurrenceType != "NEVER")
+	{
+		org.ametys.explorer.applications.calendar.Event._untilDate.show();
+		var untilDate = Date.parseDate(untilDateAsString, Date.patterns.ISO8601Long);
+		org.ametys.explorer.applications.calendar.Event._untilDate.setValue(untilDate);
+	}
+	else
+	{
+		org.ametys.explorer.applications.calendar.Event._untilDate.hide();
+	}
+
+	
 	org.ametys.explorer.applications.calendar.Event._box.setTitle("<i18n:text i18n:key='PLUGINS_EXPLORER_EVENT_TITLE_EDIT_LABEL'/>");
 	org.ametys.explorer.applications.calendar.Event._box.show();
 	org.ametys.explorer.applications.calendar.Event._setDatesEdit(startDate, endDate);
@@ -665,10 +847,39 @@
 	org.ametys.explorer.applications.calendar.Event._endDate.setValue(endDate);
 	org.ametys.explorer.applications.calendar.Event._startDateTime.setValue(startDate);
 	org.ametys.explorer.applications.calendar.Event._endDateTime.setValue(endDate);	
+	
+	org.ametys.explorer.applications.calendar.Event._untilDate.trigger.addListener('click', 
+			function()
+			{
+				if (org.ametys.explorer.applications.calendar.Event._untilDate.getValue() == "")
+				{
+					org.ametys.explorer.applications.calendar.Event._untilDate.setValue(startDate);
+					org.ametys.explorer.applications.calendar.Event._untilDate.onTriggerClick( );
+				}
+			});
 }
 
-org.ametys.explorer.applications.calendar.Event.moveEvent = function (application, event, parentId)
+org.ametys.explorer.applications.calendar.Event.moveEvent = function (application, event, parentId, isRecurrent)
 {	
+	if (isRecurrent)
+	{
+		org.ametys.explorer.applications.calendar.Event.Recurrent.Helper._openBox(
+				"<i18n:text i18n:key="PLUGINS_EXPLORER_EVENT_RECURRENT_BOX_DELETE_TITLE"/>",
+				"<i18n:text i18n:key="PLUGINS_EXPLORER_EVENT_RECURRENT_BOX_DELETE_HINT"/>",
+				getPluginResourcesUrl ('explorer') + '/img/event/event_edit_16.png',
+				function(choice) {
+					org.ametys.explorer.applications.calendar.Event.moveEventAction(application, event, parentId, choice);
+				});
+	}
+	else
+	{
+		org.ametys.explorer.applications.calendar.Event.moveEventAction(application, event, parentId, null);
+	}
+	
+}
+
+org.ametys.explorer.applications.calendar.Event.moveEventAction = function (application, event, parentId, choice)
+{
 	var params = {};
 
 	params.startDate = event.data.StartDate.format(Date.patterns.ISO8601Long);
@@ -676,9 +887,12 @@
 	
 	org.ametys.explorer.applications.calendar.Event._parentId = parentId;
 	params.parentId = org.ametys.explorer.applications.calendar.Event._parentId;
-	params.id = event.data.TrueEventId;
 	params.fullDay = event.data.IsAllDay;
 	
+	params.id = event.data.TrueEventId.split("$")[0];
+	params.choice = choice;
+	params.occurrenceDate = event.data.TrueEventId.split("$")[1];
+	
 	org.ametys.explorer.applications.calendar.Event._actionId = 2;
 	params.actionId = org.ametys.explorer.applications.calendar.Event._actionId;
 
@@ -687,14 +901,14 @@
 			params, 
 			org.ametys.servercomm.ServerComm.PRIORITY_MAJOR,
 			function (response, args) {
-				org.ametys.explorer.applications.calendar.Event.moveEventCb (response, args, application);
+				org.ametys.explorer.applications.calendar.Event.moveEventActionCb (response, args, application);
 			},
 			this, 
 			[]);
 	org.ametys.servercomm.ServerComm.getInstance().send(serverMessage);
 }
 
-org.ametys.explorer.applications.calendar.Event.moveEventCb = function (response, args, application)
+org.ametys.explorer.applications.calendar.Event.moveEventActionCb = function (response, args, application)
 {
 	if (response.selectSingleNode("message") != null &amp;&amp; 
 		response.selectSingleNode("message")[org.ametys.servercomm.ServerComm.xmlTextContent] == "Action 2 is invalid")
Index: main/plugin-explorer/resources/js/org/ametys/explorer/applications/calendar/helper/EditRecurrentEventHelper.i18n.js
===================================================================
--- main/plugin-explorer/resources/js/org/ametys/explorer/applications/calendar/helper/EditRecurrentEventHelper.i18n.js	(revision 0)
+++ main/plugin-explorer/resources/js/org/ametys/explorer/applications/calendar/helper/EditRecurrentEventHelper.i18n.js	(revision 0)
@@ -0,0 +1,111 @@
+/*
+ *  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.
+ */
+
+Ext.namespace('org.ametys.explorer.applications.calendar.Event.Recurrent.Helper');
+
+/*------------------------------------------------------------------------*/
+
+org.ametys.explorer.applications.calendar.Event.Recurrent.Helper._openBox = function(title, desc, icon, callback)
+{
+	org.ametys.explorer.applications.calendar.Event.Recurrent.Helper._delayedInitializedRecurrentEvent(title, desc, icon, callback);
+	org.ametys.explorer.applications.calendar.Event.Recurrent.Helper._box.show();
+	
+}
+
+org.ametys.explorer.applications.calendar.Event.Recurrent.Helper._delayedInitializedRecurrentEvent = function(title, desc, icon, callback)
+{
+	if (!org.ametys.explorer.applications.calendar.Event.Recurrent.Helper._box)
+	{
+		org.ametys.explorer.applications.calendar.Event.Recurrent.Helper._formPanel = new Ext.form.FormPanel({
+			defaults: {
+				xtype: 'radio',
+				hideLabel: true,
+				width: 420
+			},
+			cls: 'ametys',
+	        items: [
+	                new org.ametys.HtmlContainer ({cls: 'dialog-text-hint', html: desc }),
+	                {
+	                	boxLabel: "<i18n:text i18n:key="PLUGINS_EXPLORER_EVENT_RECURRENT_RADIO_CHOICE_1"/>",
+	                	name: 'recurrent-event-choice', 
+	                	inputValue: 'unit',
+	                    checked: true,
+	                    style: {
+	    					marginLeft: '30px',
+	    				}
+	                },
+	                {
+	                	xtype: 'container',
+	                	html: "<i18n:text i18n:key="PLUGINS_EXPLORER_EVENT_RECURRENT_RADIO_CHOICE_DESC_1"/>",
+	                	width: 370,
+	                	style: {
+	                		marginLeft: '48px',
+	                		color: '#404040'
+	    				},
+	                },
+	                {
+	                	boxLabel: "<i18n:text i18n:key="PLUGINS_EXPLORER_EVENT_RECURRENT_RADIO_CHOICE_2"/>",
+	                	name: 'recurrent-event-choice',
+	                    inputValue: 'all',
+	                    style: {
+	    					marginLeft: '30px'
+	    				}
+	                },
+	                {
+	                	xtype: 'container',
+	                	width: 370,
+	                	html: "<i18n:text i18n:key="PLUGINS_EXPLORER_EVENT_RECURRENT_RADIO_CHOICE_DESC_2"/>",
+	                	style: {
+	                		marginLeft: '48px',
+	                		color: '#404040'
+	    				},
+	                }
+	                ]
+	    });
+		
+		org.ametys.explorer.applications.calendar.Event.Recurrent.Helper._box = new org.ametys.DialogBox({
+	        layout: 'fit',
+	        title: title,
+	        icon: icon,
+	        width: 450,
+	        height: 190,
+	        autoScroll: false,	        
+	        items: [  
+	                org.ametys.explorer.applications.calendar.Event.Recurrent.Helper._formPanel
+	                  ],        
+	        closeAction: 'destroy',
+	        buttons: [{
+	            text: "<i18n:text i18n:key="PLUGINS_EXPLORER_CALENDAR_BUTTON_OK"/>",
+	            handler: function() {
+	            	var values = org.ametys.explorer.applications.calendar.Event.Recurrent.Helper._formPanel.getForm().getValues();
+	            	var choice = values['recurrent-event-choice'];
+	            	
+	            	org.ametys.explorer.applications.calendar.Event.Recurrent.Helper._cancel();
+	            	callback (choice);
+	            }
+	        }, {
+	            text: "<i18n:text i18n:key="PLUGINS_EXPLORER_CALENDAR_BUTTON_CANCEL"/>",
+	            handler: org.ametys.explorer.applications.calendar.Event.Recurrent.Helper._cancel
+	        }]        
+	    });
+	}
+}
+
+org.ametys.explorer.applications.calendar.Event.Recurrent.Helper._cancel = function()
+{
+	org.ametys.explorer.applications.calendar.Event.Recurrent.Helper._box.close();
+	delete org.ametys.explorer.applications.calendar.Event.Recurrent.Helper._box;
+}
\ No newline at end of file
Index: main/plugin-explorer/resources/js/org/ametys/explorer/applications/calendar/toolbar/ToolbarActions.i18n.js
===================================================================
--- main/plugin-explorer/resources/js/org/ametys/explorer/applications/calendar/toolbar/ToolbarActions.i18n.js	(revision 37412)
+++ main/plugin-explorer/resources/js/org/ametys/explorer/applications/calendar/toolbar/ToolbarActions.i18n.js	(working copy)
@@ -113,9 +113,12 @@
 	var eventId = application.getSelectedEventIds();
 	org.ametys.explorer.applications.calendar.toolbar.Event._application = application;
 	
+	var isRecurrent = (application.getSelectedEvents()[0].get("RecurrenceType") != "NEVER"); //FIXME multiselect
+	
 	org.ametys.explorer.applications.calendar.Event.deleteEvent(
 			node.id,
 			eventId,
+			isRecurrent,
 			function (parentId, id) {
 				application.execCommand ('refresh', {'id': parentId, 'reload': false});
 			}
Index: main/plugin-explorer/resources/js/org/ametys/explorer/applications/calendar/widget/UntilDateField.js
===================================================================
--- main/plugin-explorer/resources/js/org/ametys/explorer/applications/calendar/widget/UntilDateField.js	(revision 0)
+++ main/plugin-explorer/resources/js/org/ametys/explorer/applications/calendar/widget/UntilDateField.js	(revision 0)
@@ -0,0 +1,73 @@
+/*
+ *  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.
+ */
+
+Ext.namespace('org.ametys.form');
+
+/**
+ * org.ametys.form.UntilDateField
+ *
+ * @class This class provides a calendar and reset button field width a help icon. Use the <code>desc</option> to add the help icon.
+ * @extends Ext.form.DateField
+ * @constructor
+ * @param {Object} config Configuration options
+ */
+org.ametys.form.UntilDateField = function(config) 
+{
+	config.itemCls = "ametys-input";
+	config.labelSeparator = '';
+	
+	org.ametys.form.UntilDateField.superclass.constructor.call(this, config);
+}; 
+
+
+Ext.extend(org.ametys.form.UntilDateField, Ext.form.DateField, {
+	xtype: 'untildatefield'
+});
+
+org.ametys.form.UntilDateField.prototype.onRender = function(ct, position)
+{
+	org.ametys.form.UntilDateField.superclass.onRender.call(this, ct, position);
+	
+	this.wrap2 = this.wrap.wrap({cls:'x-form-date-until-wrap'});
+	
+	var me = this;
+	this._resetButton = new Ext.Button({
+		icon: getPluginResourcesUrl ('explorer') + '/img/event/all_the_time.png',
+		width: 16,
+		renderTo: this.wrap2,
+		listeners: {
+			'click': function()
+			{
+				me.setValue("");
+			}
+		}
+	});
+	
+	if (this.desc)
+	{
+		this.itemCt.child('div.x-form-element div.x-form-date-until-wrap').insertSibling({
+			id: this.id + '-img',
+			tag:'img',
+			style: 'padding-left: 20px; padding-top : 6px; float: left;',
+			src: getPluginResourcesUrl('core') + '/img/administrator/config/help.gif'}, 'after');
+	
+		Ext.QuickTips.register({
+		    target: this.id + '-img',
+		    text: this.desc,
+		    dismissDelay: 0 // disable automatic hiding
+		});
+	}
+}
Index: main/plugin-explorer/src/org/ametys/plugins/explorer/calendar/Calendar.java
===================================================================
--- main/plugin-explorer/src/org/ametys/plugins/explorer/calendar/Calendar.java	(revision 37412)
+++ main/plugin-explorer/src/org/ametys/plugins/explorer/calendar/Calendar.java	(working copy)
@@ -17,6 +17,7 @@
 
 import java.util.Date;
 import java.util.List;
+import java.util.Map;
 
 import org.ametys.plugins.explorer.ExplorerNode;
 import org.ametys.plugins.repository.TraversableAmetysObject;
@@ -51,10 +52,10 @@
     public String getTemplateDescription();
     
     /**
-     * Get the events of the calendar between two dates
+     * Get the events and its occurrences of the calendar between two dates
      * @param startDate Begin date
      * @param endDate End date
-     * @return the events
+     * @return the map of events and its occurrences
      */
-    public List<CalendarEvent> getEvents(Date startDate, Date endDate);
+    public Map<CalendarEvent, List<Date>> getEvents(Date startDate, Date endDate);
 }
Index: main/plugin-explorer/src/org/ametys/plugins/explorer/calendar/CalendarEvent.java
===================================================================
--- main/plugin-explorer/src/org/ametys/plugins/explorer/calendar/CalendarEvent.java	(revision 37412)
+++ main/plugin-explorer/src/org/ametys/plugins/explorer/calendar/CalendarEvent.java	(working copy)
@@ -16,6 +16,7 @@
 package org.ametys.plugins.explorer.calendar;
 
 import java.util.Date;
+import java.util.List;
 
 import org.ametys.plugins.repository.AmetysObject;
 import org.ametys.plugins.repository.AmetysRepositoryException;
@@ -93,6 +94,53 @@
      * @throws AmetysRepositoryException if an error occurs.
      */
     public Date getLastModified() throws UnknownMetadataException, AmetysRepositoryException;
+    
+    /**
+     * Retrieves the recurrence type.
+     * @return the recurrence type.
+     */
+    public EventRecurrenceTypeEnum getRecurrenceType();
+    
+    /**
+     * Retrieves if the event is recurrent.
+     * @return true if the event is recurrent.
+     */
+    public Boolean isRecurrent();
+    
+    /**
+     * Retrieves the end date of the frequency.
+     * @return the end date of the frequency.
+     */
+    public Date getRepeatUntil();
+    
+    /**
+     * Retrieves the list of excluded event date.
+     * @return the list of excluded event date.
+     */
+    public List<Date> getExcludedOccurences();
 
+    /**
+     * Retrieves the list of all event date between startDate and endDate.
+     * @param startDate the start date
+     * @param endDate the end date
+     * @return the list of all event date between startDate and endDate.
+     */
+    public List<Date> getOccurrences(Date startDate, Date endDate);
+    
+    
+    /**
+     * Retrieves the start date of the first Event which end after the date
+     * @param date the date
+     * @return the start date of the first Event which end after the date
+     */
+    public Date getFirstOccurrence(Date date);
+    
+    
+    /**
+     * Retrieves the date of the next event after the date
+     * @param date the date
+     * @return the date of the next event after the date
+     */
+    public Date getNextOccurrence(Date date);
     
 }
Index: main/plugin-explorer/src/org/ametys/plugins/explorer/calendar/EventRecurrenceTypeEnum.java
===================================================================
--- main/plugin-explorer/src/org/ametys/plugins/explorer/calendar/EventRecurrenceTypeEnum.java	(revision 0)
+++ main/plugin-explorer/src/org/ametys/plugins/explorer/calendar/EventRecurrenceTypeEnum.java	(revision 0)
@@ -0,0 +1,47 @@
+/*
+ *  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.
+ */
+package org.ametys.plugins.explorer.calendar;
+
+/**
+ * The enum of event recurrence type
+ */
+public enum EventRecurrenceTypeEnum 
+{
+    /**
+     * No recurrence
+     */
+    NEVER,
+    /**
+     * All day frequency
+     */
+    ALL_DAY,
+    /**
+     * All working day frequency
+     */
+    ALL_WORKING_DAY,
+    /**
+     * Weekly frequency
+     */
+    WEEKLY,
+    /**
+     * Biweekly frequency
+     */
+    BIWEEKLY,
+    /**
+     * Monthly frequency
+     */
+    MONTHLY;    
+}
Index: main/plugin-explorer/src/org/ametys/plugins/explorer/calendar/ModifiableCalendarEvent.java
===================================================================
--- main/plugin-explorer/src/org/ametys/plugins/explorer/calendar/ModifiableCalendarEvent.java	(revision 37412)
+++ main/plugin-explorer/src/org/ametys/plugins/explorer/calendar/ModifiableCalendarEvent.java	(working copy)
@@ -16,6 +16,7 @@
 package org.ametys.plugins.explorer.calendar;
 
 import java.util.Date;
+import java.util.List;
 
 import org.ametys.plugins.repository.AmetysRepositoryException;
 import org.ametys.plugins.repository.ModifiableAmetysObject;
@@ -29,47 +30,87 @@
 {
     /**
      * Set the title of the event
-     * @param title 
+     * @param title the title of the event
      */
     public void setTitle(String title);
     
     /**
      * Set the description of the event
-     * @param desc 
+     * @param desc the description of the event
      */
     public void setDescription(String desc);
     
     /**
      * Set the startDate of the event
-     * @param startDate 
+     * @param startDate the start date of the event
      */
     public void setStartDate(Date startDate);
 
     /**
      * Set the endDate of the event
-     * @param endDate 
+     * @param endDate the end date of the event
      */
     public void setEndDate(Date endDate);
 
     /**
      * Set if the event last all the day
-     * @param fullDay 
+     * @param fullDay all the day
      */
     public void setFullDay(Boolean fullDay);
     
     /**
      * Set the instance of workflow id
-     * @param workflowInstanceId 
+     * @param workflowInstanceId the workflow instance id
      */
     public void setWorkflowInstanceId(Long workflowInstanceId);
     
-    
-    
+    /**
+     * Set the creator of the event
+     * @param login the creator of the event
+     * @throws UnknownMetadataException if this property does not exists.
+     * @throws AmetysRepositoryException if an error occurs
+     */
     public void setCreator(String login) throws UnknownMetadataException, AmetysRepositoryException;
     
+    /**
+     * Set the creation date of the event
+     * @param date the creation date of the event
+     * @throws UnknownMetadataException if this property does not exists.
+     * @throws AmetysRepositoryException if an error occurs
+     */
     public void setCreationDate(Date date) throws UnknownMetadataException, AmetysRepositoryException;
     
+    /**
+     * Set the last contributor of the event
+     * @param login the last contributor of the event
+     * @throws UnknownMetadataException if this property does not exists.
+     * @throws AmetysRepositoryException if an error occurs
+     */
     public void setLastContributor(String login) throws UnknownMetadataException, AmetysRepositoryException;
     
+    /**
+     * Set the last modification date of the event
+     * @param date the last modification date of the event
+     * @throws UnknownMetadataException if this property does not exists.
+     * @throws AmetysRepositoryException if an error occurs
+     */
     public void setLastModified(Date date) throws UnknownMetadataException, AmetysRepositoryException;
+    
+    /**
+     * Set the recurrence type.
+     * @param recurrenceType the recurrence type
+     */
+    public void setRecurrenceType(String recurrenceType);
+    
+    /**
+     * Set the end date of the frequency.
+     * @param untilDate the end date of the recurrence
+     */
+    public void setRepeatUntil(Date untilDate);
+    
+    /**
+     * Set the list of excluded event date.
+     * @param excludedOccurrences excluded date
+     */
+    public void setExcludedOccurrences(List<Date> excludedOccurrences);
 }
Index: main/plugin-explorer/src/org/ametys/plugins/explorer/calendar/actions/DeleteCalendarEventAction.java
===================================================================
--- main/plugin-explorer/src/org/ametys/plugins/explorer/calendar/actions/DeleteCalendarEventAction.java	(revision 37412)
+++ main/plugin-explorer/src/org/ametys/plugins/explorer/calendar/actions/DeleteCalendarEventAction.java	(working copy)
@@ -15,7 +15,11 @@
  */
 package org.ametys.plugins.explorer.calendar.actions;
 
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.GregorianCalendar;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 
 import javax.jcr.AccessDeniedException;
@@ -28,8 +32,11 @@
 import org.apache.cocoon.environment.Request;
 import org.apache.cocoon.environment.SourceResolver;
 import org.apache.commons.lang.StringUtils;
+import org.joda.time.format.ISODateTimeFormat;
 
+import org.ametys.plugins.explorer.ModifiableExplorerNode;
 import org.ametys.plugins.explorer.calendar.ModifiableCalendar;
+import org.ametys.plugins.explorer.calendar.ModifiableCalendarEvent;
 import org.ametys.plugins.explorer.observation.ExplorerEvent;
 import org.ametys.plugins.explorer.observation.ExplorerEvent.EventType;
 import org.ametys.plugins.explorer.resources.actions.AbstractResourceAction;
@@ -73,37 +80,85 @@
             throw new IllegalArgumentException("No event to remove");
         }
         
-        for (String id : eventsIds)
+        String occurrencesDate = request.getParameter("occurrencesDate");
+        String[] eventOccurrencesDate = StringUtils.split(occurrencesDate, ",");
+        
+        for (int i = 0; i < eventsIds.length; i++)
         {
-            RemovableAmetysObject event = _resolver.resolveById(id);
-            
-            result.put("id", event.getId());
-            
-            String calendarId = request.getParameter("calendarId");        
-            if (calendarId == null)
-            {
-                calendarId = event.getParent().getId();
-            }
-            result.put("parentId", calendarId);
+            String id = eventsIds[i];
+            String choice = request.getParameter("choice");
+            String calendarId = request.getParameter("calendarId");
+            Map<String, Object> eventParams = new HashMap<String, Object>();
 
-            ModifiableCalendar calendar = event.getParent();
-            
-            String user = _usersProvider.getUser();
-            RightResult right = _rightManager.hasRight(user, "Plugin_Explorer_Event_Delete", "/resources" + calendar.getExplorerPath());
-            if (right != RightResult.RIGHT_OK)
+            if (StringUtils.isNotBlank(choice) && choice.equals("unit"))
             {
-                throw new AccessDeniedException("The user " + user + " cannot remove an event in calendar " + calendar.getExplorerPath());
+                ModifiableCalendarEvent event = _resolver.resolveById(id);
+                result.put("id", event.getId());
+                if (calendarId == null)
+                {
+                    calendarId = event.getParent().getId();
+                }
+                
+                ModifiableCalendar calendar = event.getParent();
+                String user = _usersProvider.getUser();
+                RightResult right = _rightManager.hasRight(user, "Plugin_Explorer_Event_Delete", "/resources" + calendar.getExplorerPath());
+                if (right != RightResult.RIGHT_OK)
+                {
+                    throw new AccessDeniedException("The user " + user + " cannot remove an event in calendar " + calendar.getExplorerPath());
+                }
+                
+                ArrayList<Date> excludedOccurrences = new ArrayList<Date>();
+                excludedOccurrences.addAll(event.getExcludedOccurences());
+                
+                long date = ISODateTimeFormat.dateTime().parseMillis(eventOccurrencesDate[i]);
+                GregorianCalendar gCalendar = new GregorianCalendar();
+                gCalendar.setTimeInMillis(date);
+                gCalendar.set(java.util.Calendar.HOUR_OF_DAY, 0);
+                gCalendar.set(java.util.Calendar.MINUTE, 0);
+                gCalendar.set(java.util.Calendar.SECOND, 0);
+                gCalendar.set(java.util.Calendar.MILLISECOND, 0);
+                
+                Date dateExcluded = gCalendar.getTime();
+                excludedOccurrences.add(dateExcluded);
+                
+                // Notify listeners
+                eventParams.put("name", event.getName());
+                eventParams.put("path", event.getPath());
+                eventParams.put("parentID", calendarId);
+                
+                event.setExcludedOccurrences(excludedOccurrences);
+                event.saveChanges();
             }
+            else
+            {
+                RemovableAmetysObject event = _resolver.resolveById(id);
+                result.put("id", event.getId());
                 
-            // Notify listeners
-            Map<String, Object> eventParams = new HashMap<String, Object>();
-            eventParams.put("name", event.getName());
-            eventParams.put("path", event.getPath());
-            eventParams.put("parentID", calendarId);
-    
-            event.remove();
-            calendar.saveChanges();
+                if (calendarId == null)
+                {
+                    calendarId = event.getParent().getId();
+                }
+                
+                ModifiableCalendar calendar = event.getParent();
+                
+                String user = _usersProvider.getUser();
+                RightResult right = _rightManager.hasRight(user, "Plugin_Explorer_Event_Delete", "/resources" + calendar.getExplorerPath());
+                if (right != RightResult.RIGHT_OK)
+                {
+                    throw new AccessDeniedException("The user " + user + " cannot remove an event in calendar " + calendar.getExplorerPath());
+                }
+                
+                // Notify listeners
+                eventParams.put("name", event.getName());
+                eventParams.put("path", event.getPath());
+                eventParams.put("parentID", calendarId);
+                
+                event.remove();
+                calendar.saveChanges();
+                
+            }
             
+            result.put("parentId", calendarId);
             _obsManager.notifyEvent(new ExplorerEvent (EventType.CALENDAR_EVENT_DELETE, _currentUserProvider.getUser(), id, eventParams));
         }
         
Index: main/plugin-explorer/src/org/ametys/plugins/explorer/calendar/generators/CalendarEventInfoGenerator.java
===================================================================
--- main/plugin-explorer/src/org/ametys/plugins/explorer/calendar/generators/CalendarEventInfoGenerator.java	(revision 37412)
+++ main/plugin-explorer/src/org/ametys/plugins/explorer/calendar/generators/CalendarEventInfoGenerator.java	(working copy)
@@ -16,10 +16,13 @@
 package org.ametys.plugins.explorer.calendar.generators;
 
 import java.io.IOException;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
 import java.util.Date;
 import java.util.GregorianCalendar;
 import java.util.HashMap;
 import java.util.List;
+import java.util.Map.Entry;
 
 import org.apache.avalon.framework.service.ServiceException;
 import org.apache.avalon.framework.service.ServiceManager;
@@ -30,6 +33,7 @@
 import org.apache.cocoon.xml.AttributesImpl;
 import org.apache.cocoon.xml.XMLUtils;
 import org.apache.commons.lang.StringUtils;
+import org.joda.time.format.ISODateTimeFormat;
 import org.xml.sax.SAXException;
 
 import org.ametys.plugins.explorer.calendar.Calendar;
@@ -84,15 +88,21 @@
         XMLUtils.startElement(contentHandler, "infos");
         
         String ids = request.getParameter("ids");
-        if (StringUtils.isNotBlank(ids))
+        String occurrencesDate = request.getParameter("occurrencesDate");
+        if (StringUtils.isNotBlank(ids) && StringUtils.isNotBlank(occurrencesDate))
         {
             String[] eventsIds = StringUtils.split(ids, ",");
+            String[] eventOccurrencesDate = StringUtils.split(occurrencesDate, ",");
 
             XMLUtils.startElement(contentHandler, "events");
-            for (String id : eventsIds)
+            for (int i = 0; i < eventsIds.length; i++)
             {
+                String id = eventsIds[i];
+                String occurrenceDate = eventOccurrencesDate[i];
+                
+                long date = ISODateTimeFormat.dateTime().parseMillis(occurrenceDate);
                 CalendarEvent event = _resolver.resolveById(id);
-                _saxEvent(event);
+                _saxEvent(event, new Date(date));
             }
             XMLUtils.endElement(contentHandler, "events");
         }
@@ -152,20 +162,28 @@
         if (saxEvents)
         {
             XMLUtils.startElement(contentHandler, "events");
-            for (CalendarEvent event : calendar.getEvents(startDate, endDate))
+            
+            for (Entry<CalendarEvent, List<Date>> entry : calendar.getEvents(startDate, endDate).entrySet()) 
             {
-                _saxEvent(event);
+                CalendarEvent event = entry.getKey();
+                List<Date> occurences = entry.getValue();
+                for (Date date : occurences)
+                {
+                    _saxEvent(event, date);
+                }
             }
+            
             XMLUtils.endElement(contentHandler, "events");
         }
 
         XMLUtils.endElement(contentHandler, "calendar");
     }
 
-    private void _saxEvent(CalendarEvent event) throws SAXException
+    private void _saxEvent(CalendarEvent event, Date date) throws SAXException
     {
         AttributesImpl attr = new AttributesImpl();
-        attr.addCDATAAttribute("id", event.getId());
+        String id = event.getId() + "$" + ParameterHelper.valueToString(date);
+        attr.addCDATAAttribute("id", id);
         attr.addCDATAAttribute("calendarId", event.getParent().getId());
         attr.addCDATAAttribute("workflowId", event.getWorkflowInstanceId() + "");
         attr.addCDATAAttribute("color", ((Calendar) event.getParent()).getColor());
@@ -196,14 +214,29 @@
         XMLUtils.createElement(contentHandler, "desc", event.getDescription());
         XMLUtils.createElement(contentHandler, "fullDay", event.getFullDay() ? "true" : "false");
         
-        Date startDateEvent = event.getStartDate();
-        Date endDateEvent = event.getEndDate();
+        XMLUtils.createElement(contentHandler, "recurrenceType", event.getRecurrenceType().toString());
+
+        String untilDateAsString = "";
+        Date untilDate = event.getRepeatUntil();
+        if (untilDate != null)
+        {
+            untilDateAsString =  ParameterHelper.valueToString(untilDate);
+        }
+        XMLUtils.createElement(contentHandler, "untilDate", untilDateAsString);
+        
+        
+        Date firstStartDateEvent = event.getStartDate();
+        Date firstEndDateEvent = event.getEndDate();
+        
+        long diff = firstEndDateEvent.getTime() - firstStartDateEvent.getTime();
+        Date startDateEvent = date;
+        Date endDateEvent = new Date(date.getTime() + diff);
         
         if (event.getFullDay())
         {
             GregorianCalendar gcStart = new GregorianCalendar();
             gcStart.setTime(startDateEvent);
-            gcStart.set(java.util.Calendar.HOUR, 0);
+            gcStart.set(java.util.Calendar.HOUR_OF_DAY, 0);
             gcStart.set(java.util.Calendar.MINUTE, 0);
             gcStart.set(java.util.Calendar.SECOND, 0);
             gcStart.set(java.util.Calendar.MILLISECOND, 0);
@@ -212,7 +245,7 @@
             
             GregorianCalendar gcEnd = new GregorianCalendar();
             gcEnd.setTime(endDateEvent);
-            gcEnd.set(java.util.Calendar.HOUR, 11);
+            gcEnd.set(java.util.Calendar.HOUR_OF_DAY, 23);
             gcEnd.set(java.util.Calendar.MINUTE, 59);
             gcEnd.set(java.util.Calendar.SECOND, 59);
             gcEnd.set(java.util.Calendar.MILLISECOND, 999);
@@ -222,6 +255,7 @@
         
         XMLUtils.createElement(contentHandler, "startDate", ParameterHelper.valueToString(startDateEvent));
         XMLUtils.createElement(contentHandler, "endDate", ParameterHelper.valueToString(endDateEvent));
+       
         _saxUser(event.getCreator(), "creator");
         XMLUtils.createElement(contentHandler, "creationDate", ParameterHelper.valueToString(event.getCreationDate()));
         _saxUser(event.getLastContributor(), "contributor");
Index: main/plugin-explorer/src/org/ametys/plugins/explorer/calendar/helper/RecurrentEventHelper.java
===================================================================
--- main/plugin-explorer/src/org/ametys/plugins/explorer/calendar/helper/RecurrentEventHelper.java	(revision 0)
+++ main/plugin-explorer/src/org/ametys/plugins/explorer/calendar/helper/RecurrentEventHelper.java	(revision 0)
@@ -0,0 +1,124 @@
+package org.ametys.plugins.explorer.calendar.helper;
+
+import java.util.Arrays;
+import java.util.Date;
+import java.util.GregorianCalendar;
+import java.util.List;
+
+import org.ametys.plugins.explorer.calendar.CalendarEvent;
+import org.ametys.runtime.config.Config;
+
+/**
+ * Helper for recurrent event operation
+ */
+public class RecurrentEventHelper
+{    
+    /**
+     * Get the next occurrence date
+     * @param event the calendar event
+     * @param date the occurrence date
+     * @return the next occurrence date
+     */
+    public static Date getNextDate(CalendarEvent event, Date date)
+    {
+        Date nextDate = null;
+        switch (event.getRecurrenceType())
+        {
+            case ALL_DAY:
+                nextDate = _nextAllDayDate(date);
+                break;
+            case ALL_WORKING_DAY:
+                nextDate = _nextAllWorkingDayDate(date);
+                break;
+            case WEEKLY:
+                nextDate = _nextWeeklyDate(date);
+                break;
+            case BIWEEKLY:
+                nextDate = _nextBiweeklyDate(date);
+                break;
+            case MONTHLY:
+                nextDate = _nextMonthlyDate(date, event);
+                break;
+            default:
+                break;
+        }
+        
+        return nextDate;
+    }
+    
+    private static Date _nextAllDayDate(Date date)
+    {
+        GregorianCalendar gcNextDate = new GregorianCalendar();
+        gcNextDate.setTime(date);
+        gcNextDate.add(GregorianCalendar.DATE, 1);
+ 
+        return  gcNextDate.getTime();
+    }
+    
+    private static Date _nextAllWorkingDayDate(Date date)
+    {
+        String workingDayAsString = Config.getInstance().getValueAsString("org.ametys.plugins.explorer.calendar.event.working.day");
+        GregorianCalendar gcNextDate = new GregorianCalendar();
+        gcNextDate.setTime(date);
+        
+        int dayId = gcNextDate.get(GregorianCalendar.DAY_OF_WEEK);
+        
+        int nbDay = 1;
+        int nextDayId = (dayId % 7) + 1;
+        List<String> workingDay = Arrays.asList(workingDayAsString.split(","));
+        while (!workingDay.contains(String.valueOf(nextDayId)))
+        {
+            nextDayId = (nextDayId % 7) + 1;
+            nbDay++;
+        }
+        
+        gcNextDate.add(GregorianCalendar.DATE, nbDay);
+        
+        return gcNextDate.getTime();
+    }
+    
+    private static Date _nextWeeklyDate(Date date)
+    {
+        GregorianCalendar gcNextDate = new GregorianCalendar();
+        gcNextDate.setTime(date);
+        gcNextDate.add(GregorianCalendar.DATE, 7);
+ 
+        return  gcNextDate.getTime();
+    }
+    
+    private static Date _nextBiweeklyDate(Date date)
+    {
+        GregorianCalendar gcNextDate = new GregorianCalendar();
+        gcNextDate.setTime(date);
+        gcNextDate.add(GregorianCalendar.DATE, 14);
+ 
+        return  gcNextDate.getTime();
+    }
+    
+    private static Date _nextMonthlyDate(Date date, CalendarEvent event)
+    {
+        GregorianCalendar gcStartDate = new GregorianCalendar();
+        gcStartDate.setTime(event.getStartDate());
+        int dayOfMonth = gcStartDate.get(GregorianCalendar.DAY_OF_MONTH);
+        
+        GregorianCalendar gcNextDate = new GregorianCalendar();
+        gcNextDate.setTime(date);
+        gcNextDate.add(GregorianCalendar.MONTH, 1);
+        int nextDayOfMonth = gcNextDate.get(GregorianCalendar.DAY_OF_MONTH);
+        int nextDayMaxOfMonth = gcNextDate.getActualMaximum(GregorianCalendar.DAY_OF_MONTH);
+
+        if (nextDayOfMonth < dayOfMonth)
+        {
+            if (dayOfMonth < nextDayMaxOfMonth)
+            {
+                gcNextDate.set(GregorianCalendar.DAY_OF_MONTH, dayOfMonth);
+            }
+            else
+            {
+                gcNextDate.set(GregorianCalendar.DAY_OF_MONTH, nextDayMaxOfMonth);
+            }
+        }
+        
+        return  gcNextDate.getTime();
+    }
+}
Index: main/plugin-explorer/src/org/ametys/plugins/explorer/calendar/jcr/JCRCalendar.java
===================================================================
--- main/plugin-explorer/src/org/ametys/plugins/explorer/calendar/jcr/JCRCalendar.java	(revision 37412)
+++ main/plugin-explorer/src/org/ametys/plugins/explorer/calendar/jcr/JCRCalendar.java	(working copy)
@@ -18,7 +18,9 @@
 import java.util.ArrayList;
 import java.util.Date;
 import java.util.GregorianCalendar;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 
 import javax.jcr.Node;
 
@@ -116,9 +118,9 @@
         return templateDescription;
     }
     
-    public List<CalendarEvent> getEvents(Date startDate, Date endDate)
+    public Map<CalendarEvent, List<Date>> getEvents(Date startDate, Date endDate)
     {
-        List<CalendarEvent> events = new ArrayList<CalendarEvent>();
+        Map<CalendarEvent, List<Date>> events = new HashMap<CalendarEvent, List<Date>>();
         
         AmetysObjectIterable<AmetysObject> childrens = this.getChildren();
         for (AmetysObject child : childrens)
@@ -134,7 +136,7 @@
                 {
                     GregorianCalendar gcStart = new GregorianCalendar();
                     gcStart.setTime(startDateEvent);
-                    gcStart.set(java.util.Calendar.HOUR, 0);
+                    gcStart.set(java.util.Calendar.HOUR_OF_DAY, 0);
                     gcStart.set(java.util.Calendar.MINUTE, 0);
                     gcStart.set(java.util.Calendar.SECOND, 0);
                     gcStart.set(java.util.Calendar.MILLISECOND, 0);
@@ -143,7 +145,7 @@
                     
                     GregorianCalendar gcEnd = new GregorianCalendar();
                     gcEnd.setTime(endDateEvent);
-                    gcEnd.set(java.util.Calendar.HOUR, 11);
+                    gcEnd.set(java.util.Calendar.HOUR_OF_DAY, 23);
                     gcEnd.set(java.util.Calendar.MINUTE, 59);
                     gcEnd.set(java.util.Calendar.SECOND, 59);
                     gcEnd.set(java.util.Calendar.MILLISECOND, 999);
@@ -151,11 +153,10 @@
                     endDateEvent = gcEnd.getTime();
                 }
                 
-                if ((startDateEvent.compareTo(startDate) >= 0 && startDateEvent.compareTo(endDate) < 0)
-                        || (endDateEvent.compareTo(startDate) > 0 && endDateEvent.compareTo(endDate) <= 0)
-                        || (startDateEvent.compareTo(startDate) < 0 && endDateEvent.compareTo(endDate) > 0))
+                List<Date> occurences = event.getOccurrences(startDate, endDate);
+                if (!occurences.isEmpty())
                 {
-                    events.add(event); 
+                    events.put(event, occurences); 
                 }
             }
         }
Index: main/plugin-explorer/src/org/ametys/plugins/explorer/calendar/jcr/JCRCalendarEvent.java
===================================================================
--- main/plugin-explorer/src/org/ametys/plugins/explorer/calendar/jcr/JCRCalendarEvent.java	(revision 37412)
+++ main/plugin-explorer/src/org/ametys/plugins/explorer/calendar/jcr/JCRCalendarEvent.java	(working copy)
@@ -15,12 +15,18 @@
  */
 package org.ametys.plugins.explorer.calendar.jcr;
 
+import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Date;
+import java.util.GregorianCalendar;
+import java.util.List;
 
 import javax.jcr.Node;
 
 import org.ametys.plugins.explorer.calendar.CalendarEvent;
+import org.ametys.plugins.explorer.calendar.EventRecurrenceTypeEnum;
 import org.ametys.plugins.explorer.calendar.ModifiableCalendarEvent;
+import org.ametys.plugins.explorer.calendar.helper.RecurrentEventHelper;
 import org.ametys.plugins.repository.AmetysObject;
 import org.ametys.plugins.repository.AmetysRepositoryException;
 import org.ametys.plugins.repository.jcr.DefaultAmetysObject;
@@ -58,6 +64,15 @@
     /** Constants for fullDay Event* */
     public static final String EVENT_FULL_DAY = "fullDay";
     
+    /** Constants for fullDay Event* */
+    public static final String EVENT_RECURRENCE_TYPE = "recurrenceType";
+    
+    /** Constants for fullDay Event* */
+    public static final String EVENT_UNTIL_DATE = "untilDate";
+    
+    /** Constants for fullDay Event* */
+    public static final String EVENT_EXCLUDED_DATE = "excludedDate";
+    
     /** Constants for workflowInstanceId Event* */
     public static final String EVENT_WORKFLOW_INSTANCE_ID = "workflowInstanceId";
     
@@ -122,6 +137,62 @@
         return getMetadataHolder().getDate(EVENT_MODIFIED);
     }
 
+    public EventRecurrenceTypeEnum getRecurrenceType()
+    {
+        EventRecurrenceTypeEnum recurrenceEnum = EventRecurrenceTypeEnum.NEVER;
+        if (getMetadataHolder().hasMetadata(EVENT_RECURRENCE_TYPE))
+        {
+            String recurrenceType = getMetadataHolder().getString(EVENT_RECURRENCE_TYPE);
+            if (recurrenceType.equals("ALL_DAY"))
+            {
+                recurrenceEnum = EventRecurrenceTypeEnum.ALL_DAY;
+            }
+            else if (recurrenceType.equals("ALL_WORKING_DAY"))
+            {
+                recurrenceEnum = EventRecurrenceTypeEnum.ALL_WORKING_DAY;
+            }
+            else if (recurrenceType.equals("WEEKLY"))
+            {
+                recurrenceEnum = EventRecurrenceTypeEnum.WEEKLY;
+            }
+            else if (recurrenceType.equals("BIWEEKLY"))
+            {
+                recurrenceEnum = EventRecurrenceTypeEnum.BIWEEKLY;
+            }
+            else if (recurrenceType.equals("MONTHLY"))
+            {
+                recurrenceEnum = EventRecurrenceTypeEnum.MONTHLY;
+            }
+        }
+        
+        return recurrenceEnum;
+    }
+    
+    public Boolean isRecurrent()
+    {
+        return !getRecurrenceType().equals(EventRecurrenceTypeEnum.NEVER);
+    }
+    
+    public Date getRepeatUntil()
+    {
+        if (getMetadataHolder().hasMetadata(EVENT_UNTIL_DATE))
+        {
+            return getMetadataHolder().getDate(EVENT_UNTIL_DATE);
+        }
+
+        return null;
+    }
+    
+    public List<Date> getExcludedOccurences()
+    {
+        List<Date> excludedOccurences = new ArrayList<Date>();
+        if (getMetadataHolder().hasMetadata(EVENT_EXCLUDED_DATE))
+        {
+            excludedOccurences = Arrays.asList(getMetadataHolder().getDateArray(EVENT_EXCLUDED_DATE));
+        }
+        return excludedOccurences;
+    }
+
     public void setTitle(String title)
     {
         getMetadataHolder().setMetadata(EVENT_TITLE, title);
@@ -171,4 +242,107 @@
     {
         getMetadataHolder().setMetadata(EVENT_MODIFIED, date);        
     }
+
+    public void setRecurrenceType(String recurrenceType)
+    {
+        getMetadataHolder().setMetadata(EVENT_RECURRENCE_TYPE, recurrenceType);  
+    }
+
+    public void setRepeatUntil(Date untilDate)
+    {
+        if (untilDate == null)
+        {
+            if (getMetadataHolder().hasMetadata(EVENT_UNTIL_DATE))
+            {
+                getMetadataHolder().removeMetadata(EVENT_UNTIL_DATE);
+            }
+        }
+        else
+        {
+            getMetadataHolder().setMetadata(EVENT_UNTIL_DATE, untilDate);  
+            
+        }
+    }
+
+    public void setExcludedOccurrences(List<Date> excludedOccurrences)
+    {
+        getMetadataHolder().setMetadata(EVENT_EXCLUDED_DATE, excludedOccurrences.toArray(new Date[excludedOccurrences.size()]));  
+    }
+
+    public List<Date> getOccurrences(Date startDate, Date endDate)
+    {
+        Date untilDate = getRepeatUntil();
+        
+        List<Date> occurences = new ArrayList<Date>();
+        Date firstDate = getFirstOccurrence(startDate);
+        if (firstDate == null || (untilDate != null && !firstDate.before(untilDate)))
+        {
+            return occurences;
+        }
+        
+        GregorianCalendar firstDateCalendar = new GregorianCalendar();
+        firstDateCalendar.setTime(firstDate);
+        firstDateCalendar.set(java.util.Calendar.HOUR_OF_DAY, 0);
+        firstDateCalendar.set(java.util.Calendar.MINUTE, 0);
+        firstDateCalendar.set(java.util.Calendar.SECOND, 0);
+        firstDateCalendar.set(java.util.Calendar.MILLISECOND, 0);
+        
+        if (!getExcludedOccurences().contains(firstDateCalendar.getTime()))
+        {
+            occurences.add(firstDate);
+        }
+        
+        Date nextDate = getNextOccurrence(firstDate);
+        while (nextDate != null && nextDate.before(endDate) && (untilDate == null || !untilDate.before(nextDate)))
+        {
+            GregorianCalendar nextDateCalendar = new GregorianCalendar();
+            nextDateCalendar.setTime(nextDate);
+            nextDateCalendar.set(java.util.Calendar.HOUR_OF_DAY, 0);
+            nextDateCalendar.set(java.util.Calendar.MINUTE, 0);
+            nextDateCalendar.set(java.util.Calendar.SECOND, 0);
+            nextDateCalendar.set(java.util.Calendar.MILLISECOND, 0);
+            
+            if (!getExcludedOccurences().contains(nextDateCalendar.getTime()))
+            {
+                occurences.add(nextDate);
+            }
+            nextDate = getNextOccurrence(nextDate);
+        }
+        
+        return occurences;
+    }
+
+    public Date getFirstOccurrence(Date date)
+    {
+        Date eventStartDate = getStartDate();
+        Date eventEndDate = getEndDate();
+        
+        if (eventEndDate.after(date))
+        {
+            return eventStartDate;
+        }
+        else
+        {
+            long eventDuringTime = eventEndDate.getTime() - eventStartDate.getTime();
+
+            Date endDate = eventEndDate;
+            Date startDate = eventStartDate;
+            while (startDate != null && endDate.before(date))
+            {
+                startDate = RecurrentEventHelper.getNextDate(this, startDate);
+                if (startDate != null)
+                {
+                    endDate = new Date(startDate.getTime() + eventDuringTime);
+                }
+            }
+            
+            return startDate;
+        }
+    }
+
+    public Date getNextOccurrence(Date date)
+    {
+        return RecurrentEventHelper.getNextDate(this, date);
+    }
+
 }
Index: main/plugin-explorer/src/org/ametys/plugins/explorer/calendar/workflow/AddEventFunction.java
===================================================================
--- main/plugin-explorer/src/org/ametys/plugins/explorer/calendar/workflow/AddEventFunction.java	(revision 37412)
+++ main/plugin-explorer/src/org/ametys/plugins/explorer/calendar/workflow/AddEventFunction.java	(working copy)
@@ -82,6 +82,10 @@
         String login = getUser(transientVars);
         String selectedNode = (String) jsParameters.get("selectedNode");
         
+        String recurrenceType = (String) jsParameters.get("recurrenceType");
+        String untilDateAsString = (String) jsParameters.get("untilDate");
+        Date untilDate = (Date) ParameterHelper.castValue(untilDateAsString, ParameterType.DATE);
+        
         long workflowId = ((WorkflowEntry) transientVars.get("entry")).getId();
         
         Map<String, String> result = (Map<String, String>) transientVars.get("result");
@@ -119,18 +123,24 @@
         event.setStartDate(startDate);
         event.setLastContributor(login);
         event.setLastModified(new Date());
-              
+
+        event.setRecurrenceType(recurrenceType);
+        if (untilDate !=  null)
+        {
+            event.setRepeatUntil(untilDate);
+        }
+        
         event.setEndDate(endDate);
         modifiableCalendar.saveChanges();
         
         transientVars.put("eventId", event.getId());
         
         result.put("id", event.getId());
-        result.put("parentID", selectedNode); 
+        result.put("parentID", selectedNode == null ? parentId : selectedNode); 
         
         // Notify listeners
         Map<String, Object> eventParams = new HashMap<String, Object>();
-        eventParams.put("parentID", selectedNode);
+        eventParams.put("parentID", selectedNode == null ? parentId : selectedNode);
         eventParams.put("name", event.getName());
         eventParams.put("path", event.getPath());
         
Index: main/plugin-explorer/src/org/ametys/plugins/explorer/calendar/workflow/CalendarWorkflowAction.java
===================================================================
--- main/plugin-explorer/src/org/ametys/plugins/explorer/calendar/workflow/CalendarWorkflowAction.java	(revision 37412)
+++ main/plugin-explorer/src/org/ametys/plugins/explorer/calendar/workflow/CalendarWorkflowAction.java	(working copy)
@@ -26,6 +26,7 @@
 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.plugins.explorer.calendar.Calendar;
 import org.ametys.plugins.explorer.calendar.CalendarEvent;
@@ -88,6 +89,14 @@
             actionId = Integer.parseInt(actionIdAsString);
         }
         
+        boolean sendMail = true;
+        String choice = request.getParameter("choice");
+        if (actionId == 2 && StringUtils.isNotBlank(choice) && choice.equals("unit"))
+        {
+            sendMail = false;
+        }
+        inputs.put("sendMail", sendMail);
+        
         if (workflowName == null)
         {
             throw new IllegalArgumentException("The workflow name is not specified");
Index: main/plugin-explorer/src/org/ametys/plugins/explorer/calendar/workflow/EditEventFunction.java
===================================================================
--- main/plugin-explorer/src/org/ametys/plugins/explorer/calendar/workflow/EditEventFunction.java	(revision 37412)
+++ main/plugin-explorer/src/org/ametys/plugins/explorer/calendar/workflow/EditEventFunction.java	(working copy)
@@ -15,22 +15,28 @@
  */
 package org.ametys.plugins.explorer.calendar.workflow;
 
+import java.util.ArrayList;
 import java.util.Date;
+import java.util.GregorianCalendar;
 import java.util.HashMap;
 import java.util.Map;
 
 import org.apache.avalon.framework.service.ServiceException;
 import org.apache.avalon.framework.service.ServiceManager;
 import org.apache.commons.lang.IllegalClassException;
+import org.apache.commons.lang.StringUtils;
+import org.joda.time.format.ISODateTimeFormat;
 
 import org.ametys.plugins.explorer.calendar.ModifiableCalendar;
 import org.ametys.plugins.explorer.calendar.ModifiableCalendarEvent;
 import org.ametys.plugins.explorer.observation.ExplorerEvent;
 import org.ametys.plugins.explorer.observation.ExplorerEvent.EventType;
 import org.ametys.plugins.explorer.observation.ExplorerObservationManager;
+import org.ametys.plugins.explorer.workflow.AbstractExplorerNodeWorkflowComponent;
 import org.ametys.plugins.repository.AmetysObject;
 import org.ametys.plugins.repository.AmetysObjectResolver;
 import org.ametys.plugins.workflow.AbstractWorkflowComponent;
+import org.ametys.plugins.workflow.Workflow;
 import org.ametys.runtime.util.parameter.ParameterHelper;
 import org.ametys.runtime.util.parameter.ParameterHelper.ParameterType;
 
@@ -50,12 +56,16 @@
     /** The observation manager */
     protected ExplorerObservationManager _obsManager;
     
+    /** The observation manager */
+    protected Workflow _workflow;
+    
     @Override
     public void service(ServiceManager smanager) throws ServiceException
     {
         super.service(smanager);
         _resolver = (AmetysObjectResolver) smanager.lookup(AmetysObjectResolver.ROLE);
         _obsManager = (ExplorerObservationManager) smanager.lookup(ExplorerObservationManager.ROLE);
+        _workflow = (Workflow) smanager.lookup(Workflow.ROLE);
     }
     
     public void execute(Map transientVars, Map args, PropertySet ps) throws WorkflowException
@@ -78,54 +88,114 @@
         String login = getUser(transientVars);
         String selectedNode = (String) jsParameters.get("selectedNode");
 
+        String recurrenceType = (String) jsParameters.get("recurrenceType");
+        String untilDateAsString = (String) jsParameters.get("untilDate");
+        Date untilDate = (Date) ParameterHelper.castValue(untilDateAsString, ParameterType.DATE);
+        String choice = (String) jsParameters.get("choice");
+        String occurrenceDate = (String) jsParameters.get("occurrenceDate");
+        
         long workflowId = ((WorkflowEntry) transientVars.get("entry")).getId();
         
         Map<String, String> result = (Map<String, String>) transientVars.get("result");
         
         ModifiableCalendarEvent event = _resolver.resolveById(eventId);
         ModifiableCalendar calendar = event.getParent();
-
-        boolean renameIfExists = (Boolean) jsParameters.get("renameIfExists");
-        assert parentId != null;
-        
-        AmetysObject object = _resolver.resolveById(parentId);
-        if (!(object instanceof ModifiableCalendar))
+        if (StringUtils.isNotBlank(choice) && choice.equals("unit"))
         {
-            throw new IllegalClassException(ModifiableCalendar.class, object.getClass());
+            if (!jsParameters.containsKey("title"))
+            {
+                jsParameters.put("title", event.getTitle());
+                jsParameters.put("desc", event.getDescription());
+                jsParameters.put("fullDay", event.getFullDay());
+            }
+            jsParameters.put("recurrenceType", "NEVER");
+            
+            HashMap<String, Object> inputs = new HashMap<String, Object>();
+            inputs.put("parameters", jsParameters);
+            inputs.put(AbstractExplorerNodeWorkflowComponent.EXPLORERNODE_KEY, calendar);
+            inputs.put("result", result);
+            String workflowName = calendar.getWorkflowName();
+            try
+            {
+                _workflow.initialize(workflowName, 1, inputs);
+                
+                ArrayList<Date> excludedOccurrences = new ArrayList<Date>();
+                excludedOccurrences.addAll(event.getExcludedOccurences());
+                
+                long date = ISODateTimeFormat.dateTime().parseMillis(occurrenceDate);
+                GregorianCalendar gCalendar = new GregorianCalendar();
+                gCalendar.setTimeInMillis(date);
+                gCalendar.set(java.util.Calendar.HOUR_OF_DAY, 0);
+                gCalendar.set(java.util.Calendar.MINUTE, 0);
+                gCalendar.set(java.util.Calendar.SECOND, 0);
+                gCalendar.set(java.util.Calendar.MILLISECOND, 0);
+                
+                Date dateExcluded = gCalendar.getTime();
+                excludedOccurrences.add(dateExcluded);
+                
+                event.setExcludedOccurrences(excludedOccurrences);
+                event.saveChanges();
+            }   
+            catch (WorkflowException e)
+            {
+                _logger.error("An error occured while creating workflow '" + workflowName + "' with action 1'", e);
+                throw e;
+            }
         }
-        
-        calendar = (ModifiableCalendar) object;
-        
-        if (!renameIfExists && calendar.hasChild(title))
+        else
         {
-           // getLogger().warn("The object '" + object.getName() + "' can not be renamed in '" + title + "' : a object of same name already exists.");
-            result.put("message", "already-exist");
-        }
+            boolean renameIfExists = (Boolean) jsParameters.get("renameIfExists");
+            assert parentId != null;
+            
+            AmetysObject object = _resolver.resolveById(parentId);
+            if (!(object instanceof ModifiableCalendar))
+            {
+                throw new IllegalClassException(ModifiableCalendar.class, object.getClass());
+            }
+            
+            calendar = (ModifiableCalendar) object;
+            
+            if (!renameIfExists && calendar.hasChild(title))
+            {
+               // getLogger().warn("The object '" + object.getName() + "' can not be renamed in '" + title + "' : a object of same name already exists.");
+                result.put("message", "already-exist");
+            }
+    
+            if (title != null)
+            {
+                event.setTitle(title);
+                event.setDescription(desc);
+                event.setFullDay(fullDay);            
+                event.setRecurrenceType(recurrenceType);
+                event.setRepeatUntil(untilDate);
+            }
+            
+            event.setWorkflowInstanceId(workflowId);
+            event.setLastContributor(login);
+            event.setLastModified(new Date());
+            
+            long oldDiff = event.getEndDate().getTime() - event.getStartDate().getTime();
+            long startTimeOccurrence = ISODateTimeFormat.dateTime().parseMillis(occurrenceDate);
+            long endTimeOccurrence = startTimeOccurrence + oldDiff;
+            
+            long startDiff = startDate.getTime() - startTimeOccurrence;
+            long endDiff = endDate.getTime() - endTimeOccurrence;
+            
+            event.setStartDate(new Date(event.getStartDate().getTime() + startDiff));
+            event.setEndDate(new Date(event.getEndDate().getTime() + endDiff));
 
-        if (title != null)
-        {
-            event.setTitle(title);
-            event.setDescription(desc);
-            event.setFullDay(fullDay);            
+            calendar.saveChanges();
+            
+            result.put("id", event.getId());
+            result.put("parentID", selectedNode == null ? parentId : selectedNode); 
+            
+            // Notify listeners
+            Map<String, Object> eventParams = new HashMap<String, Object>();
+            eventParams.put("parentID", selectedNode == null ? parentId : selectedNode);
+            eventParams.put("name", event.getName());
+            eventParams.put("path", event.getPath());
+            
+            _obsManager.notifyEvent(new ExplorerEvent ((eventId == null) ? EventType.CALENDAR_EVENT_CREATE : EventType.CALENDAR_EVENT_UPDATE, login, event.getId(), eventParams));     
         }
-        
-        event.setWorkflowInstanceId(workflowId);
-        event.setStartDate(startDate);
-        event.setLastContributor(login);
-        event.setLastModified(new Date());
-        
-        event.setEndDate(endDate);
-        calendar.saveChanges();
-        
-        result.put("id", event.getId());
-        result.put("parentID", selectedNode == null ? parentId : selectedNode); 
-        
-        // Notify listeners
-        Map<String, Object> eventParams = new HashMap<String, Object>();
-        eventParams.put("parentID", selectedNode == null ? parentId : selectedNode);
-        eventParams.put("name", event.getName());
-        eventParams.put("path", event.getPath());
-        
-        _obsManager.notifyEvent(new ExplorerEvent ((eventId == null) ? EventType.CALENDAR_EVENT_CREATE : EventType.CALENDAR_EVENT_UPDATE, login, event.getId(), eventParams));     
     }
 }
