diff --git a/pom.xml b/pom.xml
index c6709ec7901288cfac4654326cde1a459600b9b0..133905bc713ef8e64ffefe8688d2f3d2540ea8e2 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1818,6 +1818,16 @@
 					<groupId>ch.qos.logback</groupId>
 					<artifactId>logback-classic</artifactId>
 				</exclusion>
+				<!-- 
+				<exclusion>
+					<groupId>net.sf.saxon</groupId>
+					<artifactId>saxon9</artifactId>
+				</exclusion>
+				<exclusion>
+					<groupId>net.sf.saxon</groupId>
+					<artifactId>saxon9-dom</artifactId>
+				</exclusion>
+				-->
 			</exclusions>
 		</dependency>
 		<dependency>
@@ -1829,6 +1839,16 @@
 					<groupId>ch.qos.logback</groupId>
 					<artifactId>logback-classic</artifactId>
 				</exclusion>
+				<!--
+				<exclusion>
+					<groupId>net.sf.saxon</groupId>
+					<artifactId>saxon9</artifactId>
+				</exclusion>
+				<exclusion>
+					<groupId>net.sf.saxon</groupId>
+					<artifactId>saxon9-dom</artifactId>
+				</exclusion>
+				-->
 			</exclusions>
 		</dependency>
 		<dependency>
@@ -1840,6 +1860,16 @@
 					<groupId>ch.qos.logback</groupId>
 					<artifactId>logback-classic</artifactId>
 				</exclusion>
+				<!-- 
+				<exclusion>
+					<groupId>net.sf.saxon</groupId>
+					<artifactId>saxon9</artifactId>
+				</exclusion>
+				<exclusion>
+					<groupId>net.sf.saxon</groupId>
+					<artifactId>saxon9-dom</artifactId>
+				</exclusion>
+				-->
 			</exclusions>
 		</dependency>
 		<dependency>
diff --git a/src/main/java/org/olat/core/commons/persistence/_spring/core_persistence.xml b/src/main/java/org/olat/core/commons/persistence/_spring/core_persistence.xml
index b6b176d1bc59592edc8bbc952e5e6c120344d539..58e94283bac6ed2cceee8478f4a3bc4cb562bd27 100644
--- a/src/main/java/org/olat/core/commons/persistence/_spring/core_persistence.xml
+++ b/src/main/java/org/olat/core/commons/persistence/_spring/core_persistence.xml
@@ -129,6 +129,7 @@
 		<class>org.olat.instantMessaging.model.InstantMessageNotificationImpl</class>
 		<class>org.olat.ims.qti.statistics.model.QTIStatisticResult</class>
 		<class>org.olat.ims.qti.statistics.model.QTIStatisticResultSet</class>
+		<class>org.olat.ims.qti21.model.UserTestSessionImpl</class>
 		<class>org.olat.modules.qpool.model.PoolImpl</class>
 		<class>org.olat.modules.qpool.model.PoolToItem</class>
 		<class>org.olat.modules.qpool.model.PoolItemShortView</class>
diff --git a/src/main/java/org/olat/ims/qti21/OnyxToQTIWorks.java b/src/main/java/org/olat/ims/qti21/OnyxToQTIWorks.java
new file mode 100644
index 0000000000000000000000000000000000000000..8012be98bf825608fb893e5fbdd4307ca9cce72c
--- /dev/null
+++ b/src/main/java/org/olat/ims/qti21/OnyxToQTIWorks.java
@@ -0,0 +1,15 @@
+package org.olat.ims.qti21;
+
+/**
+ * 
+ * 
+ *  //TODO <rubricBlock><div></div></rubricBlock>: add a div
+ *  //TODO <prompt><p></p></prompt>: remove the p's
+ * 
+ * Initial date: 11.05.2015<br>
+ * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
+ *
+ */
+public class OnyxToQTIWorks {
+
+}
diff --git a/src/main/java/org/olat/ims/qti21/QTI21Module.java b/src/main/java/org/olat/ims/qti21/QTI21Module.java
index 34c3470d56db8b9e05df9844c7bd64cd91d9d407..39b5e3dc34aeb4ced9cefe36a59c6e386638df66 100644
--- a/src/main/java/org/olat/ims/qti21/QTI21Module.java
+++ b/src/main/java/org/olat/ims/qti21/QTI21Module.java
@@ -46,6 +46,8 @@ public class QTI21Module extends AbstractSpringModule {
 	@Override
 	public void init() {
 		RepositoryHandlerFactory.registerHandler(assessmentHandler, 10);
+		//Saxon is mandatory, JQTI need XSLT 2.0
+		//XsltFactoryUtilities.SAXON_TRANSFORMER_FACTORY_CLASS_NAME;
 	}
 
 	@Override
diff --git a/src/main/java/org/olat/ims/qti21/QTI21Service.java b/src/main/java/org/olat/ims/qti21/QTI21Service.java
new file mode 100644
index 0000000000000000000000000000000000000000..000e3324386124963e8f88f38202cdd3c70864bd
--- /dev/null
+++ b/src/main/java/org/olat/ims/qti21/QTI21Service.java
@@ -0,0 +1,36 @@
+/**
+ * <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.ims.qti21;
+
+import org.olat.core.id.Identity;
+import org.olat.repository.RepositoryEntry;
+
+/**
+ * 
+ * Initial date: 12.05.2015<br>
+ * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
+ *
+ */
+public interface QTI21Service {
+	
+	
+	public UserTestSession createTestSession(RepositoryEntry testEntry, RepositoryEntry courseEntry, Identity identity);
+
+}
diff --git a/src/main/java/org/olat/ims/qti21/UserTestSession.java b/src/main/java/org/olat/ims/qti21/UserTestSession.java
new file mode 100644
index 0000000000000000000000000000000000000000..85d9a1b4391b2951cd16b29ad00592a901aa0a7d
--- /dev/null
+++ b/src/main/java/org/olat/ims/qti21/UserTestSession.java
@@ -0,0 +1,33 @@
+/**
+ * <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.ims.qti21;
+
+import org.olat.core.id.CreateInfo;
+import org.olat.core.id.ModifiedInfo;
+
+/**
+ * 
+ * Initial date: 12.05.2015<br>
+ * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
+ *
+ */
+public interface UserTestSession extends CreateInfo, ModifiedInfo {
+
+}
diff --git a/src/main/java/org/olat/ims/qti21/manager/QTI21ServiceImpl.java b/src/main/java/org/olat/ims/qti21/manager/QTI21ServiceImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..b0190ed7036622aa485f2987bb16f46ab102f308
--- /dev/null
+++ b/src/main/java/org/olat/ims/qti21/manager/QTI21ServiceImpl.java
@@ -0,0 +1,32 @@
+package org.olat.ims.qti21.manager;
+
+import org.olat.core.commons.persistence.DB;
+import org.olat.core.id.Identity;
+import org.olat.ims.qti21.QTI21Service;
+import org.olat.ims.qti21.UserTestSession;
+import org.olat.repository.RepositoryEntry;
+import org.springframework.beans.factory.annotation.Autowired;
+
+/**
+ * 
+ * Initial date: 12.05.2015<br>
+ * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
+ *
+ */
+public class QTI21ServiceImpl implements QTI21Service {
+	
+	@Autowired
+	private DB dbInstance;
+	@Autowired
+	private TestSessionDAO testSessionDAO;
+	
+
+	@Override
+	public UserTestSession createTestSession(RepositoryEntry testEntry, RepositoryEntry courseEntry, Identity identity) {
+		return testSessionDAO.createTestSession(testEntry, courseEntry, identity);
+	}
+	
+	
+	
+
+}
diff --git a/src/main/java/org/olat/ims/qti21/manager/TestSessionDAO.java b/src/main/java/org/olat/ims/qti21/manager/TestSessionDAO.java
new file mode 100644
index 0000000000000000000000000000000000000000..04127ba6a02ff78b0f1256b05b2d51bf4e8015fb
--- /dev/null
+++ b/src/main/java/org/olat/ims/qti21/manager/TestSessionDAO.java
@@ -0,0 +1,40 @@
+package org.olat.ims.qti21.manager;
+
+import java.util.Date;
+
+import org.olat.core.commons.persistence.DB;
+import org.olat.core.id.Identity;
+import org.olat.ims.qti21.UserTestSession;
+import org.olat.ims.qti21.model.UserTestSessionImpl;
+import org.olat.repository.RepositoryEntry;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+/**
+ * 
+ * Initial date: 12.05.2015<br>
+ * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
+ *
+ */
+@Service
+public class TestSessionDAO {
+	
+	@Autowired
+	private DB dbInstance;
+	
+
+	public UserTestSession createTestSession(RepositoryEntry testEntry, RepositoryEntry courseEntry, Identity identity) {
+		UserTestSessionImpl testSession = new UserTestSessionImpl();
+		Date now = new Date();
+		testSession.setCreationDate(now);
+		testSession.setLastModified(now);
+		testSession.setTestEntry(testEntry);
+		testSession.setCourseEntry(courseEntry);
+		testSession.setAuthorMode(false);
+		testSession.setExploded(false);
+		testSession.setIdentity(identity);
+		dbInstance.getCurrentEntityManager().persist(testSession);
+		return testSession;
+	}
+
+}
diff --git a/src/main/java/org/olat/ims/qti21/model/CandidateTestEventType.java b/src/main/java/org/olat/ims/qti21/model/CandidateTestEventType.java
new file mode 100644
index 0000000000000000000000000000000000000000..5baa104543294c93fb2b6ab2d87a1203cadecfda
--- /dev/null
+++ b/src/main/java/org/olat/ims/qti21/model/CandidateTestEventType.java
@@ -0,0 +1,111 @@
+/* Copyright (c) 2012-2013, University of Edinburgh.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ *   list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright notice, this
+ *   list of conditions and the following disclaimer in the documentation and/or
+ *   other materials provided with the distribution.
+ *
+ * * Neither the name of the University of Edinburgh nor the names of its
+ *   contributors may be used to endorse or promote products derived from this
+ *   software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ *
+ * This software is derived from (and contains code from) QTItools and MathAssessEngine.
+ * QTItools is (c) 2008, University of Southampton.
+ * MathAssessEngine is (c) 2010, University of Edinburgh.
+ */
+package org.olat.ims.qti21.model;
+
+import uk.ac.ed.ph.jqtiplus.node.test.NavigationMode;
+import uk.ac.ed.ph.jqtiplus.node.test.TestPart;
+
+/**
+ * Encapsulates the test-specific events that can arise when delivering
+ * a test within a {@link CandidateSession}
+ *
+ * @author David McKain
+ */
+public enum CandidateTestEventType {
+
+    /* NB: Observe maximum length for mapped column set in CandidateEvent */
+  //1234567890123456789012345678901234567890
+
+    /**
+     * Test has been entered.
+     * <p>
+     * (In test having one {@link TestPart}, we also attempt
+     * to enter this automatically. Otherwise, the first {@link TestPart} has to be
+     * entered explicitly by the candidate via {@link #ADVANCE_TEST_PART}.)
+     */
+    ENTER_TEST,
+
+    /**
+     * Presentation of navigation menu for the current {@link TestPart}, when in nonlinear
+     * mode and while the {@link TestPart} is still interacting.
+     */
+    SELECT_MENU,
+
+    /**
+     * Ends the current {@link TestPart} and moves it into review state
+     */
+    END_TEST_PART,
+
+    /**
+     * Exits the current {@link TestPart} (if currently inside one), then advances to the next
+     * available {@link TestPart}, or exits the test if there are no more available.
+     */
+    ADVANCE_TEST_PART,
+
+    /**
+     * Exits the test, either after the combined {@link TestPart} and test feedback page
+     * (for single-part tests), or the standalone test feedback page for multi-part tests.
+     */
+    EXIT_TEST,
+
+    /** Selection of a particular item for interaction (in {@link NavigationMode#NONLINEAR}) */
+    SELECT_ITEM,
+
+    /**
+     * Finish interaction of item (in {@link NavigationMode#LINEAR}), moving to the next
+     * enterable item in the testPart.
+     */
+    FINISH_ITEM,
+
+    /**
+     * Finish interaction of item (in {@link NavigationMode#LINEAR}), with no further
+     * enterable items in the testPart, thus causing the current testPart to end.
+     */
+    FINISH_FINAL_ITEM,
+
+    /** Item Event within the currently-selected item */
+    ITEM_EVENT,
+
+    /** Return to Test Part review (while already in review state, i.e. after {@link #REVIEW_ITEM} */
+    REVIEW_TEST_PART,
+
+    /** Review of a particular item */
+    REVIEW_ITEM,
+
+    /** Solution of a particular item (in review state) */
+    SOLUTION_ITEM,
+
+    ;
+
+}
diff --git a/src/main/java/org/olat/ims/qti21/model/UserTestSessionImpl.java b/src/main/java/org/olat/ims/qti21/model/UserTestSessionImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..310f7605169b5541bdaece612f97ead390d3e679
--- /dev/null
+++ b/src/main/java/org/olat/ims/qti21/model/UserTestSessionImpl.java
@@ -0,0 +1,213 @@
+package org.olat.ims.qti21.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 org.hibernate.annotations.GenericGenerator;
+import org.olat.basesecurity.IdentityImpl;
+import org.olat.core.id.Identity;
+import org.olat.core.id.Persistable;
+import org.olat.ims.qti21.UserTestSession;
+import org.olat.repository.RepositoryEntry;
+
+/**
+ * This a custom implementation of CandidateSession
+ * 
+ * 
+ * Initial date: 12.05.2015<br>
+ * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
+ *
+ */
+@Entity(name="qtitestsession")
+@Table(name="o_qti_test_session")
+public class UserTestSessionImpl implements UserTestSession, Persistable {
+
+	private static final long serialVersionUID = -6069133323360142500L;
+
+	@Id
+	@GeneratedValue(generator = "system-uuid")
+	@GenericGenerator(name = "system-uuid", strategy = "hilo")
+	@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;
+	@Temporal(TemporalType.TIMESTAMP)
+	@Column(name="lastmodified", nullable=false, insertable=true, updatable=true)
+	private Date lastModified;
+
+	@ManyToOne(targetEntity=RepositoryEntry.class,fetch=FetchType.LAZY,optional=false)
+	@JoinColumn(name="fk_entry", nullable=false, insertable=true, updatable=false)
+    private RepositoryEntry testEntry;
+	
+	@ManyToOne(targetEntity=RepositoryEntry.class,fetch=FetchType.LAZY,optional=true)
+	@JoinColumn(name="fk_course", nullable=true, insertable=true, updatable=false)
+    private RepositoryEntry courseEntry;
+	
+
+	@ManyToOne(targetEntity=IdentityImpl.class,fetch=FetchType.LAZY,optional=false)
+	@JoinColumn(name="fk_identity", nullable=false, insertable=true, updatable=false)
+    private Identity identity;
+
+    /** Is this session running in author mode? (I.e. providing debugging information) */
+    @Column(name="q_author_mode", nullable=false, insertable=true, updatable=true)
+    private boolean authorMode;
+
+    /**
+     * Timestamp indicating when the session has been <strong>finished</strong>.
+     * This is a QTIWorks specific concept with the following meaning:
+     * <ul>
+     *   <li>
+     *     A test is marked as finished once the candidate gets to the end of the last
+     *     enterable testPart. At this time, the outcome variables are finalised and will
+     *     be sent back to the LTI TC (if appropriate). A test only finishes once.
+     *   </li>
+     *   <li>
+     *     A standalone item is marked as finished once the item session ends. At this time,
+     *     the outcome variables are sent back to the LTI TC (if appropriate). These variables
+     *     are normally final, but it is currently possible for items to reopen. The session can
+     *     finish again in this case.
+     *   </li>
+     * </ul>
+     */
+    @Temporal(TemporalType.TIMESTAMP)
+    @Column(name="q_finish_time", nullable=true, insertable=true, updatable=true)
+    private Date finishTime;
+
+    /**
+     * Timestamp indicating when the session has been terminated. Session termination can
+     * occur in two ways:
+     * <ul>
+     *   <li>When the candidate naturally exits the session</li>
+     *   <li>When the instructor explicitly terminates the session</li>
+     * </ul>
+     * Once terminated, a session is no longer available to the candidate.
+     */
+    @Temporal(TemporalType.TIMESTAMP)
+    @Column(name="q_termination_time", nullable=true, insertable=true, updatable=true)
+    private Date terminationTime;
+
+    /**
+     * Flag to indicate if this session blew up while running, either because
+     * the assessment was not runnable, or because of a logic error.
+     */
+    @Column(name="q_exploded", nullable=false, insertable=true, updatable=true)
+    private boolean exploded;
+
+    @Override
+	public Long getKey() {
+		return key;
+	}
+
+	public void setKey(Long key) {
+		this.key = key;
+	}
+
+    @Override
+	public Date getCreationDate() {
+		return creationDate;
+	}
+
+	public void setCreationDate(Date creationDate) {
+		this.creationDate = creationDate;
+	}
+
+    @Override
+	public Date getLastModified() {
+		return lastModified;
+	}
+
+    @Override
+	public void setLastModified(Date lastModified) {
+		this.lastModified = lastModified;
+	}
+
+	public RepositoryEntry getTestEntry() {
+		return testEntry;
+	}
+
+	public void setTestEntry(RepositoryEntry testEntry) {
+		this.testEntry = testEntry;
+	}
+
+	public RepositoryEntry getCourseEntry() {
+		return courseEntry;
+	}
+
+	public void setCourseEntry(RepositoryEntry courseEntry) {
+		this.courseEntry = courseEntry;
+	}
+
+	public Identity getIdentity() {
+		return identity;
+	}
+
+	public void setIdentity(Identity identity) {
+		this.identity = identity;
+	}
+
+	public boolean isAuthorMode() {
+		return authorMode;
+	}
+
+	public void setAuthorMode(boolean authorMode) {
+		this.authorMode = authorMode;
+	}
+
+	public boolean isExploded() {
+		return exploded;
+	}
+
+	public void setExploded(boolean exploded) {
+		this.exploded = exploded;
+	}
+
+	public Date getFinishTime() {
+		return finishTime;
+	}
+
+	public void setFinishTime(Date finishTime) {
+		this.finishTime = finishTime;
+	}
+
+	public Date getTerminationTime() {
+		return terminationTime;
+	}
+
+	public void setTerminationTime(Date terminationTime) {
+		this.terminationTime = terminationTime;
+	}
+
+	@Override
+	public int hashCode() {
+		return key == null ? -86534687 : key.hashCode();
+	}
+	
+	@Override
+	public boolean equals(Object obj) {
+		if(this == obj) {
+			return true;
+		}
+		if(obj instanceof UserTestSessionImpl) {
+			UserTestSessionImpl session = (UserTestSessionImpl)obj;
+			return getKey() != null && getKey().equals(session.getKey());
+		}
+		return false;
+	}
+	
+	@Override
+	public boolean equalsByPersistableKey(Persistable persistable) {
+		return equals(persistable);
+	}
+}
diff --git a/src/main/java/org/olat/ims/qti21/ui/QTI21DisplayController.java b/src/main/java/org/olat/ims/qti21/ui/QTI21DisplayController.java
index 1cdb66de645eac93ea420d17ffc1fd17821a533f..887e795413de13e7b1bcdcdfd7787381d289d3dc 100644
--- a/src/main/java/org/olat/ims/qti21/ui/QTI21DisplayController.java
+++ b/src/main/java/org/olat/ims/qti21/ui/QTI21DisplayController.java
@@ -40,11 +40,14 @@ import org.olat.fileresource.FileResourceManager;
 import org.olat.fileresource.types.ImsQTI21Resource;
 import org.olat.fileresource.types.ImsQTI21Resource.PathResourceLocator;
 import org.olat.ims.qti21.QTI21ContentPackage;
+import org.olat.ims.qti21.UserTestSession;
 import org.olat.ims.qti21.RequestTimestampContext;
+import org.olat.ims.qti21.model.CandidateTestEventType;
 import org.olat.repository.RepositoryEntry;
 
 import uk.ac.ed.ph.jqtiplus.JqtiExtensionManager;
 import uk.ac.ed.ph.jqtiplus.JqtiPlus;
+import uk.ac.ed.ph.jqtiplus.exception.QtiCandidateStateException;
 import uk.ac.ed.ph.jqtiplus.node.AssessmentObjectType;
 import uk.ac.ed.ph.jqtiplus.node.result.AssessmentResult;
 import uk.ac.ed.ph.jqtiplus.notification.NotificationLevel;
@@ -61,6 +64,7 @@ import uk.ac.ed.ph.jqtiplus.running.TestProcessingInitializer;
 import uk.ac.ed.ph.jqtiplus.running.TestSessionController;
 import uk.ac.ed.ph.jqtiplus.running.TestSessionControllerSettings;
 import uk.ac.ed.ph.jqtiplus.state.TestPlan;
+import uk.ac.ed.ph.jqtiplus.state.TestPlanNode;
 import uk.ac.ed.ph.jqtiplus.state.TestPlanNodeKey;
 import uk.ac.ed.ph.jqtiplus.state.TestProcessingMap;
 import uk.ac.ed.ph.jqtiplus.state.TestSessionState;
@@ -82,9 +86,12 @@ public class QTI21DisplayController extends FormBasicController {
 	
 	private QTI21FormItem qtiEl;
 	private TestSessionController testSessionController;
-	
-	private RequestTimestampContext requestTimestampContext = new RequestTimestampContext();
+
     private JqtiExtensionManager jqtiExtensionManager = new JqtiExtensionManager();
+	private RequestTimestampContext requestTimestampContext = new RequestTimestampContext();
+	private CandidateSessionFinisher candidateSessionFinisher = new CandidateSessionFinisher();
+	
+	private UserTestSession candidateSession;
 	
 	public QTI21DisplayController(UserRequest ureq, WindowControl wControl, RepositoryEntry entry) {
 		super(ureq, wControl, "run");
@@ -136,23 +143,173 @@ public class QTI21DisplayController extends FormBasicController {
 		if(source == qtiEl) {
 			if(event instanceof QTIWorksEvent) {
 				QTIWorksEvent qe = (QTIWorksEvent)event;
-				switch(qe.getEvent()) {
-					case selectItem: doSelectItem(qe.getSubCommand()); break;
-					case testPartNavigation: doTestPartNavigation(); break;
-					case response: doResponse(qe.getStringResponseMap()); break;
-					case endTestPart: doEndTestPart(); break;
-				}
+				processQTIEvent(qe);
 			}
 		}
 		super.formInnerEvent(ureq, source, event);
 	}
 	
+	private void processQTIEvent(QTIWorksEvent qe) {
+		switch(qe.getEvent()) {
+			case selectItem:
+				doSelectItem(qe.getSubCommand());
+				break;
+			case finishItem:
+				doFinish();
+				break;
+			case reviewItem:
+				doReviewItem(qe.getSubCommand());
+				break;
+			case itemSolution:
+				doItemSolution(qe.getSubCommand());
+				break;
+			case testPartNavigation:
+				doTestPartNavigation();
+				break;
+			case response:
+				doResponse(qe.getStringResponseMap());
+				break;
+			case endTestPart:
+				doEndTestPart();
+				break;
+			case advanceTestPart:
+				doAdvanceTestPart();
+				break;
+			case reviewTestPart:
+				doReviewTestPart();
+				break;
+			case exitTest:
+				doExitTest();
+				break;
+		}
+	}
+	
 	private void doSelectItem(String key) {
 		TestPlanNodeKey nodeKey = TestPlanNodeKey.fromString(key);
 		Date requestTimestamp = requestTimestampContext.getCurrentRequestTimestamp();
         testSessionController.selectItemNonlinear(requestTimestamp, nodeKey);
 	}
 	
+	private void doReviewItem(String key) {
+		TestPlanNodeKey itemKey = TestPlanNodeKey.fromString(key);
+		Date requestTimestamp = requestTimestampContext.getCurrentRequestTimestamp();
+		
+        //Assert.notNull(candidateSessionContext, "candidateSessionContext");
+        //Assert.notNull(itemKey, "itemKey");
+        //assertSessionType(candidateSessionContext, AssessmentObjectType.ASSESSMENT_TEST);
+        //final CandidateSession candidateSession = candidateSessionContext.getCandidateSession();
+
+        /* Get current JQTI state and create JQTI controller */
+        //final NotificationRecorder notificationRecorder = new NotificationRecorder(NotificationLevel.INFO);
+        //final CandidateEvent mostRecentEvent = assertSessionEntered(candidateSession);
+        //final TestSessionController testSessionController = candidateDataService.createTestSessionController(mostRecentEvent, notificationRecorder);
+        final TestSessionState testSessionState = testSessionController.getTestSessionState();
+
+        /* Make sure caller may do this */
+        //assertSessionNotTerminated(candidateSession);
+        try {
+            if (!testSessionController.mayReviewItem(itemKey)) {
+            	logError("CANNOT_REVIEW_TEST_ITEM", null);
+               //candidateAuditLogger.logAndThrowCandidateException(candidateSession, CandidateExceptionReason.CANNOT_REVIEW_TEST_ITEM);
+                return;
+            }
+        } catch (final QtiCandidateStateException e) {
+        	logError("CANNOT_REVIEW_TEST_ITEM", e);
+           // candidateAuditLogger.logAndThrowCandidateException(candidateSession, CandidateExceptionReason.CANNOT_REVIEW_TEST_ITEM);
+            return;
+        }  catch (final RuntimeException e) {
+        	logError("CANNOT_REVIEW_TEST_ITEM", e);
+            return;// handleExplosion(e, candidateSession);
+        }
+
+        /* Record current result state */
+        //candidateDataService.computeAndRecordTestAssessmentResult(candidateSession, testSessionController);
+
+        /* Record and log event */
+        //final CandidateEvent candidateTestEvent = candidateDataService.recordCandidateTestEvent(candidateSession,
+        //        CandidateTestEventType.REVIEW_ITEM, null, itemKey, testSessionState, notificationRecorder);
+        //candidateAuditLogger.logCandidateEvent(candidateTestEvent);
+	}
+
+	private void doItemSolution(String key) {
+		TestPlanNodeKey itemKey = TestPlanNodeKey.fromString(key);
+		
+		//Assert.notNull(candidateSessionContext, "candidateSessionContext");
+        //assertSessionType(candidateSessionContext, AssessmentObjectType.ASSESSMENT_TEST);
+        //final CandidateSession candidateSession = candidateSessionContext.getCandidateSession();
+        //Assert.notNull(itemKey, "itemKey");
+
+        /* Get current JQTI state and create JQTI controller */
+        final NotificationRecorder notificationRecorder = new NotificationRecorder(NotificationLevel.INFO);
+        //final CandidateEvent mostRecentEvent = assertSessionEntered(candidateSession);
+        //final TestSessionController testSessionController = candidateDataService.createTestSessionController(mostRecentEvent, notificationRecorder);
+        //final TestSessionState testSessionState = testSessionController.getTestSessionState();
+
+        /* Make sure caller may do this */
+        //assertSessionNotTerminated(candidateSession);
+        try {
+            if (!testSessionController.mayAccessItemSolution(itemKey)) {
+                //candidateAuditLogger.logAndThrowCandidateException(candidateSession, CandidateExceptionReason.CANNOT_SOLUTION_TEST_ITEM);
+            	logError("CANNOT_SOLUTION_TEST_ITEM", null);
+                return;
+            }
+        }
+        catch (final QtiCandidateStateException e) {
+            //candidateAuditLogger.logAndThrowCandidateException(candidateSession, CandidateExceptionReason.CANNOT_SOLUTION_TEST_ITEM);
+            logError("CANNOT_SOLUTION_TEST_ITEM", e);
+        	return;
+        } catch (final RuntimeException e) {
+        	logError("Exploded", e);
+            return;// handleExplosion(e, candidateSession);
+        }
+
+        /* Record current result state */
+        //candidateDataService.computeAndRecordTestAssessmentResult(candidateSession, testSessionController);
+
+        /* Record and log event */
+        //final CandidateEvent candidateTestEvent = candidateDataService.recordCandidateTestEvent(candidateSession,
+        //        CandidateTestEventType.SOLUTION_ITEM, null, itemKey, testSessionState, notificationRecorder);
+        //candidateAuditLogger.logCandidateEvent(candidateTestEvent);
+        
+	}
+	
+	//public CandidateSession finishLinearItem(final CandidateSessionContext candidateSessionContext)
+    // throws CandidateException {
+	private void doFinish() {
+		
+		try {
+			if (!testSessionController.mayAdvanceItemLinear()) {
+				logError("CANNOT_FINISH_LINEAR_TEST_ITEM", null);
+                return;
+            }
+		} catch (QtiCandidateStateException e) {
+         	logError("CANNOT_FINISH_LINEAR_TEST_ITEM", e);
+         	return;
+		} catch (RuntimeException e) {
+         	logError("CANNOT_FINISH_LINEAR_TEST_ITEM", e);
+			 //return handleExplosion(e, candidateSession);
+		}
+		 
+		// Update state
+		final Date requestTimestamp = requestTimestampContext.getCurrentRequestTimestamp();
+	    final TestPlanNode nextItemNode = testSessionController.advanceItemLinear(requestTimestamp);
+
+	    // Record current result state
+	    //final AssessmentResult assessmentResult = candidateDataService.computeAndRecordTestAssessmentResult(candidateSession, testSessionController);
+
+	    /* If we ended the testPart and there are now no more available testParts, then finish the session now */
+	    if (nextItemNode==null && testSessionController.findNextEnterableTestPart()==null) {
+	    	//candidateSessionFinisher.finishCandidateSession(candidateSession, assessmentResult);
+	    }
+
+	    /* Record and log event 
+	    final CandidateTestEventType eventType = nextItemNode!=null ? CandidateTestEventType.FINISH_ITEM : CandidateTestEventType.FINISH_FINAL_ITEM;
+	   	final CandidateEvent candidateTestEvent = candidateDataService.recordCandidateTestEvent(candidateSession,
+	                eventType, null, testSessionState, notificationRecorder);
+	   	*/
+		
+	}
+	
 	private void doTestPartNavigation() {
 		final Date requestTimestamp = requestTimestampContext.getCurrentRequestTimestamp();
         testSessionController.selectItemNonlinear(requestTimestamp, null);
@@ -193,6 +350,145 @@ public class QTI21DisplayController extends FormBasicController {
         testSessionController.endCurrentTestPart(requestTimestamp);
 	}
 	
+	private void doAdvanceTestPart() {
+		
+		//final CandidateSessionContext candidateSessionContext = getCandidateSessionContext();
+		//Assert.notNull(candidateSessionContext, "candidateSessionContext");
+        //assertSessionType(candidateSessionContext, AssessmentObjectType.ASSESSMENT_TEST);
+        //final CandidateSession candidateSession = candidateSessionContext.getCandidateSession();
+
+        /* Get current JQTI state and create JQTI controller */
+        //final NotificationRecorder notificationRecorder = new NotificationRecorder(NotificationLevel.INFO);
+        //final CandidateEvent mostRecentEvent = assertSessionEntered(candidateSession);
+        final TestSessionState testSessionState = testSessionController.getTestSessionState();
+
+        /* Perform action */
+        final TestPlanNode nextTestPart;
+        final Date currentTimestamp = requestTimestampContext.getCurrentRequestTimestamp();
+        try {
+            nextTestPart = testSessionController.enterNextAvailableTestPart(currentTimestamp);
+        } catch (final QtiCandidateStateException e) {
+            logError("CANNOT_ADVANCE_TEST_PART", e);
+            return;
+        } catch (final RuntimeException e) {
+            logError("RuntimeException", e);
+            return;// handleExplosion(e, candidateSession);
+        }
+
+        CandidateTestEventType eventType;
+        if (nextTestPart!=null) {
+            /* Moved into next test part */
+            eventType = CandidateTestEventType.ADVANCE_TEST_PART;
+        }
+        else {
+            /* No more test parts.
+             *
+             * For single part tests, we terminate the test completely now as the test feedback was shown with the testPart feedback.
+             * For multi-part tests, we shall keep the test open so that the test feedback can be viewed.
+             */
+            if (testSessionState.getTestPlan().getTestPartNodes().size()==1) {
+                eventType = CandidateTestEventType.EXIT_TEST;
+                testSessionController.exitTest(currentTimestamp);
+                //candidateSession.setTerminationTime(currentTimestamp);
+                //candidateSessionDao.update(candidateSession);
+            }
+            else {
+                eventType = CandidateTestEventType.ADVANCE_TEST_PART;
+            }
+        }
+
+        /* Record current result state */
+        //candidateDataService.computeAndRecordTestAssessmentResult(candidateSession, testSessionController);
+
+        /* Record and log event */
+        //final CandidateEvent candidateTestEvent = candidateDataService.recordCandidateTestEvent(candidateSession,
+        //       eventType, testSessionState, notificationRecorder);
+        //candidateAuditLogger.logCandidateEvent(candidateTestEvent);
+
+
+		
+		/*
+        String redirect;
+        if (candidateSession.isTerminated()) {
+            // We exited the test
+            //TODO fire event eXIT
+            redirect = redirectToExitUrl(candidateSessionContext, xsrfToken);
+        }
+        else {
+            // Moved onto next part
+            redirect = redirectToRenderSession(xid, xsrfToken);
+        }
+        */
+		
+	}
+	
+	private void doReviewTestPart() {
+		//Assert.notNull(candidateSessionContext, "candidateSessionContext");
+        //assertSessionType(candidateSessionContext, AssessmentObjectType.ASSESSMENT_TEST);
+        //final CandidateSession candidateSession = candidateSessionContext.getCandidateSession();
+
+        /* Get current JQTI state and create JQTI controller */
+        final NotificationRecorder notificationRecorder = new NotificationRecorder(NotificationLevel.INFO);
+        //final CandidateEvent mostRecentEvent = assertSessionEntered(candidateSession);
+        //final TestSessionController testSessionController = candidateDataService.createTestSessionController(mostRecentEvent, notificationRecorder);
+        final TestSessionState testSessionState = testSessionController.getTestSessionState();
+
+        /* Make sure caller may do this */
+        //assertSessionNotTerminated(candidateSession);
+        if (testSessionState.getCurrentTestPartKey()==null || !testSessionState.getCurrentTestPartSessionState().isEnded()) {
+        	
+            // candidateAuditLogger.logAndThrowCandidateException(candidateSession, CandidateExceptionReason.CANNOT_REVIEW_TEST_PART);
+            logError("CANNOT_REVIEW_TEST_PART", null);
+        	return;
+        }
+
+        /* Record and log event */
+        //final CandidateEvent candidateTestEvent = candidateDataService.recordCandidateTestEvent(candidateSession,
+        //        CandidateTestEventType.REVIEW_TEST_PART, null, null, testSessionState, notificationRecorder);
+        //candidateAuditLogger.logCandidateEvent(candidateTestEvent);
+	}
+	
+	/**
+	 * Exit multi-part tests
+	 */
+	private void doExitTest() {
+		//Assert.notNull(candidateSessionContext, "candidateSessionContext");
+        //assertSessionType(candidateSessionContext, AssessmentObjectType.ASSESSMENT_TEST);
+        //final CandidateSession candidateSession = candidateSessionContext.getCandidateSession();
+
+        /* Get current JQTI state and create JQTI controller */
+        final NotificationRecorder notificationRecorder = new NotificationRecorder(NotificationLevel.INFO);
+        //final CandidateEvent mostRecentEvent = assertSessionEntered(candidateSession);
+        //final TestSessionController testSessionController = candidateDataService.createTestSessionController(mostRecentEvent, notificationRecorder);
+        final TestSessionState testSessionState = testSessionController.getTestSessionState();
+
+        /* Perform action */
+        final Date currentTimestamp = requestTimestampContext.getCurrentRequestTimestamp();
+        try {
+            testSessionController.exitTest(currentTimestamp);
+        } catch (final QtiCandidateStateException e) {
+            //candidateAuditLogger.logAndThrowCandidateException(candidateSession, CandidateExceptionReason.CANNOT_EXIT_TEST);
+        	logError("CANNOT_EXIT_TEST", null);
+            return;
+        } catch (final RuntimeException e) {
+        	logError("Exploded", null);
+            return;// handleExplosion(e, candidateSession);
+        }
+
+        /* Update CandidateSession as appropriate */
+        //candidateSession.setTerminationTime(currentTimestamp);
+        //candidateSessionDao.update(candidateSession);
+
+        /* Record current result state (final) */
+        //candidateDataService.computeAndRecordTestAssessmentResult(candidateSession, testSessionController);
+
+        /* Record and log event */
+        //final CandidateEvent candidateTestEvent = candidateDataService.recordCandidateTestEvent(candidateSession,
+        //        CandidateTestEventType.EXIT_TEST, testSessionState, notificationRecorder);
+        //candidateAuditLogger.logCandidateEvent(candidateTestEvent);
+		
+	}
+	
 	//private CandidateSession enterCandidateSession(final CandidateSession candidateSession)
 	private TestSessionController enterSession() {
 		/* Set up listener to record any notifications */
@@ -329,4 +625,23 @@ public class QTI21DisplayController extends FormBasicController {
 		final int requestedLimitIntValue = requestedLimit.intValue();
 		return requestedLimitIntValue > 0 ? requestedLimitIntValue : JqtiPlus.DEFAULT_TEMPLATE_PROCESSING_LIMIT;
 	}
+	
+	private class CandidateSessionFinisher {
+		
+
+	    public void finishCandidateSession(/* final CandidateSession candidateSession,*/ final AssessmentResult assessmentResult) {
+	        /* Mark session as finished */
+	        //candidateSession.setFinishTime(requestTimestampContext.getCurrentRequestTimestamp());
+
+	        /* Also nullify LIS result info for session. These will be updated later, if pre-conditions match for sending the result back */
+	        //candidateSession.setLisOutcomeReportingStatus(null);
+	        //candidateSession.setLisScore(null);
+	       // candidateSessionDao.update(candidateSession);
+
+	        /* Finally schedule LTI result return (if appropriate and sane) */
+	        //maybeScheduleLtiOutcomes(candidateSession, assessmentResult);
+	    }
+		
+		
+	}
 }
\ No newline at end of file
diff --git a/src/main/java/org/olat/ims/qti21/ui/QTI21FormItem.java b/src/main/java/org/olat/ims/qti21/ui/QTI21FormItem.java
index d88056a62881e74f1cf4e878f9ffcfa1b5d8e1f7..7013804ad8a78b2fe9fbe24212dfbce1f21b6281 100644
--- a/src/main/java/org/olat/ims/qti21/ui/QTI21FormItem.java
+++ b/src/main/java/org/olat/ims/qti21/ui/QTI21FormItem.java
@@ -113,6 +113,17 @@ public class QTI21FormItem extends FormItemImpl {
 			String sub = uri.substring(selectItem.getPath().length());
 			QTIWorksEvent event = new QTIWorksEvent(selectItem, sub, this);
 			getRootForm().fireFormEvent(ureq, event);
+		} else if(uri.startsWith(finishItem.getPath())) {
+			QTIWorksEvent event = new QTIWorksEvent(finishItem, this);
+			getRootForm().fireFormEvent(ureq, event);
+		} else if(uri.startsWith(reviewItem.getPath())) {
+			String sub = uri.substring(reviewItem.getPath().length());
+			QTIWorksEvent event = new QTIWorksEvent(reviewItem, sub, this);
+			getRootForm().fireFormEvent(ureq, event);
+		} else if(uri.startsWith(itemSolution.getPath())) {
+			String sub = uri.substring(itemSolution.getPath().length());
+			QTIWorksEvent event = new QTIWorksEvent(itemSolution, sub, this);
+			getRootForm().fireFormEvent(ureq, event);
 		} else if(uri.startsWith(testPartNavigation.getPath())) {
 			QTIWorksEvent event = new QTIWorksEvent(testPartNavigation, this);
 			getRootForm().fireFormEvent(ureq, event);
@@ -124,6 +135,16 @@ public class QTI21FormItem extends FormItemImpl {
 		} else if(uri.startsWith(endTestPart.getPath())) {
 			QTIWorksEvent event = new QTIWorksEvent(endTestPart, this);
 			getRootForm().fireFormEvent(ureq, event);
+		} else if(uri.startsWith(advanceTestPart.getPath())) {
+			QTIWorksEvent event = new QTIWorksEvent(advanceTestPart, this);
+			getRootForm().fireFormEvent(ureq, event);
+		} else if(uri.startsWith(reviewTestPart.getPath())) {
+			QTIWorksEvent event = new QTIWorksEvent(reviewTestPart, this);
+			getRootForm().fireFormEvent(ureq, event);
+		} else if(uri.startsWith(exitTest.getPath())) {
+			QTIWorksEvent event = new QTIWorksEvent(exitTest, this);
+			getRootForm().fireFormEvent(ureq, event);
+			
 		}
 	}
 	
diff --git a/src/main/java/org/olat/ims/qti21/ui/QTIWorksEvent.java b/src/main/java/org/olat/ims/qti21/ui/QTIWorksEvent.java
index bc7558627dc0d97967507b7f75c45853f6f92cdb..a1890aac075de62f9a1e478ce08f0b857ce6986e 100644
--- a/src/main/java/org/olat/ims/qti21/ui/QTIWorksEvent.java
+++ b/src/main/java/org/olat/ims/qti21/ui/QTIWorksEvent.java
@@ -18,6 +18,40 @@ public class QTIWorksEvent extends FormEvent {
 
 	private static final long serialVersionUID = 7767258131971848645L;
 	
+	public enum Event {
+		source("source", "source"),
+		state("state", "state"),
+		result("result", "result"),
+		validation("validation", "validation"),
+		authorview("author-view", "author-view"),
+		response("response", "response"),//OK
+		testPartNavigation("test-part-navigation", "test-part-navigation"),//OK
+		selectItem("select-item", "select-item/"),//OK
+		finishItem("finish-item", "finish-item"),//impl
+		reviewTestPart("review-test-part", "review-test-part"),//impl
+		reviewItem("review-item", "review-item/"),//impl
+		itemSolution("item-solution", "item-solution/"),
+		endTestPart("end-test-part", "end-test-part"),//OK
+		advanceTestPart("advance-test-part", "advance-test-part"),//OK
+		exitTest("exit-test", "exit-test");//impl
+		
+		private final String path;
+		private final String event;
+		
+		private Event(String event, String path) {
+			this.event = event;
+			this.path = path;
+		}
+
+		public String getPath() {
+			return path;
+		}
+
+		public String event() {
+			return event;
+		}
+	}
+	
 	private final Event event;
 	private final String subCommand;
 	private final Map<Identifier, StringResponseData> stringResponseMap;
@@ -53,38 +87,5 @@ public class QTIWorksEvent extends FormEvent {
 		return stringResponseMap;
 	}
 
-	public enum Event {
-		source("source", "source"),
-		state("state", "state"),
-		result("result", "result"),
-		validation("validation", "validation"),
-		authorview("author-view", "author-view"),
-		response("response", "response"),//OK
-		testPartNavigation("test-part-navigation", "test-part-navigation"),//OK
-		selectItem("select-item", "select-item/"),//OK
-		finishItem("finish-item", "finish-item"),
-		reviewTestPart("review-test-part", "review-test-part"),
-		reviewItem("review-item", "review-item"),
-		itemSolution("item-solution", "item-solution"),
-		endTestPart("end-test-part", "end-test-part"),//OK
-		advanceTestPart("advance-test-part", "advance-test-part"),
-		exitTest("exit-test", "exit-test")
-		;
-		
-		private final String path;
-		private final String event;
-		
-		private Event(String event, String path) {
-			this.event = event;
-			this.path = path;
-		}
 
-		public String getPath() {
-			return path;
-		}
-
-		public String event() {
-			return event;
-		}
-	}
 }
diff --git a/src/main/java/org/olat/ims/qti21/ui/_content/run.html b/src/main/java/org/olat/ims/qti21/ui/_content/run.html
index ac2ba9b1cf2a96d8c89efab780e20e579cddeda7..3c89670ed09a79d10685ccaae05182b50507d2b7 100644
--- a/src/main/java/org/olat/ims/qti21/ui/_content/run.html
+++ b/src/main/java/org/olat/ims/qti21/ui/_content/run.html
@@ -1,2 +1 @@
-<h1>Run</h1>
 $r.render("qtirun")
\ No newline at end of file
diff --git a/src/main/resources/database/mysql/alter_10_x_0_to_11_0_0.sql b/src/main/resources/database/mysql/alter_10_x_0_to_11_0_0.sql
new file mode 100644
index 0000000000000000000000000000000000000000..9439252ea3cc248632fa661fa95dee68ae71c7a7
--- /dev/null
+++ b/src/main/resources/database/mysql/alter_10_x_0_to_11_0_0.sql
@@ -0,0 +1,20 @@
+create table o_qti_test_session (
+   id bigint not null,
+   creationdate datetime not null,
+   lastmodified datetime not null,
+   q_exploded bit not null default 0,
+   q_author_mode bit not null default 0,
+   q_finish_time datetime,
+   q_termination_time datetime,
+   fk_identity bigint not null,
+   fk_entry bigint not null,
+   fk_course bigint,
+   primary key (id)
+);
+alter table o_qti_test_session ENGINE = InnoDB;
+
+alter table o_qti_test_session add constraint qti_sess_to_repo_entry_idx foreign key (fk_entry) references o_repositoryentry (repositoryentry_id);
+alter table o_qti_test_session add constraint qti_sess_to_course_entry_idx foreign key (fk_course) references o_repositoryentry (repositoryentry_id);
+alter table o_qti_test_session add constraint qti_sess_to_identity_idx foreign key (fk_identity) references o_bs_identity (id);
+
+
diff --git a/src/main/resources/database/postgresql/alter_10_x_0_to_11_0_0.sql b/src/main/resources/database/postgresql/alter_10_x_0_to_11_0_0.sql
new file mode 100644
index 0000000000000000000000000000000000000000..fff55f2c2f3d86008e6f09d0d65610117bfb5b87
--- /dev/null
+++ b/src/main/resources/database/postgresql/alter_10_x_0_to_11_0_0.sql
@@ -0,0 +1,20 @@
+create table o_qti_test_session (
+   id int8 not null,
+   creationdate timestamp not null,
+   lastmodified timestamp not null,
+   q_exploded bool default false,
+   q_author_mode bool default false,
+   q_finish_time timestamp,
+   q_termination_time timestamp,
+   fk_identity int8 not null,
+   fk_entry int8 not null,
+   fk_course int8,
+   primary key (id)
+);
+
+alter table o_qti_test_session add constraint qti_sess_to_repo_entry_idx foreign key (fk_entry) references o_repositoryentry (repositoryentry_id);
+create index idx_testess_to_repo_entry_idx on o_qti_test_session (fk_entry);
+alter table o_qti_test_session add constraint qti_sess_to_course_entry_idx foreign key (fk_course) references o_repositoryentry (repositoryentry_id);
+create index idx_qti_sess_to_course_entry_idx on o_qti_test_session (fk_course);
+alter table o_qti_test_session add constraint qti_sess_to_identity_idx foreign key (fk_identity) references o_bs_identity (id);
+create index idx_qti_sess_to_identity_idx on o_qti_test_session (fk_identity);
\ No newline at end of file
diff --git a/src/main/resources/rendering-xslt/test-testpart-feedback.xsl b/src/main/resources/rendering-xslt/test-testpart-feedback.xsl
index ea740719b71feba7fda7dd0fc07628793c3c7c57..4c754d6e87b9e445198ae76b115af1e8f16c3f01 100644
--- a/src/main/resources/rendering-xslt/test-testpart-feedback.xsl
+++ b/src/main/resources/rendering-xslt/test-testpart-feedback.xsl
@@ -108,7 +108,7 @@ Renders the test(Part) feedback
     <xsl:if test="$currentTestPart/@navigationMode='nonlinear' or exists($itemSessionState/@entryTime)">
       <li class="assessmentItem">
         <form action="{$webappContextPath}{$reviewTestItemUrl}/{@key}" method="post">
-          <button type="submit">
+          <button type="submit" class="btn btn-default">
             <xsl:if test="not($reviewable)">
               <xsl:attribute name="disabled" select="'disabled'"/>
             </xsl:if>
diff --git a/src/main/resources/rendering-xslt/test-testpart-navigation.xsl b/src/main/resources/rendering-xslt/test-testpart-navigation.xsl
index 61a7057a7edd3a9d220a257e2bc808b41e5dafd6..05cab1481c7d8b8686a7311eba796963ee256faf 100644
--- a/src/main/resources/rendering-xslt/test-testpart-navigation.xsl
+++ b/src/main/resources/rendering-xslt/test-testpart-navigation.xsl
@@ -89,7 +89,7 @@ Renders the navigation for the current testPart
     <xsl:variable name="itemSessionState" select="$testSessionState/qw:item[@key=current()/@key]/qw:itemSessionState" as="element(qw:itemSessionState)"/>
     <li class="assessmentItem">
       <form action="{$webappContextPath}{$selectTestItemUrl}/{@key}" method="post">
-        <button type="submit">
+        <button type="submit" class="btn btn-default">
           <span class="questionTitle"><xsl:value-of select="@sectionPartTitle"/></span>
           <xsl:apply-templates select="$itemSessionState" mode="item-status"/>
         </button>
diff --git a/src/test/java/org/olat/ims/qti21/manager/TestSessionDAOTest.java b/src/test/java/org/olat/ims/qti21/manager/TestSessionDAOTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..57aae8f9c000c6fdaabbe889e6fb23e7a787c128
--- /dev/null
+++ b/src/test/java/org/olat/ims/qti21/manager/TestSessionDAOTest.java
@@ -0,0 +1,38 @@
+package org.olat.ims.qti21.manager;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.olat.core.commons.persistence.DB;
+import org.olat.core.id.Identity;
+import org.olat.ims.qti21.UserTestSession;
+import org.olat.repository.RepositoryEntry;
+import org.olat.test.JunitTestHelper;
+import org.olat.test.OlatTestCase;
+import org.springframework.beans.factory.annotation.Autowired;
+
+/**
+ * 
+ * Initial date: 12.05.2015<br>
+ * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
+ *
+ */
+public class TestSessionDAOTest extends OlatTestCase {
+	
+	@Autowired
+	private DB dbInstance;
+	@Autowired
+	private TestSessionDAO testSessionDao;
+	
+	@Test
+	public void createTestSession() {
+		// prepare a test and a user
+		RepositoryEntry testEntry = JunitTestHelper.createAndPersistRepositoryEntry();
+		Identity assessedIdentity = JunitTestHelper.createAndPersistIdentityAsRndUser("test-session-1");
+		dbInstance.commit();
+		
+		UserTestSession testSession = testSessionDao.createTestSession(testEntry, null, assessedIdentity);
+		Assert.assertNotNull(testSession);
+		dbInstance.commit();
+	}
+
+}