From e0032e2e9c53192aba0ad3e78855fe10344e81ad Mon Sep 17 00:00:00 2001
From: srosse <none@none>
Date: Wed, 9 Aug 2017 12:22:44 +0200
Subject: [PATCH] OO-2911, OO-2912: enhance the notifications for the
 task/group task to check the different dates, wording, subscriptions date
 chooser show the date user for the list notifications and not today

---
 .../ui/DateChooserController.java             |   3 +-
 .../ui/NotificationNewsController.java        |   7 +-
 .../DisplayOrDownloadComponentRenderer.java   |  39 +-
 .../org/olat/course/nodes/gta/GTAManager.java |  10 +
 .../java/org/olat/course/nodes/gta/Task.java  |   6 +
 .../course/nodes/gta/TaskRevisionDate.java    |  42 ++
 .../nodes/gta/manager/GTAManagerImpl.java     |  76 ++
 .../nodes/gta/manager/GTANotifications.java   | 710 +++++++++++++++---
 .../gta/manager/GTANotificationsHandler.java  |  11 +-
 .../nodes/gta/model/TaskDueDateImpl.java      |  37 +
 .../olat/course/nodes/gta/model/TaskImpl.java |  37 +
 .../nodes/gta/model/TaskRevisionDateImpl.java | 173 +++++
 .../nodes/gta/ui/DirectoryController.java     |  30 +-
 .../nodes/gta/ui/GTACoachController.java      |  26 +-
 ...CoachRevisionAndCorrectionsController.java |  32 +-
 .../gta/ui/GTACoachSelectionController.java   |  58 +-
 .../gta/ui/GTACoachedGroupListController.java |   9 +
 .../GTACoachedParticipantListController.java  |  11 +
 .../gta/ui/GTAParticipantController.java      |  36 +-
 ...ipantRevisionAndCorrectionsController.java |  28 +-
 .../course/nodes/gta/ui/GTARunController.java |  76 +-
 .../gta/ui/_content/coach_selection.html      |   5 +-
 .../gta/ui/_content/documents_readonly.html   |   5 +-
 .../gta/ui/_i18n/LocalStrings_de.properties   |  31 +-
 .../gta/ui/_i18n/LocalStrings_en.properties   |  31 +-
 .../gta/ui/_i18n/LocalStrings_fr.properties   |   7 -
 .../gta/ui/_i18n/LocalStrings_it.properties   |   7 -
 .../gta/ui/_i18n/LocalStrings_pl.properties   |   7 -
 .../ui/_i18n/LocalStrings_pt_BR.properties    |   7 -
 .../ui/component/DownloadDocumentMapper.java  |  56 ++
 .../nodes/ms/MSCourseNodeRunController.java   |  43 +-
 .../olat/course/nodes/ms/_content/run.html    |   3 +
 src/main/resources/META-INF/persistence.xml   |   1 +
 .../database/mysql/alter_11_5_x_to_12_0_0.sql |  21 +
 .../database/mysql/setupDatabase.sql          |  16 +
 .../oracle/alter_11_5_x_to_12_0_0.sql         |  17 +
 .../database/oracle/setupDatabase.sql         |  16 +
 .../postgresql/alter_11_5_x_to_12_0_0.sql     |  15 +
 .../database/postgresql/setupDatabase.sql     |  16 +
 .../nodes/gta/manager/GTAManagerTest.java     |  58 ++
 40 files changed, 1583 insertions(+), 236 deletions(-)
 create mode 100644 src/main/java/org/olat/course/nodes/gta/TaskRevisionDate.java
 create mode 100644 src/main/java/org/olat/course/nodes/gta/model/TaskRevisionDateImpl.java
 create mode 100644 src/main/java/org/olat/course/nodes/gta/ui/component/DownloadDocumentMapper.java

diff --git a/src/main/java/org/olat/core/commons/services/notifications/ui/DateChooserController.java b/src/main/java/org/olat/core/commons/services/notifications/ui/DateChooserController.java
index c96d5595edc..97888b317a3 100644
--- a/src/main/java/org/olat/core/commons/services/notifications/ui/DateChooserController.java
+++ b/src/main/java/org/olat/core/commons/services/notifications/ui/DateChooserController.java
@@ -80,8 +80,7 @@ public class DateChooserController extends FormBasicController {
 	@Override
 	protected void initForm(FormItemContainer formLayout, Controller listener, UserRequest ureq) {
 		dateChooser = uifactory.addDateChooser("news.since", null, formLayout);
-		//FIXME: Can't use time format for now, only date format due to bug OLAT-4736
-		// dateChooser.setDateChooserTimeEnabled(true);
+
 		dateChooser.setDate(initDate);
 		dateChooser.addActionListener(FormEvent.ONCHANGE);
 		
diff --git a/src/main/java/org/olat/core/commons/services/notifications/ui/NotificationNewsController.java b/src/main/java/org/olat/core/commons/services/notifications/ui/NotificationNewsController.java
index 05a8e28b1ae..ec661492981 100644
--- a/src/main/java/org/olat/core/commons/services/notifications/ui/NotificationNewsController.java
+++ b/src/main/java/org/olat/core/commons/services/notifications/ui/NotificationNewsController.java
@@ -28,6 +28,7 @@ import java.util.Date;
 import java.util.List;
 import java.util.Map;
 
+import org.olat.commons.calendar.CalendarUtils;
 import org.olat.core.commons.services.notifications.NotificationHelper;
 import org.olat.core.commons.services.notifications.NotificationsManager;
 import org.olat.core.commons.services.notifications.Subscriber;
@@ -92,13 +93,14 @@ public class NotificationNewsController extends BasicController implements
 		} else {
 			compareDate = newsSinceDate;
 		}
+		compareDate = CalendarUtils.removeTime(compareDate);
+		
 		// Main view is a velocity container
 		newsVC = createVelocityContainer("notificationsNews");
 		// Fetch data from DB and update datamodel and reuse subscribers
 		List<Subscriber> subs = updateNewsDataModel();
 		// Add date and type chooser
-		dateChooserCtr = new DateChooserController(ureq, getWindowControl(),
-				new Date());
+		dateChooserCtr = new DateChooserController(ureq, getWindowControl(), compareDate);
 		dateChooserCtr.setSubscribers(subs);
 		listenTo(dateChooserCtr);
 		newsVC.put("dateChooserCtr", dateChooserCtr.getInitialComponent());
@@ -136,6 +138,7 @@ public class NotificationNewsController extends BasicController implements
 	 * @see org.olat.core.gui.control.DefaultController#event(org.olat.core.gui.UserRequest,
 	 *      org.olat.core.gui.control.Controller, org.olat.core.gui.control.Event)
 	 */
+	@Override
 	protected void event(UserRequest ureq, Controller source, Event event) {
 		if (source == dateChooserCtr) {
 			if (event == Event.CHANGED_EVENT) {
diff --git a/src/main/java/org/olat/core/gui/components/download/DisplayOrDownloadComponentRenderer.java b/src/main/java/org/olat/core/gui/components/download/DisplayOrDownloadComponentRenderer.java
index 99a4a732809..5680ada6138 100644
--- a/src/main/java/org/olat/core/gui/components/download/DisplayOrDownloadComponentRenderer.java
+++ b/src/main/java/org/olat/core/gui/components/download/DisplayOrDownloadComponentRenderer.java
@@ -20,10 +20,9 @@
 package org.olat.core.gui.components.download;
 
 import org.olat.core.gui.components.Component;
-import org.olat.core.gui.components.ComponentRenderer;
+import org.olat.core.gui.components.DefaultComponentRenderer;
 import org.olat.core.gui.render.RenderResult;
 import org.olat.core.gui.render.Renderer;
-import org.olat.core.gui.render.RenderingState;
 import org.olat.core.gui.render.StringOutput;
 import org.olat.core.gui.render.URLBuilder;
 import org.olat.core.gui.translator.Translator;
@@ -38,16 +37,8 @@ import org.olat.core.gui.translator.Translator;
  * 
  * @author gnaegi
  */
-class DisplayOrDownloadComponentRenderer implements ComponentRenderer {
+class DisplayOrDownloadComponentRenderer extends DefaultComponentRenderer {
 
-	/**
-	 * @see org.olat.core.gui.components.ComponentRenderer#render(org.olat.core.gui.render.Renderer,
-	 *      org.olat.core.gui.render.StringOutput,
-	 *      org.olat.core.gui.components.Component,
-	 *      org.olat.core.gui.render.URLBuilder,
-	 *      org.olat.core.gui.translator.Translator,
-	 *      org.olat.core.gui.render.RenderResult, java.lang.String[])
-	 */
 	@Override
 	public void render(Renderer renderer, StringOutput sb, Component source, URLBuilder ubu, Translator translator,
 			RenderResult renderResult, String[] args) {
@@ -63,30 +54,4 @@ class DisplayOrDownloadComponentRenderer implements ComponentRenderer {
 			sb.append("</script>");
 		}
 	}
-
-	/**
-	 * @see org.olat.core.gui.components.ComponentRenderer#renderBodyOnLoadJSFunctionCall(org.olat.core.gui.render.Renderer,
-	 *      org.olat.core.gui.render.StringOutput,
-	 *      org.olat.core.gui.components.Component,
-	 *      org.olat.core.gui.render.RenderingState)
-	 */
-	@Override
-	public void renderBodyOnLoadJSFunctionCall(Renderer renderer, StringOutput sb, Component source, RenderingState rstate) {
-	// no body on-load methods (must also work in non-ajax mode)
-	}
-
-	/**
-	 * @see org.olat.core.gui.components.ComponentRenderer#renderHeaderIncludes(org.olat.core.gui.render.Renderer,
-	 *      org.olat.core.gui.render.StringOutput,
-	 *      org.olat.core.gui.components.Component,
-	 *      org.olat.core.gui.render.URLBuilder,
-	 *      org.olat.core.gui.translator.Translator,
-	 *      org.olat.core.gui.render.RenderingState)
-	 */
-	@Override
-	public void renderHeaderIncludes(Renderer renderer, StringOutput sb, Component source, URLBuilder ubu, Translator translator,
-			RenderingState rstate) {
-	// no header includes
-	}
-
 }
diff --git a/src/main/java/org/olat/course/nodes/gta/GTAManager.java b/src/main/java/org/olat/course/nodes/gta/GTAManager.java
index 49f2e071b03..e584aa1e4b4 100644
--- a/src/main/java/org/olat/course/nodes/gta/GTAManager.java
+++ b/src/main/java/org/olat/course/nodes/gta/GTAManager.java
@@ -272,6 +272,8 @@ public interface GTAManager {
 	public List<Task> getTasks(TaskList taskList, GTACourseNode gtaNode);
 	
 	public List<TaskLight> getTasksLight(RepositoryEntryRef entry, GTACourseNode gtaNode);
+	
+	public List<TaskRevisionDate> getTaskRevisions(Task task);
 
 	
 	/**
@@ -327,6 +329,14 @@ public interface GTAManager {
 
 	public Task collectTask(Task task, GTACourseNode cNode);
 	
+	/**
+	 * Task is reviewed and accepted.
+	 * @param task
+	 * @param cNode
+	 * @return
+	 */
+	public Task reviewedTask(Task task, GTACourseNode cNode);
+	
 	public Task updateTask(Task task, TaskProcess newStatus, GTACourseNode cNode);
 	
 	public TaskDueDate updateTaskDueDate(TaskDueDate taskDueDate);
diff --git a/src/main/java/org/olat/course/nodes/gta/Task.java b/src/main/java/org/olat/course/nodes/gta/Task.java
index 9eadea2c561..74abe1bb54e 100644
--- a/src/main/java/org/olat/course/nodes/gta/Task.java
+++ b/src/main/java/org/olat/course/nodes/gta/Task.java
@@ -38,6 +38,12 @@ public interface Task extends TaskRef {
 	
 	public Date getCollectionDate();
 	
+	public Date getAcceptationDate();
+	
+	public Date getSolutionDate();
+	
+	public Date getGraduationDate();
+	
 	public TaskList getTaskList();
 	
 	public Identity getIdentity();
diff --git a/src/main/java/org/olat/course/nodes/gta/TaskRevisionDate.java b/src/main/java/org/olat/course/nodes/gta/TaskRevisionDate.java
new file mode 100644
index 00000000000..ce6f0a40648
--- /dev/null
+++ b/src/main/java/org/olat/course/nodes/gta/TaskRevisionDate.java
@@ -0,0 +1,42 @@
+/**
+ * <a href="http://www.openolat.org">
+ * OpenOLAT - Online Learning and Training</a><br>
+ * <p>
+ * Licensed under the Apache License, Version 2.0 (the "License"); <br>
+ * you may not use this file except in compliance with the License.<br>
+ * You may obtain a copy of the License at the
+ * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a>
+ * <p>
+ * Unless required by applicable law or agreed to in writing,<br>
+ * software distributed under the License is distributed on an "AS IS" BASIS, <br>
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br>
+ * See the License for the specific language governing permissions and <br>
+ * limitations under the License.
+ * <p>
+ * Initial code contributed and copyrighted by<br>
+ * frentix GmbH, http://www.frentix.com
+ * <p>
+ */
+package org.olat.course.nodes.gta;
+
+import java.util.Date;
+
+/**
+ * 
+ * Initial date: 7 août 2017<br>
+ * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
+ *
+ */
+public interface TaskRevisionDate {
+	
+	public Long getKey();
+	
+	public TaskProcess getTaskStatus();
+
+	public int getRevisionLoop();
+
+	public Date getDate();
+
+	public Task getTask();
+
+}
diff --git a/src/main/java/org/olat/course/nodes/gta/manager/GTAManagerImpl.java b/src/main/java/org/olat/course/nodes/gta/manager/GTAManagerImpl.java
index a68fcd1b484..2251948e194 100644
--- a/src/main/java/org/olat/course/nodes/gta/manager/GTAManagerImpl.java
+++ b/src/main/java/org/olat/course/nodes/gta/manager/GTAManagerImpl.java
@@ -42,6 +42,7 @@ import org.olat.basesecurity.IdentityRef;
 import org.olat.core.commons.modules.bc.FolderConfig;
 import org.olat.core.commons.modules.bc.vfs.OlatRootFolderImpl;
 import org.olat.core.commons.persistence.DB;
+import org.olat.core.commons.services.notifications.NotificationsManager;
 import org.olat.core.commons.services.notifications.PublisherData;
 import org.olat.core.commons.services.notifications.SubscriptionContext;
 import org.olat.core.id.Identity;
@@ -66,6 +67,7 @@ import org.olat.course.nodes.gta.TaskLight;
 import org.olat.course.nodes.gta.TaskList;
 import org.olat.course.nodes.gta.TaskProcess;
 import org.olat.course.nodes.gta.TaskRef;
+import org.olat.course.nodes.gta.TaskRevisionDate;
 import org.olat.course.nodes.gta.model.DueDate;
 import org.olat.course.nodes.gta.model.Membership;
 import org.olat.course.nodes.gta.model.Solution;
@@ -75,6 +77,7 @@ import org.olat.course.nodes.gta.model.TaskDefinitionList;
 import org.olat.course.nodes.gta.model.TaskDueDateImpl;
 import org.olat.course.nodes.gta.model.TaskImpl;
 import org.olat.course.nodes.gta.model.TaskListImpl;
+import org.olat.course.nodes.gta.model.TaskRevisionDateImpl;
 import org.olat.course.nodes.gta.ui.events.SubmitEvent;
 import org.olat.course.run.environment.CourseEnvironment;
 import org.olat.group.BusinessGroup;
@@ -122,6 +125,8 @@ public class GTAManagerImpl implements GTAManager, DeletableGroupData {
 	@Autowired
 	private BusinessGroupService businessGroupService;
 	@Autowired
+	private NotificationsManager notificationsManager;
+	@Autowired
 	private BusinessGroupRelationDAO businessGroupRelationDao;
 	@Autowired
 	private RepositoryEntryRelationDAO repositoryEntryRelationDao;
@@ -862,6 +867,17 @@ public class GTAManagerImpl implements GTAManager, DeletableGroupData {
 
 		return tasks.isEmpty() ? null : tasks.get(0);
 	}
+
+	@Override
+	public List<TaskRevisionDate> getTaskRevisions(Task task) {
+		if(task == null || task.getKey() == null) return Collections.emptyList();
+		
+		String q = "select taskrev from gtataskrevisiondate taskrev where taskrev.task.key=:taskKey";
+		return dbInstance.getCurrentEntityManager()
+				.createQuery(q, TaskRevisionDate.class)
+			.setParameter("taskKey", task.getKey())
+			.getResultList();
+	}
 	
 	@Override
 	public AssignmentResponse assignTaskAutomatically(TaskList taskList, BusinessGroup assessedGroup, CourseEnvironment courseEnv, GTACourseNode cNode) {
@@ -1071,6 +1087,10 @@ public class GTAManagerImpl implements GTAManager, DeletableGroupData {
 		task.setTaskStatus(status);//assignment is ok -> go to submit step
 		task.setRevisionLoop(0);
 		
+		if(status == TaskProcess.graded) {
+			task.setGraduationDate(new Date());
+		}
+		
 		if(GTAType.group.name().equals(cNode.getModuleConfiguration().getStringValue(GTACourseNode.GTASK_TYPE))) {
 			task.setBusinessGroup(assessedGroup);
 		} else {
@@ -1078,6 +1098,17 @@ public class GTAManagerImpl implements GTAManager, DeletableGroupData {
 		}
 		return task;
 	}
+	
+	public TaskRevisionDate createAndPersistTaskRevisionDate(Task task, int revisionLoop, TaskProcess status) {
+		TaskRevisionDateImpl rev = new TaskRevisionDateImpl();
+		rev.setCreationDate(new Date());
+		rev.setDate(rev.getCreationDate());
+		rev.setRevisionLoop(revisionLoop);
+		rev.setStatus(status.name());
+		rev.setTask(task);
+		dbInstance.getCurrentEntityManager().persist(rev);
+		return rev;
+	}
 
 	@Override
 	public boolean isDueDateEnabled(GTACourseNode cNode) {
@@ -1407,18 +1438,61 @@ public class GTAManagerImpl implements GTAManager, DeletableGroupData {
 	public Task submitRevisions(Task task, GTACourseNode cNode) {
 		TaskImpl taskImpl = (TaskImpl)task;
 		taskImpl.setSubmissionRevisionsDate(new Date());
+		//log the date
+		createAndPersistTaskRevisionDate(taskImpl, taskImpl.getRevisionLoop(), TaskProcess.correction);
 		return updateTask(taskImpl, TaskProcess.correction, cNode);
 	}
+	
+	@Override
+	public Task reviewedTask(Task task, GTACourseNode cNode) {
+		TaskProcess solution = nextStep(TaskProcess.correction, cNode);
+		TaskImpl taskImpl = (TaskImpl)task;
+		taskImpl.setAcceptationDate(new Date());
+		return updateTask(taskImpl, solution, cNode);
+	}
 
 	@Override
 	public Task updateTask(Task task, TaskProcess newStatus, GTACourseNode cNode) {
 		TaskImpl taskImpl = (TaskImpl)task;
 		taskImpl.setTaskStatus(newStatus);
+		syncDates(taskImpl, newStatus);
 		taskImpl = dbInstance.getCurrentEntityManager().merge(taskImpl);
 		syncAssessmentEntry(taskImpl, cNode);
+		
+		//update
+		OLATResource resource = taskImpl.getTaskList().getEntry().getOlatResource();
+		notificationsManager.markPublisherNews(getSubscriptionContext(resource, cNode), null, false);
+		
 		return taskImpl;
 	}
 	
+	private void syncDates(TaskImpl taskImpl, TaskProcess newStatus) {
+		//solution date
+		if(newStatus == TaskProcess.solution || newStatus == TaskProcess.grading || newStatus == TaskProcess.graded) {
+			if(taskImpl.getSolutionDate() == null) {
+				taskImpl.setSolutionDate(new Date());
+			}
+		} else {
+			taskImpl.setSolutionDate(null);
+		}
+		
+		//graduation date
+		if(newStatus == TaskProcess.graded) {
+			if(taskImpl.getGraduationDate() == null) {
+				taskImpl.setGraduationDate(new Date());
+			}
+		} else {
+			taskImpl.setGraduationDate(null);
+		}
+		
+		//check submission date because of repon
+		if(newStatus == TaskProcess.assignment || newStatus == TaskProcess.submit) {
+			if(taskImpl.getSubmissionDate() != null) {
+				taskImpl.setSubmissionDate(null);
+			}
+		}
+	}
+	
 	@Override
 	public TaskDueDate updateTaskDueDate(TaskDueDate taskDueDate) {
 		((TaskDueDateImpl)taskDueDate).setLastModified(new Date());
@@ -1431,6 +1505,8 @@ public class GTAManagerImpl implements GTAManager, DeletableGroupData {
 		taskImpl.setTaskStatus(newStatus);
 		taskImpl.setRevisionLoop(iteration);
 		taskImpl = dbInstance.getCurrentEntityManager().merge(taskImpl);
+		//log date
+		createAndPersistTaskRevisionDate(taskImpl, iteration, newStatus);
 		syncAssessmentEntry(taskImpl, cNode);
 		return taskImpl;
 	}
diff --git a/src/main/java/org/olat/course/nodes/gta/manager/GTANotifications.java b/src/main/java/org/olat/course/nodes/gta/manager/GTANotifications.java
index ec1faaead33..132d662ed73 100644
--- a/src/main/java/org/olat/course/nodes/gta/manager/GTANotifications.java
+++ b/src/main/java/org/olat/course/nodes/gta/manager/GTANotifications.java
@@ -38,24 +38,33 @@ import org.olat.core.commons.services.notifications.model.SubscriptionListItem;
 import org.olat.core.gui.translator.Translator;
 import org.olat.core.id.Identity;
 import org.olat.core.id.context.BusinessControlFactory;
+import org.olat.core.util.Formatter;
+import org.olat.core.util.StringHelper;
 import org.olat.core.util.Util;
 import org.olat.core.util.io.SystemFileFilter;
 import org.olat.core.util.vfs.VFSContainer;
 import org.olat.core.util.vfs.VFSItem;
 import org.olat.course.CourseFactory;
 import org.olat.course.ICourse;
+import org.olat.course.assessment.AssessmentHelper;
 import org.olat.course.nodes.CourseNode;
 import org.olat.course.nodes.GTACourseNode;
 import org.olat.course.nodes.gta.GTAManager;
 import org.olat.course.nodes.gta.GTAType;
 import org.olat.course.nodes.gta.Task;
 import org.olat.course.nodes.gta.TaskList;
+import org.olat.course.nodes.gta.TaskProcess;
+import org.olat.course.nodes.gta.TaskRevisionDate;
+import org.olat.course.nodes.gta.model.DueDate;
 import org.olat.course.nodes.gta.model.Membership;
 import org.olat.course.nodes.gta.ui.GTARunController;
 import org.olat.course.run.environment.CourseEnvironment;
+import org.olat.course.run.userview.UserCourseEnvironment;
 import org.olat.group.BusinessGroup;
 import org.olat.group.BusinessGroupService;
 import org.olat.group.model.SearchBusinessGroupParams;
+import org.olat.modules.assessment.AssessmentEntry;
+import org.olat.modules.assessment.manager.AssessmentEntryDAO;
 import org.olat.repository.RepositoryEntry;
 import org.olat.repository.RepositoryService;
 import org.olat.user.UserManager;
@@ -73,32 +82,50 @@ class GTANotifications {
 	private final List<SubscriptionListItem> items = new ArrayList<>();
 	
 	private TaskList taskList;
+	private String header;
 	private String displayName;
+	private GTACourseNode gtaNode;
+	private CourseEnvironment courseEnv;
 	private Calendar cal = Calendar.getInstance();
 	
 	private final Translator translator;
+	private final Formatter formatter;
 	
 	private final GTAManager gtaManager;
 	private final UserManager userManager;
 	private final RepositoryService repositoryService;
 	private final BusinessGroupService businessGroupService;
+	private final AssessmentEntryDAO courseNodeAssessmentDao;
 	
 	public GTANotifications(Subscriber subscriber, Locale locale, Date compareDate,
 			RepositoryService repositoryService, GTAManager gtaManager,
-			BusinessGroupService businessGroupService, UserManager userManager) {
+			BusinessGroupService businessGroupService, UserManager userManager,
+			AssessmentEntryDAO courseNodeAssessmentDao) {
 		this.gtaManager = gtaManager;
 		this.userManager = userManager;
 		this.repositoryService = repositoryService;
 		this.businessGroupService = businessGroupService;
+		this.courseNodeAssessmentDao = courseNodeAssessmentDao;
 		
 		this.subscriber = subscriber;
 		this.compareDate = compareDate;
+		formatter = Formatter.getInstance(locale);
 		translator = Util.createPackageTranslator(GTARunController.class, locale);
 	}
 	
 	public String getDisplayName() {
 		return displayName;
 	}
+	
+	public String getNotifificationHeader() {
+		if(StringHelper.containsNonWhitespace(header)) {
+			return header;
+		}
+		if(GTAType.group.name().equals(gtaNode.getModuleConfiguration().getStringValue(GTACourseNode.GTASK_TYPE))) {
+			return translator.translate("notifications.group.header", new String[]{ displayName });
+		}
+		return translator.translate("notifications.individual.header", new String[]{ displayName });
+	}
 
 	public List<SubscriptionListItem> getItems() {
 		Publisher p = subscriber.getPublisher();
@@ -110,39 +137,28 @@ class GTANotifications {
 		}
 		
 		if(entry != null && node instanceof GTACourseNode) {
+			gtaNode = (GTACourseNode)node;
 			displayName = entry.getDisplayname();
-			
-			GTACourseNode gtaNode = (GTACourseNode)node;
+			courseEnv = course.getCourseEnvironment();
 			taskList = gtaManager.getTaskList(entry, gtaNode);
 			
 			if(GTAType.group.name().equals(gtaNode.getModuleConfiguration().getStringValue(GTACourseNode.GTASK_TYPE))) {
-				createBusinessGroupsSubscriptionInfo(subscriber.getIdentity(), course.getCourseEnvironment(), gtaNode);
+				createBusinessGroupsSubscriptionInfo(subscriber.getIdentity());
 			} else {
-				createIndividualSubscriptionInfo(subscriber.getIdentity(), course.getCourseEnvironment(), gtaNode);
-			}
-
-			//copy solutions
-			if(gtaNode.getModuleConfiguration().getBooleanSafe(GTACourseNode.GTASK_SAMPLE_SOLUTION)) {
-				File solutionDirectory = gtaManager.getSolutionsDirectory(course.getCourseEnvironment(), gtaNode);
-				VFSContainer solutionContainer = gtaManager.getSolutionsContainer(course.getCourseEnvironment(), gtaNode);
-				File[] solutions = solutionDirectory.listFiles(SystemFileFilter.FILES_ONLY);
-				for(File solution:solutions) {
-					String author = getAuthor(solution, solutionContainer);
-					appendSubscriptionItem("notifications.solution", new String[] { solution.getName(), author }, solution);
-				}
+				createIndividualSubscriptionInfo(subscriber.getIdentity());
 			}
 		}
 		
 		return items;
 	}
 	
-	private void createIndividualSubscriptionInfo(Identity subscriberIdentity, CourseEnvironment ce, GTACourseNode gtaNode) {
-		RepositoryEntry entry = ce.getCourseGroupManager().getCourseEntry();
+	private void createIndividualSubscriptionInfo(Identity subscriberIdentity) {
+		RepositoryEntry entry = courseEnv.getCourseGroupManager().getCourseEntry();
 		List<String> roles = repositoryService.getRoles(subscriberIdentity, entry);
 		
 		boolean owner = roles.contains(GroupRoles.owner.name());
 		boolean coach = roles.contains(GroupRoles.coach.name());
-		if(owner|| coach) {
+		if(owner || coach) {
 			Set<Identity> duplicateKiller = new HashSet<>();
 			List<Identity> assessableIdentities = new ArrayList<>();
 
@@ -175,132 +191,294 @@ class GTANotifications {
 			}
 			
 			for(Identity assessedIdentity: assessableIdentities) {
-				createIndividualSubscriptionInfo(assessedIdentity, ce, gtaNode, true);
+				createIndividualSubscriptionInfo(assessedIdentity, true);
 			}
-		}
-		
-		if(roles.contains(GroupRoles.participant.name())) {
-			createIndividualSubscriptionInfo(subscriberIdentity, ce, gtaNode, false);
-		}
-	}
 
-	private void createIndividualSubscriptionInfo(Identity assessedIdentity, CourseEnvironment ce, GTACourseNode gtaNode, boolean coach) {
-		String fullName = userManager.getUserDisplayName(assessedIdentity);
-		if(coach && gtaNode.getModuleConfiguration().getBooleanSafe(GTACourseNode.GTASK_SUBMIT)) {
-			File submitDirectory = gtaManager.getSubmitDirectory(ce, gtaNode, assessedIdentity);
-			File[] solutions = submitDirectory.listFiles(SystemFileFilter.FILES_ONLY);
-			for(File solution:solutions) {
-				appendSubscriptionItem("notifications.submission.individual", new String[] { solution.getName(), fullName }, solution);
+			createCoachSolutionItems();
+		} else {
+			Task task = gtaManager.getTask(subscriberIdentity, taskList);
+			if(task != null) {
+				header = translator.translate("notifications.individual.header.task", new String[]{ task.getTaskName(), displayName });
 			}
 		}
 		
-		if(!coach && gtaNode.getModuleConfiguration().getBooleanSafe(GTACourseNode.GTASK_REVIEW_AND_CORRECTION)) {
-			File correctionDirectory = gtaManager.getCorrectionDirectory(ce, gtaNode, assessedIdentity);
-			VFSContainer correctionContainer = gtaManager.getCorrectionContainer(ce, gtaNode, assessedIdentity);
-			File[] corrections = correctionDirectory.listFiles(SystemFileFilter.FILES_ONLY);
-			for(File correction:corrections) {
-				String author = getAuthor(correction, correctionContainer);
-				appendSubscriptionItem("notifications.correction", new String[] { correction.getName(), author }, correction);
+		if(roles.contains(GroupRoles.participant.name())) {
+			createIndividualSubscriptionInfo(subscriberIdentity, false);
+			Task task = gtaManager.getTask(subscriberIdentity, taskList);
+			if(isSolutionVisible(subscriberIdentity, null, task)) {
+				createParticipantSolutionItems(task, subscriberIdentity, null);
+			}
+			if(task != null && task.getTaskStatus() == TaskProcess.graded) {
+				createAssessmentItem(task, subscriberIdentity, false);
 			}
 		}
+	}
+
+	private void createIndividualSubscriptionInfo(Identity assessedIdentity, boolean coach) {
+		Task task = gtaManager.getTask(assessedIdentity, taskList);
 		
-		if(gtaNode.getModuleConfiguration().getBooleanSafe(GTACourseNode.GTASK_REVISION_PERIOD)) {
-			Task task = gtaManager.getTask(assessedIdentity, taskList);
-			if(task != null) {
-				int currentIteration = task.getRevisionLoop();
-				for(int i=1; i<=currentIteration; i++) {
-					//revision
-					
-					if(coach) {
-						//revision
-						File revisionDirectory = gtaManager.getRevisedDocumentsDirectory(ce, gtaNode, i, assessedIdentity);
-						File[] revisions = revisionDirectory.listFiles(SystemFileFilter.FILES_ONLY);
-						for(File revision:revisions) {
-							appendSubscriptionItem("notifications.revision.individual", new String[] { revision.getName(), fullName }, revision);
-						}
-					} else {
-						//corrections
-						File correctionDirectory = gtaManager.getRevisedDocumentsCorrectionsDirectory(ce, gtaNode, i, assessedIdentity);
-						VFSContainer correctionContainer = gtaManager.getRevisedDocumentsCorrectionsContainer(ce, gtaNode, i, assessedIdentity);
-						File[] corrections = correctionDirectory.listFiles(SystemFileFilter.FILES_ONLY);
-						for(File correction:corrections) {
-							String author = getAuthor(correction, correctionContainer);
-							appendSubscriptionItem("notifications.correction", new String[] { correction.getName(), author }, correction);
-						}
+		if(coach && gtaNode.getModuleConfiguration().getBooleanSafe(GTACourseNode.GTASK_SUBMIT)) {
+			task = checkSubmitStep(assessedIdentity, null, task);
+			//show after the step submit, if submission date after compare date
+			if(task != null && notInStep(task, TaskProcess.assignment, TaskProcess.submit)
+					&& task.getSubmissionDate() != null && task.getSubmissionDate().after(compareDate)) {
+				
+				Date submissionDate = task.getSubmissionDate();
+				String fullName = userManager.getUserDisplayName(assessedIdentity);
+				File submitDirectory = gtaManager.getSubmitDirectory(courseEnv, gtaNode, assessedIdentity);
+				File[] submissions = submitDirectory.listFiles(SystemFileFilter.FILES_ONLY);
+				if(submissions.length == 0) {
+					String[] params = new String[] {
+							task.getTaskName(),		// {0}
+							displayName,				// {1}
+							fullName					// {2}
+					};
+					appendSubscriptionItem("notifications.submission.individual", params, assessedIdentity, submissionDate, coach);
+				} else {
+					for(File submission:submissions) {
+						String[] params = new String[] {
+								task.getTaskName(),		// {0}
+								displayName,				// {1}
+								submission.getName(),	// {2}
+								fullName					// {3}
+						};
+						appendSubscriptionItemForFile("notifications.submission.individual.doc", params, assessedIdentity,
+								"[submit:0]", submission, submissionDate, coach);
 					}
 				}
 			}
 		}
+		
+		createReviewAndRevisionsItems(task, assessedIdentity, null, coach);
+		createAcceptedItem(task, assessedIdentity, null, coach);
 	}
 	
-	private void createBusinessGroupsSubscriptionInfo(Identity subscriberIdentity, CourseEnvironment ce, GTACourseNode gtaNode) {
-		RepositoryEntry entry = ce.getCourseGroupManager().getCourseEntry();
+	private void createBusinessGroupsSubscriptionInfo(Identity subscriberIdentity) {
+		RepositoryEntry entry = courseEnv.getCourseGroupManager().getCourseEntry();
 
 		Membership membership = gtaManager.getMembership(subscriberIdentity, entry, gtaNode);
 		boolean owner = repositoryService.hasRole(subscriberIdentity, entry, GroupRoles.owner.name());
 		if(owner) {
 			List<BusinessGroup> groups = gtaManager.getBusinessGroups(gtaNode);
 			for(BusinessGroup group:groups) {
-				createBusinessGroupsSubscriptionItems(group, ce, gtaNode, true);
+				createBusinessGroupsSubscriptionItems(group, true);
 			}
-		} else if(membership.isCoach() ) {
+			createCoachSolutionItems();
+		} else if(membership.isCoach()) {
 			List<BusinessGroup> groups = gtaManager.getCoachedBusinessGroups(subscriberIdentity, gtaNode);
 			for(BusinessGroup group:groups) {
-				createBusinessGroupsSubscriptionItems(group, ce, gtaNode, true);
+				createBusinessGroupsSubscriptionItems(group, true);
 			}
+			createCoachSolutionItems();
 		}
 		
 		if(membership.isParticipant()) {
 			List<BusinessGroup> groups = gtaManager.getParticipatingBusinessGroups(subscriberIdentity, gtaNode);
+			if(groups.size() == 1 && !owner && !membership.isCoach()) {
+				Task task = gtaManager.getTask(groups.get(0), taskList);
+				if(task != null) {
+					header = translator.translate("notifications.group.header.task", new String[]{ task.getTaskName(), displayName });
+				}
+			}
+			
 			for(BusinessGroup group:groups) {
-				createBusinessGroupsSubscriptionItems(group, ce, gtaNode, false);
+				createBusinessGroupsSubscriptionItems(group, false);
+				Task task = gtaManager.getTask(group, taskList);
+				if(createParticipantSolutionItems(task, null, group)) {
+					break;
+				}
 			}
 		}
 	}
 	
-	private void createBusinessGroupsSubscriptionItems(BusinessGroup group, CourseEnvironment ce, GTACourseNode gtaNode, boolean coach) {
+	private void createBusinessGroupsSubscriptionItems(BusinessGroup group, boolean coach) {
+		Task task = gtaManager.getTask(group, taskList);
+		
 		if(gtaNode.getModuleConfiguration().getBooleanSafe(GTACourseNode.GTASK_SUBMIT)) {
-			File submitDirectory = gtaManager.getSubmitDirectory(ce, gtaNode, group);
-			VFSContainer submitContainer = gtaManager.getSubmitContainer(ce, gtaNode, group);
-			File[] documents = submitDirectory.listFiles(SystemFileFilter.FILES_ONLY);
-			for(File document:documents) {
-				String author = getAuthor(document, submitContainer);
-				appendSubscriptionItem("notifications.submission.group", new String[] { document.getName(), group.getName(), author }, document);
+			task = checkSubmitStep(null, group, task);
+			if(task != null && notInStep(task, TaskProcess.assignment, TaskProcess.submit)
+					&& (task.getSubmissionDate() == null || task.getSubmissionDate().after(compareDate))) {
+				File submitDirectory = gtaManager.getSubmitDirectory(courseEnv, gtaNode, group);
+				VFSContainer submitContainer = gtaManager.getSubmitContainer(courseEnv, gtaNode, group);
+				
+				Date submissionDate = task.getSubmissionDate();
+				File[] submisssions = submitDirectory.listFiles(SystemFileFilter.FILES_ONLY);
+				if(submisssions.length == 0) {
+					String[] params = new String[] { 
+							task.getTaskName(),
+							displayName,
+							group.getName()
+					};
+					appendSubscriptionItem("notifications.submission.group", params, group, submissionDate, coach);
+				} else {
+				
+					for(File submission:submisssions) {
+						String author = getAuthor(submission, submitContainer);
+						String[] params = new String[] {
+								task.getTaskName(),		// {0}
+								displayName,				// {1}
+								submission.getName(),		// {2}
+								author,					// {3}
+								group.getName()					// {3}
+						};
+						appendSubscriptionItemForFile("notifications.submission.group.doc", params, group,
+								"[submit:0]", submission, submissionDate, coach);
+					}
+				}
 			}
 		}
+
+		createReviewAndRevisionsItems(task, null, group, coach);
+		createAcceptedItem(task, null, group, coach);
+	}
+	
+	private void createReviewAndRevisionsItems(Task task, Identity assessedIdentity, BusinessGroup group, boolean coach) {
+		if(task == null) return;
 		
+		String name;
+		if(group != null) {
+			name = group.getName();
+		} else {
+			name = userManager.getUserDisplayName(assessedIdentity);
+		}
+
+		List<TaskRevisionDate> taskRevisions = gtaManager.getTaskRevisions(task);
 		if(!coach && gtaNode.getModuleConfiguration().getBooleanSafe(GTACourseNode.GTASK_REVIEW_AND_CORRECTION)) {
-			File correctionDirectory = gtaManager.getCorrectionDirectory(ce, gtaNode, group);
-			VFSContainer correctionContainer = gtaManager.getCorrectionContainer(ce, gtaNode, group);
-			File[] corrections = correctionDirectory.listFiles(SystemFileFilter.FILES_ONLY);
-			for(File correction:corrections) {
-				String author = getAuthor(correction, correctionContainer);
-				appendSubscriptionItem("notifications.correction", new String[] { correction.getName(), author }, correction);
+			//check task revision 1
+			if(task != null && notInStep(task, TaskProcess.assignment, TaskProcess.submit, TaskProcess.review)
+					&& checkRevisionLoop(TaskProcess.revision, 1, taskRevisions)) {
+				File correctionDirectory;
+				VFSContainer correctionContainer;
+				if(group != null) {
+					correctionDirectory = gtaManager.getCorrectionDirectory(courseEnv, gtaNode, group);
+					correctionContainer = gtaManager.getCorrectionContainer(courseEnv, gtaNode, group);
+				} else {
+					correctionDirectory = gtaManager.getCorrectionDirectory(courseEnv, gtaNode, assessedIdentity);
+					correctionContainer = gtaManager.getCorrectionContainer(courseEnv, gtaNode, assessedIdentity);
+				}
+
+				Date correctionDate = getRevisionLoopDate(TaskProcess.revision, 1, taskRevisions);
+				if(task.getRevisionsDueDate() != null) {
+					String[] params = new String[] {
+							task.getTaskName(),
+							displayName,
+							formatter.formatDateAndTime(task.getRevisionsDueDate())	
+					};
+					appendSubscriptionItem("notifications.correction.duedate", params, assessedIdentity, correctionDate, coach);
+				} else {
+					String[] params = new String[] {
+							task.getTaskName(),
+							displayName
+					};
+					appendSubscriptionItem("notifications.correction", params, assessedIdentity, correctionDate, coach);
+				}
+
+				File[] corrections = correctionDirectory.listFiles(SystemFileFilter.FILES_ONLY);
+				for(File correction:corrections) {
+					String author = getAuthor(correction, correctionContainer);
+					String[] params = new String[] {
+							task.getTaskName(),
+							displayName,
+							correction.getName(),
+							author
+					};
+					appendSubscriptionItemForFile("notifications.correction.doc", params, assessedIdentity,
+							"[correction:0]", correction, correctionDate, coach);
+				}
 			}
 		}
 		
 		if(gtaNode.getModuleConfiguration().getBooleanSafe(GTACourseNode.GTASK_REVISION_PERIOD)) {
-			Task task = gtaManager.getTask(group, taskList);
-			if(task != null) {
+			task = checkRevisionStep(assessedIdentity, null, task);
+			if(task != null && notInStep(task, TaskProcess.assignment, TaskProcess.submit, TaskProcess.review)) {
 				int currentIteration = task.getRevisionLoop();
 				for(int i=1; i<=currentIteration; i++) {
-					//revision
-					File revisionDirectory = gtaManager.getRevisedDocumentsDirectory(ce, gtaNode, i, group);
-					VFSContainer revisionContainer = gtaManager.getRevisedDocumentsContainer(ce, gtaNode, i, group);
+					
+					if(coach) {
+						// revision of the students
+						if(checkRevisionLoop(TaskProcess.correction, i, taskRevisions)) {
+							File revisionDirectory; 
+							VFSContainer revisionContainer;
+							if(group != null) {
+								revisionDirectory = gtaManager.getRevisedDocumentsDirectory(courseEnv, gtaNode, i, group);
+								revisionContainer = gtaManager.getRevisedDocumentsContainer(courseEnv, gtaNode, i, group);
+							} else {
+								revisionDirectory = gtaManager.getRevisedDocumentsDirectory(courseEnv, gtaNode, i, assessedIdentity);
+								revisionContainer = gtaManager.getRevisedDocumentsContainer(courseEnv, gtaNode, i, assessedIdentity);
+							}
 
-					File[] revisions = revisionDirectory.listFiles(SystemFileFilter.FILES_ONLY);
-					for(File revision:revisions) {
-						String author = getAuthor(revision, revisionContainer);
-						appendSubscriptionItem("notifications.revision.group", new String[] { revision.getName(), group.getName(), author }, revision);
-					}
-					//corrections
-					if(!coach) {
-						File correctionDirectory = gtaManager.getRevisedDocumentsCorrectionsDirectory(ce, gtaNode, i, group);
-						VFSContainer correctionContainer = gtaManager.getRevisedDocumentsCorrectionsContainer(ce, gtaNode, i, group);
+							Date revisionDate = getRevisionLoopDate(TaskProcess.correction, i, taskRevisions);
+							File[] revisions = revisionDirectory.listFiles(SystemFileFilter.FILES_ONLY);
+							if(revisions.length == 0) {
+								String[] params = new String[] {
+										task.getTaskName(),
+										displayName,
+										name
+								};
+								if(group != null) {
+									appendSubscriptionItem("notifications.revision.group", params, group, revisionDate, coach);
+								} else {
+									appendSubscriptionItem("notifications.revision.individual", params, assessedIdentity, revisionDate, coach);
+								}
+							} else {
+								for(File revision:revisions) {
+									String author = getAuthor(revision, revisionContainer);
+									String[] params = new String[] {
+											task.getTaskName(),
+											displayName,
+											revision.getName(),
+											name,
+											author
+									};
+									if(group != null) {
+										appendSubscriptionItemForFile("notifications.revision.group.doc", params, group,
+												"[revision:" + i + "]", revision, revisionDate, coach);
+									} else {
+										appendSubscriptionItemForFile("notifications.revision.individual.doc", params, assessedIdentity,
+												"[revision:" + i + "]", revision, revisionDate, coach);
+									}
+								}
+							}
+						}
+					} else if(checkRevisionLoop(TaskProcess.revision, i, taskRevisions)) {
+						// corrections of the coach
+						File correctionDirectory;
+						VFSContainer correctionContainer;
+						if(group != null) {
+							correctionDirectory = gtaManager.getRevisedDocumentsCorrectionsDirectory(courseEnv, gtaNode, i, group);
+							correctionContainer = gtaManager.getRevisedDocumentsCorrectionsContainer(courseEnv, gtaNode, i, group);
+						} else {
+							correctionDirectory = gtaManager.getRevisedDocumentsCorrectionsDirectory(courseEnv, gtaNode, i, assessedIdentity);
+							correctionContainer = gtaManager.getRevisedDocumentsCorrectionsContainer(courseEnv, gtaNode, i, assessedIdentity);
+						}
+
+						Date correctionDate = getRevisionLoopDate(TaskProcess.revision, i, taskRevisions);
+						if(task.getRevisionsDueDate() != null) {
+							String[] params = new String[] {
+									task.getTaskName(),
+									displayName,
+									formatter.formatDateAndTime(task.getRevisionsDueDate())
+							};
+							appendSubscriptionItem("notifications.correction.duedate", params, assessedIdentity, correctionDate, coach);
+						} else {
+							String[] params = new String[] {
+									task.getTaskName(),
+									displayName
+							};
+							appendSubscriptionItem("notifications.correction", params, assessedIdentity, correctionDate, coach);
+						}
+						
 						File[] corrections = correctionDirectory.listFiles(SystemFileFilter.FILES_ONLY);
 						for(File correction:corrections) {
 							String author = getAuthor(correction, correctionContainer);
-							appendSubscriptionItem("notifications.correction", new String[] { correction.getName(), author }, correction);
+							String[] params = new String[] {
+									task.getTaskName(),
+									displayName,
+									correction.getName(),
+									author
+							};
+							appendSubscriptionItemForFile("notifications.correction.doc", params, assessedIdentity,
+									"[correction:" + i + "]", correction, correctionDate, coach);
 						}
 					}
 				}
@@ -308,6 +486,259 @@ class GTANotifications {
 		}
 	}
 	
+	private void createAcceptedItem(Task task, Identity assessedIdentity, BusinessGroup assessedGroup, boolean coach) {
+		if(task == null || task.getAcceptationDate() == null || coach) return;
+
+		if(task.getAcceptationDate().after(compareDate)) {
+			RepositoryEntry courseEntry = courseEnv.getCourseGroupManager().getCourseEntry();
+			String[] params = new String[] { 
+				task.getTaskName(),
+				courseEntry.getDisplayname()
+			};
+			if(assessedGroup != null) {
+				appendSubscriptionItem("notifications.accepted", params, assessedGroup, task.getAcceptationDate(), coach);
+			} else {
+				appendSubscriptionItem("notifications.accepted", params, assessedIdentity, task.getAcceptationDate(), coach);
+			}
+		}
+	}
+	
+	private void createAssessmentItem(Task task, Identity assessedIdentity, boolean coach) {
+		if(task == null || task.getGraduationDate() == null) return;
+		
+		if(task.getGraduationDate().after(compareDate)) {
+			RepositoryEntry courseEntry = courseEnv.getCourseGroupManager().getCourseEntry();
+			AssessmentEntry assessment = courseNodeAssessmentDao.loadAssessmentEntry(assessedIdentity, courseEntry, gtaNode.getIdent());
+			boolean resultsVisible = assessment != null
+					&& (assessment.getUserVisibility() == null || assessment.getUserVisibility().booleanValue());
+			if(resultsVisible) {
+				String score = null;
+				String status = null;
+				if(gtaNode.hasScoreConfigured() && assessment.getScore() != null) {
+					score = AssessmentHelper.getRoundedScore(assessment.getScore());
+				}
+				if(gtaNode.hasPassedConfigured() && assessment.getPassed() != null) {
+					status = assessment.getPassed().booleanValue()
+							? translator.translate("notifications.assessment.passed.true") : translator.translate("notifications.assessment.passed.false");
+				}
+
+				Date graduationDate = task.getGraduationDate();
+				String[] params = new String[] {
+						task.getTaskName(),
+						courseEntry.getDisplayname(),
+						score,
+						status
+				};
+				
+				if(score != null && status != null) {
+					appendSubscriptionItem("notifications.assessment.score.passed", params, assessedIdentity, graduationDate, coach);
+				} else if(score != null) {
+					appendSubscriptionItem("notifications.assessment.score", params, assessedIdentity, graduationDate, coach);
+				} else if(status != null) {
+					appendSubscriptionItem("notifications.assessment.passed", params, assessedIdentity, graduationDate, coach);
+				}
+
+				ICourse course = CourseFactory.loadCourse(courseEnv.getCourseGroupManager().getCourseEntry());
+				UserCourseEnvironment assessedUserCourseEnv = AssessmentHelper
+						.createAndInitUserCourseEnvironment(assessedIdentity, course);
+				List<File> docs = gtaNode.getIndividualAssessmentDocuments(assessedUserCourseEnv);
+				for(File doc:docs) {
+					String[] docParams = new String[] {
+							task.getTaskName(),
+							courseEntry.getDisplayname(),
+							doc.getName()
+					};
+					appendSubscriptionItemForFile("notifications.assessment.doc", docParams, assessedIdentity,
+							"[assessment:0]",  doc, graduationDate, coach);
+				}
+			}
+		}
+	}
+	
+	private void createCoachSolutionItems() {
+		//copy solutions
+		if(gtaNode.getModuleConfiguration().getBooleanSafe(GTACourseNode.GTASK_SAMPLE_SOLUTION)) {
+			File solutionDirectory = gtaManager.getSolutionsDirectory(courseEnv, gtaNode);
+			VFSContainer solutionContainer = gtaManager.getSolutionsContainer(courseEnv, gtaNode);
+			File[] solutions = solutionDirectory.listFiles(SystemFileFilter.FILES_ONLY);
+			for(File solution:solutions) {
+				String author = getAuthor(solution, solutionContainer);
+				String[] params = new String[] {
+					displayName,
+					solution.getName(),
+					author
+				};
+				appendSubscriptionItemForFile("notifications.solution", params, "",
+						"[solution:0]" , solution, null, true);
+			}
+		}
+	}
+	
+	private boolean createParticipantSolutionItems(Task task, Identity assessedIdentity, BusinessGroup group) {
+		//copy solutions
+		if(gtaNode.getModuleConfiguration().getBooleanSafe(GTACourseNode.GTASK_SAMPLE_SOLUTION)) {
+			RepositoryEntry re = courseEnv.getCourseGroupManager().getCourseEntry();
+			DueDate dueDate = gtaManager.getSolutionDueDate(task, assessedIdentity, group, gtaNode, re, true);
+
+			Date solutionDate = null;
+			if(dueDate == null) {
+				if(inStep(task, TaskProcess.solution, TaskProcess.grading, TaskProcess.graded)) {
+					solutionDate = task.getSolutionDate();
+				} else {
+					return false;
+				}
+			} else if(dueDate != null && dueDate.getDueDate() != null) {
+				if(task == null) {
+					if(inStep(task, TaskProcess.assignment, TaskProcess.submit, TaskProcess.review, TaskProcess.correction, TaskProcess.revision)
+						&& !dueDate.isRelative() && gtaNode.getModuleConfiguration().getBooleanSafe(GTACourseNode.GTASK_SAMPLE_SOLUTION_VISIBLE_ALL, false)) {
+						solutionDate = dueDate.getDueDate();
+					} else {
+						return false;
+					}
+				} else {
+					solutionDate = dueDate.getDueDate();
+				}
+			} else if(task != null) {
+				solutionDate = task.getSolutionDate();
+			}
+
+			if(solutionDate != null) {
+				if(task.getSolutionDate() != null && task.getSolutionDate().after(solutionDate)) {
+					solutionDate = task.getSolutionDate();
+				}
+				
+				File solutionDirectory = gtaManager.getSolutionsDirectory(courseEnv, gtaNode);
+				VFSContainer solutionContainer = gtaManager.getSolutionsContainer(courseEnv, gtaNode);
+				File[] solutions = solutionDirectory.listFiles(SystemFileFilter.FILES_ONLY);
+				for(File solution:solutions) {
+					Date date = solutionDate;
+					
+					//check if there are new solutions too
+					cal.setTimeInMillis(solution.lastModified());
+					Date modificationDate = cal.getTime();
+					if(date.before(modificationDate)) {
+						date = modificationDate;
+					}
+					
+					if(date.after(compareDate)) {
+						String author = getAuthor(solution, solutionContainer);
+						if(task != null) {
+							String[] params = new String[] {
+									task.getTaskName(),
+									displayName,
+									solution.getName(),
+									author
+								};
+							appendSubscriptionItemForFile("notifications.solution.task", params, assessedIdentity,
+									"[solution:0]" , solution, date, false);
+						} else {
+							String[] params = new String[] {
+									displayName,
+									solution.getName(),
+									author
+								};
+							appendSubscriptionItemForFile("notifications.solution", params, assessedIdentity,
+									"[solution:0]" , solution, date, false);
+						}
+					}
+				}
+				return true;
+			}
+		}
+		return false;
+	}	
+	
+	private Task checkSubmitStep(Identity assessedIdentity, BusinessGroup assessedGroup, Task task) {
+		if(task != null && task.getTaskStatus() == TaskProcess.submit) {
+			RepositoryEntry re = courseEnv.getCourseGroupManager().getCourseEntry();
+			DueDate dueDate = gtaManager.getSubmissionDueDate(task, assessedIdentity, assessedGroup, gtaNode, re, true);
+			if(dueDate != null && dueDate.getDueDate() != null && dueDate.getDueDate().before(new Date())) {
+				task = gtaManager.nextStep(task, gtaNode);
+				doUpdateAttempts(assessedIdentity, assessedGroup);
+			}
+		}
+		return task;
+	}
+	
+	private boolean checkRevisionLoop(TaskProcess status, int revisionLoop,  List<TaskRevisionDate> taskRevisions) {
+		for(TaskRevisionDate taskRevision:taskRevisions) {
+			if(taskRevision.getTaskStatus() == status && taskRevision.getRevisionLoop() == revisionLoop) {
+				Date date = taskRevision.getDate();
+				if(date.after(compareDate)) {
+					return true;
+				}
+			}
+		}
+		return false;
+	}
+	
+	private Date getRevisionLoopDate(TaskProcess status, int revisionLoop,  List<TaskRevisionDate> taskRevisions) {
+		for(TaskRevisionDate taskRevision:taskRevisions) {
+			if(taskRevision.getTaskStatus() == status && taskRevision.getRevisionLoop() == revisionLoop) {
+				return taskRevision.getDate();
+			}
+		}
+		return null;
+	}
+	
+	/**
+	 * This checks the revision due date.
+	 * 
+	 * @param assessedIdentity The identity which is assessed
+	 * @param assessedGroup The group which is assessed
+	 * @param task The task
+	 * @return An updated task if the status as automatically changed
+	 */
+	private Task checkRevisionStep(Identity assessedIdentity, BusinessGroup assessedGroup, Task task) {
+		if(task != null) {
+			if(task != null && task.getTaskStatus() == TaskProcess.revision
+					&& task.getRevisionsDueDate() != null
+					&& task.getRevisionsDueDate().compareTo(new Date()) < 0) {
+				//push to the next step
+				task = gtaManager.nextStep(task, gtaNode);
+				doUpdateAttempts(assessedIdentity, assessedGroup);
+			}
+		}
+		return task;
+	}
+	
+	private boolean isSolutionVisible(Identity assessedIdentity, BusinessGroup assessedGroup, Task task) {
+		if(task != null) {
+			RepositoryEntry re = courseEnv.getCourseGroupManager().getCourseEntry();
+			DueDate availableDate = gtaManager.getSolutionDueDate(task, assessedIdentity, assessedGroup, gtaNode, re, true);
+			boolean visible = availableDate == null || 
+					(availableDate.getDueDate() != null && availableDate.getDueDate().compareTo(new Date()) <= 0);
+			if(visible) {
+				if(task.getTaskStatus() == TaskProcess.solution || task.getTaskStatus() == TaskProcess.grading || task.getTaskStatus() == TaskProcess.graded) {
+					// step solution or beyond
+					return true;
+				} else if((task == null || task.getTaskStatus() == TaskProcess.assignment || task.getTaskStatus() == TaskProcess.submit
+						|| task.getTaskStatus() == TaskProcess.review || task.getTaskStatus() == TaskProcess.correction
+						|| task.getTaskStatus() == TaskProcess.revision)
+						&& gtaNode.getModuleConfiguration().getBooleanSafe(GTACourseNode.GTASK_SAMPLE_SOLUTION_VISIBLE_ALL, false)) {
+					// steps before solution only if configured to
+					return true;
+				}
+			}
+		}
+		return false;
+	}
+	
+	private void doUpdateAttempts(Identity assessedIdentity, BusinessGroup assessedGroup) {
+		ICourse course = CourseFactory.loadCourse(courseEnv.getCourseGroupManager().getCourseEntry());
+		if(GTAType.group.name().equals(gtaNode.getModuleConfiguration().getStringValue(GTACourseNode.GTASK_TYPE))) {
+			List<Identity> identities = businessGroupService.getMembers(assessedGroup, GroupRoles.participant.name());
+			for(Identity identity:identities) {
+				UserCourseEnvironment uce = AssessmentHelper.createAndInitUserCourseEnvironment(identity, course);
+				gtaNode.incrementUserAttempts(uce);
+			}
+		} else {
+			UserCourseEnvironment assessedUserCourseEnv = AssessmentHelper
+					.createAndInitUserCourseEnvironment(assessedIdentity, course);
+			gtaNode.incrementUserAttempts(assessedUserCourseEnv);
+		}
+	}
+	
 	private String getAuthor(File file, VFSContainer container) {
 		String author = null;
 		VFSItem item = container.resolve(file.getName());
@@ -323,16 +754,91 @@ class GTANotifications {
 		return author == null ? "" : author;
 	}
 	
-	private void appendSubscriptionItem(String notificationKey, String[] params, File file) {
-		cal.setTimeInMillis(file.lastModified());
-		Date modDate = cal.getTime();
-		if(modDate.compareTo(compareDate) >= 0) {
+
+	private void appendSubscriptionItemForFile(String notificationKey, String[] params, Identity identity,
+			String fileCategory, File file, Date modificationDate, boolean coach) {
+		String idPath = "[Identity:" + identity.getKey() + "]";
+		appendSubscriptionItemForFile(notificationKey, params, idPath, fileCategory, file, modificationDate, coach);
+	}
+	
+	private void appendSubscriptionItemForFile(String notificationKey, String[] params, BusinessGroup group,
+			String fileCategory, File file, Date modificationDate, boolean coach) {
+		String idPath = "[BusinessGroup:" + group.getKey() + "]";
+		appendSubscriptionItemForFile(notificationKey, params, idPath, fileCategory, file, modificationDate, coach);
+	}
+	
+	private void appendSubscriptionItemForFile(String notificationKey, String[] params, String idPath,
+			String fileCategory, File file, Date modificationDate, boolean coach) {
+		if(modificationDate == null) {
+			cal.setTimeInMillis(file.lastModified());
+			modificationDate = cal.getTime();
+		}
+		if(modificationDate.compareTo(compareDate) >= 0) {
 			String desc = translator.translate(notificationKey, params);
 			String businessPath = subscriber.getPublisher().getBusinessPath();
+			if(coach) {
+				businessPath += "[Coach:0]";
+			}
+			if(StringHelper.containsNonWhitespace(idPath) ) {
+				businessPath += idPath;
+			}
+			if(StringHelper.containsNonWhitespace(fileCategory)) {
+				businessPath += fileCategory;
+			}
+			if(file != null) {
+				businessPath += "[path=" + file.getName() + ":0]";
+			}
 			String urlToSend = BusinessControlFactory.getInstance().getURLFromBusinessPathString(businessPath);
 			String iconCssClass = GTANotificationsHandler.CSS_CLASS_ICON;
-			SubscriptionListItem item = new SubscriptionListItem(desc, urlToSend, businessPath, modDate, iconCssClass);
+			SubscriptionListItem item = new SubscriptionListItem(desc, urlToSend, businessPath, modificationDate, iconCssClass);
 			items.add(item);
 		}
 	}
+	
+	private void appendSubscriptionItem(String notificationKey, String[] params, Identity identity, Date modificationDate, boolean coach) {
+		String path =  "[Identity:" + identity.getKey() + "]";
+		appendSubscriptionItem(notificationKey, params, path, modificationDate, coach);
+	}
+	
+	private void appendSubscriptionItem(String notificationKey, String[] params, BusinessGroup group, Date modificationDate, boolean coach) {
+		String path =  "[BusinessGroup:" + group.getKey() + "]";
+		appendSubscriptionItem(notificationKey, params, path, modificationDate, coach);
+	}
+	
+	private void appendSubscriptionItem(String notificationKey, String[] params, String path, Date modificationDate, boolean coach) {
+		String desc = translator.translate(notificationKey, params);
+		String businessPath = subscriber.getPublisher().getBusinessPath();
+		if(coach) {
+			businessPath += "[Coach:0]";
+		}
+		if(StringHelper.containsNonWhitespace(path)) {
+			businessPath += path;
+		}
+		String urlToSend = BusinessControlFactory.getInstance().getURLFromBusinessPathString(businessPath);
+		String iconCssClass = GTANotificationsHandler.CSS_CLASS_ICON;
+		SubscriptionListItem item = new SubscriptionListItem(desc, urlToSend, businessPath, modificationDate, iconCssClass);
+		items.add(item);
+	}
+	
+	private boolean notInStep(Task task, TaskProcess... status) {
+		boolean ok = true;
+		if(task == null) {
+			ok &= false;
+		} else {
+			for(TaskProcess state:status) {
+				ok &= task.getTaskStatus() != state;
+			}
+		}
+		return ok;
+	}
+	
+	private boolean inStep(Task task, TaskProcess... status) {
+		boolean ok = false;
+		if(task != null) {
+			for(TaskProcess state:status) {
+				ok |= task.getTaskStatus() == state;
+			}
+		}
+		return ok;
+	}
 }
diff --git a/src/main/java/org/olat/course/nodes/gta/manager/GTANotificationsHandler.java b/src/main/java/org/olat/course/nodes/gta/manager/GTANotificationsHandler.java
index 5b4bae76aaf..1ffba243223 100644
--- a/src/main/java/org/olat/course/nodes/gta/manager/GTANotificationsHandler.java
+++ b/src/main/java/org/olat/course/nodes/gta/manager/GTANotificationsHandler.java
@@ -37,6 +37,7 @@ import org.olat.core.util.Util;
 import org.olat.course.nodes.gta.GTAManager;
 import org.olat.course.nodes.gta.ui.GTARunController;
 import org.olat.group.BusinessGroupService;
+import org.olat.modules.assessment.manager.AssessmentEntryDAO;
 import org.olat.repository.RepositoryManager;
 import org.olat.repository.RepositoryService;
 import org.olat.user.UserManager;
@@ -63,6 +64,8 @@ public class GTANotificationsHandler implements NotificationsHandler  {
 	private RepositoryService repositoryService;
 	@Autowired
 	private BusinessGroupService businessGroupService;
+	@Autowired
+	private AssessmentEntryDAO courseNodeAssessmentDao;
 
 	@Override
 	public SubscriptionInfo createSubscriptionInfo(Subscriber subscriber, Locale locale, Date compareDate) {
@@ -70,20 +73,16 @@ public class GTANotificationsHandler implements NotificationsHandler  {
 		Date latestNews = p.getLatestNewsDate();
 	
 		SubscriptionInfo si;
-	
 		// there could be news for me, investigate deeper
 		try {
 			if (NotificationsManager.getInstance().isPublisherValid(p) && compareDate.before(latestNews)) {
-				Translator translator = Util.createPackageTranslator(GTARunController.class, locale);
-				
 				GTANotifications notifications = new GTANotifications(subscriber, locale, compareDate,
-						repositoryService, gtaManager, businessGroupService, userManager);
+						repositoryService, gtaManager, businessGroupService, userManager, courseNodeAssessmentDao);
 				List<SubscriptionListItem> items = notifications.getItems();
 				if(items.isEmpty()) {
 					si = NotificationsManager.getInstance().getNoSubscriptionInfo();
 				} else {
-					String displayName = notifications.getDisplayName();
-					String title = translator.translate("notifications.header", new String[]{ displayName });
+					String title = notifications.getNotifificationHeader();
 					TitleItem titleItem = new TitleItem(title, CSS_CLASS_ICON);
 					si = new SubscriptionInfo(subscriber.getKey(), p.getType(), titleItem, items);
 				}
diff --git a/src/main/java/org/olat/course/nodes/gta/model/TaskDueDateImpl.java b/src/main/java/org/olat/course/nodes/gta/model/TaskDueDateImpl.java
index 3b181508103..f4fde0fdaab 100644
--- a/src/main/java/org/olat/course/nodes/gta/model/TaskDueDateImpl.java
+++ b/src/main/java/org/olat/course/nodes/gta/model/TaskDueDateImpl.java
@@ -98,6 +98,16 @@ public class TaskDueDateImpl implements TaskDueDate, CreateInfo, Persistable, Mo
 	@Column(name="g_collection_date", nullable=true, insertable=true, updatable=false)
 	private Date collectionDate;
 	
+	@Temporal(TemporalType.TIMESTAMP)
+	@Column(name="g_acceptation_date", nullable=true, insertable=true, updatable=false)
+	private Date acceptationDate;
+	@Temporal(TemporalType.TIMESTAMP)
+	@Column(name="g_solution_date", nullable=true, insertable=true, updatable=false)
+	private Date solutionDate;
+	@Temporal(TemporalType.TIMESTAMP)
+	@Column(name="g_graduation_date", nullable=true, insertable=true, updatable=false)
+	private Date graduationDate;
+	
 	@Temporal(TemporalType.TIMESTAMP)
 	@Column(name="g_assignment_due_date", nullable=true, insertable=true, updatable=true)
 	private Date assignmentDueDate;
@@ -227,6 +237,33 @@ public class TaskDueDateImpl implements TaskDueDate, CreateInfo, Persistable, Mo
 		this.collectionDate = collectionDate;
 	}
 
+	@Override
+	public Date getAcceptationDate() {
+		return acceptationDate;
+	}
+
+	public void setAcceptationDate(Date acceptationDate) {
+		this.acceptationDate = acceptationDate;
+	}
+	
+	@Override
+	public Date getSolutionDate() {
+		return solutionDate;
+	}
+
+	public void setSolutionDate(Date solutionDate) {
+		this.solutionDate = solutionDate;
+	}
+
+	@Override
+	public Date getGraduationDate() {
+		return graduationDate;
+	}
+
+	public void setGraduationDate(Date graduationDate) {
+		this.graduationDate = graduationDate;
+	}
+
 	@Override
 	public Date getAssignmentDueDate() {
 		return assignmentDueDate;
diff --git a/src/main/java/org/olat/course/nodes/gta/model/TaskImpl.java b/src/main/java/org/olat/course/nodes/gta/model/TaskImpl.java
index 7c0a0c742d5..844e1b02b8c 100644
--- a/src/main/java/org/olat/course/nodes/gta/model/TaskImpl.java
+++ b/src/main/java/org/olat/course/nodes/gta/model/TaskImpl.java
@@ -100,6 +100,16 @@ public class TaskImpl implements Task, CreateInfo, Persistable, ModifiedInfo {
 	@Column(name="g_collection_date", nullable=true, insertable=true, updatable=true)
 	private Date collectionDate;
 	
+	@Temporal(TemporalType.TIMESTAMP)
+	@Column(name="g_acceptation_date", nullable=true, insertable=true, updatable=true)
+	private Date acceptationDate;
+	@Temporal(TemporalType.TIMESTAMP)
+	@Column(name="g_solution_date", nullable=true, insertable=true, updatable=true)
+	private Date solutionDate;
+	@Temporal(TemporalType.TIMESTAMP)
+	@Column(name="g_graduation_date", nullable=true, insertable=true, updatable=true)
+	private Date graduationDate;
+	
 	@Temporal(TemporalType.TIMESTAMP)
 	@Column(name="g_assignment_due_date", nullable=true, insertable=true, updatable=false)
 	private Date assignmentDueDate;
@@ -229,6 +239,33 @@ public class TaskImpl implements Task, CreateInfo, Persistable, ModifiedInfo {
 		this.collectionDate = collectionDate;
 	}
 
+	@Override
+	public Date getAcceptationDate() {
+		return acceptationDate;
+	}
+
+	public void setAcceptationDate(Date acceptationDate) {
+		this.acceptationDate = acceptationDate;
+	}
+	
+	@Override
+	public Date getSolutionDate() {
+		return solutionDate;
+	}
+
+	public void setSolutionDate(Date solutionDate) {
+		this.solutionDate = solutionDate;
+	}
+
+	@Override
+	public Date getGraduationDate() {
+		return graduationDate;
+	}
+
+	public void setGraduationDate(Date graduationDate) {
+		this.graduationDate = graduationDate;
+	}
+
 	@Override
 	public Date getAssignmentDueDate() {
 		return assignmentDueDate;
diff --git a/src/main/java/org/olat/course/nodes/gta/model/TaskRevisionDateImpl.java b/src/main/java/org/olat/course/nodes/gta/model/TaskRevisionDateImpl.java
new file mode 100644
index 00000000000..c2d1dcd3688
--- /dev/null
+++ b/src/main/java/org/olat/course/nodes/gta/model/TaskRevisionDateImpl.java
@@ -0,0 +1,173 @@
+/**
+ * <a href="http://www.openolat.org">
+ * OpenOLAT - Online Learning and Training</a><br>
+ * <p>
+ * Licensed under the Apache License, Version 2.0 (the "License"); <br>
+ * you may not use this file except in compliance with the License.<br>
+ * You may obtain a copy of the License at the
+ * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a>
+ * <p>
+ * Unless required by applicable law or agreed to in writing,<br>
+ * software distributed under the License is distributed on an "AS IS" BASIS, <br>
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br>
+ * See the License for the specific language governing permissions and <br>
+ * limitations under the License.
+ * <p>
+ * Initial code contributed and copyrighted by<br>
+ * frentix GmbH, http://www.frentix.com
+ * <p>
+ */
+package org.olat.course.nodes.gta.model;
+
+import java.util.Date;
+
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.FetchType;
+import javax.persistence.GeneratedValue;
+import javax.persistence.Id;
+import javax.persistence.JoinColumn;
+import javax.persistence.ManyToOne;
+import javax.persistence.Table;
+import javax.persistence.Temporal;
+import javax.persistence.TemporalType;
+import javax.persistence.Transient;
+
+import org.hibernate.annotations.GenericGenerator;
+import org.hibernate.annotations.Parameter;
+import org.olat.core.id.CreateInfo;
+import org.olat.core.id.Persistable;
+import org.olat.core.util.StringHelper;
+import org.olat.course.nodes.gta.Task;
+import org.olat.course.nodes.gta.TaskProcess;
+import org.olat.course.nodes.gta.TaskRevisionDate;
+
+/**
+ * 
+ * Initial date: 7 août 2017<br>
+ * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
+ *
+ */
+@Entity(name="gtataskrevisiondate")
+@Table(name="o_gta_task_revision_date")
+public class TaskRevisionDateImpl implements CreateInfo, Persistable, TaskRevisionDate {
+
+	private static final long serialVersionUID = -1476103422622666128L;
+
+	@Id
+	@GeneratedValue(generator = "system-uuid")
+	@GenericGenerator(name = "system-uuid", strategy = "enhanced-sequence", parameters={
+		@Parameter(name="sequence_name", value="hibernate_unique_key"),
+		@Parameter(name="force_table_use", value="true"),
+		@Parameter(name="optimizer", value="legacy-hilo"),
+		@Parameter(name="value_column", value="next_hi"),
+		@Parameter(name="increment_size", value="32767"),
+		@Parameter(name="initial_value", value="32767")
+	})
+	@Column(name="id", nullable=false, unique=true, insertable=true, updatable=false)
+	private Long key;
+	
+	@Temporal(TemporalType.TIMESTAMP)
+	@Column(name="creationdate", nullable=false, insertable=true, updatable=false)
+	private Date creationDate;
+	
+	@Column(name="g_status", nullable=false, insertable=true, updatable=true)
+	private String status;
+	@Column(name="g_rev_loop", nullable=false, insertable=true, updatable=true)
+	private int revisionLoop;
+	@Temporal(TemporalType.TIMESTAMP)
+	@Column(name="g_date", nullable=true, insertable=true, updatable=false)
+	private Date date;
+	
+	@ManyToOne(targetEntity=TaskImpl.class,fetch=FetchType.LAZY,optional=false)
+	@JoinColumn(name="fk_task", nullable=false, insertable=true, updatable=false)
+	private Task task;
+
+	@Override
+	public Long getKey() {
+		return key;
+	}
+
+	@Override
+	public Date getCreationDate() {
+		return creationDate;
+	}
+
+	public String getStatus() {
+		return status;
+	}
+
+	public void setStatus(String status) {
+		this.status = status;
+	}
+	
+	@Override
+	@Transient
+	public TaskProcess getTaskStatus() {
+		if(StringHelper.containsNonWhitespace(status)) {
+			return TaskProcess.valueOf(status);
+		}
+		return null;
+	}
+	
+	public void setTaskStatus(TaskProcess taskStatus) {
+		if(taskStatus == null) {
+			status = null;
+		} else {
+			status = taskStatus.name();
+		}
+	}
+
+	@Override
+	public int getRevisionLoop() {
+		return revisionLoop;
+	}
+
+	public void setRevisionLoop(int revisionLoop) {
+		this.revisionLoop = revisionLoop;
+	}
+
+	@Override
+	public Date getDate() {
+		return date;
+	}
+
+	public void setDate(Date date) {
+		this.date = date;
+	}
+
+	@Override
+	public Task getTask() {
+		return task;
+	}
+
+	public void setTask(Task task) {
+		this.task = task;
+	}
+
+	public void setCreationDate(Date creationDate) {
+		this.creationDate = creationDate;
+	}
+
+	@Override
+	public int hashCode() {
+		return key == null ? 848502 : key.hashCode();
+	}
+
+	@Override
+	public boolean equals(Object obj) {
+		if(obj == this) {
+			return true;
+		}
+		if(obj instanceof TaskRevisionDateImpl) {
+			TaskRevisionDateImpl rev = (TaskRevisionDateImpl)obj;
+			return key != null && key.equals(rev.getKey());
+		}
+		return super.equals(obj);
+	}
+
+	@Override
+	public boolean equalsByPersistableKey(Persistable persistable) {
+		return equals(persistable);
+	}
+}
diff --git a/src/main/java/org/olat/course/nodes/gta/ui/DirectoryController.java b/src/main/java/org/olat/course/nodes/gta/ui/DirectoryController.java
index ab4cf8d3214..6d997abfe85 100644
--- a/src/main/java/org/olat/course/nodes/gta/ui/DirectoryController.java
+++ b/src/main/java/org/olat/course/nodes/gta/ui/DirectoryController.java
@@ -29,6 +29,7 @@ import org.olat.core.commons.modules.bc.meta.tagged.MetaTagged;
 import org.olat.core.commons.modules.singlepage.SinglePageController;
 import org.olat.core.gui.UserRequest;
 import org.olat.core.gui.components.Component;
+import org.olat.core.gui.components.download.DisplayOrDownloadComponent;
 import org.olat.core.gui.components.link.Link;
 import org.olat.core.gui.components.link.LinkFactory;
 import org.olat.core.gui.components.velocity.VelocityContainer;
@@ -37,15 +38,20 @@ import org.olat.core.gui.control.Event;
 import org.olat.core.gui.control.WindowControl;
 import org.olat.core.gui.control.controller.BasicController;
 import org.olat.core.gui.control.generic.closablewrapper.CloseableModalController;
+import org.olat.core.gui.control.generic.dtabs.Activateable2;
 import org.olat.core.gui.media.FileMediaResource;
 import org.olat.core.gui.media.MediaResource;
 import org.olat.core.gui.util.CSSHelper;
+import org.olat.core.id.context.BusinessControlFactory;
+import org.olat.core.id.context.ContextEntry;
+import org.olat.core.id.context.StateEntry;
 import org.olat.core.util.CodeHelper;
 import org.olat.core.util.Formatter;
 import org.olat.core.util.StringHelper;
 import org.olat.core.util.io.SystemFileFilter;
 import org.olat.core.util.vfs.VFSContainer;
 import org.olat.core.util.vfs.VFSItem;
+import org.olat.course.nodes.gta.ui.component.DownloadDocumentMapper;
 import org.olat.fileresource.ZippedDirectoryMediaResource;
 import org.olat.user.UserManager;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -56,11 +62,15 @@ import org.springframework.beans.factory.annotation.Autowired;
  * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
  *
  */
-public class DirectoryController extends BasicController {
+public class DirectoryController extends BasicController implements Activateable2 {
 	
 	private Link bulkReviewLink;
+	private final VelocityContainer mainVC;
+	private final DisplayOrDownloadComponent download;
+	
 	
 	private final String zipName;
+	private final String mapperUri;
 	private final File documentsDir;
 	private final VFSContainer documentsContainer;
 	
@@ -87,9 +97,13 @@ public class DirectoryController extends BasicController {
 		
 		format = Formatter.getInstance(ureq.getLocale());
 		
-		VelocityContainer mainVC = createVelocityContainer("documents_readonly");
+		mainVC = createVelocityContainer("documents_readonly");
 		mainVC.contextPut("description", translate(i18nDescription));
 		
+		mapperUri = registerMapper(ureq, new DownloadDocumentMapper(documentsDir));
+		download = new DisplayOrDownloadComponent("download", null);
+		mainVC.put("download", download);
+		
 		if(StringHelper.containsNonWhitespace(i18nBulkDownload)) {
 			bulkReviewLink = LinkFactory.createCustomLink("bulk", "bulk", null, Link.BUTTON + Link.NONTRANSLATED, mainVC, this);
 			bulkReviewLink.setIconLeftCSS("o_icon o_icon_download");
@@ -135,6 +149,18 @@ public class DirectoryController extends BasicController {
 		//
 	}
 
+	@Override
+	public void activate(UserRequest ureq, List<ContextEntry> entries, StateEntry state) {
+		if(entries == null || entries.isEmpty()) return;
+		
+		String path = BusinessControlFactory.getInstance().getPath(entries.get(0));
+		File document = new File(documentsDir, path);
+		if(document.exists()) {
+			String url = mapperUri + "/" + document.getName();
+			download.triggerFileDownload(url);
+		}
+	}
+
 	@Override
 	protected void event(UserRequest ureq, Component source, Event event) {
 		if(bulkReviewLink == source) {
diff --git a/src/main/java/org/olat/course/nodes/gta/ui/GTACoachController.java b/src/main/java/org/olat/course/nodes/gta/ui/GTACoachController.java
index 2d909b7ce66..e9e2dfae3a5 100644
--- a/src/main/java/org/olat/course/nodes/gta/ui/GTACoachController.java
+++ b/src/main/java/org/olat/course/nodes/gta/ui/GTACoachController.java
@@ -34,9 +34,12 @@ import org.olat.core.gui.control.Controller;
 import org.olat.core.gui.control.Event;
 import org.olat.core.gui.control.WindowControl;
 import org.olat.core.gui.control.generic.closablewrapper.CloseableModalController;
+import org.olat.core.gui.control.generic.dtabs.Activateable2;
 import org.olat.core.gui.control.generic.modal.DialogBoxController;
 import org.olat.core.gui.control.generic.modal.DialogBoxUIFactory;
 import org.olat.core.id.Identity;
+import org.olat.core.id.context.ContextEntry;
+import org.olat.core.id.context.StateEntry;
 import org.olat.core.util.coordinate.CoordinatorManager;
 import org.olat.core.util.io.SystemFilenameFilter;
 import org.olat.core.util.mail.ContactList;
@@ -68,8 +71,7 @@ import org.springframework.beans.factory.annotation.Autowired;
  * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
  *
  */
-public class GTACoachController extends GTAAbstractController implements AssessmentFormCallback {
-
+public class GTACoachController extends GTAAbstractController implements AssessmentFormCallback, Activateable2 {
 
 	private DirectoryController solutionsCtrl;
 	private DirectoryController correctionsCtrl;
@@ -479,6 +481,23 @@ public class GTACoachController extends GTAAbstractController implements Assessm
 			process(ureq);
 		}
 	}
+	
+	@Override
+	public void activate(UserRequest ureq, List<ContextEntry> entries, StateEntry state) {
+		if(entries == null || entries.size() <= 1) return;
+		
+		String type = entries.get(0).getOLATResourceable().getResourceableTypeName();
+		if("Submit".equalsIgnoreCase(type)) {
+			if(submittedDocCtrl != null) {
+				List<ContextEntry> subEntries = entries.subList(1, entries.size());
+				submittedDocCtrl.activate(ureq, subEntries, null);
+			}	
+		} else if("Revision".equalsIgnoreCase(type)) {
+			if(revisionDocumentsCtrl != null) {
+				revisionDocumentsCtrl.activate(ureq, entries, null);
+			}
+		}
+	}
 
 	@Override
 	protected void event(UserRequest ureq, Component source, Event event) {
@@ -602,8 +621,7 @@ public class GTACoachController extends GTAAbstractController implements Assessm
 	
 	private void doReviewedDocument(UserRequest ureq, Task task) {
 		//go to solution, grading or graded
-		TaskProcess nextStep = gtaManager.nextStep(TaskProcess.correction, gtaNode);
-		gtaManager.updateTask(task, nextStep, gtaNode);
+		gtaManager.reviewedTask(task, gtaNode);
 		showInfo("coach.documents.successfully.reviewed");
 		gtaManager.log("Review", "documents reviewed", task, getIdentity(), assessedIdentity, assessedGroup, courseEnv, gtaNode);
 		
diff --git a/src/main/java/org/olat/course/nodes/gta/ui/GTACoachRevisionAndCorrectionsController.java b/src/main/java/org/olat/course/nodes/gta/ui/GTACoachRevisionAndCorrectionsController.java
index 2c0e8a8138a..7b18c1776b2 100644
--- a/src/main/java/org/olat/course/nodes/gta/ui/GTACoachRevisionAndCorrectionsController.java
+++ b/src/main/java/org/olat/course/nodes/gta/ui/GTACoachRevisionAndCorrectionsController.java
@@ -21,7 +21,9 @@ package org.olat.course.nodes.gta.ui;
 
 import java.io.File;
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 
 import org.olat.basesecurity.GroupRoles;
 import org.olat.core.gui.UserRequest;
@@ -35,10 +37,13 @@ import org.olat.core.gui.control.Event;
 import org.olat.core.gui.control.WindowControl;
 import org.olat.core.gui.control.controller.BasicController;
 import org.olat.core.gui.control.generic.closablewrapper.CloseableModalController;
+import org.olat.core.gui.control.generic.dtabs.Activateable2;
 import org.olat.core.gui.control.generic.modal.DialogBoxController;
 import org.olat.core.gui.control.generic.modal.DialogBoxUIFactory;
 import org.olat.core.id.Identity;
 import org.olat.core.id.OLATResourceable;
+import org.olat.core.id.context.ContextEntry;
+import org.olat.core.id.context.StateEntry;
 import org.olat.core.util.coordinate.CoordinatorManager;
 import org.olat.core.util.io.SystemFilenameFilter;
 import org.olat.core.util.vfs.VFSContainer;
@@ -67,13 +72,14 @@ import org.springframework.beans.factory.annotation.Autowired;
  * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
  *
  */
-public class GTACoachRevisionAndCorrectionsController extends BasicController {
+public class GTACoachRevisionAndCorrectionsController extends BasicController implements Activateable2 {
 	
 	private final VelocityContainer mainVC;
 	private Link returnToRevisionsButton, closeRevisionsButton, collectButton;
 	
 	private CloseableModalController cmc;
-	private DirectoryController revisionsCtrl, correctionsCtrl;
+	private Map<Integer,DirectoryController> loopToRevisionCtrl = new HashMap<>();
+	//private DirectoryController revisionsCtrl, correctionsCtrl;
 	private SubmitDocumentsController uploadCorrectionsCtrl;
 	private ConfirmRevisionsController confirmReturnToRevisionsCtrl;
 	private DialogBoxController confirmCloseRevisionProcessCtrl, confirmCollectCtrl;
@@ -185,10 +191,11 @@ public class GTACoachRevisionAndCorrectionsController extends BasicController {
 
 		boolean hasDocuments = TaskHelper.hasDocuments(documentsDir);
 		if(hasDocuments) {
-			revisionsCtrl = new DirectoryController(ureq, getWindowControl(), documentsDir, documentsContainer,
+			DirectoryController revisionsCtrl = new DirectoryController(ureq, getWindowControl(), documentsDir, documentsContainer,
 					"coach.revisions.description", "bulk.revisions", "revisions.zip");
 			listenTo(revisionsCtrl);
 			mainVC.put(cmpName, revisionsCtrl.getInitialComponent());
+			loopToRevisionCtrl.put(iteration, revisionsCtrl);
 		} else if (assignedTask.getTaskStatus() == TaskProcess.revision) {
 			String msg = "<i class='o_icon o_icon_error'> </i> " + translate("coach.corrections.rejected");
 			TextFactory.createTextComponentFromString(cmpName, msg, null, true, mainVC);
@@ -211,7 +218,7 @@ public class GTACoachRevisionAndCorrectionsController extends BasicController {
 		
 		boolean hasDocuments = TaskHelper.hasDocuments(documentsDir);
 		if(hasDocuments) {
-			correctionsCtrl = new DirectoryController(ureq, getWindowControl(), documentsDir, documentsContainer,
+			DirectoryController correctionsCtrl = new DirectoryController(ureq, getWindowControl(), documentsDir, documentsContainer,
 					"run.coach.corrections.description", "bulk.review", "review");
 			listenTo(correctionsCtrl);
 			mainVC.put(cmpName, correctionsCtrl.getInitialComponent());
@@ -260,6 +267,20 @@ public class GTACoachRevisionAndCorrectionsController extends BasicController {
 		closeRevisionsButton.setVisible(!coachCourseEnv.isCourseReadOnly());
 	}
 	
+	@Override
+	public void activate(UserRequest ureq, List<ContextEntry> entries, StateEntry state) {
+		if(entries == null || entries.size() <= 1) return;
+		
+		String type = entries.get(0).getOLATResourceable().getResourceableTypeName();
+		if("Revision".equalsIgnoreCase(type)) {
+			int revisionLoop = entries.get(0).getOLATResourceable().getResourceableId().intValue();
+			if(loopToRevisionCtrl.containsKey(revisionLoop)) {
+				List<ContextEntry> subEntriess = entries.subList(1, entries.size());
+				loopToRevisionCtrl.get(revisionLoop).activate(ureq, subEntriess, null);
+			}
+		}
+	}
+
 	@Override
 	protected void event(UserRequest ureq, Controller source, Event event) {
 		if(uploadCorrectionsCtrl == source) {
@@ -386,8 +407,7 @@ public class GTACoachRevisionAndCorrectionsController extends BasicController {
 	}
 	
 	private void doCloseRevisionProcess() {
-		TaskProcess nextStep = gtaManager.nextStep(TaskProcess.correction, gtaNode);
-		assignedTask = gtaManager.updateTask(assignedTask, nextStep, gtaNode);
+		assignedTask = gtaManager.reviewedTask(assignedTask, gtaNode);
 		gtaManager.log("Revision", "close revision", assignedTask, getIdentity(), assessedIdentity, assessedGroup, courseEnv, gtaNode);
 	}
 }
diff --git a/src/main/java/org/olat/course/nodes/gta/ui/GTACoachSelectionController.java b/src/main/java/org/olat/course/nodes/gta/ui/GTACoachSelectionController.java
index 1b1e7ad24b7..1db0a9b946c 100644
--- a/src/main/java/org/olat/course/nodes/gta/ui/GTACoachSelectionController.java
+++ b/src/main/java/org/olat/course/nodes/gta/ui/GTACoachSelectionController.java
@@ -20,6 +20,7 @@
  */
 package org.olat.course.nodes.gta.ui;
 
+import java.io.File;
 import java.util.Collections;
 import java.util.List;
 
@@ -29,6 +30,7 @@ import org.olat.core.commons.services.notifications.SubscriptionContext;
 import org.olat.core.commons.services.notifications.ui.ContextualSubscriptionController;
 import org.olat.core.gui.UserRequest;
 import org.olat.core.gui.components.Component;
+import org.olat.core.gui.components.download.DisplayOrDownloadComponent;
 import org.olat.core.gui.components.link.Link;
 import org.olat.core.gui.components.link.LinkFactory;
 import org.olat.core.gui.components.velocity.VelocityContainer;
@@ -36,13 +38,19 @@ import org.olat.core.gui.control.Controller;
 import org.olat.core.gui.control.Event;
 import org.olat.core.gui.control.WindowControl;
 import org.olat.core.gui.control.controller.BasicController;
+import org.olat.core.gui.control.generic.dtabs.Activateable2;
 import org.olat.core.id.Identity;
+import org.olat.core.id.context.BusinessControlFactory;
+import org.olat.core.id.context.ContextEntry;
+import org.olat.core.id.context.StateEntry;
+import org.olat.core.util.resource.OresHelper;
 import org.olat.course.archiver.ArchiveResource;
 import org.olat.course.groupsandrights.CourseGroupManager;
 import org.olat.course.nodes.ArchiveOptions;
 import org.olat.course.nodes.GTACourseNode;
 import org.olat.course.nodes.gta.GTAManager;
 import org.olat.course.nodes.gta.GTAType;
+import org.olat.course.nodes.gta.ui.component.DownloadDocumentMapper;
 import org.olat.course.nodes.gta.ui.events.SelectBusinessGroupEvent;
 import org.olat.course.nodes.gta.ui.events.SelectIdentityEvent;
 import org.olat.course.run.environment.CourseEnvironment;
@@ -59,7 +67,7 @@ import org.springframework.beans.factory.annotation.Autowired;
  * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
  *
  */
-public class GTACoachSelectionController extends BasicController {
+public class GTACoachSelectionController extends BasicController implements Activateable2 {
 
 	private GTACoachController coachingCtrl;
 	private GTACoachedGroupListController groupListCtrl;
@@ -67,6 +75,9 @@ public class GTACoachSelectionController extends BasicController {
 	
 	private final Link backLink, downloadButton;
 	private final VelocityContainer mainVC;
+
+	private final String solutionMapperUri;
+	private final DisplayOrDownloadComponent solutionDownloadCmp;
 	
 	private final GTACourseNode gtaNode;
 	private final CourseEnvironment courseEnv;
@@ -90,6 +101,11 @@ public class GTACoachSelectionController extends BasicController {
 		mainVC = createVelocityContainer("coach_selection");
 		backLink = LinkFactory.createLinkBack(mainVC, this);
 		
+		File solutionsDir = gtaManager.getSolutionsDirectory(courseEnv, gtaNode);
+		solutionMapperUri = registerMapper(ureq, new DownloadDocumentMapper(solutionsDir));
+		solutionDownloadCmp = new DisplayOrDownloadComponent("download", null);
+		mainVC.put("solutionDownload", solutionDownloadCmp);
+		
 		downloadButton = LinkFactory.createButton("bulk.download.title", mainVC, this);
 		downloadButton.setTranslator(getTranslator());
 		
@@ -136,6 +152,33 @@ public class GTACoachSelectionController extends BasicController {
 		//
 	}
 
+	@Override
+	public void activate(UserRequest ureq, List<ContextEntry> entries, StateEntry state) {
+		if(entries == null || entries.isEmpty()) return;
+		
+		String type = entries.get(0).getOLATResourceable().getResourceableTypeName();
+		Long key = entries.get(0).getOLATResourceable().getResourceableId();
+		if("Identity".equalsIgnoreCase(type)) {
+			if(participantListCtrl != null && participantListCtrl.hasIdentityKey(key)) {
+				Identity selectedIdentity = securityManager.loadIdentityByKey(key);
+				List<ContextEntry> subEntries = entries.subList(1, entries.size());
+				doSelectParticipant(ureq, selectedIdentity).activate(ureq, subEntries, entries.get(0).getTransientState());
+			}
+		} else if("BusinessGroup".equalsIgnoreCase(type)) {
+			if(groupListCtrl != null) {
+				BusinessGroup group = groupListCtrl.getBusinessGroup(key);
+				if(group != null) {
+					List<ContextEntry> subEntries = entries.subList(1, entries.size());
+					doSelectBusinessGroup(ureq, group).activate(ureq, subEntries, entries.get(0).getTransientState());
+				}
+			}
+		} else if("Solution".equals(type) && entries.size() > 1) {
+			String path = BusinessControlFactory.getInstance().getPath(entries.get(1));
+			String url = solutionMapperUri + "/" + path;
+			solutionDownloadCmp.triggerFileDownload(url);
+		}
+	}
+
 	@Override
 	protected void event(UserRequest ureq, Controller source, Event event) {
 		if(groupListCtrl == source) {
@@ -195,17 +238,22 @@ public class GTACoachSelectionController extends BasicController {
 		}
 	}
 	
-	private void doSelectBusinessGroup(UserRequest ureq, BusinessGroup group) {
+	private Activateable2 doSelectBusinessGroup(UserRequest ureq, BusinessGroup group) {
 		removeAsListenerAndDispose(coachingCtrl);
-		coachingCtrl = new GTACoachController(ureq, getWindowControl(), courseEnv, gtaNode, coachCourseEnv, group, true, true, false);
+		
+		WindowControl swControl = addToHistory(ureq, OresHelper.clone(group), null);
+		coachingCtrl = new GTACoachController(ureq, swControl, courseEnv, gtaNode, coachCourseEnv, group, true, true, false);
 		listenTo(coachingCtrl);
 		mainVC.put("selection", coachingCtrl.getInitialComponent());
+		return coachingCtrl;
 	}
 	
-	private void doSelectParticipant(UserRequest ureq, Identity identity) {
+	private Activateable2 doSelectParticipant(UserRequest ureq, Identity identity) {
 		removeAsListenerAndDispose(coachingCtrl);
-		coachingCtrl = new GTACoachController(ureq, getWindowControl(), courseEnv, gtaNode, coachCourseEnv, identity, true, true, false);
+		WindowControl swControl = addToHistory(ureq, OresHelper.createOLATResourceableInstance("Identity", identity.getKey()), null);
+		coachingCtrl = new GTACoachController(ureq, swControl, courseEnv, gtaNode, coachCourseEnv, identity, true, true, false);
 		listenTo(coachingCtrl);
 		mainVC.put("selection", coachingCtrl.getInitialComponent());
+		return coachingCtrl;
 	}
 }
diff --git a/src/main/java/org/olat/course/nodes/gta/ui/GTACoachedGroupListController.java b/src/main/java/org/olat/course/nodes/gta/ui/GTACoachedGroupListController.java
index d42f1a5d9c1..885473c6de7 100644
--- a/src/main/java/org/olat/course/nodes/gta/ui/GTACoachedGroupListController.java
+++ b/src/main/java/org/olat/course/nodes/gta/ui/GTACoachedGroupListController.java
@@ -87,6 +87,15 @@ public class GTACoachedGroupListController extends GTACoachedListController {
 		initForm(ureq);
 		updateModel();
 	}
+	
+	public BusinessGroup getBusinessGroup(Long key) {
+		for(BusinessGroup group:coachedGroups) {
+			if(group.getKey().equals(key)) {
+				return group;
+			}
+		}
+		return null;
+	}
 
 	@Override
 	protected void initForm(FormItemContainer formLayout, Controller listener, UserRequest ureq) {
diff --git a/src/main/java/org/olat/course/nodes/gta/ui/GTACoachedParticipantListController.java b/src/main/java/org/olat/course/nodes/gta/ui/GTACoachedParticipantListController.java
index 1d39dc2d3ac..38e46c283d5 100644
--- a/src/main/java/org/olat/course/nodes/gta/ui/GTACoachedParticipantListController.java
+++ b/src/main/java/org/olat/course/nodes/gta/ui/GTACoachedParticipantListController.java
@@ -131,6 +131,17 @@ public class GTACoachedParticipantListController extends GTACoachedListControlle
 		updateModel();
 	}
 	
+	public boolean hasIdentityKey(Long identityKey) {
+		if(assessableIdentities != null) {
+			for(UserPropertiesRow row:assessableIdentities) {
+				if(row.getIdentityKey().equals(identityKey)) {
+					return true;
+				}
+			}
+		}
+		return false;
+	}
+	
 	public List<Identity> getAssessableIdentities() {
 		List<Identity> identities = new ArrayList<>();
 		collectIdentities(new Consumer<Identity>() {
diff --git a/src/main/java/org/olat/course/nodes/gta/ui/GTAParticipantController.java b/src/main/java/org/olat/course/nodes/gta/ui/GTAParticipantController.java
index 567e0b9ec60..7e4e6cda5ae 100644
--- a/src/main/java/org/olat/course/nodes/gta/ui/GTAParticipantController.java
+++ b/src/main/java/org/olat/course/nodes/gta/ui/GTAParticipantController.java
@@ -39,9 +39,12 @@ import org.olat.core.gui.control.Controller;
 import org.olat.core.gui.control.Event;
 import org.olat.core.gui.control.WindowControl;
 import org.olat.core.gui.control.generic.closablewrapper.CloseableCalloutWindowController;
+import org.olat.core.gui.control.generic.dtabs.Activateable2;
 import org.olat.core.gui.control.generic.modal.DialogBoxController;
 import org.olat.core.gui.control.generic.modal.DialogBoxUIFactory;
 import org.olat.core.id.Identity;
+import org.olat.core.id.context.ContextEntry;
+import org.olat.core.id.context.StateEntry;
 import org.olat.core.util.StringHelper;
 import org.olat.core.util.coordinate.CoordinatorManager;
 import org.olat.core.util.io.SystemFilenameFilter;
@@ -75,7 +78,7 @@ import org.springframework.beans.factory.annotation.Autowired;
  * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
  *
  */
-public class GTAParticipantController extends GTAAbstractController {
+public class GTAParticipantController extends GTAAbstractController implements Activateable2 {
 	
 	private Link submitButton, openGroupButton, changeGroupLink;
 
@@ -105,7 +108,6 @@ public class GTAParticipantController extends GTAAbstractController {
 	protected void initContainer(UserRequest ureq) {
 		mainVC = createVelocityContainer("run");
 		putInitialPanel(mainVC);
-		
 		initFlow() ;
 	}
 
@@ -411,7 +413,7 @@ public class GTAParticipantController extends GTAAbstractController {
 			documentsContainer = gtaManager.getCorrectionContainer(courseEnv, gtaNode, getIdentity());
 		}
 		
-		if(TaskHelper.hasDocuments(documentsDir)) {
+		if(!waiting && TaskHelper.hasDocuments(documentsDir)) {
 			correctionsCtrl = new DirectoryController(ureq, getWindowControl(), documentsDir, documentsContainer,
 					"run.corrections.description", "bulk.review", "review");
 			listenTo(correctionsCtrl);
@@ -630,6 +632,34 @@ public class GTAParticipantController extends GTAAbstractController {
 		//
 	}
 
+	@Override
+	public void activate(UserRequest ureq, List<ContextEntry> entries, StateEntry state) {
+		if(entries == null || entries.isEmpty()) return;
+		
+		String type = entries.get(0).getOLATResourceable().getResourceableTypeName();
+		if("Correction".equalsIgnoreCase(type)) {
+			int revisionLoop = entries.get(0).getOLATResourceable().getResourceableId().intValue();
+			if(revisionLoop == 0) {
+				if(correctionsCtrl != null) {
+					List<ContextEntry> subEntries = entries.subList(1, entries.size());
+					correctionsCtrl.activate(ureq, subEntries, null);
+				}
+			} else if(revisionDocumentsCtrl != null) {
+				revisionDocumentsCtrl.activate(ureq, entries, null);
+			}
+		} else if("Solution".equalsIgnoreCase(type)) {
+			if(solutionsCtrl != null) {
+				List<ContextEntry> subEntries = entries.subList(1, entries.size());
+				solutionsCtrl.activate(ureq, subEntries, null);
+			}
+		} else if("Assessment".equalsIgnoreCase(type)) {
+			if(gradingCtrl != null) {
+				List<ContextEntry> subEntries = entries.subList(1, entries.size());
+				gradingCtrl.activate(ureq, subEntries, null);
+			}
+		}
+	}
+
 	@Override
 	protected void processEvent(TaskMultiUserEvent event) {
 		if(TaskMultiUserEvent.SUMBIT_TASK.equals(event.getCommand())) {
diff --git a/src/main/java/org/olat/course/nodes/gta/ui/GTAParticipantRevisionAndCorrectionsController.java b/src/main/java/org/olat/course/nodes/gta/ui/GTAParticipantRevisionAndCorrectionsController.java
index cf5f6506f04..e191342351d 100644
--- a/src/main/java/org/olat/course/nodes/gta/ui/GTAParticipantRevisionAndCorrectionsController.java
+++ b/src/main/java/org/olat/course/nodes/gta/ui/GTAParticipantRevisionAndCorrectionsController.java
@@ -22,7 +22,9 @@ package org.olat.course.nodes.gta.ui;
 import java.io.File;
 import java.util.ArrayList;
 import java.util.Date;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 
 import org.olat.basesecurity.GroupRoles;
 import org.olat.core.gui.UserRequest;
@@ -34,10 +36,13 @@ import org.olat.core.gui.control.Controller;
 import org.olat.core.gui.control.Event;
 import org.olat.core.gui.control.WindowControl;
 import org.olat.core.gui.control.controller.BasicController;
+import org.olat.core.gui.control.generic.dtabs.Activateable2;
 import org.olat.core.gui.control.generic.modal.DialogBoxController;
 import org.olat.core.gui.control.generic.modal.DialogBoxUIFactory;
 import org.olat.core.id.Identity;
 import org.olat.core.id.OLATResourceable;
+import org.olat.core.id.context.ContextEntry;
+import org.olat.core.id.context.StateEntry;
 import org.olat.core.util.StringHelper;
 import org.olat.core.util.coordinate.CoordinatorManager;
 import org.olat.core.util.io.SystemFilenameFilter;
@@ -66,14 +71,14 @@ import org.springframework.beans.factory.annotation.Autowired;
  * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
  *
  */
-public class GTAParticipantRevisionAndCorrectionsController extends BasicController {
+public class GTAParticipantRevisionAndCorrectionsController extends BasicController implements Activateable2 {
 	
 	private Link submitRevisionButton;
 	private final VelocityContainer mainVC;
 
 	private DialogBoxController confirmSubmitDialog;
 	private SubmitDocumentsController uploadRevisionsCtrl;
-	private DirectoryController correctionsCtrl, revisionsCtrl;
+	private final Map<Integer,DirectoryController> revisionLoopToCorrectionsCtrl = new HashMap<>();
 	
 	private Task assignedTask;
 	private final boolean businessGroupTask;
@@ -196,7 +201,7 @@ public class GTAParticipantRevisionAndCorrectionsController extends BasicControl
 
 		boolean hasDocument = TaskHelper.hasDocuments(documentsDir);
 		if(hasDocument) {
-			revisionsCtrl = new DirectoryController(ureq, getWindowControl(), documentsDir, documentsContainer,
+			DirectoryController revisionsCtrl = new DirectoryController(ureq, getWindowControl(), documentsDir, documentsContainer,
 					"run.revised.description", "bulk.submitted.revisions", "revisions.zip");
 			listenTo(revisionsCtrl);
 			mainVC.put(cmpName, revisionsCtrl.getInitialComponent());
@@ -217,14 +222,29 @@ public class GTAParticipantRevisionAndCorrectionsController extends BasicControl
 		
 		boolean hasDocument = TaskHelper.hasDocuments(documentsDir);
 		if(hasDocument) {
-			correctionsCtrl = new DirectoryController(ureq, getWindowControl(), documentsDir, documentsContainer,
+			DirectoryController correctionsCtrl = new DirectoryController(ureq, getWindowControl(), documentsDir, documentsContainer,
 					"run.corrections.description", "bulk.review", "review");
 			listenTo(correctionsCtrl);
 			mainVC.put(cmpName, correctionsCtrl.getInitialComponent());
+			revisionLoopToCorrectionsCtrl.put(iteration, correctionsCtrl);
 		}
 		return hasDocument;
 	}
 
+	@Override
+	public void activate(UserRequest ureq, List<ContextEntry> entries, StateEntry state) {
+		if(entries == null || entries.size() <= 1) return;
+		
+		String type = entries.get(0).getOLATResourceable().getResourceableTypeName();
+		int revisionLoop = entries.get(0).getOLATResourceable().getResourceableId().intValue();
+		if("Correction".equalsIgnoreCase(type)) {
+			if(revisionLoopToCorrectionsCtrl.containsKey(revisionLoop)) {
+				List<ContextEntry> subEntries = entries.subList(1, entries.size());
+				revisionLoopToCorrectionsCtrl.get(revisionLoop).activate(ureq, subEntries, null);
+			}
+		}
+	}
+
 	@Override
 	protected void event(UserRequest ureq, Controller source, Event event) {
 		if(uploadRevisionsCtrl == source) {
diff --git a/src/main/java/org/olat/course/nodes/gta/ui/GTARunController.java b/src/main/java/org/olat/course/nodes/gta/ui/GTARunController.java
index 242da92eccb..9fb75bed33d 100644
--- a/src/main/java/org/olat/course/nodes/gta/ui/GTARunController.java
+++ b/src/main/java/org/olat/course/nodes/gta/ui/GTARunController.java
@@ -19,6 +19,8 @@
  */
 package org.olat.course.nodes.gta.ui;
 
+import java.util.List;
+
 import org.olat.core.gui.UserRequest;
 import org.olat.core.gui.components.Component;
 import org.olat.core.gui.components.link.Link;
@@ -31,7 +33,11 @@ import org.olat.core.gui.control.Controller;
 import org.olat.core.gui.control.Event;
 import org.olat.core.gui.control.WindowControl;
 import org.olat.core.gui.control.controller.BasicController;
+import org.olat.core.gui.control.generic.dtabs.Activateable2;
 import org.olat.core.gui.control.generic.messages.MessageUIFactory;
+import org.olat.core.id.context.ContextEntry;
+import org.olat.core.id.context.StateEntry;
+import org.olat.core.util.resource.OresHelper;
 import org.olat.course.nodes.GTACourseNode;
 import org.olat.course.nodes.gta.GTAManager;
 import org.olat.course.nodes.gta.model.Membership;
@@ -46,7 +52,7 @@ import org.springframework.beans.factory.annotation.Autowired;
  * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
  *
  */
-public class GTARunController extends BasicController {
+public class GTARunController extends BasicController implements Activateable2 {
 	
 	private GTAParticipantController runCtrl;
 	private GTACoachSelectionController coachCtrl;
@@ -77,14 +83,14 @@ public class GTARunController extends BasicController {
 			
 			segmentView = SegmentViewFactory.createSegmentView("segments", mainVC, this);
 			runLink = LinkFactory.createLink("run.run", mainVC, this);
-			segmentView.addSegment(runLink, true);
+			segmentView.addSegment(runLink, false);
 			coachLink = LinkFactory.createLink("run.coach", mainVC, this);
-			segmentView.addSegment(coachLink, false);
+			segmentView.addSegment(coachLink, true);
 			if(isManagementTabAvalaible(config)) {
 				manageLink = LinkFactory.createLink("run.manage.coach", mainVC, this);
 				segmentView.addSegment(manageLink, false);
 			}
-			doOpenRun(ureq);
+			doOpenCoach(ureq);
 			mainVC.put("segments", segmentView);
 			putInitialPanel(mainVC);
 		} else if(isManagementTabAvalaible(config)) {
@@ -120,6 +126,37 @@ public class GTARunController extends BasicController {
 				&& (config.getBooleanSafe(GTACourseNode.GTASK_ASSIGNMENT) || config.getBooleanSafe(GTACourseNode.GTASK_SAMPLE_SOLUTION));
 	}
 
+	@Override
+	public void activate(UserRequest ureq, List<ContextEntry> entries, StateEntry state) {
+		if(entries == null || entries.isEmpty()) return;
+		
+		String type = entries.get(0).getOLATResourceable().getResourceableTypeName();
+		if("coach".equalsIgnoreCase(type)) {
+			if(coachLink != null) {
+				List<ContextEntry> subEntries = entries.subList(1, entries.size());
+				doOpenCoach(ureq).activate(ureq, subEntries, entries.get(0).getTransientState());
+				if(segmentView != null) {
+					segmentView.select(coachLink);
+				}
+			}
+		} else if("management".equalsIgnoreCase(type)) {
+			if(manageLink != null) {
+				doManage(ureq);
+				if(segmentView != null) {
+					segmentView.select(manageLink);
+				}
+			}
+		} else if("identity".equalsIgnoreCase(type)) {
+			if(getIdentity().getKey().equals(entries.get(0).getOLATResourceable().getResourceableId())) {
+				List<ContextEntry> subEntries = entries.subList(1, entries.size());
+				doOpenRun(ureq).activate(ureq, subEntries, entries.get(0).getTransientState());
+				if(segmentView != null) {
+					segmentView.select(runLink);
+				}
+			}
+		}
+	}
+
 	@Override
 	protected void event(UserRequest ureq, Component source, Event event) {
 		if(source == segmentView) {
@@ -143,25 +180,39 @@ public class GTARunController extends BasicController {
 		//
 	}
 	
-	private void doOpenRun(UserRequest ureq) {
+	private Activateable2 doOpenRun(UserRequest ureq) {
 		if(runCtrl == null) {
 			createRun(ureq);
+		} else {
+			addToHistory(ureq, runCtrl);
 		}
-		mainVC.put("segmentCmp", runCtrl.getInitialComponent());
+		if(mainVC != null) {
+			mainVC.put("segmentCmp", runCtrl.getInitialComponent());
+		}
+		return runCtrl;
 	}
 	
-	private void doOpenCoach(UserRequest ureq) {
+	private Activateable2 doOpenCoach(UserRequest ureq) {
 		if(coachCtrl == null) {
 			createCoach(ureq);
+		} else {
+			addToHistory(ureq, coachCtrl);
 		}
-		mainVC.put("segmentCmp", coachCtrl.getInitialComponent());
+		if(mainVC != null) {
+			mainVC.put("segmentCmp", coachCtrl.getInitialComponent());
+		}
+		return coachCtrl;
 	}
 	
 	private void doManage(UserRequest ureq) {
 		if(manageCtrl == null) {
 			createManage(ureq);
+		} else {
+			addToHistory(ureq, manageCtrl);
+		}
+		if(mainVC != null) {
+			mainVC.put("segmentCmp", manageCtrl.getInitialComponent());
 		}
-		mainVC.put("segmentCmp", manageCtrl.getInitialComponent());
 	}
 	
 	private GTAParticipantController createRun(UserRequest ureq) {
@@ -175,14 +226,17 @@ public class GTARunController extends BasicController {
 	private GTACoachSelectionController createCoach(UserRequest ureq) {
 		removeAsListenerAndDispose(coachCtrl);
 		
-		coachCtrl = new GTACoachSelectionController(ureq, getWindowControl(), userCourseEnv, gtaNode);
+		WindowControl swControl = addToHistory(ureq, OresHelper.createOLATResourceableType("coach"), null);
+		coachCtrl = new GTACoachSelectionController(ureq, swControl, userCourseEnv, gtaNode);
 		listenTo(coachCtrl);
 		return coachCtrl;
 	}
 	
 	private GTACoachManagementController createManage(UserRequest ureq) {
 		removeAsListenerAndDispose(manageCtrl);
-		manageCtrl = new GTACoachManagementController(ureq, getWindowControl(), userCourseEnv, gtaNode);
+		
+		WindowControl swControl = addToHistory(ureq, OresHelper.createOLATResourceableType("management"), null);
+		manageCtrl = new GTACoachManagementController(ureq, swControl, userCourseEnv, gtaNode);
 		listenTo(manageCtrl);
 		return manageCtrl;
 	}
diff --git a/src/main/java/org/olat/course/nodes/gta/ui/_content/coach_selection.html b/src/main/java/org/olat/course/nodes/gta/ui/_content/coach_selection.html
index ca363c74ba8..391b4852ad5 100644
--- a/src/main/java/org/olat/course/nodes/gta/ui/_content/coach_selection.html
+++ b/src/main/java/org/olat/course/nodes/gta/ui/_content/coach_selection.html
@@ -27,4 +27,7 @@
 	#end
 	$r.render("list")
 #end
-</div>
\ No newline at end of file
+</div>
+#if($r.available("solutionDownload"))
+	$r.render("solutionDownload")
+#end
\ No newline at end of file
diff --git a/src/main/java/org/olat/course/nodes/gta/ui/_content/documents_readonly.html b/src/main/java/org/olat/course/nodes/gta/ui/_content/documents_readonly.html
index 7dc61ff6a75..72d5a1ee0a6 100644
--- a/src/main/java/org/olat/course/nodes/gta/ui/_content/documents_readonly.html
+++ b/src/main/java/org/olat/course/nodes/gta/ui/_content/documents_readonly.html
@@ -9,4 +9,7 @@
 #foreach($docInfo in $linkNames)
 	<li>$r.render($docInfo.linkName) #if($docInfo.uploadedBy)<small>$r.translate("uploaded.by",$docInfo.uploadedBy)</small>#end #if($r.isNotNull($docInfo.lastModified))<small>$r.translate("lastmodified",$docInfo.lastModified)</small>#end</li>
 #end
-</ul>
\ No newline at end of file
+</ul>
+#if($r.available("download"))
+	$r.render("download")
+#end
\ No newline at end of file
diff --git a/src/main/java/org/olat/course/nodes/gta/ui/_i18n/LocalStrings_de.properties b/src/main/java/org/olat/course/nodes/gta/ui/_i18n/LocalStrings_de.properties
index d5ffcdf00a1..4bbde7169b5 100644
--- a/src/main/java/org/olat/course/nodes/gta/ui/_i18n/LocalStrings_de.properties
+++ b/src/main/java/org/olat/course/nodes/gta/ui/_i18n/LocalStrings_de.properties
@@ -123,13 +123,30 @@ mailto.group=E-Mail an Gruppe
 mailto.user=E-Mail an Benutzer 
 max.documents=Max. Anzahl von Dokumenten
 no.submission=Nicht abgegeben
-notifications.correction=Neue Korrektur "{0}" von "{1}"
-notifications.header=Gruppenaufgabe in Kurs "{0}"
-notifications.revision.group=Neue \u00DCberarbeitungen "{0}" von "{2}" f\u00FCr die Gruppe "{1}" hochgeladen
-notifications.revision.individual=Neue \u00DCberarbeitungen "{0}" von "{1}"
-notifications.solution=Neue Musterl\u00F6sung "{0}"
-notifications.submission.group=Neue abgegebene Dokumente "{0}" von "{2}" f\u00FCr Gruppe "{1}" hochgeladen
-notifications.submission.individual=Neue abgegebene Dokumente "{0}" von "{1}"
+notifications.accepted=Ihre Aufgabe "{0}" im Kurs "{1}" wurde akzeptiert.
+notifications.assessment.score.passed=Für die Aufgabe "{0}" im Kurs "{1}" haben Sie eine Bewertung erhalten. Ihre Punktzahl beträgt {2} Punkte und Sie haben {3}.
+notifications.assessment.score=Für die Aufgabe "{0}" im Kurs "{1}" haben Sie eine Bewertung erhalten. Ihre Punktzahl beträgt {2} Punkte.
+notifications.assessment.passed=Für die Aufgabe "{0}" im Kurs "{1}" haben Sie eine Bewertung erhalten. Sie haben {3}.
+notifications.assessment.doc=Für die Aufgabe "{0}" im Kurs "{1}" stehen bereit Bewertungsdokumente zum Download: "{2}".
+notifications.assessment.passed.true=bestanden
+notifications.assessment.passed.false=nicht bestanden
+notifications.group.header=Gruppenaufgabe in Kurs "{0}"
+notifications.group.header.task=Gruppenaufgabe "{0}" in Kurs "{1}"
+notifications.individual.header=Aufgabe in Kurs "{0}"
+notifications.individual.header.task=Aufgabe "{0}" in Kurs "{1}"
+notifications.solution=Im Kurs "{0}" steht die folgende Musterlösung zum Download bereit: "{1}".
+notifications.solution.task=Für die Aufgabe "{0}" im Kurs "{1}" steht die folgende Musterlösung zum Download bereit: "{2}".
+notifications.correction=Für Ihre Aufgabe "{0}" im Kurs "{1}" wurde eine Überarbeitung angefordert.
+notifications.correction.duedate=$\:notifications.correction Die Überarbeitungsfrist wurde auf den {3} gesetzt.
+notifications.correction.doc=Sie haben ein Feedback für die Aufgabe "{0}" im Kurs "{1}" von {3} erhalten: "{2}".
+notifications.submission.individual=Die Aufgabe "{0}" im Kurs "{1}" wurde von "{2}" abgegeben.
+notifications.submission.group=Die Aufgabe "{0}" im Kurs "{1}" wurde f\u00FCr Gruppe "{2}" abgegeben.
+notifications.submission.group.doc==Für die Aufgabe "{0}" im Kurs "{1}" wurde eine neue Dokument "{2}" von "{3}" f\u00FCr Gruppe "{4}" hochgeladen.
+notifications.submission.individual.doc=Für die Aufgabe "{0}" im Kurs "{1}" wurde eine neue Dokument "{2}" von "{3}" hochgeladen.
+notifications.revision.group=Für die Aufgabe "{0}" im Kurs "{1}" wurde eine neue \u00DCberarbeitungen f\u00FCr die Gruppe "{2}" abgegeben.
+notifications.revision.individual=Für die Aufgabe "{0}" im Kurs "{1}" wurde eine neue \u00DCberarbeitungen von "{2}" abgegeben.
+notifications.revision.group.doc=Für die Aufgabe "{0}" im Kurs "{1}" wurde eine neue \u00DCberarbeitungen "{2}" f\u00FCr die Gruppe "{3}" hochgeladen.
+notifications.revision.individual.doc=Für die Aufgabe "{0}" im Kurs "{1}" wurde eine neue \u00DCberarbeitungen "{2}" von "{3}" hochgeladen.
 open.group=Gruppe \u00F6ffnen
 pane.tab.accessibility=Zugang
 pane.tab.assignment=Aufgabenstellung
diff --git a/src/main/java/org/olat/course/nodes/gta/ui/_i18n/LocalStrings_en.properties b/src/main/java/org/olat/course/nodes/gta/ui/_i18n/LocalStrings_en.properties
index 50ec2a04c38..7ae1dcdf12e 100644
--- a/src/main/java/org/olat/course/nodes/gta/ui/_i18n/LocalStrings_en.properties
+++ b/src/main/java/org/olat/course/nodes/gta/ui/_i18n/LocalStrings_en.properties
@@ -119,13 +119,30 @@ mailto.group=Mail to group
 mailto.user=Mail to user
 max.documents=Max. number of documents
 no.submission=No submission
-notifications.correction=New correction "{0}" of "{1}"
-notifications.header=Group task in course "{0}"
-notifications.revision.group=New revisions "{0}" of "{2}" uploaded for group "{1}"
-notifications.revision.individual=New revisions "{0}" of "{1}"
-notifications.solution=New sample solutions "{0}"
-notifications.submission.group=New documents "{0}" von "{2}" submitted for group "{1}"
-notifications.submission.individual=New submitted documents "{0}" of "{1}"
+notifications.accepted=Your task "{0}" in the course "{1}" has been accepted.
+notifications.assessment.score.passed=You have got a grading for the task "{0}" in the course "{1}". Your score ist {2} and you have {3}.
+notifications.assessment.score=You have got a grading for the task "{0}" in the course "{1}". Your score ist {2}.
+notifications.assessment.passed=You have got a grading for the task "{0}" in the course "{1}". You have {3}.
+notifications.assessment.doc=You have got a grading document for the task "{0}" in the course "{1}": "{2}".
+notifications.solution=In the course "{0}" the following sample solution is available for download: "{1}".
+notifications.solution.task=For the task "{0}" in the course "{1}" the following sample solution is available for download: "{2}".
+notifications.assessment.passed.true=passed
+notifications.assessment.passed.false=failed
+notifications.group.header=Group task in course "{0}"
+notifications.group.header.task=Group task "{0}" in course "{1}"
+notifications.individual.header=Task in course "{0}"
+notifications.individual.header.task=Task "{0}" in course "{1}"
+notifications.correction=You have received a feedback for the task "{0}" in the course "{1}".
+notifications.correction.duedate=$\:notifications.correction The deadline for the revision is set to the {2}.
+notifications.correction.doc=You have received a feedback for the task "{0}" in the course "{1}" by {3}: "{2}".
+notifications.submission.individual=The task "{0}" in course "{1}" was submitted by "{2}".
+notifications.submission.group=The task "{0}" in course "{1}" was submitted for the group "{2}".
+notifications.submission.group.doc==For the task "{0}" in course "{1}" was a new document "{2}" submitted by "{3}" for the group "{4}".
+notifications.submission.individual.doc=For the task "{0}" in course "{1}" was a new document "{2}" submitted by "{3}".
+notifications.revision.group=For the task "{0}" in course "{1}" was a new revision submitted for the group "{2}".
+notifications.revision.individual=For the task "{0}" in course "{1}" was a new revision submitted by "{2}".
+notifications.revision.group.doc=For the task "{0}" in course "{1}" was a new revision "{2}" submitted for the group "{3}".
+notifications.revision.individual.doc=For the task "{0}" in course "{1}" was a new revision "{2}" submitted by "{3}".
 open.group=Open group
 pane.tab.accessibility=Access
 pane.tab.assignment=Assignment
diff --git a/src/main/java/org/olat/course/nodes/gta/ui/_i18n/LocalStrings_fr.properties b/src/main/java/org/olat/course/nodes/gta/ui/_i18n/LocalStrings_fr.properties
index 2746cb83bf0..4b613d07336 100644
--- a/src/main/java/org/olat/course/nodes/gta/ui/_i18n/LocalStrings_fr.properties
+++ b/src/main/java/org/olat/course/nodes/gta/ui/_i18n/LocalStrings_fr.properties
@@ -118,13 +118,6 @@ mail.confirm.assignment.subject=Affectation
 mailto.group=Courriel au groupe
 mailto.user=Courriel au utilisateur
 max.documents=Nombre maximum de documents
-notifications.correction=Nouvelles corrections "{0}" de "{1}"
-notifications.header=Devoirs de groupe du cours "{0}"
-notifications.revision.group=De nouveaux remaniements "{0}" de "{2}" pour le groupe "{1}" ont \u00E9t\u00E9 t\u00E9l\u00E9charg\u00E9s.
-notifications.revision.individual=De nouveaux remaniements "{0}" de "{1}"
-notifications.solution=Nouvelle solution "{0}"
-notifications.submission.group=De nouveaux documents "{0}" ont \u00E9t\u00E9 d\u00E9pos\u00E9s par "{1}" pour le groupe "{1}".
-notifications.submission.individual=De nouveaux documents "{0}" d\u00E9pos\u00E9s par "{1}"
 open.group=Ouvrir le groupe
 pane.tab.accessibility=Acc\u00E8s
 pane.tab.assignment=Affectation
diff --git a/src/main/java/org/olat/course/nodes/gta/ui/_i18n/LocalStrings_it.properties b/src/main/java/org/olat/course/nodes/gta/ui/_i18n/LocalStrings_it.properties
index 9fa475155bb..fb6f1a00a44 100644
--- a/src/main/java/org/olat/course/nodes/gta/ui/_i18n/LocalStrings_it.properties
+++ b/src/main/java/org/olat/course/nodes/gta/ui/_i18n/LocalStrings_it.properties
@@ -118,13 +118,6 @@ mail.confirm.assignment.subject=Assegnazione
 mailto.group=E-mail al gruppo
 mailto.user=E-mail all'utente
 max.documents=Numero massimo di documenti
-notifications.correction=Nuova correzione "{0}" di "{1}"
-notifications.header=Compito di gruppo nel corso "{0}"
-notifications.revision.group=Nuove revisioni "{0}" di "{2}" caricate per il gruppo "{1}"
-notifications.revision.individual=Nuove revisioni "{0}" di "{1}"
-notifications.solution=Nuova soluzione "{0}"
-notifications.submission.group=Nuovi documenti "{0}" di "{2}" consegnati per il gruppo "{1}"
-notifications.submission.individual=Nuovi documenti consegnati "{0}" di "{1}"
 open.group=Aprire il gruppo
 pane.tab.accessibility=Accesso
 pane.tab.assignment=Assegnazione
diff --git a/src/main/java/org/olat/course/nodes/gta/ui/_i18n/LocalStrings_pl.properties b/src/main/java/org/olat/course/nodes/gta/ui/_i18n/LocalStrings_pl.properties
index 6b3776df2b3..3be48f06b75 100644
--- a/src/main/java/org/olat/course/nodes/gta/ui/_i18n/LocalStrings_pl.properties
+++ b/src/main/java/org/olat/course/nodes/gta/ui/_i18n/LocalStrings_pl.properties
@@ -114,13 +114,6 @@ mail.confirm.assignment.subject=Zadanie
 mailto.group=E-mail do grupy
 mailto.user=E-mail do uczestnika
 max.documents=Max. liczba dokument\u00F3w
-notifications.correction=New correction "{0}" of "{1}"
-notifications.header=Zadanie grupowe w kursie "{0}"
-notifications.revision.group=New revisions "{0}" of "{2}" uploaded for group "{1}"
-notifications.revision.individual=New revisions "{0}" of "{1}"
-notifications.solution=Nowe przyk\u0142adowe rozwi\u0105zanie dla "{0}"
-notifications.submission.group=New documents "{0}" out of "{2}" submitted for group "{1}"
-notifications.submission.individual=New submitted documents "{0}" of "{1}"
 open.group=Open group
 pane.tab.accessibility=Dost\u0119p
 pane.tab.assignment=Assignment
diff --git a/src/main/java/org/olat/course/nodes/gta/ui/_i18n/LocalStrings_pt_BR.properties b/src/main/java/org/olat/course/nodes/gta/ui/_i18n/LocalStrings_pt_BR.properties
index ce1170b59d7..9961972fc82 100644
--- a/src/main/java/org/olat/course/nodes/gta/ui/_i18n/LocalStrings_pt_BR.properties
+++ b/src/main/java/org/olat/course/nodes/gta/ui/_i18n/LocalStrings_pt_BR.properties
@@ -118,13 +118,6 @@ mail.confirm.assignment.subject=Atribui\u00E7\u00E3o
 mailto.group=Email para o grupo
 mailto.user=Email para o usu\u00E1rio
 max.documents=N\u00FAmero m\u00E1ximo de documentos
-notifications.correction=Nova corre\u00E7\u00E3o "{0}" of "{1}"
-notifications.header=Tarefa de Grupo no curso "{0}"
-notifications.revision.group=Novas revis\u00F5es "{0}" de "{2}" enviadas para o grupo "{1}"
-notifications.revision.individual=Novas revis\u00F5es "{0}" de "{1}"
-notifications.solution=Nova solu\u00E7\u00F5es de amostra "{0}"
-notifications.submission.group=Novos documentos "{0}" de "{2}" apresentados para o grupo "{1}"
-notifications.submission.individual=Novos documentos enviados "{0}" of "{1}"
 open.group=Abrir grupo
 pane.tab.accessibility=Acesso
 pane.tab.assignment=Atribui\u00E7\u00E3o
diff --git a/src/main/java/org/olat/course/nodes/gta/ui/component/DownloadDocumentMapper.java b/src/main/java/org/olat/course/nodes/gta/ui/component/DownloadDocumentMapper.java
new file mode 100644
index 00000000000..40a472e86c3
--- /dev/null
+++ b/src/main/java/org/olat/course/nodes/gta/ui/component/DownloadDocumentMapper.java
@@ -0,0 +1,56 @@
+/**
+ * <a href="http://www.openolat.org">
+ * OpenOLAT - Online Learning and Training</a><br>
+ * <p>
+ * Licensed under the Apache License, Version 2.0 (the "License"); <br>
+ * you may not use this file except in compliance with the License.<br>
+ * You may obtain a copy of the License at the
+ * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a>
+ * <p>
+ * Unless required by applicable law or agreed to in writing,<br>
+ * software distributed under the License is distributed on an "AS IS" BASIS, <br>
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br>
+ * See the License for the specific language governing permissions and <br>
+ * limitations under the License.
+ * <p>
+ * Initial code contributed and copyrighted by<br>
+ * frentix GmbH, http://www.frentix.com
+ * <p>
+ */
+package org.olat.course.nodes.gta.ui.component;
+
+import java.io.File;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.olat.core.dispatcher.mapper.Mapper;
+import org.olat.core.gui.media.FileMediaResource;
+import org.olat.core.gui.media.MediaResource;
+import org.olat.core.gui.media.NotFoundMediaResource;
+
+/**
+ * 
+ * Initial date: 9 août 2017<br>
+ * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
+ *
+ */
+public class DownloadDocumentMapper implements Mapper {
+	
+	private final File documentDir;
+	
+	public DownloadDocumentMapper(File documentDir) {
+		this.documentDir = documentDir;
+	}
+
+	@Override
+	public MediaResource handle(String relPath, HttpServletRequest request) {
+		if(relPath != null && relPath.startsWith("/")) {
+			relPath = relPath.substring(1, relPath.length());
+		}
+		File document = new File(documentDir, relPath);
+		if(document.exists() && document.getParentFile().equals(documentDir)) {
+			return new FileMediaResource(document, true);
+		}
+		return new NotFoundMediaResource(relPath);
+	}
+}
diff --git a/src/main/java/org/olat/course/nodes/ms/MSCourseNodeRunController.java b/src/main/java/org/olat/course/nodes/ms/MSCourseNodeRunController.java
index 58037ef4cd0..f383cb3e4c3 100644
--- a/src/main/java/org/olat/course/nodes/ms/MSCourseNodeRunController.java
+++ b/src/main/java/org/olat/course/nodes/ms/MSCourseNodeRunController.java
@@ -30,10 +30,15 @@ import java.util.List;
 
 import org.olat.core.gui.UserRequest;
 import org.olat.core.gui.components.Component;
+import org.olat.core.gui.components.download.DisplayOrDownloadComponent;
 import org.olat.core.gui.components.velocity.VelocityContainer;
 import org.olat.core.gui.control.Event;
 import org.olat.core.gui.control.WindowControl;
 import org.olat.core.gui.control.controller.BasicController;
+import org.olat.core.gui.control.generic.dtabs.Activateable2;
+import org.olat.core.id.context.BusinessControlFactory;
+import org.olat.core.id.context.ContextEntry;
+import org.olat.core.id.context.StateEntry;
 import org.olat.core.util.Formatter;
 import org.olat.core.util.StringHelper;
 import org.olat.core.util.Util;
@@ -55,9 +60,12 @@ import org.springframework.beans.factory.annotation.Autowired;
  * Initial Date:  Jun 16, 2004
  * @author gnaegi
  */
-public class MSCourseNodeRunController extends BasicController {
+public class MSCourseNodeRunController extends BasicController implements Activateable2 {
 
 	private final VelocityContainer myContent;
+	private DisplayOrDownloadComponent download;
+	
+	private String mapperUri;
 	private final boolean showLog;
 	private boolean hasScore, hasPassed, hasComment;
 	private final UserCourseEnvironment userCourseEnv;
@@ -132,8 +140,8 @@ public class MSCourseNodeRunController extends BasicController {
 		myContent.contextPut("changelogconfig", courseModule.isDisplayChangeLog());
 
 		// Push variables to velocity page
-		exposeConfigToVC(ureq, config);		
-		exposeUserDataToVC(ureq, userCourseEnv, courseNode);
+		exposeConfigToVC(ureq);		
+		exposeUserDataToVC(ureq);
 		putInitialPanel(myContent);
 	}
 	
@@ -158,6 +166,20 @@ public class MSCourseNodeRunController extends BasicController {
 		return hasComment;
 	}
 
+	@Override
+	public void activate(UserRequest ureq, List<ContextEntry> entries, StateEntry state) {
+		if(entries == null || entries.isEmpty()) return;
+		
+		String type = entries.get(0).getOLATResourceable().getResourceableTypeName();
+		if(type.startsWith("path")) {
+			if(download != null) {
+				String path = BusinessControlFactory.getInstance().getPath(entries.get(0));
+				String url = mapperUri + "/" + path;
+				download.triggerFileDownload(url);
+			}
+		}
+	}
+
 	/**
 	 * @see org.olat.core.gui.control.DefaultController#event(org.olat.core.gui.UserRequest, org.olat.core.gui.components.Component, org.olat.core.gui.control.Event)
 	 */
@@ -170,21 +192,22 @@ public class MSCourseNodeRunController extends BasicController {
 		}
 	}
 	
-	private void exposeConfigToVC(UserRequest ureq, ModuleConfiguration config) {
+	private void exposeConfigToVC(UserRequest ureq) {
+		ModuleConfiguration config = courseNode.getModuleConfiguration();
 	    myContent.contextPut(MSCourseNode.CONFIG_KEY_HAS_SCORE_FIELD, config.get(MSCourseNode.CONFIG_KEY_HAS_SCORE_FIELD));
 	    myContent.contextPut(MSCourseNode.CONFIG_KEY_HAS_PASSED_FIELD, config.get(MSCourseNode.CONFIG_KEY_HAS_PASSED_FIELD));
 	    myContent.contextPut(MSCourseNode.CONFIG_KEY_HAS_COMMENT_FIELD, config.get(MSCourseNode.CONFIG_KEY_HAS_COMMENT_FIELD));
 	    String infoTextUser = (String) config.get(MSCourseNode.CONFIG_KEY_INFOTEXT_USER);
 	    if(StringHelper.containsNonWhitespace(infoTextUser)) {
-	    	myContent.contextPut(MSCourseNode.CONFIG_KEY_INFOTEXT_USER, infoTextUser);
-	    	myContent.contextPut("in-disclaimer", isPanelOpen(ureq, "disclaimer", true));
+	    		myContent.contextPut(MSCourseNode.CONFIG_KEY_INFOTEXT_USER, infoTextUser);
+	    		myContent.contextPut("in-disclaimer", isPanelOpen(ureq, "disclaimer", true));
 	    }
 	    myContent.contextPut(MSCourseNode.CONFIG_KEY_PASSED_CUT_VALUE, AssessmentHelper.getRoundedScore((Float)config.get(MSCourseNode.CONFIG_KEY_PASSED_CUT_VALUE)));
 	    myContent.contextPut(MSCourseNode.CONFIG_KEY_SCORE_MIN, AssessmentHelper.getRoundedScore((Float)config.get(MSCourseNode.CONFIG_KEY_SCORE_MIN)));
 	    myContent.contextPut(MSCourseNode.CONFIG_KEY_SCORE_MAX, AssessmentHelper.getRoundedScore((Float)config.get(MSCourseNode.CONFIG_KEY_SCORE_MAX)));
 	}
 	
-	private void exposeUserDataToVC(UserRequest ureq, UserCourseEnvironment userCourseEnv, PersistentAssessableCourseNode courseNode) {
+	private void exposeUserDataToVC(UserRequest ureq) {
 		AssessmentEntry assessmentEntry = courseNode.getUserAssessmentEntry(userCourseEnv);
 		if(assessmentEntry == null) {
 			myContent.contextPut("hasPassedValue", Boolean.FALSE);
@@ -213,10 +236,14 @@ public class MSCourseNodeRunController extends BasicController {
 				
 				if(courseNode.hasIndividualAsssessmentDocuments()) {
 					List<File> docs = courseNode.getIndividualAssessmentDocuments(userCourseEnv);
-					String mapperUri = registerCacheableMapper(ureq, null, new DocumentsMapper(docs));
+					mapperUri = registerCacheableMapper(ureq, null, new DocumentsMapper(docs));
 					myContent.contextPut("docsMapperUri", mapperUri);
 					myContent.contextPut("docs", docs);
 					myContent.contextPut("in-assessmentDocuments", isPanelOpen(ureq, "assessmentDocuments", true));
+					if(download == null) {
+						download = new DisplayOrDownloadComponent("", null);
+						myContent.put("download", download);
+					}
 				}
 			}
 		}
diff --git a/src/main/java/org/olat/course/nodes/ms/_content/run.html b/src/main/java/org/olat/course/nodes/ms/_content/run.html
index c0a109a7d83..4104256a4ab 100644
--- a/src/main/java/org/olat/course/nodes/ms/_content/run.html
+++ b/src/main/java/org/olat/course/nodes/ms/_content/run.html
@@ -156,4 +156,7 @@
 	    #o_togglebox_end()
 	</div>
 	#end
+#end
+#if($r.available("download"))
+	$r.render("download")
 #end
\ No newline at end of file
diff --git a/src/main/resources/META-INF/persistence.xml b/src/main/resources/META-INF/persistence.xml
index 4b8a40a57ca..9e839a67c67 100644
--- a/src/main/resources/META-INF/persistence.xml
+++ b/src/main/resources/META-INF/persistence.xml
@@ -95,6 +95,7 @@
 		<class>org.olat.course.nodes.gta.model.TaskLightImpl</class>
 		<class>org.olat.course.nodes.gta.model.TaskDueDateImpl</class>
 		<class>org.olat.course.nodes.gta.model.TaskListImpl</class>
+		<class>org.olat.course.nodes.gta.model.TaskRevisionDateImpl</class>
 		<class>org.olat.course.certificate.model.CertificateImpl</class>
 		<class>org.olat.course.certificate.model.CertificateStandalone</class>
 		<class>org.olat.course.certificate.model.CertificateLightImpl</class>
diff --git a/src/main/resources/database/mysql/alter_11_5_x_to_12_0_0.sql b/src/main/resources/database/mysql/alter_11_5_x_to_12_0_0.sql
index f4b34b37891..86c43c0d8c0 100644
--- a/src/main/resources/database/mysql/alter_11_5_x_to_12_0_0.sql
+++ b/src/main/resources/database/mysql/alter_11_5_x_to_12_0_0.sql
@@ -225,3 +225,24 @@ alter table o_gta_task add column g_submission_due_date datetime default null;
 alter table o_gta_task add column g_revisions_due_date datetime default null;
 alter table o_gta_task add column g_solution_due_date datetime default null;
 
+alter table o_gta_task add column g_acceptation_date datetime default null;
+alter table o_gta_task add column g_solution_date datetime default null;
+alter table o_gta_task add column g_graduation_date datetime default null;
+
+create table o_gta_task_revision_date (
+  id bigint not null auto_increment,
+  creationdate datetime not null,
+  g_status varchar(36) not null,
+  g_rev_loop bigint not null,
+  g_date datetime not null,
+  fk_task bigint not null,
+  primary key (id)
+);
+alter table o_gta_task_revision_date ENGINE = InnoDB;
+
+alter table o_gta_task_revision_date add constraint gtaskrev_to_task_idx foreign key (fk_task) references o_gta_task (id);
+
+
+
+
+
diff --git a/src/main/resources/database/mysql/setupDatabase.sql b/src/main/resources/database/mysql/setupDatabase.sql
index d86c282c9cf..538c65bf213 100644
--- a/src/main/resources/database/mysql/setupDatabase.sql
+++ b/src/main/resources/database/mysql/setupDatabase.sql
@@ -1858,6 +1858,9 @@ create table o_gta_task (
    g_submission_date datetime,
    g_submission_revisions_date datetime,
    g_collection_date datetime,
+   g_acceptation_date datetime,
+   g_solution_date datetime,
+   g_graduation_date datetime,
    g_assignment_due_date datetime,
    g_submission_due_date datetime,
    g_revisions_due_date datetime,
@@ -1869,6 +1872,16 @@ create table o_gta_task (
    primary key (id)
 );
 
+create table o_gta_task_revision_date (
+  id bigint not null auto_increment,
+  creationdate datetime not null,
+  g_status varchar(36) not null,
+  g_rev_loop bigint not null,
+  g_date datetime not null,
+  fk_task bigint not null,
+  primary key (id)
+);
+
 create table o_rem_reminder (
    id bigint not null,
    creationdate datetime not null,
@@ -2400,6 +2413,7 @@ alter table o_cl_checkbox ENGINE = InnoDB;
 alter table o_cl_check ENGINE = InnoDB;
 alter table o_gta_task_list ENGINE = InnoDB;
 alter table o_gta_task ENGINE = InnoDB;
+alter table o_gta_task_revision_date ENGINE = InnoDB;
 alter table o_cer_template ENGINE = InnoDB;
 alter table o_cer_certificate ENGINE = InnoDB;
 alter table o_rem_reminder ENGINE = InnoDB;
@@ -2622,6 +2636,8 @@ alter table o_gta_task add constraint gtask_to_bgroup_idx foreign key (fk_busine
 
 alter table o_gta_task_list add constraint gta_list_to_repo_entry_idx foreign key (fk_entry) references o_repositoryentry (repositoryentry_id);
 
+alter table o_gta_task_revision_date add constraint gtaskrev_to_task_idx foreign key (fk_task) references o_gta_task (id);
+
 -- reminders
 alter table o_rem_reminder add constraint rem_reminder_to_repo_entry_idx foreign key (fk_entry) references o_repositoryentry (repositoryentry_id);
 alter table o_rem_reminder add constraint rem_reminder_to_creator_idx foreign key (fk_creator) references o_bs_identity (id);
diff --git a/src/main/resources/database/oracle/alter_11_5_x_to_12_0_0.sql b/src/main/resources/database/oracle/alter_11_5_x_to_12_0_0.sql
index be4eb0da87a..435c868534c 100644
--- a/src/main/resources/database/oracle/alter_11_5_x_to_12_0_0.sql
+++ b/src/main/resources/database/oracle/alter_11_5_x_to_12_0_0.sql
@@ -226,3 +226,20 @@ alter table o_gta_task add g_submission_due_date date default null;
 alter table o_gta_task add g_revisions_due_date date default null;
 alter table o_gta_task add g_solution_due_date date default null;
 
+alter table o_gta_task add g_acceptation_date date default null;
+alter table o_gta_task add g_solution_date date default null;
+alter table o_gta_task add g_graduation_date date default null;
+
+create table o_gta_task_revision_date (
+  id number(20) generated always as identity,
+  creationdate date not null,
+  g_status varchar2(36 char) not null,
+  g_rev_loop number(20) not null,
+  g_date date not null,
+  fk_task number(20) not null,
+  primary key (id)
+);
+
+alter table o_gta_task_revision_date add constraint gtaskrev_to_task_idx foreign key (fk_task) references o_gta_task (id);
+create index idx_gtaskrev_to_task_idx on o_gta_task_revision_date (fk_task);
+
diff --git a/src/main/resources/database/oracle/setupDatabase.sql b/src/main/resources/database/oracle/setupDatabase.sql
index fb1a38997b7..e18daca7471 100644
--- a/src/main/resources/database/oracle/setupDatabase.sql
+++ b/src/main/resources/database/oracle/setupDatabase.sql
@@ -1882,6 +1882,9 @@ create table o_gta_task (
    g_submission_date date,
    g_submission_revisions_date date,
    g_collection_date date,
+   g_acceptation_date date,
+   g_solution_date date,
+   g_graduation_date date,
    g_assignment_due_date date,
    g_submission_due_date date,
    g_revisions_due_date date,
@@ -1892,6 +1895,16 @@ create table o_gta_task (
    primary key (id)
 );
 
+create table o_gta_task_revision_date (
+  id number(20) generated always as identity,
+  creationdate date not null,
+  g_status varchar2(36 char) not null,
+  g_rev_loop number(20) not null,
+  g_date date not null,
+  fk_task number(20) not null,
+  primary key (id)
+);
+
 create table o_rem_reminder (
    id number(20) not null,
    creationdate date not null,
@@ -2693,6 +2706,9 @@ create index idx_gtask_to_bgroup_idx on o_gta_task (fk_businessgroup);
 alter table o_gta_task_list add constraint gta_list_to_repo_entry_idx foreign key (fk_entry) references o_repositoryentry (repositoryentry_id);
 create index idx_gta_list_to_repo_entry_idx on o_gta_task_list (fk_entry);
 
+alter table o_gta_task_revision_date add constraint gtaskrev_to_task_idx foreign key (fk_task) references o_gta_task (id);
+create index idx_gtaskrev_to_task_idx on o_gta_task_revision_date (fk_task);
+
 -- reminders
 alter table o_rem_reminder add constraint rem_reminder_to_repo_entry_idx foreign key (fk_entry) references o_repositoryentry (repositoryentry_id);
 create index idx_reminder_to_repo_entry_idx on o_rem_reminder (fk_entry);
diff --git a/src/main/resources/database/postgresql/alter_11_5_x_to_12_0_0.sql b/src/main/resources/database/postgresql/alter_11_5_x_to_12_0_0.sql
index e3fafcc00df..7380438f3ec 100644
--- a/src/main/resources/database/postgresql/alter_11_5_x_to_12_0_0.sql
+++ b/src/main/resources/database/postgresql/alter_11_5_x_to_12_0_0.sql
@@ -226,7 +226,22 @@ alter table o_gta_task add column g_submission_due_date timestamp default null;
 alter table o_gta_task add column g_revisions_due_date timestamp default null;
 alter table o_gta_task add column g_solution_due_date timestamp default null;
 
+alter table o_gta_task add column g_acceptation_date timestamp default null;
+alter table o_gta_task add column g_solution_date timestamp default null;
+alter table o_gta_task add column g_graduation_date timestamp default null;
 
+create table o_gta_task_revision_date (
+  id bigserial not null,
+  creationdate timestamp not null,
+  g_status varchar(36) not null,
+  g_rev_loop int8 not null,
+  g_date timestamp not null,
+  fk_task int8 not null,
+  primary key (id)
+);
+
+alter table o_gta_task_revision_date add constraint gtaskrev_to_task_idx foreign key (fk_task) references o_gta_task (id);
+create index idx_gtaskrev_to_task_idx on o_gta_task_revision_date (fk_task);
 
 
 
diff --git a/src/main/resources/database/postgresql/setupDatabase.sql b/src/main/resources/database/postgresql/setupDatabase.sql
index 14dcca0325f..d34929ad20e 100644
--- a/src/main/resources/database/postgresql/setupDatabase.sql
+++ b/src/main/resources/database/postgresql/setupDatabase.sql
@@ -1855,6 +1855,9 @@ create table o_gta_task (
    g_submission_date timestamp,
    g_submission_revisions_date timestamp,
    g_collection_date timestamp,
+   g_acceptation_date timestamp,
+   g_solution_date timestamp,
+   g_graduation_date timestamp,
    g_assignment_due_date timestamp,
    g_submission_due_date timestamp,
    g_revisions_due_date timestamp,
@@ -1866,6 +1869,16 @@ create table o_gta_task (
    primary key (id)
 );
 
+create table o_gta_task_revision_date (
+  id bigserial not null,
+  creationdate timestamp not null,
+  g_status varchar(36) not null,
+  g_rev_loop int8 not null,
+  g_date timestamp not null,
+  fk_task int8 not null,
+  primary key (id)
+);
+
 create table o_rem_reminder (
    id int8 not null,
    creationdate timestamp not null,
@@ -2542,6 +2555,9 @@ create index idx_gtask_to_bgroup_idx on o_gta_task (fk_businessgroup);
 alter table o_gta_task_list add constraint gta_list_to_repo_entry_idx foreign key (fk_entry) references o_repositoryentry (repositoryentry_id);
 create index idx_gta_list_to_repo_entry_idx on o_gta_task_list (fk_entry);
 
+alter table o_gta_task_revision_date add constraint gtaskrev_to_task_idx foreign key (fk_task) references o_gta_task (id);
+create index idx_gtaskrev_to_task_idx on o_gta_task_revision_date (fk_task);
+
 -- reminders
 alter table o_rem_reminder add constraint rem_reminder_to_repo_entry_idx foreign key (fk_entry) references o_repositoryentry (repositoryentry_id);
 create index idx_reminder_to_repo_entry_idx on o_rem_reminder (fk_entry);
diff --git a/src/test/java/org/olat/course/nodes/gta/manager/GTAManagerTest.java b/src/test/java/org/olat/course/nodes/gta/manager/GTAManagerTest.java
index 42328a37563..0b8aa388381 100644
--- a/src/test/java/org/olat/course/nodes/gta/manager/GTAManagerTest.java
+++ b/src/test/java/org/olat/course/nodes/gta/manager/GTAManagerTest.java
@@ -35,6 +35,8 @@ import org.olat.course.nodes.gta.AssignmentResponse.Status;
 import org.olat.course.nodes.gta.GTAType;
 import org.olat.course.nodes.gta.Task;
 import org.olat.course.nodes.gta.TaskList;
+import org.olat.course.nodes.gta.TaskProcess;
+import org.olat.course.nodes.gta.TaskRevisionDate;
 import org.olat.course.nodes.gta.model.TaskListImpl;
 import org.olat.group.BusinessGroup;
 import org.olat.group.manager.BusinessGroupDAO;
@@ -609,6 +611,62 @@ public class GTAManagerTest extends OlatTestCase {
 		Assert.assertEquals(1, notDeletedAssignedTasks2_2.size());
 	}
 	
+	@Test
+	public void createTaskRevisionDate() {
+		//prepare
+		Identity participant = JunitTestHelper.createAndPersistIdentityAsRndUser("gta-user-20");
+		RepositoryEntry re = JunitTestHelper.createAndPersistRepositoryEntry("", false);
+		GTACourseNode node = new GTACourseNode();
+		node.getModuleConfiguration().setStringValue(GTACourseNode.GTASK_TYPE, GTAType.individual.name());
+		TaskList tasks = gtaManager.createIfNotExists(re, node);
+		dbInstance.commit();
+		
+		//create task
+		Task task = gtaManager.createAndPersistTask(null, tasks, TaskProcess.assignment, null, participant, node);
+		dbInstance.commitAndCloseSession();
+		
+		//create the revision log
+		TaskRevisionDate taskRevision = gtaManager.createAndPersistTaskRevisionDate(task, 2, TaskProcess.correction);
+		Assert.assertNotNull(taskRevision);
+		dbInstance.commitAndCloseSession();
+		Assert.assertNotNull(taskRevision.getKey());
+		Assert.assertNotNull(taskRevision.getDate());
+		Assert.assertEquals(task, taskRevision.getTask());
+		Assert.assertEquals(2, taskRevision.getRevisionLoop());
+		Assert.assertEquals(TaskProcess.correction, taskRevision.getTaskStatus());
+	}
+	
+	@Test
+	public void getTaskRevisions() {
+		//prepare
+		Identity participant = JunitTestHelper.createAndPersistIdentityAsRndUser("gta-user-21");
+		RepositoryEntry re = JunitTestHelper.createAndPersistRepositoryEntry("", false);
+		GTACourseNode node = new GTACourseNode();
+		node.getModuleConfiguration().setStringValue(GTACourseNode.GTASK_TYPE, GTAType.individual.name());
+		TaskList tasks = gtaManager.createIfNotExists(re, node);
+		dbInstance.commit();
+		
+		//create a task
+		Task task = gtaManager.createAndPersistTask(null, tasks, TaskProcess.assignment, null, participant, node);
+		dbInstance.commitAndCloseSession();
+		
+		//add the revision log
+		TaskRevisionDate taskRevision = gtaManager.createAndPersistTaskRevisionDate(task, 2, TaskProcess.correction);
+		Assert.assertNotNull(taskRevision);
+		dbInstance.commitAndCloseSession();
+		
+		//load the revisions
+		List<TaskRevisionDate> taskRevisions = gtaManager.getTaskRevisions(task);
+		Assert.assertNotNull(taskRevisions);
+		Assert.assertEquals(1, taskRevisions.size());
+		TaskRevisionDate loadedTaskRevision = taskRevisions.get(0);
+		Assert.assertNotNull(loadedTaskRevision.getKey());
+		Assert.assertNotNull(loadedTaskRevision.getDate());
+		Assert.assertEquals(task, loadedTaskRevision.getTask());
+		Assert.assertEquals(2, loadedTaskRevision.getRevisionLoop());
+		Assert.assertEquals(TaskProcess.correction, loadedTaskRevision.getTaskStatus());
+	}
+	
 	@Test
 	public void roundRobin() {
 		String[] slots = new String[]{ "A", "B", "C" };
-- 
GitLab