From a545c63fbfe5f7d4f22a62020bd4566d3497efd6 Mon Sep 17 00:00:00 2001
From: srosse <none@none>
Date: Fri, 17 May 2013 10:32:49 +0200
Subject: [PATCH] OO-558: implement LTI 1.1 with outcome service

---
 pom.xml                                       |   2 +-
 .../java/org/olat/_spring/mainContext.xml     |   3 +-
 .../persistence/_spring/core_persistence.xml  |   3 +-
 .../core/dispatcher/mapper/MapperService.java |  12 +
 .../dispatcher/mapper/manager/MapperDAO.java  |  48 +-
 .../mapper/manager/MapperServiceImpl.java     |  19 +-
 .../mapper/model/PersistedMapper.hbm.xml      |  22 -
 .../mapper/model/PersistedMapper.java         |  81 ++-
 .../control/controller/BasicController.java   |  15 +-
 .../org/olat/core/gui/media/ServletUtil.java  |   2 +-
 .../NewCachePersistingAssessmentManager.java  |   8 +-
 .../olat/course/nodes/BasicLTICourseNode.java | 212 +++++++-
 .../basiclti/CourseNodeOutcomeMapper.java     | 180 +++++++
 .../course/nodes/basiclti/LTIConfigForm.java  | 488 +++++++++++++++++-
 .../basiclti/LTICourseNodeConfiguration.java  |  42 --
 .../nodes/basiclti/LTICourseNodeContext.java  | 127 +++++
 .../nodes/basiclti/LTIEditController.java     |  10 +-
 .../nodes/basiclti/LTIRunController.java      | 425 +++++++--------
 .../nodes/basiclti/_content/custom.html       |  14 +
 .../course/nodes/basiclti/_content/edit.html  |  10 +-
 .../nodes/basiclti/_content/overview.html     |  47 ++
 .../course/nodes/basiclti/_content/run.html   |  16 +-
 .../basiclti/_i18n/LocalStrings_de.properties |  25 +
 .../basiclti/_i18n/LocalStrings_en.properties |  17 +
 .../java/org/olat/ims/_spring/imsContext.xml  |  17 +
 .../org/olat/ims/cp/_spring/cpContext.xml     |  10 -
 .../java/org/olat/ims/lti/LTIContext.java     |  57 ++
 .../java/org/olat/ims/lti/LTIManager.java     |  54 ++
 .../java/org/olat/ims/lti/LTIOutcome.java     |  53 ++
 .../olat/ims/lti/manager/LTIManagerImpl.java  | 287 ++++++++++
 .../olat/ims/lti/model/LTIOutcomeImpl.java    | 202 ++++++++
 .../lti/ui/LTIResultDetailsController.java    | 111 ++++
 .../org/olat/ims/lti/ui/OutcomeMapper.java    | 186 +++++++
 .../org/olat/ims/lti/ui/PostDataMapper.java   |  50 ++
 .../org/olat/ims/lti/ui/TalkBackMapper.java   |  56 ++
 .../lti/ui/_i18n/LocalStrings_de.properties   |   5 +
 .../lti/ui/_i18n/LocalStrings_en.properties   |   4 +
 .../lti/ui/_i18n/LocalStrings_fr.properties   |   4 +
 .../org/olat/ims/qti/_spring/qtiContext.xml   |   7 +-
 .../org/olat/modules/ModuleConfiguration.java |  17 +
 .../_spring/userPropertiesContext.xml         |  16 +
 .../database/mysql/alter_8_4_0_to_9_0_0.sql   |  23 +
 .../database/mysql/setupDatabase.sql          |  33 +-
 .../database/oracle/alter_8_4_0_to_9_0_0.sql  |  22 +
 .../database/oracle/setupDatabase.sql         |  18 +
 .../postgresql/alter_8_4_0_to_9_0_0.sql       |  29 +-
 .../database/postgresql/setupDatabase.sql     |  19 +
 .../core/dispatcher/mapper/MapperDAOTest.java |  46 +-
 .../java/org/olat/ims/lti/LTIManagerTest.java | 102 ++++
 .../java/org/olat/test/AllTestsJunit4.java    |   1 +
 50 files changed, 2852 insertions(+), 405 deletions(-)
 delete mode 100644 src/main/java/org/olat/core/dispatcher/mapper/model/PersistedMapper.hbm.xml
 create mode 100644 src/main/java/org/olat/course/nodes/basiclti/CourseNodeOutcomeMapper.java
 create mode 100644 src/main/java/org/olat/course/nodes/basiclti/LTICourseNodeContext.java
 create mode 100644 src/main/java/org/olat/course/nodes/basiclti/_content/custom.html
 create mode 100644 src/main/java/org/olat/course/nodes/basiclti/_content/overview.html
 create mode 100644 src/main/java/org/olat/ims/_spring/imsContext.xml
 delete mode 100644 src/main/java/org/olat/ims/cp/_spring/cpContext.xml
 create mode 100644 src/main/java/org/olat/ims/lti/LTIContext.java
 create mode 100644 src/main/java/org/olat/ims/lti/LTIManager.java
 create mode 100644 src/main/java/org/olat/ims/lti/LTIOutcome.java
 create mode 100644 src/main/java/org/olat/ims/lti/manager/LTIManagerImpl.java
 create mode 100644 src/main/java/org/olat/ims/lti/model/LTIOutcomeImpl.java
 create mode 100644 src/main/java/org/olat/ims/lti/ui/LTIResultDetailsController.java
 create mode 100644 src/main/java/org/olat/ims/lti/ui/OutcomeMapper.java
 create mode 100644 src/main/java/org/olat/ims/lti/ui/PostDataMapper.java
 create mode 100644 src/main/java/org/olat/ims/lti/ui/TalkBackMapper.java
 create mode 100644 src/main/java/org/olat/ims/lti/ui/_i18n/LocalStrings_de.properties
 create mode 100644 src/main/java/org/olat/ims/lti/ui/_i18n/LocalStrings_en.properties
 create mode 100644 src/main/java/org/olat/ims/lti/ui/_i18n/LocalStrings_fr.properties
 create mode 100644 src/test/java/org/olat/ims/lti/LTIManagerTest.java

diff --git a/pom.xml b/pom.xml
index a8be5bc61a7..d4703f93316 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1703,7 +1703,7 @@
 		<dependency>
 			<groupId>org.sakaiproject.basiclti</groupId>
 			<artifactId>basiclti-util</artifactId>
-			<version>1.4</version>
+			<version>2.2-SNAPSHOT</version>
 		</dependency>
 		<!-- J2EE dependencies but provided -->
 		<dependency>
diff --git a/src/main/java/org/olat/_spring/mainContext.xml b/src/main/java/org/olat/_spring/mainContext.xml
index d2310dfe2db..ddc4e5aafc6 100644
--- a/src/main/java/org/olat/_spring/mainContext.xml
+++ b/src/main/java/org/olat/_spring/mainContext.xml
@@ -29,8 +29,7 @@
 	<import resource="classpath:/org/olat/group/_spring/businessGroupContext.xml"/>
 	<import resource="classpath:/org/olat/gui/demo/_spring/guiDemoContext.xml"/>
 	<import resource="classpath:/org/olat/home/_spring/homeContext.xml"/>
-	<import resource="classpath:/org/olat/ims/cp/_spring/cpContext.xml"/>
-	<import resource="classpath:/org/olat/ims/qti/_spring/qtiContext.xml"/>
+	<import resource="classpath:/org/olat/ims/_spring/imsContext.xml"/>
 	<import resource="classpath:/org/olat/instantMessaging/_spring/instantMessagingContext.xml"/>
 	<import resource="classpath:/org/olat/ldap/_spring/ldapContext.xml"/>
 	<import resource="classpath:/org/olat/login/_spring/loginContext.xml"/>
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 09cfe2eb396..0b2db1bc748 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
@@ -76,7 +76,6 @@
 		<mapping-file>org/olat/portfolio/model/structel/StructureElement.hbm.xml</mapping-file>
 		<mapping-file>org/olat/portfolio/model/notification/Notifications.hbm.xml</mapping-file>
 		<mapping-file>org/olat/portfolio/model/restriction/CollectRestriction.hbm.xml</mapping-file>
-		<mapping-file>org/olat/core/dispatcher/mapper/model/PersistedMapper.hbm.xml</mapping-file>
 		<mapping-file>org/olat/core/commons/services/commentAndRating/impl/UserCommentImpl.hbm.xml</mapping-file>
 		<mapping-file>org/olat/core/commons/services/commentAndRating/impl/UserRatingImpl.hbm.xml</mapping-file>
 		<mapping-file>org/olat/core/commons/services/mark/impl/MarkImpl.hbm.xml</mapping-file>
@@ -88,6 +87,7 @@
 		<mapping-file>org/olat/course/db/impl/CourseDBEntryImpl.hbm.xml</mapping-file>
 		<mapping-file>org/olat/modules/coach/model/EfficiencyStatementStatEntry.hbm.xml</mapping-file>
 		
+		<class>org.olat.core.dispatcher.mapper.model.PersistedMapper</class>
 		<class>org.olat.group.model.BusinessGroupParticipantViewImpl</class>
 		<class>org.olat.group.model.BusinessGroupOwnerViewImpl</class>
 		<class>org.olat.instantMessaging.model.InstantMessageImpl</class>
@@ -112,6 +112,7 @@
 		<class>org.olat.modules.qpool.model.QEducationalContext</class>
 		<class>org.olat.modules.qpool.model.QItemType</class>
 		<class>org.olat.modules.qpool.model.QLicense</class>
+		<class>org.olat.ims.lti.model.LTIOutcomeImpl</class>
 		<properties>
 			<property name="hibernate.generate_statistics" value="true"/>
 			<property name="hibernate.archive.autodetection" value=""/>
diff --git a/src/main/java/org/olat/core/dispatcher/mapper/MapperService.java b/src/main/java/org/olat/core/dispatcher/mapper/MapperService.java
index 4ffcc9f2a59..eaa7dd60cb1 100644
--- a/src/main/java/org/olat/core/dispatcher/mapper/MapperService.java
+++ b/src/main/java/org/olat/core/dispatcher/mapper/MapperService.java
@@ -79,6 +79,18 @@ public interface MapperService {
 	 */
 	public String register(UserSession session, String mapperId, Mapper mapper);
 	
+	/**
+	 * Same as above but with a defined expiration time
+	 * @param session
+	 * @param mapperId
+	 * @param mapper
+	 * @param expiration Expiration time in seconds
+	 * @return
+	 */
+	public String register(UserSession session, String mapperId, Mapper mapper, int expiration);
+	
+	
+	
 	public Mapper getMapperById(UserSession session, String id);
 	
 	public void cleanUp(String sessionId);
diff --git a/src/main/java/org/olat/core/dispatcher/mapper/manager/MapperDAO.java b/src/main/java/org/olat/core/dispatcher/mapper/manager/MapperDAO.java
index 36b95908365..556079f676d 100644
--- a/src/main/java/org/olat/core/dispatcher/mapper/manager/MapperDAO.java
+++ b/src/main/java/org/olat/core/dispatcher/mapper/manager/MapperDAO.java
@@ -20,6 +20,7 @@
 package org.olat.core.dispatcher.mapper.manager;
 
 import java.io.Serializable;
+import java.util.Calendar;
 import java.util.Date;
 import java.util.List;
 
@@ -43,10 +44,17 @@ public class MapperDAO {
 	@Autowired
 	private DB dbInstance;
 	
-	public PersistedMapper persistMapper(String sessionId, String mapperId, Serializable mapper) {
+	public PersistedMapper persistMapper(String sessionId, String mapperId, Serializable mapper, int expirationTime) {
 		PersistedMapper m = new PersistedMapper();
 		m.setMapperId(mapperId);
-		m.setLastModified(new Date());
+		Date currentDate = new Date();
+		m.setLastModified(currentDate);
+		if(expirationTime > 0) {
+			Calendar cal = Calendar.getInstance();
+			cal.setTime(currentDate);
+			cal.add(Calendar.SECOND, expirationTime);
+			m.setExpirationDate(cal.getTime());
+		}
 		m.setOriginalSessionId(sessionId);
 		
 		String configuration = XStreamHelper.createXStreamInstance().toXML(mapper);
@@ -56,7 +64,7 @@ public class MapperDAO {
 		return m;
 	}
 	
-	public boolean updateConfiguration(String mapperId, Serializable mapper) {
+	public boolean updateConfiguration(String mapperId, Serializable mapper, int expirationTime) {
 		PersistedMapper m = loadForUpdate(mapperId);
 		if(m == null) {
 			return false;
@@ -64,18 +72,22 @@ public class MapperDAO {
 		
 		String configuration = XStreamHelper.createXStreamInstance().toXML(mapper);
 		m.setXmlConfiguration(configuration);
-		m.setLastModified(new Date());
+		Date currentDate = new Date();
+		m.setLastModified(currentDate);
+		if(expirationTime > 0) {
+			Calendar cal = Calendar.getInstance();
+			cal.setTime(currentDate);
+			cal.add(Calendar.SECOND, expirationTime);
+			m.setExpirationDate(cal.getTime());
+		}
+
 		dbInstance.getCurrentEntityManager().merge(m);
 		return true;
 	}
 	
 	private PersistedMapper loadForUpdate(String mapperId) {
-		StringBuilder q = new StringBuilder();
-		q.append("select mapper from ").append(PersistedMapper.class.getName()).append(" as mapper ")
-		 .append(" where mapper.mapperId=:mapperId order by mapper.key");
-		
 		List<PersistedMapper> mappers = dbInstance.getCurrentEntityManager()
-				.createQuery(q.toString(), PersistedMapper.class)
+				.createNamedQuery("loadMapperByKeyOrdered", PersistedMapper.class)
 				.setParameter("mapperId", mapperId)
 				.setFirstResult(0)
 				.setMaxResults(1)
@@ -84,24 +96,16 @@ public class MapperDAO {
 	}
 	
 	public PersistedMapper loadByMapperId(String mapperId) {
-		StringBuilder q = new StringBuilder();
-		q.append("select mapper from ").append(PersistedMapper.class.getName()).append(" as mapper ")
-		 .append(" where mapper.mapperId=:mapperId");
-		
 		List<PersistedMapper> mappers = dbInstance.getCurrentEntityManager()
-				.createQuery(q.toString(), PersistedMapper.class)
+				.createNamedQuery("loadMapperByKey", PersistedMapper.class)
 				.setParameter("mapperId", mapperId)
 				.getResultList();
 		return mappers.isEmpty() ? null : mappers.get(0);
 	}
 	
 	public Mapper retrieveMapperById(String mapperId) {
-		StringBuilder q = new StringBuilder();
-		q.append("select mapper from ").append(PersistedMapper.class.getName()).append(" as mapper ")
-		 .append(" where mapper.mapperId=:mapperId");
-		
 		List<PersistedMapper> mappers = dbInstance.getCurrentEntityManager()
-				.createQuery(q.toString(), PersistedMapper.class)
+				.createNamedQuery("loadMapperByKey", PersistedMapper.class)
 				.setParameter("mapperId", mapperId)
 				.getResultList();
 		PersistedMapper pm = mappers.isEmpty() ? null : mappers.get(0);
@@ -119,12 +123,14 @@ public class MapperDAO {
 	
 	public int deleteMapperByDate(Date limit) {
 		StringBuilder q = new StringBuilder();
-		q.append("delete from ").append(PersistedMapper.class.getName()).append(" as mapper ")
-		 .append(" where mapper.lastModified<:limit");
+		q.append("delete from pmapper as mapper where ")
+		 .append(" (mapper.expirationDate is null and mapper.lastModified<:limit)")
+		 .append(" or (mapper.expirationDate<:now)");
 
 		return dbInstance.getCurrentEntityManager()
 				.createQuery(q.toString())
 				.setParameter("limit", limit, TemporalType.TIMESTAMP)
+				.setParameter("now", new Date(), TemporalType.TIMESTAMP)
 				.executeUpdate();
 	}
 }
diff --git a/src/main/java/org/olat/core/dispatcher/mapper/manager/MapperServiceImpl.java b/src/main/java/org/olat/core/dispatcher/mapper/manager/MapperServiceImpl.java
index e8cd8370e4c..9951f38cec8 100644
--- a/src/main/java/org/olat/core/dispatcher/mapper/manager/MapperServiceImpl.java
+++ b/src/main/java/org/olat/core/dispatcher/mapper/manager/MapperServiceImpl.java
@@ -103,7 +103,7 @@ public class MapperServiceImpl implements MapperService {
 		}
 		
 		if(mapper instanceof Serializable) {
-			mapperDao.persistMapper(sessionId, mapid, (Serializable)mapper);
+			mapperDao.persistMapper(sessionId, mapid, (Serializable)mapper, -1);
 		}
 		return WebappHelper.getServletContextPath() + DispatcherAction.PATH_MAPPED + mapid;
 	}	
@@ -113,20 +113,25 @@ public class MapperServiceImpl implements MapperService {
 	 */
 	@Override
 	public String register(UserSession session, String mapperId, Mapper mapper) {
+		return register(session, mapperId, mapper, -1);
+	}
+
+	@Override
+	public String register(UserSession session, String mapperId, Mapper mapper, int expirationTime) {
 		String encryptedMapId = Encoder.encrypt(mapperId);
 		MapperKey mapperKey = new MapperKey(session, encryptedMapId);
 		boolean alreadyLoaded = mapperKeyToMapper.containsKey(mapperKey);
 		if(mapper instanceof Serializable) {
 			if(alreadyLoaded) {
-				if(!mapperDao.updateConfiguration(encryptedMapId, (Serializable)mapper)) {
-					mapperDao.persistMapper(null, encryptedMapId, (Serializable)mapper);
+				if(!mapperDao.updateConfiguration(encryptedMapId, (Serializable)mapper, expirationTime)) {
+					mapperDao.persistMapper(null, encryptedMapId, (Serializable)mapper, expirationTime);
 				}
 			} else {
 				PersistedMapper persistedMapper = mapperDao.loadByMapperId(encryptedMapId);
 				if(persistedMapper == null) {
-					mapperDao.persistMapper(null, encryptedMapId, (Serializable)mapper);
+					mapperDao.persistMapper(null, encryptedMapId, (Serializable)mapper, expirationTime);
 				} else {
-					mapperDao.updateConfiguration(encryptedMapId, (Serializable)mapper);
+					mapperDao.updateConfiguration(encryptedMapId, (Serializable)mapper, expirationTime);
 				}
 			}
 		}
@@ -176,7 +181,7 @@ public class MapperServiceImpl implements MapperService {
 				Mapper mapper = mapperKeyToMapper.remove(mapKey);
 				if(mapper != null) {
 					if(mapper instanceof Serializable) {
-						mapperDao.updateConfiguration(mapKey.getMapperId(), (Serializable)mapper);
+						mapperDao.updateConfiguration(mapKey.getMapperId(), (Serializable)mapper, -1);
 					}
 					mapperToMapperKey.remove(mapper);
 				}
@@ -192,7 +197,7 @@ public class MapperServiceImpl implements MapperService {
 			if(mapperKey != null) {
 				mapperKeyToMapper.remove(mapperKey);
 				if(mapper instanceof Serializable) {
-					mapperDao.updateConfiguration(mapperKey.getMapperId(), (Serializable)mapper);
+					mapperDao.updateConfiguration(mapperKey.getMapperId(), (Serializable)mapper, -1);
 				}
 			}
 		}
diff --git a/src/main/java/org/olat/core/dispatcher/mapper/model/PersistedMapper.hbm.xml b/src/main/java/org/olat/core/dispatcher/mapper/model/PersistedMapper.hbm.xml
deleted file mode 100644
index 6adc1fb9d1b..00000000000
--- a/src/main/java/org/olat/core/dispatcher/mapper/model/PersistedMapper.hbm.xml
+++ /dev/null
@@ -1,22 +0,0 @@
-<?xml version="1.0"?>
-<!DOCTYPE hibernate-mapping PUBLIC 
-        "-//Hibernate/Hibernate Mapping DTD//EN"
-        "http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">
-
-<hibernate-mapping default-lazy="false">
-  <class name="org.olat.core.dispatcher.mapper.model.PersistedMapper" table="o_mapper">
-	<cache usage="transactional" />
-
-    <id name="key" column="id" type="long" unsaved-value="null">
-      <generator class="hilo"/>
-    </id>
-
-	<property name="creationDate" column="creationdate" type="timestamp" />
-	<property name="lastModified" column="lastmodified" type="timestamp" />
-	
-	<property name="mapperId" column="mapper_uuid" type="string" />
-	<property name="originalSessionId" column="orig_session_id" type="string" />
-	<property name="xmlConfiguration" column="xml_config" type="string" />
-  </class>  
-</hibernate-mapping>
-
diff --git a/src/main/java/org/olat/core/dispatcher/mapper/model/PersistedMapper.java b/src/main/java/org/olat/core/dispatcher/mapper/model/PersistedMapper.java
index bf2d89ee021..0a4a5afb923 100644
--- a/src/main/java/org/olat/core/dispatcher/mapper/model/PersistedMapper.java
+++ b/src/main/java/org/olat/core/dispatcher/mapper/model/PersistedMapper.java
@@ -21,21 +21,79 @@ package org.olat.core.dispatcher.mapper.model;
 
 import java.util.Date;
 
-import org.olat.core.commons.persistence.PersistentObject;
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.GeneratedValue;
+import javax.persistence.Id;
+import javax.persistence.NamedQueries;
+import javax.persistence.NamedQuery;
+import javax.persistence.Table;
+import javax.persistence.Temporal;
+import javax.persistence.TemporalType;
+
+import org.hibernate.annotations.GenericGenerator;
+import org.olat.core.id.CreateInfo;
 import org.olat.core.id.ModifiedInfo;
+import org.olat.core.id.Persistable;
 
 /**
  * 
  * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
  */
-public class PersistedMapper extends PersistentObject implements ModifiedInfo {
+@Entity(name="pmapper")
+@Table(name="o_mapper")
+@NamedQueries({
+	@NamedQuery(name="loadMapperByKeyOrdered", query="select mapper from pmapper as mapper where mapper.mapperId=:mapperId order by mapper.key"),
+	@NamedQuery(name="loadMapperByKey", query="select mapper from pmapper as mapper where mapper.mapperId=:mapperId")
+})
+public class PersistedMapper implements CreateInfo, ModifiedInfo, Persistable {
 
 	private static final long serialVersionUID = 7297417374497607347L;
 	
+	@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;
+	
+	@Temporal(TemporalType.TIMESTAMP)
+	@Column(name="expirationdate", nullable=true, insertable=true, updatable=true)
+	private Date expirationDate;
+
+	@Column(name="mapper_uuid", nullable=true, insertable=true, updatable=false)
 	private String mapperId;
+
+	@Column(name="orig_session_id", nullable=true, insertable=true, updatable=false)
 	private String originalSessionId;
+
+	@Column(name="xml_config", nullable=true, insertable=true, updatable=true)
 	private String xmlConfiguration;
-	private Date lastModified;
+	
+	@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() {
@@ -43,8 +101,16 @@ public class PersistedMapper extends PersistentObject implements ModifiedInfo {
 	}
 
 	@Override
-	public void setLastModified(Date lastModified) {
-		this.lastModified = lastModified;
+	public void setLastModified(Date date) {
+		this.lastModified = date;
+	}
+
+	public Date getExpirationDate() {
+		return expirationDate;
+	}
+
+	public void setExpirationDate(Date expirationDate) {
+		this.expirationDate = expirationDate;
 	}
 
 	public String getMapperId() {
@@ -87,4 +153,9 @@ public class PersistedMapper extends PersistentObject implements ModifiedInfo {
 		}
 		return false;
 	}
+
+	@Override
+	public boolean equalsByPersistableKey(Persistable persistable) {
+		return equals(persistable);
+	}
 }
\ No newline at end of file
diff --git a/src/main/java/org/olat/core/gui/control/controller/BasicController.java b/src/main/java/org/olat/core/gui/control/controller/BasicController.java
index 91aaf73bbf6..adad4f745a7 100644
--- a/src/main/java/org/olat/core/gui/control/controller/BasicController.java
+++ b/src/main/java/org/olat/core/gui/control/controller/BasicController.java
@@ -211,6 +211,19 @@ public abstract class BasicController extends DefaultController {
 	 * @return The mapper base URL
 	 */
 	protected String registerCacheableMapper(UserRequest ureq, String cacheableMapperID, Mapper m) {
+		return registerCacheableMapper(ureq, cacheableMapperID, m, -1);
+	}
+	
+	/**
+	 * Convenience method: registers a cacheable mapper which will be
+	 * automatically deregistered upon dispose of the controller
+	 * @param ureq
+	 * @param cacheableMapperID
+	 * @param m The mapper
+	 * @param expirationTime -1 is the default bevahiour, else is expiration time in seconds
+	 * @return
+	 */
+	protected String registerCacheableMapper(UserRequest ureq, String cacheableMapperID, Mapper m, int expirationTime) {
 		if (mappers == null) {
 			mappers = new ArrayList<Mapper>(2);
 		}
@@ -219,7 +232,7 @@ public abstract class BasicController extends DefaultController {
 			// use non cacheable as fallback
 			mapperBaseURL =  CoreSpringFactory.getImpl(MapperService.class).register(ureq.getUserSession(), m);			
 		} else {
-			mapperBaseURL =  CoreSpringFactory.getImpl(MapperService.class).register(ureq.getUserSession(), cacheableMapperID, m);
+			mapperBaseURL =  CoreSpringFactory.getImpl(MapperService.class).register(ureq.getUserSession(), cacheableMapperID, m, expirationTime);
 		}
 		// registration was successful, add to our mapper list
 		mappers.add(m);
diff --git a/src/main/java/org/olat/core/gui/media/ServletUtil.java b/src/main/java/org/olat/core/gui/media/ServletUtil.java
index ba9935b62d2..6e900c4fb66 100644
--- a/src/main/java/org/olat/core/gui/media/ServletUtil.java
+++ b/src/main/java/org/olat/core/gui/media/ServletUtil.java
@@ -66,7 +66,7 @@ public class ServletUtil {
 	public static void printOutRequestParameter(HttpServletRequest request) {
 		for(Enumeration<String> names=request.getParameterNames(); names.hasMoreElements(); ) {
 			String name = names.nextElement();
-			System.out.println(name + " :: " + request.getParameter(name));
+			log.info(name + " :: " + request.getParameter(name));
 		}
 	}
 	
diff --git a/src/main/java/org/olat/course/assessment/NewCachePersistingAssessmentManager.java b/src/main/java/org/olat/course/assessment/NewCachePersistingAssessmentManager.java
index 8d69839b8a2..f9c46004975 100644
--- a/src/main/java/org/olat/course/assessment/NewCachePersistingAssessmentManager.java
+++ b/src/main/java/org/olat/course/assessment/NewCachePersistingAssessmentManager.java
@@ -363,7 +363,7 @@ public class NewCachePersistingAssessmentManager extends BasicManager implements
 	 * @param score
 	 * @param coursePropManager
 	 */
-	void saveNodeScore(CourseNode courseNode, Identity identity, Identity assessedIdentity, Float score, CoursePropertyManager coursePropManager) {
+	void saveNodeScore(CourseNode courseNode, Identity assessedIdentity, Float score, CoursePropertyManager coursePropManager) {
     // olat:::: introduce a createOrUpdate method in the cpm and also if applicable in the general propertymanager
 		if (score != null) {
 			Property scoreProperty = coursePropManager.findCourseNodeProperty(courseNode, assessedIdentity, null, SCORE);
@@ -438,7 +438,7 @@ public class NewCachePersistingAssessmentManager extends BasicManager implements
 	 * @param passed
 	 * @param coursePropManager
 	 */
-	void saveNodePassed(CourseNode courseNode, Identity identity, Identity assessedIdentity, Boolean passed, CoursePropertyManager coursePropManager) {		
+	void saveNodePassed(CourseNode courseNode, Identity assessedIdentity, Boolean passed, CoursePropertyManager coursePropManager) {		
 		  Property passedProperty = coursePropManager.findCourseNodeProperty(courseNode, assessedIdentity, null, PASSED);
 		  if (passedProperty == null && passed!=null) {					
 			  String pass = passed.toString();
@@ -846,8 +846,8 @@ public class NewCachePersistingAssessmentManager extends BasicManager implements
 				Long attempts = null;
 				Codepoint.codepoint(NewCachePersistingAssessmentManager.class, "doInSyncUpdateUserEfficiencyStatement");
 				log.debug("codepoint reached: doInSyncUpdateUserEfficiencyStatement by identity: " + identity.getName());
-				saveNodeScore(courseNode, identity, assessedIdentity, scoreEvaluation.getScore(), cpm);
-				saveNodePassed(courseNode, identity, assessedIdentity, scoreEvaluation.getPassed(), cpm);
+				saveNodeScore(courseNode, assessedIdentity, scoreEvaluation.getScore(), cpm);
+				saveNodePassed(courseNode, assessedIdentity, scoreEvaluation.getPassed(), cpm);
 				saveAssessmentID(courseNode, assessedIdentity, scoreEvaluation.getAssessmentID(), cpm);				
 				if(incrementUserAttempts) {
 					attempts = incrementNodeAttemptsProperty(courseNode, assessedIdentity, cpm);
diff --git a/src/main/java/org/olat/course/nodes/BasicLTICourseNode.java b/src/main/java/org/olat/course/nodes/BasicLTICourseNode.java
index 54d07fbbb90..ae94108cfdd 100644
--- a/src/main/java/org/olat/course/nodes/BasicLTICourseNode.java
+++ b/src/main/java/org/olat/course/nodes/BasicLTICourseNode.java
@@ -32,30 +32,48 @@ import org.olat.core.gui.components.stack.StackedController;
 import org.olat.core.gui.control.Controller;
 import org.olat.core.gui.control.WindowControl;
 import org.olat.core.gui.control.generic.tabbable.TabbableController;
+import org.olat.core.id.Identity;
+import org.olat.core.logging.OLATRuntimeException;
 import org.olat.core.util.Util;
 import org.olat.course.ICourse;
+import org.olat.course.assessment.AssessmentManager;
 import org.olat.course.editor.CourseEditorEnv;
 import org.olat.course.editor.NodeEditController;
 import org.olat.course.editor.StatusDescription;
-import org.olat.course.nodes.AbstractAccessableCourseNode;
-import org.olat.course.nodes.CourseNode;
-import org.olat.course.nodes.StatusDescriptionHelper;
 import org.olat.course.nodes.basiclti.LTIConfigForm;
 import org.olat.course.nodes.basiclti.LTIEditController;
 import org.olat.course.nodes.basiclti.LTIRunController;
+import org.olat.course.nodes.scorm.ScormEditController;
 import org.olat.course.run.navigation.NodeRunConstructionResult;
+import org.olat.course.run.scoring.ScoreEvaluation;
 import org.olat.course.run.userview.NodeEvaluation;
 import org.olat.course.run.userview.UserCourseEnvironment;
+import org.olat.ims.lti.ui.LTIResultDetailsController;
 import org.olat.modules.ModuleConfiguration;
 import org.olat.repository.RepositoryEntry;
+import org.olat.resource.OLATResource;
 
 /**
  * @author guido
  * @author Charles Severance
  */
-public class BasicLTICourseNode extends AbstractAccessableCourseNode {
+public class BasicLTICourseNode extends AbstractAccessableCourseNode implements AssessableCourseNode {
 
+	private static final long serialVersionUID = 2210572148308757127L;
 	private static final String TYPE = "lti";
+
+	public static final String CONFIG_KEY_AUTHORROLE = "authorRole";
+	public static final String CONFIG_KEY_COACHROLE = "coachRole";
+	public static final String CONFIG_KEY_PARTICIPANTROLE = "participantRole";
+	public static final String CONFIG_KEY_SCALEVALUE = "scaleFactor";
+	public static final String CONFIG_KEY_HAS_SCORE_FIELD = MSCourseNode.CONFIG_KEY_HAS_SCORE_FIELD;
+	public static final String CONFIG_KEY_HAS_PASSED_FIELD = MSCourseNode.CONFIG_KEY_HAS_PASSED_FIELD;
+	public static final String CONFIG_KEY_PASSED_CUT_VALUE = MSCourseNode.CONFIG_KEY_PASSED_CUT_VALUE;
+	public static final String CONFIG_HEIGHT = "displayHeight";
+	public static final String CONFIG_WIDTH = "displayWidth";
+	public static final String CONFIG_HEIGHT_AUTO = ScormEditController.CONFIG_HEIGHT_AUTO;
+	public static final String CONFIG_DISPLAY = "display";
+	
 	
 	// NLS support:
 	
@@ -89,10 +107,13 @@ public class BasicLTICourseNode extends AbstractAccessableCourseNode {
 	 *      org.olat.course.run.userview.UserCourseEnvironment,
 	 *      org.olat.course.run.userview.NodeEvaluation)
 	 */
+	@Override
 	public NodeRunConstructionResult createNodeRunConstructionResult(UserRequest ureq, WindowControl wControl,
 			UserCourseEnvironment userCourseEnv, NodeEvaluation ne, String nodecmd) {
 		updateModuleConfigDefaults(false);
-		return new NodeRunConstructionResult(new LTIRunController(wControl, getModuleConfiguration(), ureq, this, userCourseEnv.getCourseEnvironment()));
+		LTIRunController runCtrl = new LTIRunController(wControl, getModuleConfiguration(), ureq, this, userCourseEnv);
+		Controller ctrl = TitledWrapperHelper.getWrapper(ureq, wControl, runCtrl, this, "o_lti_icon");
+		return new NodeRunConstructionResult(ctrl);
 	}
 
 	/**
@@ -101,6 +122,7 @@ public class BasicLTICourseNode extends AbstractAccessableCourseNode {
 	 *      org.olat.course.run.userview.UserCourseEnvironment,
 	 *      org.olat.course.run.userview.NodeEvaluation)
 	 */
+	@Override
 	public Controller createPreviewController(UserRequest ureq, WindowControl wControl, UserCourseEnvironment userCourseEnv, NodeEvaluation ne) {
 		return createNodeRunConstructionResult(ureq, wControl, userCourseEnv, ne, null).getRunController();
 	}
@@ -108,6 +130,7 @@ public class BasicLTICourseNode extends AbstractAccessableCourseNode {
 	/**
 	 * @see org.olat.course.nodes.CourseNode#isConfigValid()
 	 */
+	@Override
 	public StatusDescription isConfigValid() {
 
 		/*
@@ -133,6 +156,7 @@ public class BasicLTICourseNode extends AbstractAccessableCourseNode {
 	/**
 	 * @see org.olat.course.nodes.CourseNode#isConfigValid(org.olat.course.run.userview.UserCourseEnvironment)
 	 */
+	@Override
 	public StatusDescription[] isConfigValid(CourseEditorEnv cev) {
 		oneClickStatusCache = null;
 		// only here we know which translator to take for translating condition
@@ -146,6 +170,7 @@ public class BasicLTICourseNode extends AbstractAccessableCourseNode {
 	/**
 	 * @see org.olat.course.nodes.CourseNode#getReferencedRepositoryEntry()
 	 */
+	@Override
 	public RepositoryEntry getReferencedRepositoryEntry() {
 		return null;
 	}
@@ -153,6 +178,7 @@ public class BasicLTICourseNode extends AbstractAccessableCourseNode {
 	/**
 	 * @see org.olat.course.nodes.CourseNode#needsReferenceToARepositoryEntry()
 	 */
+	@Override
 	public boolean needsReferenceToARepositoryEntry() {
 		return false;
 	}
@@ -165,6 +191,7 @@ public class BasicLTICourseNode extends AbstractAccessableCourseNode {
 	 *          from previous node configuration version, set default to maintain
 	 *          previous behaviour
 	 */
+	@Override
 	public void updateModuleConfigDefaults(boolean isNewNode) {
 		ModuleConfiguration config = getModuleConfiguration();
 		if (isNewNode) {
@@ -185,4 +212,177 @@ public class BasicLTICourseNode extends AbstractAccessableCourseNode {
 		}
 	}
 
-}
+	@Override
+	public Float getMaxScoreConfiguration() {
+		if (!hasScoreConfigured()) {
+			throw new OLATRuntimeException(MSCourseNode.class, "getMaxScore not defined when hasScore set to false", null);
+		}
+		ModuleConfiguration config = getModuleConfiguration();
+		Float scaleFactor = (Float) config.get(CONFIG_KEY_SCALEVALUE);
+		if(scaleFactor == null || scaleFactor.floatValue() < 0.0000001f) {
+			return new Float(1.0f);
+		}
+		return 1.0f * scaleFactor.floatValue();//LTI 1.1 return between 0.0 - 1.0
+	}
+
+	@Override
+	public Float getMinScoreConfiguration() {
+		if (!hasScoreConfigured()) { 
+			throw new OLATRuntimeException(MSCourseNode.class, "getMaxScore not defined when hasScore set to false", null);
+		}
+		return new Float(0.0f);
+	}
+
+	@Override
+	public Float getCutValueConfiguration() {
+		if (!hasPassedConfigured()) { 
+			throw new OLATRuntimeException(MSCourseNode.class, "getCutValue not defined when hasPassed set to false", null);
+		}
+		ModuleConfiguration config = getModuleConfiguration();
+		return config.getFloatEntry(CONFIG_KEY_PASSED_CUT_VALUE);
+	}
+
+	@Override
+	public boolean hasScoreConfigured() {
+		ModuleConfiguration config = getModuleConfiguration();
+		Boolean score = config.getBooleanEntry(CONFIG_KEY_HAS_SCORE_FIELD);
+		return (score == null) ? false : score.booleanValue();
+	}
+
+	@Override
+	public boolean hasPassedConfigured() {
+		ModuleConfiguration config = getModuleConfiguration();
+		Boolean passed = config.getBooleanEntry(CONFIG_KEY_HAS_PASSED_FIELD);
+		return (passed == null) ? false : passed.booleanValue();
+	}
+
+	@Override
+	public boolean hasCommentConfigured() {
+		return false;
+	}
+
+	@Override
+	public boolean hasAttemptsConfigured() {
+		return false;
+	}
+
+	@Override
+	public boolean hasDetails() {
+		return true;
+	}
+
+	@Override
+	public boolean hasStatusConfigured() {
+		return false;
+	}
+
+	@Override
+	public boolean isEditableConfigured() {
+		return true;
+	}
+
+	@Override
+	public ScoreEvaluation getUserScoreEvaluation(UserCourseEnvironment userCourseEnv) {
+		// read score from properties
+		AssessmentManager am = userCourseEnv.getCourseEnvironment().getAssessmentManager();
+		Identity mySelf = userCourseEnv.getIdentityEnvironment().getIdentity();
+		Boolean passed = null;
+		Float score = null;
+		// only db lookup if configured, else return null
+		if (hasPassedConfigured()) passed = am.getNodePassed(this, mySelf);
+		if (hasScoreConfigured()) score = am.getNodeScore(this, mySelf);
+
+		ScoreEvaluation se = new ScoreEvaluation(score, passed);
+		return se;
+	}
+
+	@Override
+	public String getUserUserComment(UserCourseEnvironment userCourseEnvironment) {
+		AssessmentManager am = userCourseEnvironment.getCourseEnvironment().getAssessmentManager();
+		Identity mySelf = userCourseEnvironment.getIdentityEnvironment().getIdentity();
+		String userCommentValue = am.getNodeComment(this, mySelf);
+		return userCommentValue;
+	}
+
+	@Override
+	public String getUserCoachComment(UserCourseEnvironment userCourseEnvironment) {
+		AssessmentManager am = userCourseEnvironment.getCourseEnvironment().getAssessmentManager();
+		Identity mySelf = userCourseEnvironment.getIdentityEnvironment().getIdentity();
+		String coachCommentValue = am.getNodeCoachComment(this, mySelf);
+		return coachCommentValue;
+	}
+
+	@Override
+	public String getUserLog(UserCourseEnvironment userCourseEnvironment) {
+		// TODO Auto-generated method stub
+		return null;
+	}
+
+	@Override
+	public Integer getUserAttempts(UserCourseEnvironment userCourseEnvironment) {
+		AssessmentManager am = userCourseEnvironment.getCourseEnvironment().getAssessmentManager();
+		Identity mySelf = userCourseEnvironment.getIdentityEnvironment().getIdentity();
+		Integer userAttemptsValue = am.getNodeAttempts(this, mySelf);
+		return userAttemptsValue;
+	}
+
+	@Override
+	public String getDetailsListView(UserCourseEnvironment userCourseEnvironment) {
+		return null;
+	}
+
+	@Override
+	public String getDetailsListViewHeaderKey() {
+		return null;
+	}
+
+	@Override
+	public Controller getDetailsEditController(UserRequest ureq, WindowControl wControl,
+			StackedController stackPanel, UserCourseEnvironment userCourseEnvironment) {
+		Identity assessedIdentity = userCourseEnvironment.getIdentityEnvironment().getIdentity();
+		OLATResource resource = userCourseEnvironment.getCourseEnvironment().getCourseGroupManager().getCourseResource();
+		return new LTIResultDetailsController(ureq, wControl, assessedIdentity, resource, getIdent());
+	}
+
+	@Override
+	public void updateUserScoreEvaluation(ScoreEvaluation scoreEvaluation, UserCourseEnvironment userCourseEnvironment,
+			Identity coachingIdentity, boolean incrementAttempts) {
+		
+		AssessmentManager am = userCourseEnvironment.getCourseEnvironment().getAssessmentManager();
+		Identity mySelf = userCourseEnvironment.getIdentityEnvironment().getIdentity();
+		am.saveScoreEvaluation(this, coachingIdentity, mySelf, new ScoreEvaluation(scoreEvaluation.getScore(), scoreEvaluation.getPassed()), userCourseEnvironment, incrementAttempts);
+	}
+
+	@Override
+	public void updateUserUserComment(String userComment, UserCourseEnvironment userCourseEnvironment, Identity coachingIdentity) {
+		if (userComment != null) {
+			AssessmentManager am = userCourseEnvironment.getCourseEnvironment().getAssessmentManager();
+			Identity mySelf = userCourseEnvironment.getIdentityEnvironment().getIdentity();
+			am.saveNodeComment(this, coachingIdentity, mySelf, userComment);
+		}
+	}
+
+	@Override
+	public void incrementUserAttempts(UserCourseEnvironment userCourseEnvironment) {
+		AssessmentManager am = userCourseEnvironment.getCourseEnvironment().getAssessmentManager();
+		Identity mySelf = userCourseEnvironment.getIdentityEnvironment().getIdentity();
+		am.incrementNodeAttempts(this, mySelf, userCourseEnvironment);
+	}
+
+	@Override
+	public void updateUserAttempts(Integer userAttempts, UserCourseEnvironment userCourseEnvironment, Identity coachingIdentity) {
+		if (userAttempts != null) {
+			AssessmentManager am = userCourseEnvironment.getCourseEnvironment().getAssessmentManager();
+			Identity mySelf = userCourseEnvironment.getIdentityEnvironment().getIdentity();
+			am.saveNodeAttempts(this, coachingIdentity, mySelf, userAttempts);
+		}
+	}
+
+	@Override
+	public void updateUserCoachComment(String coachComment, UserCourseEnvironment userCourseEnvironment) {
+		AssessmentManager am = userCourseEnvironment.getCourseEnvironment().getAssessmentManager();
+		if (coachComment != null) {
+			am.saveNodeCoachComment(this, userCourseEnvironment.getIdentityEnvironment().getIdentity(), coachComment);
+		}
+	}
+}
\ No newline at end of file
diff --git a/src/main/java/org/olat/course/nodes/basiclti/CourseNodeOutcomeMapper.java b/src/main/java/org/olat/course/nodes/basiclti/CourseNodeOutcomeMapper.java
new file mode 100644
index 00000000000..1a12b7b0464
--- /dev/null
+++ b/src/main/java/org/olat/course/nodes/basiclti/CourseNodeOutcomeMapper.java
@@ -0,0 +1,180 @@
+/**
+ * <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.basiclti;
+
+import java.util.Map;
+import java.util.TreeMap;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.imsglobal.basiclti.XMLMap;
+import org.imsglobal.pox.IMSPOXRequest;
+import org.olat.core.id.Identity;
+import org.olat.core.id.IdentityEnvironment;
+import org.olat.core.logging.activity.IUserActivityLogger;
+import org.olat.core.logging.activity.ThreadLocalUserActivityLoggerInstaller;
+import org.olat.core.logging.activity.UserActivityLoggerImpl;
+import org.olat.course.CourseFactory;
+import org.olat.course.ICourse;
+import org.olat.course.nodes.BasicLTICourseNode;
+import org.olat.course.nodes.CourseNode;
+import org.olat.course.run.scoring.ScoreEvaluation;
+import org.olat.course.run.userview.UserCourseEnvironment;
+import org.olat.course.run.userview.UserCourseEnvironmentImpl;
+import org.olat.ims.lti.ui.OutcomeMapper;
+import org.olat.resource.OLATResource;
+import org.olat.util.logging.activity.LoggingResourceable;
+
+/**
+ * 
+ * Initial date: 14.05.2013<br>
+ * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
+ *
+ */
+public class CourseNodeOutcomeMapper extends OutcomeMapper {
+
+	private static final long serialVersionUID = -4596920091938826925L;
+	
+	private Long courseOresId;
+	private String courseNodeId;
+	
+	public CourseNodeOutcomeMapper() {
+		//
+	}
+	
+	public CourseNodeOutcomeMapper(Identity assessedId, OLATResource resource, String courseNodeId,
+			String oauth_consumer_key, String oauth_secret, String sourcedId) {
+		super(assessedId, resource, courseNodeId, oauth_consumer_key, oauth_secret, sourcedId);
+		this.courseOresId = resource.getResourceableId();
+		this.courseNodeId = courseNodeId;
+	}
+	
+	@Override
+	protected void reconnectUserSession(HttpServletRequest request) {
+		super.reconnectUserSession(request);
+		
+		ThreadLocalUserActivityLoggerInstaller.initUserActivityLogger(request);
+		ICourse course = CourseFactory.loadCourse(courseOresId);
+		CourseNode cn = course.getRunStructure().getNode(courseNodeId);
+		
+		IUserActivityLogger logger = UserActivityLoggerImpl.setupLoggerForController(null);
+		logger.addLoggingResourceInfo(LoggingResourceable.wrap(course));
+		logger.addLoggingResourceInfo(LoggingResourceable.wrap(cn));
+	}
+
+	@Override
+	protected boolean doUpdateResult(Float score) {
+		ICourse course = CourseFactory.loadCourse(courseOresId);
+		CourseNode node = course.getRunStructure().getNode(courseNodeId);
+		if(node instanceof BasicLTICourseNode) {
+			BasicLTICourseNode ltiNode = (BasicLTICourseNode)node;
+			
+			Identity assessedId = getIdentity();
+			Float cutValue = getCutValue(ltiNode);
+			
+			Float scaledScore = null;
+			Boolean passed = null;
+			if(score != null) {
+				float scale = getScalingFactor(ltiNode);
+				scaledScore = score * scale;
+				if(cutValue != null) {
+					passed = scaledScore >= cutValue;
+				}
+			}
+			
+			ScoreEvaluation eval = new ScoreEvaluation(scaledScore, passed);
+			UserCourseEnvironment userCourseEnv = getUserCourseEnvironment(course);
+			ltiNode.updateUserScoreEvaluation(eval, userCourseEnv, assessedId, false);
+		}
+		
+		return super.doUpdateResult(score);
+	}
+
+	@Override
+	protected boolean doDeleteResult() {
+		ICourse course = CourseFactory.loadCourse(courseOresId);
+		CourseNode node = course.getRunStructure().getNode(courseNodeId);
+		if(node instanceof BasicLTICourseNode) {
+			BasicLTICourseNode ltiNode = (BasicLTICourseNode)node;
+			Identity assessedId = getIdentity();
+			ScoreEvaluation eval = new ScoreEvaluation(null, null);
+			UserCourseEnvironment userCourseEnv = getUserCourseEnvironment(course);
+			ltiNode.updateUserScoreEvaluation(eval, userCourseEnv, assessedId, false);
+		}
+
+		return super.doDeleteResult();
+	}
+
+	@Override
+	protected String doReadResult(IMSPOXRequest pox) {
+		ICourse course = CourseFactory.loadCourse(courseOresId);
+		CourseNode node = course.getRunStructure().getNode(courseNodeId);
+		if(node instanceof BasicLTICourseNode) {
+			BasicLTICourseNode ltiNode = (BasicLTICourseNode)node;
+			UserCourseEnvironment userCourseEnv = getUserCourseEnvironment(course);
+			ScoreEvaluation eval = ltiNode.getUserScoreEvaluation(userCourseEnv);
+			String score = "";
+			if(eval != null && eval.getScore() != null) {
+				float scaledScore = eval.getScore();
+				if(scaledScore > 0.0f) {
+					float scale = getScalingFactor(ltiNode);
+					scaledScore= scaledScore / scale;
+				}
+				score = Float.toString(scaledScore);
+			}
+			Map<String,Object> theMap = new TreeMap<String,Object>();
+			theMap.put("/readResultResponse/result/sourcedId", getSourcedId());
+			theMap.put("/readResultResponse/result/resultScore/textString", score);
+			theMap.put("/readResultResponse/result/resultScore/language", "en");
+			String theXml = XMLMap.getXMLFragment(theMap, true);
+			return pox.getResponseSuccess("Result read",theXml);
+		}
+		return super.doReadResult(pox);
+	}
+	
+	private UserCourseEnvironment getUserCourseEnvironment(ICourse course) {
+		IdentityEnvironment identityEnvironment = new IdentityEnvironment();
+		identityEnvironment.setIdentity(getIdentity());
+		UserCourseEnvironmentImpl userCourseEnv = new UserCourseEnvironmentImpl(identityEnvironment, course.getCourseEnvironment());
+		return userCourseEnv;
+	}
+	
+	private float getScalingFactor(BasicLTICourseNode ltiNode) {
+		if(ltiNode.hasScoreConfigured()) {
+			Float scale = ltiNode.getModuleConfiguration().getFloatEntry(BasicLTICourseNode.CONFIG_KEY_SCALEVALUE);
+			if(scale == null) {
+				return 1.0f;
+			}
+			return scale.floatValue();
+		}
+		return 1.0f;
+	}
+	
+	private Float getCutValue(BasicLTICourseNode ltiNode) {
+		if(ltiNode.hasPassedConfigured()) {
+			Float cutValue = ltiNode.getCutValueConfiguration();
+			if(cutValue == null) {
+				return null;
+			}
+			return cutValue;
+		}
+		return null;
+	}
+}
diff --git a/src/main/java/org/olat/course/nodes/basiclti/LTIConfigForm.java b/src/main/java/org/olat/course/nodes/basiclti/LTIConfigForm.java
index 44ce7746feb..3544cc2af13 100644
--- a/src/main/java/org/olat/course/nodes/basiclti/LTIConfigForm.java
+++ b/src/main/java/org/olat/course/nodes/basiclti/LTIConfigForm.java
@@ -27,19 +27,35 @@ package org.olat.course.nodes.basiclti;
 
 import java.net.MalformedURLException;
 import java.net.URL;
+import java.util.ArrayList;
+import java.util.List;
 
+import org.imsglobal.basiclti.BasicLTIUtil;
+import org.olat.core.CoreSpringFactory;
 import org.olat.core.gui.UserRequest;
-
+import org.olat.core.gui.components.form.flexible.FormItem;
 import org.olat.core.gui.components.form.flexible.FormItemContainer;
+import org.olat.core.gui.components.form.flexible.elements.FormLink;
+import org.olat.core.gui.components.form.flexible.elements.MultipleSelectionElement;
 import org.olat.core.gui.components.form.flexible.elements.SelectionElement;
+import org.olat.core.gui.components.form.flexible.elements.SingleSelection;
 import org.olat.core.gui.components.form.flexible.elements.TextElement;
 import org.olat.core.gui.components.form.flexible.impl.FormBasicController;
+import org.olat.core.gui.components.form.flexible.impl.FormEvent;
+import org.olat.core.gui.components.form.flexible.impl.FormLayoutContainer;
+import org.olat.core.gui.components.link.Link;
 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.translator.Translator;
 import org.olat.core.logging.OLATRuntimeException;
+import org.olat.core.util.CodeHelper;
 import org.olat.core.util.StringHelper;
+import org.olat.course.nodes.BasicLTICourseNode;
+import org.olat.ims.lti.LTIManager;
 import org.olat.modules.ModuleConfiguration;
+import org.olat.user.UserManager;
+import org.olat.user.propertyhandlers.UserPropertyHandler;
 
 /**
  * Description:<BR/>
@@ -65,6 +81,8 @@ public class LTIConfigForm extends FormBasicController {
   public static final String CONFIG_KEY_CUSTOM = "custom";
   public static final String CONFIG_KEY_SENDNAME = "sendname";
   public static final String CONFIG_KEY_SENDEMAIL = "sendemail";
+  
+	public static final String usageIdentifyer = LTIManager.class.getCanonicalName();
 	
 	private ModuleConfiguration config;
 	
@@ -75,16 +93,56 @@ public class LTIConfigForm extends FormBasicController {
 	private SelectionElement sendName;
 	private SelectionElement sendEmail;
 	private SelectionElement doDebug;
-	
-	private TextElement tcustom;
-	
+
+	private TextElement scaleFactorEl;
+	private TextElement cutValueEl;
+	private MultipleSelectionElement isAssessableEl;
+	private MultipleSelectionElement authorRoleEl, coachRoleEl, participantRoleEl;
+	private FormLayoutContainer customParamLayout;
+	private SingleSelection displayEl, heightEl, widthEl;
+
 	private String fullURI;
-	private String customConfig;
 	private Boolean sendNameConfig;
 	private Boolean sendEmailConfig;
 	private Boolean doDebugConfig;
+	private boolean isAssessable;
 	private String key, pass;
 	
+	private List<NameValuePair> nameValuePairs = new ArrayList<NameValuePair>();
+	
+
+	private String[] ltiRolesKeys = new String[]{
+			"Learner", "Instructor", "Administrator", "TeachingAssistant", "ContentDeveloper", "Mentor"
+	};
+	private String[] ltiRolesValues = new String[]{
+			"Learner", "Instructor", "Administrator", "TeachingAssistant", "ContentDeveloper", "Mentor"
+	};
+	
+	private String[] displayKeys = new String[]{
+			"iframe", "window"
+	};
+	private String[] displayValues;
+	
+	private String[] customTypeKeys = new String[] {
+		"free", "userprops"	
+	};
+	private String[] customTypeValues;
+	
+	private String[] heightKeys = new String[]{ BasicLTICourseNode.CONFIG_HEIGHT_AUTO, "460", "480", 
+			"500", "520", "540", "560", "580",
+			"600", "620", "640", "660", "680",
+			"700", "720", "730", "760", "780",
+			"800", "820", "840", "860", "880",
+			"900", "920", "940", "960", "980",
+			"1000", "1020", "1040", "1060", "1080",
+			"1100", "1120", "1140", "1160", "1180",
+			"1200", "1220", "1240", "1260", "1280",
+			"1300", "1320", "1340", "1360", "1380"
+	};
+	private String[] heightValues;
+	private String[] userPropKeys;
+	private String[] userPropValues;
+	
 	/**
 	 * Constructor for the tunneling configuration form
 	 * @param name
@@ -96,6 +154,38 @@ public class LTIConfigForm extends FormBasicController {
 		this.config = config;
 		int configVersion = config.getConfigurationVersion();
 		
+		UserManager userManager = CoreSpringFactory.getImpl(UserManager.class);
+		Translator userPropsTranslator = userManager.getPropertyHandlerTranslator(getTranslator());
+		
+		displayValues = new String[]{
+				translate("display.config.window.iframe"), translate("display.config.window.window")
+		};
+		
+		heightValues = new String[]{ translate("height.auto"), "460px", "480px", 
+				"500px", "520px", "540px", "560px", "580px",
+				"600px", "620px", "640px", "660px", "680px",
+				"700px", "720px", "730px", "760px", "780px",
+				"800px", "820px", "840px", "860px", "880px",
+				"900px", "920px", "940px", "960px", "980px",
+				"1000px", "1020px", "1040px", "1060px", "1080px",
+				"1100px", "1120px", "1140px", "1160px", "1180px",
+				"1200px", "1220px", "1240px", "1260px", "1280px",
+				"1300px", "1320px", "1340px", "1360px", "1380px"
+		};
+		
+		customTypeValues = new String[]{
+				translate("display.config.free"), translate("display.config.free.userprops")
+		};
+		
+		List<UserPropertyHandler> userPropertyHandlers = userManager.getUserPropertyHandlersFor(usageIdentifyer, true);
+		userPropKeys = new String[userPropertyHandlers.size()];
+		userPropValues = new String[userPropertyHandlers.size()];
+		for (int i=userPropertyHandlers.size(); i-->0; ) {
+			UserPropertyHandler handler = userPropertyHandlers.get(i);
+			userPropKeys[i] = handler.getName();
+			userPropValues[i] = userPropsTranslator.translate(handler.i18nFormElementLabelKey());
+		}
+		
 		String proto = (String)config.get(CONFIGKEY_PROTO);
 		String host = (String)config.get(CONFIGKEY_HOST);
 		String uri = (String)config.get(CONFIGKEY_URI);
@@ -122,11 +212,12 @@ public class LTIConfigForm extends FormBasicController {
 		sendEmailConfig = config.getBooleanEntry(CONFIG_KEY_SENDEMAIL);
     if (sendEmailConfig == null) sendEmailConfig = Boolean.FALSE;
 
-		customConfig = (String) config.get(CONFIG_KEY_CUSTOM);
-    if (customConfig == null) customConfig = " ";
-
+		
 		doDebugConfig = config.getBooleanEntry(CONFIG_KEY_DEBUG);
     if (doDebugConfig == null) doDebugConfig = Boolean.FALSE;
+    
+    Boolean assessable = config.getBooleanEntry(BasicLTICourseNode.CONFIG_KEY_HAS_SCORE_FIELD);
+    isAssessable = assessable == null ? false : assessable.booleanValue();
 
     initForm(ureq);
 	}
@@ -138,6 +229,7 @@ public class LTIConfigForm extends FormBasicController {
 			
 		thost = uifactory.addTextElement("host", "LTConfigForm.url", 255, fullURI, formLayout);
 		thost.setExampleKey("LTConfigForm.url.example", null);
+		thost.setDisplaySize(64);
 		
 		tkey  = uifactory.addTextElement ("key","LTConfigForm.key", 255, key, formLayout);
 		tkey.setExampleKey ("LTConfigForm.key.example", null);
@@ -151,14 +243,180 @@ public class LTIConfigForm extends FormBasicController {
 		sendEmail = uifactory.addCheckboxesVertical("sendEmail", "display.config.sendEmail", formLayout, new String[]{"xx"}, new String[]{null}, null, 1);
 		sendEmail.select("xx", sendEmailConfig);
 		
-		tcustom = uifactory.addTextAreaElement("tcustom", "display.config.custom", -1, 6, 40, true, customConfig, formLayout);
+		String page = velocity_root + "/custom.html";
+		customParamLayout = FormLayoutContainer.createCustomFormLayout("custom_fields", getTranslator(), page);
+		customParamLayout.setRootForm(mainForm);
+		customParamLayout.setLabel("display.config.custom", null);
+		formLayout.add(customParamLayout);
+		customParamLayout.contextPut("nameValuePairs", nameValuePairs);
+		updateNameValuePair((String)config.get(CONFIG_KEY_CUSTOM));
+		if(nameValuePairs.isEmpty()) {
+			createNameValuePair("", "", -1);
+		}
 		
 		doDebug = uifactory.addCheckboxesVertical("doDebug", "display.config.doDebug", formLayout, new String[]{"xx"}, new String[]{null}, null, 1);
 		doDebug.select("xx", doDebugConfig);
 		
+		uifactory.addSpacerElement("display", formLayout, false);
+		
+		String display = config.getStringValue(BasicLTICourseNode.CONFIG_DISPLAY, "iframe");
+		displayEl = uifactory.addRadiosVertical("display.window", "display.config.window", formLayout, displayKeys, displayValues);
+		for(String displayKey:displayKeys) {
+			if(displayKey.equals(display)) {
+				displayEl.select(displayKey, true);
+			}
+		}
+		
+		String height = config.getStringValue(BasicLTICourseNode.CONFIG_HEIGHT, BasicLTICourseNode.CONFIG_HEIGHT_AUTO);
+		heightEl = uifactory.addDropdownSingleselect("display.height", "display.config.height", formLayout, heightKeys, heightValues, null);
+		for(String heightKey:heightKeys) {
+			if(heightKey.equals(height)) {
+				heightEl.select(heightKey, true);
+			}
+		}
+
+		String width = config.getStringValue(BasicLTICourseNode.CONFIG_WIDTH, BasicLTICourseNode.CONFIG_HEIGHT_AUTO);
+		widthEl = uifactory.addDropdownSingleselect("display.width", "display.config.width", formLayout, heightKeys, heightValues, null);
+		for(String heightKey:heightKeys) {
+			if(heightKey.equals(width)) {
+				widthEl.select(heightKey, true);
+			}
+		}
+
+		uifactory.addSpacerElement("scoring", formLayout, false);
+		
+		//add score info
+		String[] assessableKeys = new String[]{ "on" };
+		String[] assessableValues = new String[]{ "" };
+		isAssessableEl = uifactory.addCheckboxesHorizontal("isassessable", "assessable.label", formLayout, assessableKeys, assessableValues, null);
+		isAssessableEl.addActionListener(this, FormEvent.ONCHANGE);
+		if(isAssessable) {
+			isAssessableEl.select("on", true);
+		}
+	
+    Float scaleValue = config.getFloatEntry(BasicLTICourseNode.CONFIG_KEY_SCALEVALUE);
+		String scaleFactor = scaleValue == null ? "1.0" : scaleValue.toString();
+		scaleFactorEl = uifactory.addTextElement("scale", "scaleFactor", 10, scaleFactor, formLayout);
+		scaleFactorEl.setDisplaySize(3);
+		scaleFactorEl.setVisible(isAssessable);
+		
+		Float cutValue = config.getFloatEntry(BasicLTICourseNode.CONFIG_KEY_PASSED_CUT_VALUE);
+		String cut = cutValue == null ? "" : cutValue.toString();
+		cutValueEl = uifactory.addTextElement("cutvalue", "cutvalue.label", 10, cut, formLayout);
+		cutValueEl.setDisplaySize(3);
+		cutValueEl.setVisible(isAssessable);
+		
+		uifactory.addSpacerElement("roles", formLayout, false);
+
+		authorRoleEl = uifactory.addCheckboxesHorizontal("author", "author.roles", formLayout, ltiRolesKeys, ltiRolesValues, null);
+		udpateRoles(authorRoleEl, BasicLTICourseNode.CONFIG_KEY_AUTHORROLE, "Instructor,Administrator,TeachingAssistant,ContentDeveloper,Mentor"); 
+		coachRoleEl = uifactory.addCheckboxesHorizontal("coach", "coach.roles", formLayout, ltiRolesKeys, ltiRolesValues, null);
+		udpateRoles(coachRoleEl, BasicLTICourseNode.CONFIG_KEY_COACHROLE, "Instructor,TeachingAssistant,Mentor");
+		participantRoleEl = uifactory.addCheckboxesHorizontal("participant", "participant.roles", formLayout, ltiRolesKeys, ltiRolesValues, null);
+		udpateRoles(participantRoleEl, BasicLTICourseNode.CONFIG_KEY_PARTICIPANTROLE, "Learner"); 
+		
+		uifactory.addSpacerElement("buttons", formLayout, false);
 		uifactory.addFormSubmitButton("save", formLayout);
 	}
 	
+	@Override
+	protected void doDispose() {
+		//
+	}
+	
+	private void updateNameValuePair(String custom) {
+		if(StringHelper.containsNonWhitespace(custom)) {
+			String[] params = custom.split("[\n;]");
+			for (int i = 0; i < params.length; i++) {
+				String param = params[i];
+				if (StringHelper.containsNonWhitespace(param)) {
+					int pos = param.indexOf("=");
+					if (pos > 1 && pos + 1 < param.length()) {
+						String key = BasicLTIUtil.mapKeyName(param.substring(0, pos));
+						if(key != null) {
+							String value = param.substring(pos + 1).trim();
+							if (value.length() >= 1) {
+								createNameValuePair(key, value, -1);
+							}
+						}
+					}
+				}
+			}
+		}
+	}
+	
+	private void createNameValuePair(String key, String value, int index) {
+		String guid = Long.toString(CodeHelper.getRAMUniqueID());
+		NameValuePair pair = new NameValuePair(guid);
+		
+		TextElement nameEl = uifactory.addTextElement("name_" + guid, null, 15, key, customParamLayout);
+		nameEl.setDisplaySize(16);
+		pair.setNameEl(nameEl);
+		
+		SingleSelection typeEl = uifactory.addDropdownSingleselect("typ_" + guid, customParamLayout, customTypeKeys, customTypeValues, null);
+		typeEl.setUserObject(pair);
+		typeEl.addActionListener(this, FormEvent.ONCHANGE);
+		pair.setCustomType(typeEl);
+		
+		boolean userprops = value != null && value.startsWith(LTIManager.USER_PROPS_PREFIX);
+		if(userprops) {
+			typeEl.select("userprops", true);
+			value = value.substring(LTIManager.USER_PROPS_PREFIX.length(), value.length());
+		} else {
+			typeEl.select("free", true);
+		}
+		
+		SingleSelection userPropsChoice = uifactory.addDropdownSingleselect("userprops_" + guid, customParamLayout, userPropKeys, userPropValues, null);
+		userPropsChoice.setUserObject(pair);
+		userPropsChoice.setVisible(userprops);
+		if(userprops) {
+			for(String userPropKey:userPropKeys) {
+				if(userPropKey.equals(value)) {
+					userPropsChoice.select(userPropKey, true);
+				}
+			}
+		}
+		pair.setUserPropsChoice(userPropsChoice);
+
+		TextElement valEl = uifactory.addTextElement("val_" + guid, null, 15, value, customParamLayout);
+		valEl.setDisplaySize(16);
+		valEl.setVisible(!userprops);
+		pair.setValueEl(valEl);
+		
+		FormLink addButton = uifactory.addFormLink("add_" + guid, "add", null, customParamLayout, Link.BUTTON_XSMALL);
+		addButton.setUserObject(pair);
+		pair.setAddButton(addButton);
+		FormLink removeButton = uifactory.addFormLink("rm_" + guid, "remove", null, customParamLayout, Link.BUTTON_XSMALL);
+		removeButton.setUserObject(pair);
+		pair.setRemoveButton(removeButton);
+		
+		if(index < 0 || index >= nameValuePairs.size()) {
+			nameValuePairs.add(pair);
+		} else {
+			nameValuePairs.add(index, pair);
+		}
+	}
+	
+	private void udpateRoles(MultipleSelectionElement roleEl, String configKey, String defaultRoles) {
+    Object configRoles = config.get(configKey);
+    String roles = defaultRoles;
+    if(configRoles instanceof String) {
+    	roles = (String)configRoles;
+    }
+    String[] roleArr = roles.split(",");
+    for(String role:roleArr) {
+    	roleEl.select(role, true);
+    }
+	}
+	
+	private String getRoles(MultipleSelectionElement roleEl) {
+		StringBuilder sb = new StringBuilder();
+		for(String key:roleEl.getSelectedKeys()) {
+			if(sb.length() > 0) sb.append(',');
+			sb.append(key);
+		}
+		return sb.toString();
+	}
 	
 	protected static StringBuilder getFullURL(String proto, String host, Integer port, String uri, String query) {
 		StringBuilder fullURL = new StringBuilder();
@@ -185,15 +443,81 @@ public class LTIConfigForm extends FormBasicController {
 
 	@Override
 	protected boolean validateFormLogic(UserRequest ureq) { 
+		boolean allOk = true;
 		try {
 			new URL(thost.getValue());
 		} catch (MalformedURLException e) {
 			thost.setErrorKey("LTConfigForm.invalidurl", null);
-			return false;
+			allOk &= false;
 		}
-		return true;
+		allOk &= validateFloat(cutValueEl);
+		allOk &= validateFloat(scaleFactorEl);
+		return allOk & super.validateFormLogic(ureq);
 	}
 	
+	private boolean validateFloat(TextElement el) {
+		boolean allOk = true;
+		
+		el.clearError();
+		if(el.isVisible()) {
+			String value = el.getValue();
+			if(StringHelper.containsNonWhitespace(value)) {
+				try {
+					Float.parseFloat(value);
+				} catch(Exception e) {
+					el.setErrorKey("form.error.wrongFloat", null);
+					allOk = false;
+				}
+			}
+		}
+		
+		return allOk;
+	}
+	
+	@Override
+	protected void formInnerEvent(UserRequest ureq, FormItem source, FormEvent event) {
+		if(source == isAssessableEl) {
+			boolean assessEnabled = isAssessableEl.isAtLeastSelected(1);
+			scaleFactorEl.setVisible(assessEnabled);
+			cutValueEl.setVisible(assessEnabled);
+			flc.setDirty(true);
+		} else if(source instanceof FormLink && source.getName().startsWith("add_")) {
+			NameValuePair pair = (NameValuePair)source.getUserObject();
+			doAddNameValuePair(pair);
+		} else if(source instanceof FormLink && source.getName().startsWith("rm_")) {
+			NameValuePair pair = (NameValuePair)source.getUserObject();
+			doRemoveNameValuePair(pair);
+		} else if(source instanceof SingleSelection && source.getName().startsWith("typ_")) {
+			NameValuePair pair = (NameValuePair)source.getUserObject();
+			SingleSelection typeChoice = (SingleSelection)source;
+			if("free".equals(typeChoice.getSelectedKey())) {
+				pair.getUserPropsChoice().setVisible(false);
+				pair.getValueEl().setVisible(true);
+			} else if("userprops".equals(typeChoice.getSelectedKey())) {
+				pair.getUserPropsChoice().setVisible(true);
+				pair.getValueEl().setVisible(false);
+			}
+			customParamLayout.setDirty(true);
+		}
+		super.formInnerEvent(ureq, source, event);
+	}
+
+	@Override
+	protected void formOK(UserRequest ureq) {
+		fireEvent (ureq, Event.DONE_EVENT);
+	}
+	
+	private void doAddNameValuePair(NameValuePair parentPair) {
+		int index = nameValuePairs.indexOf(parentPair);
+		createNameValuePair("", "", index + 1);
+		customParamLayout.setDirty(true);
+	}
+	
+	private void doRemoveNameValuePair(NameValuePair pair) {
+		nameValuePairs.remove(pair);
+		customParamLayout.setDirty(true);
+	}
+
 	/**
 	 * @return the updated module configuration using the form data
 	 */
@@ -214,11 +538,81 @@ public class LTIConfigForm extends FormBasicController {
 		config.set(CONFIGKEY_KEY, getFormKey());
 		config.set(CONFIGKEY_PASS, tpass.getValue());
 		config.set(CONFIG_KEY_DEBUG, Boolean.toString(doDebug.isSelected(0)));
-		config.set(CONFIG_KEY_CUSTOM, tcustom.getValue());
+		config.set(CONFIG_KEY_CUSTOM, getCustomConfig());
 		config.set(CONFIG_KEY_SENDNAME, Boolean.toString(sendName.isSelected(0)));
 		config.set(CONFIG_KEY_SENDEMAIL, Boolean.toString(sendEmail.isSelected(0)));
+		
+		if(isAssessableEl.isAtLeastSelected(1)) {
+			config.setBooleanEntry(BasicLTICourseNode.CONFIG_KEY_HAS_SCORE_FIELD, Boolean.TRUE);
+			
+			String scaleValue = scaleFactorEl.getValue();
+			Float scaleVal = Float.parseFloat(scaleValue);
+			if(scaleVal.floatValue() > 0.0f) {
+				config.set(BasicLTICourseNode.CONFIG_KEY_SCALEVALUE, scaleVal);
+			} else {
+				config.remove(BasicLTICourseNode.CONFIG_KEY_SCALEVALUE);
+			}
+
+			String cutValue = cutValueEl.getValue();
+			Float cutVal = (StringHelper.containsNonWhitespace(cutValue)) ? Float.parseFloat(cutValue) : null;
+			if(cutVal != null && cutVal.floatValue() > 0.0f) {
+				config.setBooleanEntry(BasicLTICourseNode.CONFIG_KEY_HAS_PASSED_FIELD, Boolean.TRUE);
+				config.set(BasicLTICourseNode.CONFIG_KEY_PASSED_CUT_VALUE, cutValue);
+			} else {
+				config.setBooleanEntry(BasicLTICourseNode.CONFIG_KEY_HAS_PASSED_FIELD, Boolean.FALSE);
+				config.remove(BasicLTICourseNode.CONFIG_KEY_PASSED_CUT_VALUE);
+			}	
+		} else {
+			config.setBooleanEntry(BasicLTICourseNode.CONFIG_KEY_HAS_SCORE_FIELD, Boolean.FALSE);
+			config.setBooleanEntry(BasicLTICourseNode.CONFIG_KEY_HAS_PASSED_FIELD, Boolean.FALSE);
+			config.remove(BasicLTICourseNode.CONFIG_KEY_SCALEVALUE);
+			config.remove(BasicLTICourseNode.CONFIG_KEY_PASSED_CUT_VALUE);
+		}
+
+		config.set(BasicLTICourseNode.CONFIG_KEY_AUTHORROLE, getRoles(authorRoleEl));
+		config.set(BasicLTICourseNode.CONFIG_KEY_COACHROLE, getRoles(coachRoleEl));
+		config.set(BasicLTICourseNode.CONFIG_KEY_PARTICIPANTROLE, getRoles(participantRoleEl));
+		
+		if(displayEl.isOneSelected()) {
+			config.set(BasicLTICourseNode.CONFIG_DISPLAY, displayEl.getSelectedKey());
+		} else {
+			config.set(BasicLTICourseNode.CONFIG_DISPLAY, "iframe");
+		}
+		if(heightEl.isOneSelected()) {
+			config.set(BasicLTICourseNode.CONFIG_HEIGHT, heightEl.getSelectedKey());
+		}
+		if(widthEl.isOneSelected()) {
+			config.set(BasicLTICourseNode.CONFIG_WIDTH, widthEl.getSelectedKey());
+		}
 		return config;
 	}
+	
+	private String getCustomConfig() {
+		StringBuilder sb = new StringBuilder();
+		for(NameValuePair pair:nameValuePairs) {
+			String key = pair.getNameEl().getValue();
+			if(!StringHelper.containsNonWhitespace(key)
+					|| !pair.getCustomType().isOneSelected()) {
+				continue;
+			}
+			String value = null;
+			String type = pair.getCustomType().getSelectedKey();
+			if("free".equals(type)) {
+				value = pair.getValueEl().getValue();
+			} else if("userprops".equals(type)) {
+				if(pair.getUserPropsChoice().isOneSelected()) {
+					value = LTIManager.USER_PROPS_PREFIX + pair.getUserPropsChoice().getSelectedKey();
+				}
+			}
+			if(!StringHelper.containsNonWhitespace(value)) {
+				continue;
+			}
+				
+			if(sb.length() > 0) sb.append(";");
+			sb.append(key).append('=').append(value);
+		}
+		return sb.toString();
+	}
 
 	private String getFormKey() {
 		if (StringHelper.containsNonWhitespace(tkey.getValue()))
@@ -226,14 +620,70 @@ public class LTIConfigForm extends FormBasicController {
 		else 
 			return null;
 	}
+	
+	public static class NameValuePair {
+		private TextElement nameEl;
+		private TextElement valueEl;
+		private SingleSelection customType;
+		private SingleSelection userPropsChoice;
+		private FormLink addButton;
+		private FormLink removeButton;
+		private final String guid;
+		
+		public NameValuePair(String guid) {
+			this.guid = guid;
+		}
+		
+		public String getGuid() {
+			return guid;
+		}
 
-	@Override
-	protected void formOK(UserRequest ureq) {
-		fireEvent (ureq, Event.DONE_EVENT);
-	}
+		public TextElement getNameEl() {
+			return nameEl;
+		}
+		
+		public void setNameEl(TextElement nameEl) {
+			this.nameEl = nameEl;
+		}
+		
+		public SingleSelection getCustomType() {
+			return customType;
+		}
 
-	@Override
-	protected void doDispose() {
-		//
+		public void setCustomType(SingleSelection customType) {
+			this.customType = customType;
+		}
+
+		public SingleSelection getUserPropsChoice() {
+			return userPropsChoice;
+		}
+
+		public void setUserPropsChoice(SingleSelection userPropsChoice) {
+			this.userPropsChoice = userPropsChoice;
+		}
+
+		public TextElement getValueEl() {
+			return valueEl;
+		}
+		
+		public void setValueEl(TextElement valueEl) {
+			this.valueEl = valueEl;
+		}
+
+		public FormLink getAddButton() {
+			return addButton;
+		}
+
+		public void setAddButton(FormLink addButton) {
+			this.addButton = addButton;
+		}
+
+		public FormLink getRemoveButton() {
+			return removeButton;
+		}
+
+		public void setRemoveButton(FormLink removeButton) {
+			this.removeButton = removeButton;
+		}
 	}
 }
diff --git a/src/main/java/org/olat/course/nodes/basiclti/LTICourseNodeConfiguration.java b/src/main/java/org/olat/course/nodes/basiclti/LTICourseNodeConfiguration.java
index 48752cf6a99..d00b92ab490 100644
--- a/src/main/java/org/olat/course/nodes/basiclti/LTICourseNodeConfiguration.java
+++ b/src/main/java/org/olat/course/nodes/basiclti/LTICourseNodeConfiguration.java
@@ -25,10 +25,8 @@
 
 package org.olat.course.nodes.basiclti;
 
-import java.util.List;
 import java.util.Locale;
 
-import org.olat.core.extensions.ExtensionResource;
 import org.olat.core.gui.translator.Translator;
 import org.olat.core.util.Util;
 import org.olat.course.nodes.AbstractCourseNodeConfiguration;
@@ -82,47 +80,7 @@ public class LTICourseNodeConfiguration extends AbstractCourseNodeConfiguration
 	//
 	// OLATExtension interface implementations.
 	//
-
 	public String getName() {
 		return getAlias();
 	}
-
-	/**
-	 * @see org.olat.core.extensions.OLATExtension#getExtensionResources()
-	 */
-	public List getExtensionResources() {
-		// no ressources, part of main css
-		return null;
-	}
-
-	/**
-	 * @see org.olat.core.extensions.OLATExtension#getExtensionCSS()
-	 */
-	public ExtensionResource getExtensionCSS() {
-		// no ressources, part of main css
-		return null;
-	}
-
-	/**
-	 * @see org.olat.core.extensions.OLATExtension#setURLBuilder(org.olat.core.gui.render.URLBuilder)
-	 */
-	public void setExtensionResourcesBaseURI(String ubi) {
-	// no need for the URLBuilder
-	}
-
-	/**
-	 * @see org.olat.core.extensions.OLATExtension#setup()
-	 */
-	public void setup() {
-	// nothing to do here
-	}
-
-	/**
-	 * @see org.olat.core.extensions.OLATExtension#tearDown()
-	 */
-	public void tearDown() {
-	// nothing to do here
-	}
-
-
 }
diff --git a/src/main/java/org/olat/course/nodes/basiclti/LTICourseNodeContext.java b/src/main/java/org/olat/course/nodes/basiclti/LTICourseNodeContext.java
new file mode 100644
index 00000000000..3dc224cbf2e
--- /dev/null
+++ b/src/main/java/org/olat/course/nodes/basiclti/LTICourseNodeContext.java
@@ -0,0 +1,127 @@
+/**
+ * <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.basiclti;
+
+import org.olat.core.id.Identity;
+import org.olat.course.nodes.CourseNode;
+import org.olat.course.run.environment.CourseEnvironment;
+import org.olat.ims.lti.LTIContext;
+
+/**
+ * 
+ * Initial date: 13.05.2013<br>
+ * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
+ *
+ */
+public class LTICourseNodeContext implements LTIContext {
+	
+	private final String roles;
+	private final String sourcedId;
+	private final String backMapperUri;
+	private final String outcomeMapperUri;
+	private final CourseNode courseNode;
+	private final CourseEnvironment courseEnv;
+	
+	private final String customProperties;
+	
+	private String target;
+	private String width;
+	private String height;
+	
+	public LTICourseNodeContext(CourseEnvironment courseEnv, CourseNode courseNode,
+			String roles, String sourcedId, String backMapperUri, String outcomeMapperUri,
+			String customProperties, String target, String width, String height) {
+		this.roles = roles;
+		this.sourcedId = sourcedId;
+		this.courseEnv = courseEnv;
+		this.courseNode = courseNode;
+		this.backMapperUri = backMapperUri;
+		this.outcomeMapperUri = outcomeMapperUri;
+		this.customProperties = customProperties;
+		this.target = target;
+		this.width = width;
+		this.height = height;
+	}
+
+	@Override
+	public String getSourcedId() {
+		return sourcedId;
+	}
+
+	@Override
+	public String getTalkBackMapperUri() {
+		return backMapperUri;
+	}
+	
+	@Override
+	public String getOutcomeMapperUri() {
+		return outcomeMapperUri;
+	}
+
+	@Override
+	public String getResourceId() {
+		return courseNode.getIdent();
+	}
+
+	@Override
+	public String getResourceTitle() {
+		return courseNode.getShortTitle();
+	}
+
+	@Override
+	public String getResourceDescription() {
+		return courseNode.getLongTitle();
+	}
+
+	@Override
+	public String getContextId() {
+		return courseEnv.getCourseResourceableId().toString();
+	}
+
+	@Override
+	public String getContextTitle() {
+		return courseEnv.getCourseTitle();
+	}
+
+	@Override
+	public String getRoles(Identity identity) {
+		return roles;
+	}
+	
+	@Override
+	public String getCustomProperties() {
+		return customProperties;
+	}
+
+	@Override
+	public String getTarget() {
+		return target;
+	}
+
+	@Override
+	public String getPreferredWidth() {
+		return width;
+	}
+
+	@Override
+	public String getPreferredHeight() {
+		return height;
+	}
+}
\ No newline at end of file
diff --git a/src/main/java/org/olat/course/nodes/basiclti/LTIEditController.java b/src/main/java/org/olat/course/nodes/basiclti/LTIEditController.java
index 7937bfd3f1b..6546512c851 100644
--- a/src/main/java/org/olat/course/nodes/basiclti/LTIEditController.java
+++ b/src/main/java/org/olat/course/nodes/basiclti/LTIEditController.java
@@ -76,7 +76,7 @@ public class LTIEditController extends ActivateableTabbableDefaultController imp
 	private TabbedPane myTabbedPane;
 	private Controller previewLayoutCtr;
 	private Link previewButton;
-	private Controller tunnelRunCtr;
+	private Controller previewLtiCtr;
 	private final StackedController stackPanel;
 
 	/**
@@ -124,14 +124,14 @@ public class LTIEditController extends ActivateableTabbableDefaultController imp
 	public void event(UserRequest ureq, Component source, Event event) {
 		if (source == previewButton) { // those must be links
 			
-			removeAsListenerAndDispose(tunnelRunCtr);
-			tunnelRunCtr = new LTIRunController(getWindowControl(), config, ureq, courseNode, editCourseEnv);
-			listenTo(tunnelRunCtr);
+			removeAsListenerAndDispose(previewLtiCtr);
+			previewLtiCtr = new LTIRunController(getWindowControl(), config, ureq, courseNode, editCourseEnv);
+			listenTo(previewLtiCtr);
 			
 			// preview layout: only center column (col3) used
 			removeAsListenerAndDispose(previewLayoutCtr);
 
-			previewLayoutCtr = new LayoutMain3ColsController(ureq, getWindowControl(), tunnelRunCtr);
+			previewLayoutCtr = new LayoutMain3ColsController(ureq, getWindowControl(), previewLtiCtr);
 			listenTo(previewLayoutCtr);
 			this.stackPanel.pushController(translate("preview"), previewLayoutCtr);
 		}
diff --git a/src/main/java/org/olat/course/nodes/basiclti/LTIRunController.java b/src/main/java/org/olat/course/nodes/basiclti/LTIRunController.java
index ca0c0dad1ae..86a407e1096 100644
--- a/src/main/java/org/olat/course/nodes/basiclti/LTIRunController.java
+++ b/src/main/java/org/olat/course/nodes/basiclti/LTIRunController.java
@@ -27,38 +27,38 @@ package org.olat.course.nodes.basiclti;
 
 import java.net.MalformedURLException;
 import java.net.URL;
-import java.util.List;
-import java.util.Locale;
-import java.util.Properties;
-
-import javax.servlet.http.HttpServletRequest;
+import java.util.Map;
 
 import org.imsglobal.basiclti.BasicLTIUtil;
-import org.olat.basesecurity.Authentication;
-import org.olat.basesecurity.BaseSecurityManager;
+import org.olat.core.CoreSpringFactory;
+import org.olat.core.commons.fullWebApp.popup.BaseFullWebappPopupLayoutFactory;
 import org.olat.core.dispatcher.mapper.Mapper;
 import org.olat.core.gui.UserRequest;
 import org.olat.core.gui.components.Component;
+import org.olat.core.gui.components.link.Link;
+import org.olat.core.gui.components.link.LinkFactory;
 import org.olat.core.gui.components.panel.Panel;
 import org.olat.core.gui.components.velocity.VelocityContainer;
 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.media.MediaResource;
-import org.olat.core.gui.media.StringMediaResource;
+import org.olat.core.gui.control.creator.ControllerCreator;
+import org.olat.core.gui.control.generic.popup.PopupBrowserWindow;
 import org.olat.core.helpers.Settings;
-import org.olat.core.id.Identity;
 import org.olat.core.id.Roles;
-import org.olat.core.id.User;
-import org.olat.core.id.UserConstants;
 import org.olat.core.util.StringHelper;
-import org.olat.core.util.WebappHelper;
+import org.olat.course.groupsandrights.CourseGroupManager;
 import org.olat.course.nodes.BasicLTICourseNode;
 import org.olat.course.run.environment.CourseEnvironment;
-import org.olat.ldap.ui.LDAPAuthenticationController;
+import org.olat.course.run.scoring.ScoreEvaluation;
+import org.olat.course.run.userview.UserCourseEnvironment;
+import org.olat.ims.lti.LTIContext;
+import org.olat.ims.lti.LTIManager;
+import org.olat.ims.lti.ui.PostDataMapper;
+import org.olat.ims.lti.ui.TalkBackMapper;
 import org.olat.modules.ModuleConfiguration;
-import org.olat.shibboleth.ShibbolethDispatcher;
+import org.olat.resource.OLATResource;
 
 /**
  * Description:<br>
@@ -69,14 +69,36 @@ import org.olat.shibboleth.ShibbolethDispatcher;
  */
 public class LTIRunController extends BasicController {
 
-	private VelocityContainer run;
+	private Link startButton;
+	private final Panel mainPanel;
+	private VelocityContainer run, startPage;
 	private BasicLTICourseNode courseNode;
-	private Panel main;
 	private ModuleConfiguration config;
-	private CourseEnvironment courseEnv;
-	private String postData;
-	private Mapper contentMapper;
-	private Mapper talkbackMapper;
+	private final CourseEnvironment courseEnv;
+	private UserCourseEnvironment userCourseEnv;
+	
+	private final Roles roles;
+	private final LTIManager ltiManager;
+	private final boolean newWindow;
+	
+	public LTIRunController(WindowControl wControl, ModuleConfiguration config, UserRequest ureq, BasicLTICourseNode ltCourseNode,
+			CourseEnvironment courseEnv) {
+		super(ureq, wControl);
+		this.courseNode = ltCourseNode;
+		this.config = config;
+		this.roles = ureq.getUserSession().getRoles();
+		this.courseEnv = courseEnv;
+		newWindow = false;
+		ltiManager = CoreSpringFactory.getImpl(LTIManager.class);
+
+		run = createVelocityContainer("run");
+		// push title and learning objectives, only visible on intro page
+		run.contextPut("menuTitle", courseNode.getShortTitle());
+		run.contextPut("displayTitle", courseNode.getLongTitle());
+
+		doBasicLTI(ureq, run);
+		mainPanel = putInitialPanel(run);
+	}
 
 	/**
 	 * Constructor for tunneling run controller
@@ -88,241 +110,175 @@ public class LTIRunController extends BasicController {
 	 * @param cenv the course environment
 	 */
 	public LTIRunController(WindowControl wControl, ModuleConfiguration config, UserRequest ureq, BasicLTICourseNode ltCourseNode,
-			CourseEnvironment cenv) {
+			UserCourseEnvironment userCourseEnv) {
 		super(ureq, wControl);
 		this.courseNode = ltCourseNode;
 		this.config = config;
-		this.courseEnv = cenv;
-
-		main = new Panel("ltrunmain");
-		doBasicLTI(ureq);
-		this.putInitialPanel(main);
-	}
-
-	public void event(UserRequest ureq, Component source, Event event) {
-		//nothing to do
-	}
-
-	protected void event(UserRequest ureq, Controller source, Event event) {
-	// nothing to do
-	}
+		this.userCourseEnv = userCourseEnv;
+		this.roles = ureq.getUserSession().getRoles();
+		courseEnv = userCourseEnv.getCourseEnvironment();
+		ltiManager = CoreSpringFactory.getImpl(LTIManager.class);
+		
+		mainPanel = new Panel("ltiContainer");
+		putInitialPanel(mainPanel);
 
-	private void doBasicLTI(UserRequest ureq) {
 		run = createVelocityContainer("run");
-
 		// push title and learning objectives, only visible on intro page
 		run.contextPut("menuTitle", courseNode.getShortTitle());
 		run.contextPut("displayTitle", courseNode.getLongTitle());
+		
+		String display = config.getStringValue(BasicLTICourseNode.CONFIG_DISPLAY, "iframe");
+		newWindow = "window".equals(display);
+
+		startPage = createVelocityContainer("overview");
+		startPage.contextPut("menuTitle", courseNode.getShortTitle());
+		startPage.contextPut("displayTitle", courseNode.getLongTitle());
+		
+		startButton = LinkFactory.createButton("start", startPage, this);
+    if(newWindow) {
+    	startButton.setTarget("_help");
+    }
+
+
+		Boolean assessable = config.getBooleanEntry(BasicLTICourseNode.CONFIG_KEY_HAS_SCORE_FIELD);
+		if(assessable != null && assessable.booleanValue()) {
+	    startPage.contextPut("isassessable", assessable);
+	    
+	    Integer attempts = courseNode.getUserAttempts(userCourseEnv);
+	    startPage.contextPut("attempts", attempts);
+	    
+	    ScoreEvaluation eval = courseNode.getUserScoreEvaluation(userCourseEnv);
+	    Float cutValue = config.getFloatEntry(BasicLTICourseNode.CONFIG_KEY_PASSED_CUT_VALUE);
+	    if(cutValue != null) {
+	    	startPage.contextPut("hasPassedValue", Boolean.TRUE);
+	    	startPage.contextPut("passed", eval.getPassed());
+	    }
+	    startPage.contextPut("score", eval.getScore()); 
+	    mainPanel.setContent(startPage);
+		} else if(newWindow) {
+	    mainPanel.setContent(startPage);
+		} else {
+			doBasicLTI(ureq, run);
+			mainPanel.setContent(run);
+		}
+	}
 
+	public void event(UserRequest ureq, Component source, Event event) {
+		if(source == startButton) {
+			courseNode.incrementUserAttempts(userCourseEnv);
+			if(newWindow) {
+				//wrap the content controller into a full header layout
+				ControllerCreator layoutCtrlr = BaseFullWebappPopupLayoutFactory.createAuthMinimalPopupLayout(ureq, new ControllerCreator() {
+					@Override
+					public Controller createController(UserRequest lureq,	WindowControl lwControl) {
+						return new LTIPopedController(lureq, lwControl);
+					}
+				});
+				//open in new browser window
+				PopupBrowserWindow pbw = getWindowControl().getWindowBackOffice().getWindowManager().createNewPopupBrowserWindowFor(ureq, layoutCtrlr);
+				pbw.open(ureq);
+			} else {
+				doBasicLTI(ureq, run);
+				mainPanel.setContent(run);
+			}
+		}
+	}
+	
+	private String getUrl() {
 		// put url in template to show content on extern page
 		URL url = null;
 		try {
-			url = new URL((String) config.get(LTIConfigForm.CONFIGKEY_PROTO), (String) config.get(LTIConfigForm.CONFIGKEY_HOST), ((Integer) config
+			url = new URL((String)config.get(LTIConfigForm.CONFIGKEY_PROTO), (String) config.get(LTIConfigForm.CONFIGKEY_HOST), ((Integer) config
 					.get(LTIConfigForm.CONFIGKEY_PORT)).intValue(), (String) config.get(LTIConfigForm.CONFIGKEY_URI));
 		} catch (MalformedURLException e) {
 			// this should not happen since the url was already validated in edit mode
-			run.contextPut("url", "");
+			return null;
 		}
-		if (url != null) {
-			StringBuilder sb = new StringBuilder(128);
-			sb.append(url.toString());
-			// since the url only includes the path, but not the query (?...), append
-			// it here, if any
-			String query = (String) config.get(LTIConfigForm.CONFIGKEY_QUERY);
-			if (query != null) {
-				sb.append("?");
-				sb.append(query);
-			}
-			run.contextPut("url", sb.toString());
-
-			String key = (String) config.get(LTIConfigForm.CONFIGKEY_KEY);
-			String pass = (String) config.get(LTIConfigForm.CONFIGKEY_PASS);
-			String debug = (String) config.get(LTIConfigForm.CONFIG_KEY_DEBUG);
-
-			talkbackMapper = new Mapper() {
-
-				@Override
-				public MediaResource handle(String relPath, HttpServletRequest request) {
-					/**
-					 * this is the place for error handling coming from the LTI tool, depending on error state
-					 * may present some information for the user or just add some information to the olat.log file
-					 */
-					StringMediaResource mediares = new StringMediaResource();
-					StringBuilder sb = new StringBuilder();
-					sb.append("lti_msg: ").append(request.getParameter("lti_msg")).append("<br/>");
-					sb.append("lti_errormsg: ").append(request.getParameter("lti_errormsg")).append("<br/>");
-					sb.append("lti_log: ").append(request.getParameter("lti_log")).append("<br/>");
-					sb.append("lti_errorlog: ").append(request.getParameter("lti_errorlog")).append("<br/>");
-					mediares.setData("<html><body>" + sb.toString() + "</body></html>");
-					mediares.setContentType("text/html");
-					mediares.setEncoding("UTF-8");
-					return mediares;
-				}
-			};
-			String backMapperUrl = registerMapper(ureq, talkbackMapper);
-
-			String serverUri = ureq.getHttpReq().getScheme()+"://"+ureq.getHttpReq().getServerName()+":"+ureq.getHttpReq().getServerPort();
-
-			Properties props = LTIProperties(ureq);
-			setProperty(props, "launch_presentation_return_url", serverUri + backMapperUrl + "/");
-			props = BasicLTIUtil.signProperties(props, sb.toString(), "POST", key, pass, null, null, null);
-
-			postData = BasicLTIUtil.postLaunchHTML(props, sb.toString(), "true".equals(debug));
-
-			contentMapper = new Mapper() {
-				@Override
-				public MediaResource handle(String relPath, HttpServletRequest request) {
-					StringMediaResource mediares = new StringMediaResource();
-					mediares.setData(postData);
-					mediares.setContentType("text/html");
-					mediares.setEncoding("UTF-8");
-					return mediares;
-				}
-
-			};
-			logDebug("Basic LTI Post data: "+postData, null);
 
+		StringBuilder querySb = new StringBuilder(128);
+		querySb.append(url.toString());
+		// since the url only includes the path, but not the query (?...), append
+		// it here, if any
+		String query = (String) config.get(LTIConfigForm.CONFIGKEY_QUERY);
+		if (query != null) {
+			querySb.append("?");
+			querySb.append(query);
 		}
-		String mapperUri = registerMapper(ureq, contentMapper);
-		run.contextPut("mapperUri", mapperUri + "/");
-
-		main.setContent(run);
+		return querySb.toString();
 	}
-
 	
 
-	private Properties LTIProperties(UserRequest ureq) {
-		final Identity ident = ureq.getIdentity();
-		final Locale loc = ureq.getLocale();
-		User u = ident.getUser();
-		final String lastName = u.getProperty(UserConstants.LASTNAME, loc);
-		final String firstName = u.getProperty(UserConstants.FIRSTNAME, loc);
-		final String email = u.getProperty(UserConstants.EMAIL, loc);
-
-		String custom = (String) config.get(LTIConfigForm.CONFIG_KEY_CUSTOM);
-		boolean sendname = Boolean.valueOf((String)config.get(LTIConfigForm.CONFIG_KEY_SENDNAME));
-		boolean sendemail = Boolean.valueOf((String) config.get(LTIConfigForm.CONFIG_KEY_SENDEMAIL));
-
-		Properties props = new Properties();
-		setProperty(props, "resource_link_id", courseNode.getIdent());
-		setProperty(props, "resource_link_title", courseNode.getShortTitle());
-		setProperty(props, "resource_link_description", courseNode.getLongTitle());
-		setProperty(props, "user_id", u.getKey() + "");		
-		setProperty(props, "lis_person_sourcedid", createPersonSourceId());
-		
-		setProperty(props, "launch_presentation_locale", loc.toString());
-		setProperty(props, "launch_presentation_document_target", "iframe");
-
-		if (sendname) {
-			setProperty(props, "lis_person_name_given", firstName);
-			setProperty(props, "lis_person_name_family", lastName);
-			setProperty(props, "lis_person_name_full", firstName+" "+lastName);
-		}
-		if (sendemail) {
-			setProperty(props, "lis_person_contact_email_primary", email);
-		}
-
-		setProperty(props, "roles", setRoles(ureq.getUserSession().getRoles()));
-		setProperty(props, "context_id", courseEnv.getCourseResourceableId().toString());
-		setProperty(props, "context_label", courseEnv.getCourseTitle());
-		setProperty(props, "context_title", courseEnv.getCourseTitle());
-		setProperty(props, "context_type", "CourseSection");
 
-		// Pull in and parse the custom parameters
-		// Note to Chuck - move this into BasicLTI Util
-		if (custom != null) {
-			String[] params = custom.split("[\n;]");
-			for (int i = 0; i < params.length; i++) {
-				String param = params[i];
-				if (param == null) continue;
-				if (param.length() < 1) continue;
-				int pos = param.indexOf("=");
-				if (pos < 1) continue;
-				if (pos + 1 > param.length()) continue;
-				String key = BasicLTIUtil.mapKeyName(param.substring(0, pos));
-				if (key == null) continue;
-				String value = param.substring(pos + 1);
-				value = value.trim();
-				if (value.length() < 1) continue;
-				if (value == null) continue;
-				setProperty(props, "custom_" + key, value);
-			}
-		}
+	private void doBasicLTI(UserRequest ureq, VelocityContainer container) {
+		String url = getUrl();
+		container.contextPut("url", url == null ? "" : url);
 
-		setProperty(props, "tool_consumer_instance_guid", Settings.getServerconfig("server_fqdn"));
-		setProperty(props, "tool_consumer_instance_name", WebappHelper.getInstanceId());
-		setProperty(props, "tool_consumer_instance_contact_email", WebappHelper.getMailConfig("mailSupport"));
+		String oauth_consumer_key = (String) config.get(LTIConfigForm.CONFIGKEY_KEY);
+		String oauth_secret = (String) config.get(LTIConfigForm.CONFIGKEY_PASS);
+		String debug = (String) config.get(LTIConfigForm.CONFIG_KEY_DEBUG);
+		String serverUri = Settings.createServerURI();
+		String sourcedId = courseEnv.getCourseResourceableId() + "_" + courseNode.getIdent() + "_" + getIdentity().getKey();
+		OLATResource courseResource = courseEnv.getCourseGroupManager().getCourseResource();
 		
+		Mapper talkbackMapper = new TalkBackMapper();
+		String backMapperUrl = registerCacheableMapper(ureq, sourcedId + "_talkback", talkbackMapper);
+		String backMapperUri = serverUri + backMapperUrl + "/";
+
+		Mapper outcomeMapper = new CourseNodeOutcomeMapper(getIdentity(), courseResource, courseNode.getIdent(),
+				oauth_consumer_key, oauth_secret, sourcedId);
+		String outcomeMapperUrl = registerCacheableMapper(ureq, sourcedId, outcomeMapper, LTIManager.EXPIRATION_TIME);
+		String outcomeMapperUri = serverUri + outcomeMapperUrl + "/";
+
+		boolean sendname = config.getBooleanSafe(LTIConfigForm.CONFIG_KEY_SENDNAME, false);
+		boolean sendmail = config.getBooleanSafe(LTIConfigForm.CONFIG_KEY_SENDEMAIL, false);
+		String ltiRoles = getLTIRoles();
+		String target = config.getStringValue(BasicLTICourseNode.CONFIG_DISPLAY);
+		String width = config.getStringValue(BasicLTICourseNode.CONFIG_WIDTH);
+		String height = config.getStringValue(BasicLTICourseNode.CONFIG_HEIGHT);
+		String custom = (String)config.get(LTIConfigForm.CONFIG_KEY_CUSTOM);
+		container.contextPut("height", height);
+		container.contextPut("width", width);
+		LTIContext context = new LTICourseNodeContext(courseEnv, courseNode, ltiRoles,
+				sourcedId, backMapperUri, outcomeMapperUri, custom, target, width, height);
+		Map<String,String> props = ltiManager.forgeLTIProperties(getIdentity(), getLocale(), context, sendname, sendmail);
+		props = ltiManager.sign(props, url, oauth_consumer_key, oauth_secret);
+
+		String postData = BasicLTIUtil.postLaunchHTML(props, url, "true".equals(debug));
+		Mapper contentMapper = new PostDataMapper(postData);
+		logDebug("Basic LTI Post data: " + postData, null);
 
-		return props;
-	}
-	
-	/**
-	 * Generates the LTI lis_person_sourcedid property for the current user.
-	 * Uses the Shib ID or LDAP ID if available, otherwhise a combination of
-	 * server domain name and identity key
-	 * 
-	 * @return personSourceId
-	 */
-	private String createPersonSourceId() {
-		// The person source ID is used as user identifier. The rule is as follows: 
-		// 1) if a shibboleth authentication token is availble, use the ShibbolethModule.getDefaultUIDAttribute() 
-		// 2) if a LDAP authentication token is available, use the LDAPConstants.LDAP_USER_IDENTIFYER
-		// 3) as fallback use the system URL together with the identity username
-		String personSourceId = null;
-		// Use the shibboleth ID as person source identificator
-		List<Authentication> authMethods = BaseSecurityManager.getInstance().getAuthentications(getIdentity());
-		for (Authentication method : authMethods) {
-			String provider = method.getProvider();
-			if (ShibbolethDispatcher.PROVIDER_SHIB.equals(provider)) {
-				personSourceId = method.getAuthusername();
-				// done, case 1)
-				break;
-			} else if (LDAPAuthenticationController.PROVIDER_LDAP.equals(provider)) {
-				personSourceId = method.getAuthusername();
-				// normally done, case 2). however, lets continue because we might still find a case 1)
-			}			
-			// ignore all other authentication providers
-		}
-		if (!StringHelper.containsNonWhitespace(personSourceId)) {
-			// fallback to the serverDomainName:identityId as case 3)
-			personSourceId = Settings.getServerconfig("server_fqdn") + ":" + getIdentity().getKey();
-		}
-		return personSourceId;
-	}
-
-	public static void setProperty(Properties props, String key, String value) {
-		if (value == null) return;
-		if (value.trim().length() < 1) return;
-		props.setProperty(key, value);
+		String mapperUri = registerMapper(ureq, contentMapper);
+		container.contextPut("mapperUri", mapperUri + "/");
 	}
 
-	/**
-	 * A comma-separated list of URN values for roles. If this list is non-empty,
-	 * it should contain at least one role from the LIS System Role, LIS
-	 * Institution Role, or LIS Context Role vocabularies (See Appendix A of
-	 * LTI_BasicLTI_Implementation_Guide_rev1.pdf).
-	 * 
-	 * @param roles
-	 * @return
-	 */
-	private String setRoles(Roles roles) {
-		StringBuilder rolesStr;
+	
+	private String getLTIRoles() {
 		if (roles.isGuestOnly()) {
-			rolesStr = new StringBuilder("Guest");
-		} else {
-			rolesStr = new StringBuilder("Learner");
-			boolean coach = courseEnv.getCourseGroupManager().isIdentityCourseCoach(getIdentity());
-			if (coach) {
-				rolesStr.append(",").append("Instructor");
+			return "Guest";
+		}
+		CourseGroupManager groupManager = courseEnv.getCourseGroupManager();
+		boolean admin = groupManager.isIdentityCourseAdministrator(getIdentity());
+		if(admin || roles.isOLATAdmin()) {
+			String authorRole = config.getStringValue(BasicLTICourseNode.CONFIG_KEY_AUTHORROLE);
+			if(StringHelper.containsNonWhitespace(authorRole)) {
+				return authorRole;
 			}
-			boolean admin = courseEnv.getCourseGroupManager().isIdentityCourseAdministrator(getIdentity());
-			if (roles.isOLATAdmin() || admin) {
-				rolesStr.append(",").append("Administrator");
+			return "Instructor,Administrator";
+		}
+		boolean coach = groupManager.isIdentityCourseCoach(getIdentity());
+		if(coach) {
+			String coachRole = config.getStringValue(BasicLTICourseNode.CONFIG_KEY_COACHROLE);
+			if(StringHelper.containsNonWhitespace(coachRole)) {
+				return coachRole;
 			}
+			return "Instructor";
 		}
 		
-		return rolesStr.toString();
+		String participantRole = config.getStringValue(BasicLTICourseNode.CONFIG_KEY_PARTICIPANTROLE);
+		if(StringHelper.containsNonWhitespace(participantRole)) {
+			return participantRole;
+		}
+		return "Learner";
 	}
 	
 	/**
@@ -332,5 +288,24 @@ public class LTIRunController extends BasicController {
 	protected void doDispose() {
 		//
 	}
+	
+	private class LTIPopedController extends BasicController {
+		
+		public LTIPopedController(UserRequest ureq, WindowControl wControl) {
+			super(ureq, wControl);
+			VelocityContainer run = createVelocityContainer("run");
+			doBasicLTI(ureq,  run);
+			putInitialPanel(run);
+		}
+
+		@Override
+		protected void event(UserRequest ureq, Component source, Event event) {
+			//
+		}
 
-}
+		@Override
+		protected void doDispose() {
+			//
+		}
+	}
+}
\ No newline at end of file
diff --git a/src/main/java/org/olat/course/nodes/basiclti/_content/custom.html b/src/main/java/org/olat/course/nodes/basiclti/_content/custom.html
new file mode 100644
index 00000000000..1271117eb58
--- /dev/null
+++ b/src/main/java/org/olat/course/nodes/basiclti/_content/custom.html
@@ -0,0 +1,14 @@
+<table>
+	<tbody>
+		#foreach($nvp in $nameValuePairs)
+		<tr>
+			<td>custom_</td>
+			<td>$r.render("name_${nvp.guid}")</td>
+			<td>$r.render("typ_${nvp.guid}")</td>
+			<td>$r.render("val_${nvp.guid}")$r.render("userprops_${nvp.guid}")</td>
+			<td>$r.render("add_${nvp.guid}")</td>
+			<td>$r.render("rm_${nvp.guid}")</td>
+		</tr>
+		#end
+	</tbody>
+</table>
diff --git a/src/main/java/org/olat/course/nodes/basiclti/_content/edit.html b/src/main/java/org/olat/course/nodes/basiclti/_content/edit.html
index f615d698944..d56cb11cda8 100644
--- a/src/main/java/org/olat/course/nodes/basiclti/_content/edit.html
+++ b/src/main/java/org/olat/course/nodes/basiclti/_content/edit.html
@@ -1,8 +1,6 @@
-
-	#if ($showPreviewButton)
-		<div class="b_float_right">$r.render("command.preview")</div>
-		<br/>
-	#end
-	$r.render("ltConfigForm")
+#if ($showPreviewButton)
+	<div class="o_buttons_box_right"><br/><br/><br/>$r.render("command.preview")</div>
+#end
+$r.render("ltConfigForm")
 
 
diff --git a/src/main/java/org/olat/course/nodes/basiclti/_content/overview.html b/src/main/java/org/olat/course/nodes/basiclti/_content/overview.html
new file mode 100644
index 00000000000..fa42526c5b4
--- /dev/null
+++ b/src/main/java/org/olat/course/nodes/basiclti/_content/overview.html
@@ -0,0 +1,47 @@
+#if ($isassessable)	
+	<div class="o_course_run_scoreinfo">
+		<h4>$r.translate("score.title")</h4>		
+		<table>
+			<tbody>
+				<tr>
+					<td>			
+						$r.translate("attempts.yourattempts"): 
+					</td>
+					<td>
+						$attempts
+					</td>
+				</tr>
+	#if($attempts > 0)
+		#if ($score)
+				<tr>
+					<td>			
+						$r.translate("score.yourscore"): 
+					</td>
+					<td>
+						$score
+					</td>
+				</tr>
+		#end
+				<tr>
+					<td>			
+						$r.translate("passed.yourpassed"): 
+					</td>
+					<td>
+						#if($hasPassedValue && $passed == true)		
+							<span class="o_passed">$r.translate("passed.yes")</span>
+						#elseif($hasPassedValue && $passed == false)		
+							<span class="o_notpassed">$r.translate("passed.no")</span>
+						#end
+					</td>
+				</tr>	
+			</tbody>
+		</table>
+	#else
+			</tbody>
+		</table>
+
+		<div class="o_course_run_scoreinfo_noinfo">$r.translate("score.noscoreinfoyet")</div>
+	#end
+	</div>
+#end
+$r.render("start")
\ No newline at end of file
diff --git a/src/main/java/org/olat/course/nodes/basiclti/_content/run.html b/src/main/java/org/olat/course/nodes/basiclti/_content/run.html
index eb0c9977fa4..9abe81df8db 100644
--- a/src/main/java/org/olat/course/nodes/basiclti/_content/run.html
+++ b/src/main/java/org/olat/course/nodes/basiclti/_content/run.html
@@ -1,9 +1,11 @@
 <div class="b_iframe_wrapper">
-<iframe  id="IMSBasicLTIFrame" src="$mapperUri"  marginwidth="0"
-marginheight="0" height="400">
-<script type="text/javascript">
-## no window.onresize due to IE bug which triggers recurstion: http://snook.ca/archives/javascript/ie6_fires_onresize/
-jQuery(function() {b_resizeIframeToMainMaxHeight("IMSBasicLTIFrame");});
-</script>
-</iframe>
+	<iframe  id="IMSBasicLTIFrame" src="$mapperUri"  marginwidth="0" marginheight="0" #if($width) width="${width}px" #end height="#if($height)${height}#else{400}#{end}px" style="#if($width) width:${width}px;#end #if($height)height:${height}px;#{end}"></iframe>
 </div>
+#if(!$height)
+	<script type="text/javascript">
+	/* <![CDATA[ */ 
+		## no window.onresize due to IE bug which triggers recurstion: http://snook.ca/archives/javascript/ie6_fires_onresize/
+		jQuery(function() {b_resizeIframeToMainMaxHeight("IMSBasicLTIFrame");});
+	/* ]]> */
+	</script>
+#end
diff --git a/src/main/java/org/olat/course/nodes/basiclti/_i18n/LocalStrings_de.properties b/src/main/java/org/olat/course/nodes/basiclti/_i18n/LocalStrings_de.properties
index 27c4c3d4462..ed0a5d1b303 100644
--- a/src/main/java/org/olat/course/nodes/basiclti/_i18n/LocalStrings_de.properties
+++ b/src/main/java/org/olat/course/nodes/basiclti/_i18n/LocalStrings_de.properties
@@ -18,8 +18,33 @@ display.config.sendName=Name zum Anbieter senden
 display.config.sendEmail=E-Mailadresse zum Anbieter senden
 display.config.custom=Spezielle Konfiguration (Name=Wert)
 display.config.doDebug=Gesendete Information anzeigen
+display.config.window=Anzeige
+display.config.window.iframe=iframe
+display.config.window.window=Neue Fenster
+display.config.height=$org.olat.course.nodes.scorm\:height.label
+display.config.width=Breite Anzeigefläche
+display.config.free=Text
+display.config.free.userprops=Benutzer
+add=+
+remove=-
+height.auto=$org.olat.course.nodes.scorm\:height.auto
 command.preview=Vorschau anzeigen
+scaleFactor=Scaling factor
+assessable.label=Resultat aus LTI 1.1 ĂĽbertragen
+cutvalue.label=Notwendige Punktzahl fĂĽr 'bestanden'
+form.error.wrongFloat=$org.olat.course.assessment\:form.error.wrongFloat
+participant.roles=Teilnehmer
+coach.roles=Betreuer
+author.roles=Besitzer
 preview=Vorschau
+start=LTI-Lerninhalt anzeigen
+score.title=$org.olat.course.nodes.scorm\:score.title
+attempts.yourattempts=$org.olat.course.nodes.scorm\:attempts.yourattempts
+score.noscoreinfoyet=Zu diesem LTI-Lerninhalt gibt es noch keine Punktangaben, da Sie ihn noch nie absolviert haben.
+score.yourscore=$org.olat.course.nodes.scorm\:score.yourscore
+passed.yourpassed=$org.olat.course.nodes.scorm\:passed.yourpassed
+passed.no=$org.olat.course.nodes.scorm\:passed.no
+passed.yes=$org.olat.course.nodes.scorm\:passed.yes
 chelp.ced-lti-conf.title=LTI-Seite konfigurieren
 cshelp.lti1=LTI steht fĂĽr "Learning Tool Interoperability" und ist ein IMS Standard zur Einbindung von externen Lernapplikationen in eine Lernplattform.
 chelp.lti2= Mit der LTI-Seite können Sie externe Software wie zum Beispiel einen externen Chat, ein Mediawiki, einen Testeditor oder ein virtuelles Chemielabor in Ihren Kurs integrieren.
diff --git a/src/main/java/org/olat/course/nodes/basiclti/_i18n/LocalStrings_en.properties b/src/main/java/org/olat/course/nodes/basiclti/_i18n/LocalStrings_en.properties
index 80a1843a9de..8ce69d11929 100644
--- a/src/main/java/org/olat/course/nodes/basiclti/_i18n/LocalStrings_en.properties
+++ b/src/main/java/org/olat/course/nodes/basiclti/_i18n/LocalStrings_en.properties
@@ -29,6 +29,23 @@ display.config.custom=Specific configuration (name\=value)
 display.config.doDebug=Show information sent
 display.config.sendEmail=Send e-mail address to provider
 display.config.sendName=Send name to provider
+display.config.window=Display
+display.config.window.iframe=iframe
+display.config.window.window=New window
+display.config.height=$org.olat.course.nodes.scorm\:height.label
+display.config.width=Display width
+display.config.free=Text
+display.config.free.userprops=User
+add=+
+remove=-
+height.auto=$org.olat.course.nodes.scorm\:height.auto
+scaleFactor=Scaling factor
+assessable.label=Transfer score from LTI 1.1
+cutvalue.label=Score needed to pass
+form.error.wrongFloat=$org.olat.course.assessment\:form.error.wrongFloat
+participant.roles=Participant
+coach.roles=Coach
+author.roles=Author
 error.hostmissing.long=In the tab "Page content" a host has to be configured for the external page "{0}"
 error.hostmissing.short=No host indicated for "{0}".
 form.title=Configuration of LTI page
diff --git a/src/main/java/org/olat/ims/_spring/imsContext.xml b/src/main/java/org/olat/ims/_spring/imsContext.xml
new file mode 100644
index 00000000000..a47212cbefa
--- /dev/null
+++ b/src/main/java/org/olat/ims/_spring/imsContext.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<beans xmlns="http://www.springframework.org/schema/beans"
+	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+	xmlns:context="http://www.springframework.org/schema/context" 
+	xsi:schemaLocation="
+  http://www.springframework.org/schema/beans 
+  http://www.springframework.org/schema/beans/spring-beans.xsd
+  http://www.springframework.org/schema/context 
+  http://www.springframework.org/schema/context/spring-context.xsd">
+  
+	<context:component-scan base-package="org.olat.ims.lti.manager,org.olat.ims.qti.qpool" />
+
+	<import resource="classpath:/org/olat/ims/qti/_spring/qtiContext.xml"/>
+
+	<bean id="org.olat.ims.cp.CPManager" class="org.olat.ims.cp.CPManagerImpl"/> 
+	
+</beans>
\ No newline at end of file
diff --git a/src/main/java/org/olat/ims/cp/_spring/cpContext.xml b/src/main/java/org/olat/ims/cp/_spring/cpContext.xml
deleted file mode 100644
index b7aae99bbde..00000000000
--- a/src/main/java/org/olat/ims/cp/_spring/cpContext.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<beans xmlns="http://www.springframework.org/schema/beans"
-	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-	xsi:schemaLocation="
-  http://www.springframework.org/schema/beans 
-  http://www.springframework.org/schema/beans/spring-beans.xsd">
-
-	<bean id="org.olat.ims.cp.CPManager" class="org.olat.ims.cp.CPManagerImpl"  /> 
-	
-</beans>
\ No newline at end of file
diff --git a/src/main/java/org/olat/ims/lti/LTIContext.java b/src/main/java/org/olat/ims/lti/LTIContext.java
new file mode 100644
index 00000000000..45843e5e409
--- /dev/null
+++ b/src/main/java/org/olat/ims/lti/LTIContext.java
@@ -0,0 +1,57 @@
+/**
+ * <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.lti;
+
+import org.olat.core.id.Identity;
+
+/**
+ * 
+ * Initial date: 13.05.2013<br>
+ * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
+ *
+ */
+public interface LTIContext {
+	
+	public String getSourcedId();
+	
+	public String getTalkBackMapperUri();
+	
+	public String getOutcomeMapperUri();
+	
+	public String getResourceId();
+	
+	public String getResourceTitle();
+	
+	public String getResourceDescription();
+	
+	public String getContextId();
+	
+	public String getContextTitle();
+	
+	public String getRoles(Identity identity);
+	
+	public String getCustomProperties();
+	
+	public String getTarget();
+	
+	public String getPreferredWidth();
+	
+	public String getPreferredHeight();
+}
diff --git a/src/main/java/org/olat/ims/lti/LTIManager.java b/src/main/java/org/olat/ims/lti/LTIManager.java
new file mode 100644
index 00000000000..88ec60a65e1
--- /dev/null
+++ b/src/main/java/org/olat/ims/lti/LTIManager.java
@@ -0,0 +1,54 @@
+/**
+ * <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.lti;
+
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+import org.olat.core.id.Identity;
+import org.olat.resource.OLATResource;
+
+/**
+ * 
+ * Initial date: 13.05.2013<br>
+ * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
+ *
+ */
+public interface LTIManager {
+	
+	public static final String USER_PROPS_PREFIX = "$userprops_";
+	public static final int EXPIRATION_TIME = 3600 * 24 * 30 * 6;//6 months
+
+	public Map<String,String> forgeLTIProperties(Identity identity, Locale locale,
+			LTIContext context, boolean sendName, boolean sendEmail);
+	
+	public Map<String,String> sign(Map<String,String> props, String url, String oauthKey, String oauthSecret);
+	
+	
+	public LTIOutcome createOutcome(Identity identity, OLATResource resource, String resSubPath,
+			String action, String outcomeKey, String outcomeValue);
+	
+	public LTIOutcome loadOutcomeByKey(Long key);
+
+	public List<LTIOutcome> loadOutcomes(Identity identity, OLATResource resource, String resSubPath);
+	
+
+}
diff --git a/src/main/java/org/olat/ims/lti/LTIOutcome.java b/src/main/java/org/olat/ims/lti/LTIOutcome.java
new file mode 100644
index 00000000000..f31696ba61f
--- /dev/null
+++ b/src/main/java/org/olat/ims/lti/LTIOutcome.java
@@ -0,0 +1,53 @@
+/**
+ * <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.lti;
+
+import java.util.Date;
+
+import org.olat.core.id.Identity;
+import org.olat.resource.OLATResource;
+
+/**
+ * 
+ * Initial date: 15.05.2013<br>
+ * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
+ *
+ */
+public interface LTIOutcome {
+	
+	public Long getKey();
+
+	public Date getCreationDate();
+
+	public Date getLastModified();
+
+	public Identity getAssessedIdentity();
+
+	public OLATResource getResource();
+
+	public String getResSubPath();
+	
+	public String getAction();
+
+	public String getOutcomeKey();
+
+	public String getOutcomeValue();
+
+}
diff --git a/src/main/java/org/olat/ims/lti/manager/LTIManagerImpl.java b/src/main/java/org/olat/ims/lti/manager/LTIManagerImpl.java
new file mode 100644
index 00000000000..93a978bd8c7
--- /dev/null
+++ b/src/main/java/org/olat/ims/lti/manager/LTIManagerImpl.java
@@ -0,0 +1,287 @@
+/**
+ * <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.lti.manager;
+
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+import javax.persistence.TypedQuery;
+
+import org.imsglobal.basiclti.BasicLTIUtil;
+import org.olat.basesecurity.Authentication;
+import org.olat.basesecurity.BaseSecurityManager;
+import org.olat.core.commons.persistence.DB;
+import org.olat.core.helpers.Settings;
+import org.olat.core.id.Identity;
+import org.olat.core.id.User;
+import org.olat.core.id.UserConstants;
+import org.olat.core.util.StringHelper;
+import org.olat.core.util.WebappHelper;
+import org.olat.ims.lti.LTIContext;
+import org.olat.ims.lti.LTIManager;
+import org.olat.ims.lti.LTIOutcome;
+import org.olat.ims.lti.model.LTIOutcomeImpl;
+import org.olat.ldap.ui.LDAPAuthenticationController;
+import org.olat.resource.OLATResource;
+import org.olat.shibboleth.ShibbolethDispatcher;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+/**
+ * 
+ * Initial date: 13.05.2013<br>
+ * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
+ *
+ */
+@Service
+public class LTIManagerImpl implements LTIManager {
+
+	@Autowired
+	private DB dbInstance;
+	
+	@Override
+	public LTIOutcome createOutcome(Identity identity, OLATResource resource,
+			String resSubPath, String action, String outcomeKey, String outcomeValue) {
+		LTIOutcomeImpl outcome = new LTIOutcomeImpl();
+		outcome.setAssessedIdentity(identity);
+		outcome.setResource(resource);
+		if(StringHelper.containsNonWhitespace(resSubPath)) {
+			outcome.setResSubPath(resSubPath);
+		}
+		outcome.setCreationDate(new Date());
+		outcome.setLastModified(new Date());
+		outcome.setAction(action);
+		outcome.setOutcomeKey(outcomeKey);
+		outcome.setOutcomeValue(outcomeValue);
+		dbInstance.getCurrentEntityManager().persist(outcome);
+		return outcome;
+	}
+
+	@Override
+	public LTIOutcome loadOutcomeByKey(Long key) {
+		List<LTIOutcome> outcomes = dbInstance.getCurrentEntityManager()
+				.createNamedQuery("loadLTIOutcomeByKey", LTIOutcome.class).
+				setParameter("outcomeKey", key)
+				.getResultList();
+		
+		if(outcomes.isEmpty()) {
+			return null;
+		}
+		return outcomes.get(0);
+	}
+
+	@Override
+	public List<LTIOutcome> loadOutcomes(Identity identity, OLATResource resource, String resSubPath) {
+		
+		StringBuilder sb = new StringBuilder();
+		sb.append("select outcome from ltioutcome outcome where outcome.assessedIdentity.key=:identityKey and outcome.resource=:resource");
+		if(StringHelper.containsNonWhitespace(resSubPath)) {
+			sb.append(" and outcome.resSubPath=:resSubPath");
+		} else {
+			sb.append(" and outcome.resSubPath is null");
+		}
+		
+		TypedQuery<LTIOutcome> outcomes = dbInstance.getCurrentEntityManager()
+				.createQuery(sb.toString(), LTIOutcome.class)
+				.setParameter("identityKey", identity.getKey())
+				.setParameter("resource", resource);
+		
+		if(StringHelper.containsNonWhitespace(resSubPath)) {
+			outcomes.setParameter("resSubPath", resSubPath);
+		}
+		return outcomes.getResultList();
+	}
+
+	@Override
+	public Map<String,String> sign(Map<String,String> props, String url, String oauthKey, String oauthSecret) {
+		String oauth_consumer_key = oauthKey;
+		String oauth_consumer_secret = oauthSecret;
+		String tool_consumer_instance_guid = Settings.getServerconfig("server_fqdn");
+		String tool_consumer_instance_description = null;
+		String tool_consumer_instance_url = null;
+		String tool_consumer_instance_name = WebappHelper.getInstanceId();
+		String tool_consumer_instance_contact_email = WebappHelper.getMailConfig("mailSupport");
+
+		Map<String,String> signedProps = BasicLTIUtil.signProperties(props, url, "POST",
+				oauth_consumer_key,
+				oauth_consumer_secret,
+				tool_consumer_instance_guid,
+				tool_consumer_instance_description,
+				tool_consumer_instance_url,
+				tool_consumer_instance_name,
+				tool_consumer_instance_contact_email);
+		
+		return signedProps;
+	}
+
+	@Override
+	public Map<String,String> forgeLTIProperties(Identity identity, Locale locale, LTIContext context,
+			boolean sendName, boolean sendEmail) {
+		final Identity ident = identity;
+		final Locale loc = locale;
+		final User u = ident.getUser();
+		final String lastName = u.getProperty(UserConstants.LASTNAME, loc);
+		final String firstName = u.getProperty(UserConstants.FIRSTNAME, loc);
+		final String email = u.getProperty(UserConstants.EMAIL, loc);
+
+		Map<String,String> props = new HashMap<String,String>();
+		setProperty(props, "resource_link_id", context.getResourceId());
+		setProperty(props, "resource_link_title", context.getResourceTitle());
+		setProperty(props, "resource_link_description", context.getResourceDescription());
+		//launch
+		setProperty(props, "launch_presentation_locale", loc.toString());
+		setProperty(props, "launch_presentation_document_target", context.getTarget());
+		setProperty(props, "launch_presentation_return_url", context.getTalkBackMapperUri());
+		if(StringHelper.containsNonWhitespace(context.getPreferredWidth())) {
+			setProperty(props, "launch_presentation_width", context.getPreferredWidth());
+		}
+		if(StringHelper.containsNonWhitespace(context.getPreferredHeight())) {
+			setProperty(props, "launch_presentation_height", context.getPreferredHeight());
+		}
+		//consumer infos
+		setProperty(props, "tool_consumer_info_product_family_code", "openolat");
+		setProperty(props, "tool_consumer_info_version", Settings.getVersion());		
+		//outcome
+		if(StringHelper.containsNonWhitespace(context.getOutcomeMapperUri())) {
+			//setProperty(props, "ext_ims_lis_basic_outcome_url", context.getOutcomeMapperUri());
+			//setProperty(props, "ext_ims_lis_resultvalue_sourcedids", "decimal");
+			setProperty(props, "lis_result_sourcedid", context.getSourcedId());
+			setProperty(props, "lis_outcome_service_url", context.getOutcomeMapperUri());
+		}
+		//user data
+		setProperty(props, "user_id", u.getKey().toString());		
+		setProperty(props, "lis_person_sourcedid", createPersonSourceId(identity));
+		if (sendName) {
+			setProperty(props, "lis_person_name_given", firstName);
+			setProperty(props, "lis_person_name_family", lastName);
+			setProperty(props, "lis_person_name_full", firstName+" "+lastName);
+		}
+		if (sendEmail) {
+			setProperty(props, "lis_person_contact_email_primary", email);
+		}
+
+		setProperty(props, "roles", context.getRoles(identity));
+		setProperty(props, "context_id", context.getContextId());
+		setProperty(props, "context_label", context.getContextTitle());
+		setProperty(props, "context_title", context.getContextTitle());
+		setProperty(props, "context_type", "CourseSection");
+
+		setCustomProperties(context.getCustomProperties(), identity, props);
+
+		return props;
+	}
+	
+	private void setCustomProperties(String custom, Identity identity, Map<String,String> props) {
+		if (!StringHelper.containsNonWhitespace(custom)) return;
+		
+		String[] params = custom.split("[\n;]");
+		for (int i = 0; i < params.length; i++) {
+			String param = params[i];
+			if (!StringHelper.containsNonWhitespace(param)) {
+				continue;
+			}
+			int pos = param.indexOf("=");
+			if (pos < 1 || pos + 1 > param.length()) {
+				continue;
+			}
+			
+			String key = BasicLTIUtil.mapKeyName(param.substring(0, pos));
+			if(!StringHelper.containsNonWhitespace(key)) {
+				continue;
+			}
+			
+			String value = param.substring(pos + 1).trim();
+			if(value.length() < 1) {
+				continue;
+			}
+			
+			if(value.startsWith(LTIManager.USER_PROPS_PREFIX)) {
+				String userProp = value.substring(LTIManager.USER_PROPS_PREFIX.length(), value.length());
+				value = identity.getUser().getProperty(userProp, null);
+			}
+			setProperty(props, "custom_" + key, value);
+		}
+	}
+	
+	public void setProperty(Map<String,String> props, String key, String value) {
+		if (value == null) return;
+		if (value.trim().length() < 1) return;
+		props.put(key, value);
+	}
+
+	/**
+	 * A comma-separated list of URN values for roles. If this list is non-empty,
+	 * it should contain at least one role from the LIS System Role, LIS
+	 * Institution Role, or LIS Context Role vocabularies (See Appendix A of
+	 * LTI_BasicLTI_Implementation_Guide_rev1.pdf).
+	 * 
+	 * @param roles
+	 * @return
+	 */
+	/*private String setRoles(Identity identity, Roles roles, LTIContext context) {
+		StringBuilder rolesStr;
+		if (roles.isGuestOnly()) {
+			rolesStr = new StringBuilder("Guest");
+		} else {
+			rolesStr = new StringBuilder("Learner");
+			boolean coach = context.isCoach(identity);
+			if (coach) {
+				rolesStr.append(",").append("Instructor");
+			}
+			boolean admin = context.isAdmin(identity);
+			if (roles.isOLATAdmin() || admin) {
+				rolesStr.append(",").append("Administrator");
+			}
+		}
+		
+		return rolesStr.toString();
+	}*/
+	
+	private String createPersonSourceId(Identity identity) {
+		// The person source ID is used as user identifier. The rule is as follows: 
+		// 1) if a shibboleth authentication token is availble, use the ShibbolethModule.getDefaultUIDAttribute() 
+		// 2) if a LDAP authentication token is available, use the LDAPConstants.LDAP_USER_IDENTIFYER
+		// 3) as fallback use the system URL together with the identity username
+		String personSourceId = null;
+		// Use the shibboleth ID as person source identificator
+		List<Authentication> authMethods = BaseSecurityManager.getInstance().getAuthentications(identity);
+		for (Authentication method : authMethods) {
+			String provider = method.getProvider();
+			if (ShibbolethDispatcher.PROVIDER_SHIB.equals(provider)) {
+				personSourceId = method.getAuthusername();
+				// done, case 1)
+				break;
+			} else if (LDAPAuthenticationController.PROVIDER_LDAP.equals(provider)) {
+				personSourceId = method.getAuthusername();
+				// normally done, case 2). however, lets continue because we might still find a case 1)
+			}			
+			// ignore all other authentication providers
+		}
+		if (!StringHelper.containsNonWhitespace(personSourceId)) {
+			// fallback to the serverDomainName:identityId as case 3)
+			personSourceId = Settings.getServerconfig("server_fqdn") + ":" + identity.getKey();
+		}
+		return personSourceId;
+	}
+
+}
diff --git a/src/main/java/org/olat/ims/lti/model/LTIOutcomeImpl.java b/src/main/java/org/olat/ims/lti/model/LTIOutcomeImpl.java
new file mode 100644
index 00000000000..9b37d5c7146
--- /dev/null
+++ b/src/main/java/org/olat/ims/lti/model/LTIOutcomeImpl.java
@@ -0,0 +1,202 @@
+/**
+ * <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.lti.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.NamedQueries;
+import javax.persistence.NamedQuery;
+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.CreateInfo;
+import org.olat.core.id.Identity;
+import org.olat.core.id.ModifiedInfo;
+import org.olat.core.id.Persistable;
+import org.olat.ims.lti.LTIOutcome;
+import org.olat.resource.OLATResource;
+import org.olat.resource.OLATResourceImpl;
+
+/**
+ * 
+ * Initial date: 15.05.2013<br>
+ * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
+ *
+ */
+@Entity(name="ltioutcome")
+@Table(name="o_lti_outcome")
+@NamedQueries({
+	@NamedQuery(name="loadLTIOutcomeByKey", query="select outcome from ltioutcome outcome where outcome.key=:outcomeKey")
+})
+public class LTIOutcomeImpl implements LTIOutcome, CreateInfo, ModifiedInfo, Persistable{
+
+	private static final long serialVersionUID = 8645018375238423824L;
+
+	@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=IdentityImpl.class,fetch=FetchType.LAZY,optional=false)
+	@JoinColumn(name="fk_identity_id", nullable=false, updatable=false)
+	private Identity assessedIdentity;
+	@ManyToOne(targetEntity=OLATResourceImpl.class,fetch=FetchType.LAZY,optional=false)
+	@JoinColumn(name="fk_resource_id", nullable=false, updatable=false)
+	private OLATResource resource;
+
+	@Column(name="r_ressubpath", nullable=true, insertable=true, updatable=false)
+	private String resSubPath;
+	@Column(name="r_action", nullable=true, insertable=true, updatable=true)
+	private String action;
+	@Column(name="r_outcome_key", nullable=true, insertable=true, updatable=false)
+	private String outcomeKey;
+	@Column(name="r_outcome_value", nullable=true, insertable=true, updatable=true)
+	private String outcomeValue;
+	
+	@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 date) {
+		this.lastModified = date;
+	}
+
+	@Override
+	public Identity getAssessedIdentity() {
+		return assessedIdentity;
+	}
+
+	public void setAssessedIdentity(Identity assessedIdentity) {
+		this.assessedIdentity = assessedIdentity;
+	}
+
+	@Override
+	public OLATResource getResource() {
+		return resource;
+	}
+
+	public void setResource(OLATResource resource) {
+		this.resource = resource;
+	}
+
+	@Override
+	public String getResSubPath() {
+		return resSubPath;
+	}
+
+	public void setResSubPath(String resSubPath) {
+		this.resSubPath = resSubPath;
+	}
+
+	public String getAction() {
+		return action;
+	}
+
+	public void setAction(String action) {
+		this.action = action;
+	}
+
+	@Override
+	public String getOutcomeKey() {
+		return outcomeKey;
+	}
+
+	public void setOutcomeKey(String outcomeKey) {
+		this.outcomeKey = outcomeKey;
+	}
+
+	@Override
+	public String getOutcomeValue() {
+		return outcomeValue;
+	}
+
+	public void setOutcomeValue(String value) {
+		this.outcomeValue = value;
+	}
+
+	@Override
+	public int hashCode() {
+		return key == null ? 13256 : key.hashCode();
+	}
+
+	@Override
+	public boolean equals(Object obj) {
+		if(this == obj) {
+			return true;
+		}
+		if(obj instanceof LTIOutcomeImpl) {
+			LTIOutcomeImpl q = (LTIOutcomeImpl)obj;
+			return key != null && key.equals(q.key);
+		}
+		return false;
+	}
+
+	@Override
+	public boolean equalsByPersistableKey(Persistable persistable) {
+		return equals(persistable);
+	}
+
+	@Override
+	public String toString() {
+		StringBuilder sb = new StringBuilder();
+		sb.append("ltiOutcome[key=").append(this.key)
+			.append("]").append(super.toString());
+		return sb.toString();
+	}
+}
\ No newline at end of file
diff --git a/src/main/java/org/olat/ims/lti/ui/LTIResultDetailsController.java b/src/main/java/org/olat/ims/lti/ui/LTIResultDetailsController.java
new file mode 100644
index 00000000000..3f3053e6001
--- /dev/null
+++ b/src/main/java/org/olat/ims/lti/ui/LTIResultDetailsController.java
@@ -0,0 +1,111 @@
+/**
+ * <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.lti.ui;
+
+import java.util.List;
+
+import org.olat.core.CoreSpringFactory;
+import org.olat.core.gui.UserRequest;
+import org.olat.core.gui.components.Component;
+import org.olat.core.gui.components.table.DefaultColumnDescriptor;
+import org.olat.core.gui.components.table.DefaultTableDataModel;
+import org.olat.core.gui.components.table.TableController;
+import org.olat.core.gui.components.table.TableGuiConfiguration;
+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.id.Identity;
+import org.olat.ims.lti.LTIManager;
+import org.olat.ims.lti.LTIOutcome;
+import org.olat.resource.OLATResource;
+
+/**
+ * 
+ * Initial date: 15.05.2013<br>
+ * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
+ *
+ */
+public class LTIResultDetailsController extends BasicController {
+	
+	private TableController summaryTableCtr;
+	
+	private final Identity assessedIdentity;
+	private final OLATResource resource;
+	private final String resSubPath;
+	private LTIManager ltiManager;
+
+	public LTIResultDetailsController(UserRequest ureq, WindowControl wControl,
+			Identity assessedIdentity, OLATResource resource, String resSubPath) {
+		super(ureq, wControl);
+		
+		this.assessedIdentity = assessedIdentity;
+		this.resource = resource;
+		this.resSubPath = resSubPath;
+		ltiManager = CoreSpringFactory.getImpl(LTIManager.class);
+		init(ureq);
+	}
+	
+	protected void init(UserRequest ureq) {
+		TableGuiConfiguration summaryTableConfig = new TableGuiConfiguration();
+		summaryTableConfig.setDownloadOffered(true);
+		
+		summaryTableCtr = new TableController(summaryTableConfig, ureq, getWindowControl(), getTranslator());
+		summaryTableCtr.addColumnDescriptor(new DefaultColumnDescriptor("table.header.date", 0, null, ureq.getLocale()));
+		summaryTableCtr.addColumnDescriptor(new DefaultColumnDescriptor("table.header.action", 1, null, ureq.getLocale()));
+		summaryTableCtr.addColumnDescriptor(new DefaultColumnDescriptor("table.header.key", 2, null, ureq.getLocale()));
+		summaryTableCtr.addColumnDescriptor(new DefaultColumnDescriptor("table.header.value", 3, null, ureq.getLocale()));
+
+		List<LTIOutcome> outcomes = ltiManager.loadOutcomes(assessedIdentity, resource, resSubPath);
+		summaryTableCtr.setTableDataModel(new OutcomeTableDataModel(outcomes));
+		listenTo(summaryTableCtr);
+		putInitialPanel(summaryTableCtr.getInitialComponent());
+	}
+
+	@Override
+	protected void doDispose() {
+		//
+	}
+
+	@Override
+	protected void event(UserRequest ureq, Component source, Event event) {
+		//
+	}
+
+	public static class OutcomeTableDataModel extends DefaultTableDataModel<LTIOutcome> {
+		public OutcomeTableDataModel(List<LTIOutcome> datas) {
+			super(datas);
+		}
+
+		public int getColumnCount() {
+			return 3;
+		}
+
+		public Object getValueAt(int row, int col) {
+			LTIOutcome data = getObject(row);
+			switch(col) {
+				case 0: return data.getCreationDate();
+				case 1: return data.getAction();
+				case 2: return data.getOutcomeKey();
+				case 3: return data.getOutcomeValue();
+				default: return "ERROR";
+			}
+		}
+	}
+}
\ No newline at end of file
diff --git a/src/main/java/org/olat/ims/lti/ui/OutcomeMapper.java b/src/main/java/org/olat/ims/lti/ui/OutcomeMapper.java
new file mode 100644
index 00000000000..75958fff3ac
--- /dev/null
+++ b/src/main/java/org/olat/ims/lti/ui/OutcomeMapper.java
@@ -0,0 +1,186 @@
+/**
+ * <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.lti.ui;
+
+import java.io.Serializable;
+import java.util.Map;
+import java.util.TreeMap;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.imsglobal.basiclti.XMLMap;
+import org.imsglobal.pox.IMSPOXRequest;
+import org.olat.basesecurity.BaseSecurity;
+import org.olat.core.CoreSpringFactory;
+import org.olat.core.dispatcher.mapper.Mapper;
+import org.olat.core.gui.media.MediaResource;
+import org.olat.core.gui.media.StringMediaResource;
+import org.olat.core.id.Identity;
+import org.olat.core.logging.OLog;
+import org.olat.core.logging.Tracing;
+import org.olat.core.util.SessionInfo;
+import org.olat.core.util.UserSession;
+import org.olat.core.util.session.UserSessionManager;
+import org.olat.ims.lti.LTIManager;
+import org.olat.resource.OLATResource;
+
+/**
+ * 
+ * Initial date: 13.05.2013<br>
+ * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
+ *
+ */
+public class OutcomeMapper implements Mapper, Serializable {
+	private static final long serialVersionUID = 7954337449619783210L;
+	private static final OLog log = Tracing.createLoggerFor(OutcomeMapper.class);
+
+	private static final String READ_RESULT_REQUEST = "readResultRequest";
+	private static final String DELETE_RESULT_REQUEST = "deleteResultRequest";
+	private static final String REPLACE_RESULT_REQUEST = "replaceResultRequest";
+	
+	private String sourcedId;
+	private Identity identity;
+	private Long identityKey;
+	private OLATResource resource;
+	private String resSubPath;
+	private String oauth_consumer_key;
+	private String oauth_secret;
+	
+	public OutcomeMapper() {
+		//
+	}
+	
+	public OutcomeMapper(Identity identity, OLATResource resource, String resSubPath,
+			String oauth_consumer_key, String oauth_secret,  String sourcedId) {
+		this.sourcedId = sourcedId;
+		this.oauth_consumer_key = oauth_consumer_key;
+		this.oauth_secret = oauth_secret;
+		this.identity = identity;
+		this.identityKey = identity.getKey();
+		this.resource = resource;
+		this.resSubPath = resSubPath;
+	}
+
+	@Override
+	public MediaResource handle(String relPath, HttpServletRequest request) {
+		reconnectUserSession(request);
+
+		String contentType = request.getContentType();
+		log.audit("LTI outcome for: " + identityKey);
+		if (contentType != null && contentType.startsWith("application/xml") ) {
+			String xmlResponse = doPostXml(request);
+			return createMediaResource(xmlResponse, "application/xml");
+		}
+
+		return createMediaResource("Hello", "text/plain");
+	}
+	
+	public Identity getIdentity() {
+		return identity;
+	}
+	
+	public String getSourcedId() {
+		return sourcedId;
+	}
+	
+	protected void reconnectUserSession(HttpServletRequest request) {
+		if(identity == null) {
+			identity = CoreSpringFactory.getImpl(BaseSecurity.class).loadIdentityByKey(identityKey);
+		}
+		
+		UserSession usess = CoreSpringFactory.getImpl(UserSessionManager.class).getUserSession(request);
+		if(usess == null) {
+			usess = new UserSession();
+		} 
+		if(usess.getSessionInfo() == null) {
+			usess.setSessionInfo(new SessionInfo(identityKey, identity.getName(), request.getSession(true)));
+		}
+		if(usess.getIdentityEnvironment() == null || usess.getIdentity() == null) {
+			usess.setIdentity(identity);
+		}
+	}
+	
+	private MediaResource createMediaResource(String body, String mimeType) {
+		StringMediaResource mediares = new StringMediaResource();
+		mediares.setData(body);
+		mediares.setContentType(mimeType);
+		mediares.setEncoding("UTF-8");
+		return mediares;
+	}
+	
+	private String doPostXml(HttpServletRequest request) {
+		IMSPOXRequest pox = new IMSPOXRequest(oauth_consumer_key, oauth_secret, request);
+		if(!pox.valid) {
+			log.error("LTI outcome, OAuth verification failed: " + pox.errorMessage);
+			return pox.getResponseFailure("OAuth verification failed", null);
+		}
+
+		String lti_message_type = pox.getOperation();
+		Map<String,String> body = pox.getBodyMap();
+		String reqSourceId =  body.get("/resultRecord/sourcedGUID/sourcedId");
+		if(!sourcedId.equals(reqSourceId)) {
+			log.error("LTI outcome sourcedId doesn't match: " + reqSourceId);
+		}
+		if(REPLACE_RESULT_REQUEST.equals(lti_message_type)) {
+			String scoreString = body.get("/resultRecord/result/resultScore/textString");
+			if(doUpdateResult(Float.parseFloat(scoreString))) {
+				Map<String,Object> theMap = new TreeMap<String,Object>();
+				theMap.put("/replaceResultRequest", "");
+				String theXml = XMLMap.getXMLFragment(theMap, true);
+				return pox.getResponseSuccess("Update result",theXml);
+			} else {
+				return pox.getResponseFailure("Update result failed", null);
+			}
+		} else if(DELETE_RESULT_REQUEST.equals(lti_message_type)) {
+			if(doDeleteResult()) {
+				Map<String,Object> theMap = new TreeMap<String,Object>();
+				theMap.put("/deleteResultRequest", "");
+				String theXml = XMLMap.getXMLFragment(theMap, true);
+				return pox.getResponseSuccess("Result deleted",theXml);
+			} else {
+				return pox.getResponseFailure("Delete result failed", null);
+			}
+		} else if (READ_RESULT_REQUEST.equals(lti_message_type)) {
+			return doReadResult(pox);
+		}
+		
+		return pox.getResponseFailure("Not implemented", null);
+	}
+	
+	protected String doReadResult(IMSPOXRequest pox) {
+		return pox.getResponseFailure("Not implemented", null);
+	}
+	
+	protected boolean doUpdateResult(Float score) {
+		String outcomeValue = score == null ? null : score.toString();
+		saveOutcome(REPLACE_RESULT_REQUEST, "/resultRecord/result/resultScore/textString", outcomeValue);
+		return true;
+	}
+	
+	protected boolean doDeleteResult() {
+		saveOutcome(DELETE_RESULT_REQUEST, "/resultRecord/result/resultScore/textString", null);
+		return true;
+	}
+	
+	private void saveOutcome(String action, String outcomeKey, String outcomeValue) {
+
+		CoreSpringFactory.getImpl(LTIManager.class).createOutcome(identity, resource, resSubPath, action, outcomeKey, outcomeValue);
+	}
+}
diff --git a/src/main/java/org/olat/ims/lti/ui/PostDataMapper.java b/src/main/java/org/olat/ims/lti/ui/PostDataMapper.java
new file mode 100644
index 00000000000..f4661804ae7
--- /dev/null
+++ b/src/main/java/org/olat/ims/lti/ui/PostDataMapper.java
@@ -0,0 +1,50 @@
+/**
+ * <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.lti.ui;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.olat.core.dispatcher.mapper.Mapper;
+import org.olat.core.gui.media.MediaResource;
+import org.olat.core.gui.media.StringMediaResource;
+
+/**
+ * 
+ * Initial date: 13.05.2013<br>
+ * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
+ *
+ */
+public class PostDataMapper implements Mapper {
+	
+	private final String postData;
+	
+	public PostDataMapper(String postData) {
+		this.postData = postData;
+	}
+
+	@Override
+	public MediaResource handle(String relPath, HttpServletRequest request) {
+		StringMediaResource mediares = new StringMediaResource();
+		mediares.setData(postData);
+		mediares.setContentType("text/html");
+		mediares.setEncoding("UTF-8");
+		return mediares;
+	}
+}
\ No newline at end of file
diff --git a/src/main/java/org/olat/ims/lti/ui/TalkBackMapper.java b/src/main/java/org/olat/ims/lti/ui/TalkBackMapper.java
new file mode 100644
index 00000000000..693bd934659
--- /dev/null
+++ b/src/main/java/org/olat/ims/lti/ui/TalkBackMapper.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.ims.lti.ui;
+
+import java.io.Serializable;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.olat.core.dispatcher.mapper.Mapper;
+import org.olat.core.gui.media.MediaResource;
+import org.olat.core.gui.media.StringMediaResource;
+
+/**
+ * 
+ * Initial date: 13.05.2013<br>
+ * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
+ *
+ */
+public class TalkBackMapper implements Mapper, Serializable {
+
+	private static final long serialVersionUID = -8319259842325597955L;
+
+	@Override
+	public MediaResource handle(String relPath, HttpServletRequest request) {	
+		StringMediaResource mediares = new StringMediaResource();
+		StringBuilder sb = new StringBuilder();
+		sb.append("<html><head><title>").append("LTI talk back").append("</title></head><body>")
+		  .append("lti_msg: ").append(request.getParameter("lti_msg")).append("<br/>")
+		  .append("lti_errormsg: ").append(request.getParameter("lti_errormsg")).append("<br/>")
+		  .append("lti_log: ").append(request.getParameter("lti_log")).append("<br/>")
+		  .append("lti_errorlog: ").append(request.getParameter("lti_errorlog")).append("<br/>")
+		  .append("</body></html>");
+		//ServletUtil.printOutRequestParameter(request);
+		mediares.setData(sb.toString());
+		mediares.setContentType("text/html");
+		mediares.setEncoding("UTF-8");
+		return mediares;
+	}
+}
\ No newline at end of file
diff --git a/src/main/java/org/olat/ims/lti/ui/_i18n/LocalStrings_de.properties b/src/main/java/org/olat/ims/lti/ui/_i18n/LocalStrings_de.properties
new file mode 100644
index 00000000000..eb2d274839a
--- /dev/null
+++ b/src/main/java/org/olat/ims/lti/ui/_i18n/LocalStrings_de.properties
@@ -0,0 +1,5 @@
+table.header.date=Datum
+table.header.action=Aktion
+table.header.key=Key
+table.header.value=Wert
+
diff --git a/src/main/java/org/olat/ims/lti/ui/_i18n/LocalStrings_en.properties b/src/main/java/org/olat/ims/lti/ui/_i18n/LocalStrings_en.properties
new file mode 100644
index 00000000000..f1663d4735d
--- /dev/null
+++ b/src/main/java/org/olat/ims/lti/ui/_i18n/LocalStrings_en.properties
@@ -0,0 +1,4 @@
+table.header.date=Date
+table.header.action=Action
+table.header.key=Key
+table.header.value=Value
\ No newline at end of file
diff --git a/src/main/java/org/olat/ims/lti/ui/_i18n/LocalStrings_fr.properties b/src/main/java/org/olat/ims/lti/ui/_i18n/LocalStrings_fr.properties
new file mode 100644
index 00000000000..4b5d91258ba
--- /dev/null
+++ b/src/main/java/org/olat/ims/lti/ui/_i18n/LocalStrings_fr.properties
@@ -0,0 +1,4 @@
+table.header.date=Date
+table.header.action=Action
+table.header.key=Clé
+table.header.value=Valeur
\ No newline at end of file
diff --git a/src/main/java/org/olat/ims/qti/_spring/qtiContext.xml b/src/main/java/org/olat/ims/qti/_spring/qtiContext.xml
index 33f35d53806..df8fab0fff8 100644
--- a/src/main/java/org/olat/ims/qti/_spring/qtiContext.xml
+++ b/src/main/java/org/olat/ims/qti/_spring/qtiContext.xml
@@ -1,14 +1,9 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <beans xmlns="http://www.springframework.org/schema/beans"
 	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-	xmlns:context="http://www.springframework.org/schema/context" 
 	xsi:schemaLocation="
   http://www.springframework.org/schema/beans 
-  http://www.springframework.org/schema/beans/spring-beans.xsd
-  http://www.springframework.org/schema/context 
-  http://www.springframework.org/schema/context/spring-context.xsd">
-
-	<context:component-scan base-package="org.olat.ims.qti.qpool" />
+  http://www.springframework.org/schema/beans/spring-beans.xsd">
 
 <bean id="qtiModule" class="org.olat.ims.qti.QTIModule" depends-on="database" >
 	<property name="qtiRepositoryHandlers">
diff --git a/src/main/java/org/olat/modules/ModuleConfiguration.java b/src/main/java/org/olat/modules/ModuleConfiguration.java
index 08b8b211b8b..9becbcf28b1 100644
--- a/src/main/java/org/olat/modules/ModuleConfiguration.java
+++ b/src/main/java/org/olat/modules/ModuleConfiguration.java
@@ -111,6 +111,23 @@ public class ModuleConfiguration implements Serializable {
 		Boolean set = val.equals("true")? Boolean.TRUE : Boolean.FALSE;
 		return set;
 	}
+	
+	public Float getFloatEntry(String config_key) {
+		Object val = get(config_key);
+		Float floatValue = null;
+		if (val == null) {
+			floatValue = null;
+		} else if( val instanceof Float) {
+			floatValue = (Float)val;
+		} else if( val instanceof String) {
+			try {
+				floatValue = new Float((String)val);
+			} catch(NumberFormatException e) {
+				//
+			}
+		}
+		return floatValue;
+	}
 
 	/**
 	 * 
diff --git a/src/main/java/org/olat/user/propertyhandlers/_spring/userPropertiesContext.xml b/src/main/java/org/olat/user/propertyhandlers/_spring/userPropertiesContext.xml
index 4d5b2e3444a..ca2583c97f2 100644
--- a/src/main/java/org/olat/user/propertyhandlers/_spring/userPropertiesContext.xml
+++ b/src/main/java/org/olat/user/propertyhandlers/_spring/userPropertiesContext.xml
@@ -796,6 +796,22 @@
 					</bean>
 				</entry>
 				
+				<entry key="org.olat.ims.lti.LTIManager">
+					<bean class="org.olat.user.propertyhandlers.UserPropertyUsageContext">
+						<property name="description" value="Can be used for LTI" />
+						<property name="propertyHandlers">
+							<list>
+								<ref bean="userPropertyFirstName" />
+								<ref bean="userPropertyLastName" />
+								<ref bean="userPropertyEmail" />	
+								<ref bean="userPropertyInstitutionalName" />
+								<ref bean="userPropertyInstitutionalUserIdentifier" />	
+								<ref bean="userPropertyInstitutionalEmail" />						
+							</list>
+						</property>
+					</bean>
+				</entry>
+				
 				<!-- fxdiff: FXOLAT-227 user properties used for group-mail-function -->
 				<entry key="org.olat.group.ui.run.BusinessGroupSendToChooserForm">
 					<bean class="org.olat.user.propertyhandlers.UserPropertyUsageContext">
diff --git a/src/main/resources/database/mysql/alter_8_4_0_to_9_0_0.sql b/src/main/resources/database/mysql/alter_8_4_0_to_9_0_0.sql
index 0bb3a179e1b..6bff1cff0ab 100644
--- a/src/main/resources/database/mysql/alter_8_4_0_to_9_0_0.sql
+++ b/src/main/resources/database/mysql/alter_8_4_0_to_9_0_0.sql
@@ -320,3 +320,26 @@ alter table o_qp_item add constraint idx_qp_item_license_id foreign key (fk_lice
 alter table o_qp_taxonomy_level add constraint idx_qp_field_2_parent_id foreign key (fk_parent_field) references o_qp_taxonomy_level(id);
 
 alter table o_qp_item_type add unique (q_type(200));
+
+
+-- lti
+create table o_lti_outcome (
+   id bigint not null,
+   creationdate datetime not null,
+   lastmodified datetime not null,
+   r_ressubpath varchar(2048),
+   r_action varchar(255) not null,
+   r_outcome_key varchar(255) not null,
+   r_outcome_value varchar(2048),
+   fk_resource_id bigint not null,
+   fk_identity_id bigint not null,
+   primary key (id)
+);
+
+alter table o_lti_outcome add constraint idx_lti_outcome_ident_id foreign key (fk_identity_id) references o_bs_identity(id);
+alter table o_lti_outcome add constraint idx_lti_outcome_rsrc_id foreign key (fk_resource_id) references o_olatresource(resource_id);
+
+-- mapper
+alter table o_mapper add column expirationdate datetime;
+
+
diff --git a/src/main/resources/database/mysql/setupDatabase.sql b/src/main/resources/database/mysql/setupDatabase.sql
index 91ee4abe456..fa5c5416432 100644
--- a/src/main/resources/database/mysql/setupDatabase.sql
+++ b/src/main/resources/database/mysql/setupDatabase.sql
@@ -1079,6 +1079,7 @@ create table o_mapper (
    id int8 not null,
    lastmodified timestamp,
    creationdate timestamp,
+   expirationdate datetime,
    mapper_uuid varchar(64),
    orig_session_id varchar(64),
    xml_config TEXT,
@@ -1086,7 +1087,7 @@ create table o_mapper (
 );
 
 -- question item
-create table if not exists o_qp_pool (
+create table o_qp_pool (
    id bigint not null,
    creationdate datetime not null,
    lastmodified datetime not null,
@@ -1096,7 +1097,7 @@ create table if not exists o_qp_pool (
    primary key (id)
 );
 
-create table if not exists o_qp_taxonomy_level (
+create table o_qp_taxonomy_level (
    id bigint not null,
    creationdate datetime not null,
    lastmodified datetime not null,
@@ -1107,7 +1108,7 @@ create table if not exists o_qp_taxonomy_level (
    primary key (id)
 );
 
-create table if not exists o_qp_item (
+create table o_qp_item (
    id bigint not null,
    q_identifier varchar(36) not null,
    q_master_identifier varchar(36),
@@ -1141,7 +1142,7 @@ create table if not exists o_qp_item (
    primary key (id)
 );
 
-create table if not exists o_qp_pool_2_item (
+create table o_qp_pool_2_item (
    id bigint not null,
    creationdate datetime not null,
    q_editable bit default 0,
@@ -1150,7 +1151,7 @@ create table if not exists o_qp_pool_2_item (
    primary key (id)
 );
 
-create table if not exists o_qp_share_item (
+create table o_qp_share_item (
    id bigint not null,
    creationdate datetime not null,
    q_editable bit default 0,
@@ -1159,7 +1160,7 @@ create table if not exists o_qp_share_item (
    primary key (id)
 );
 
-create table if not exists o_qp_item_collection (
+create table o_qp_item_collection (
    id bigint not null,
    creationdate datetime not null,
    lastmodified datetime not null,
@@ -1168,7 +1169,7 @@ create table if not exists o_qp_item_collection (
    primary key (id)
 );
 
-create table if not exists o_qp_collection_2_item (
+create table o_qp_collection_2_item (
    id bigint not null,
    creationdate datetime not null,
    fk_collection_id bigint not null,
@@ -1176,7 +1177,7 @@ create table if not exists o_qp_collection_2_item (
    primary key (id)
 );
 
-create table if not exists o_qp_edu_context (
+create table o_qp_edu_context (
    id bigint not null,
    creationdate datetime not null,
    q_level varchar(256) not null,
@@ -1201,6 +1202,20 @@ create table if not exists o_qp_license (
    primary key (id)
 );
 
+create table o_lti_outcome (
+   id bigint not null,
+   creationdate datetime not null,
+   lastmodified datetime not null,
+   r_ressubpath varchar(2048),
+   r_action varchar(255) not null,
+   r_outcome_key varchar(255) not null,
+   r_outcome_value varchar(2048),
+   fk_resource_id bigint not null,
+   fk_identity_id bigint not null,
+   primary key (id)
+);
+
+
 -- user view
 create view o_bs_identity_short_v as (
    select
@@ -2034,6 +2049,8 @@ alter table o_qp_taxonomy_level add constraint idx_qp_field_2_parent_id foreign
 
 alter table o_qp_item_type add unique (q_type(200));
 
+alter table o_lti_outcome add constraint idx_lti_outcome_ident_id foreign key (fk_identity_id) references o_bs_identity(id);
+alter table o_lti_outcome add constraint idx_lti_outcome_rsrc_id foreign key (fk_resource_id) references o_olatresource(resource_id);
 
 insert into hibernate_unique_key values ( 0 );
 
diff --git a/src/main/resources/database/oracle/alter_8_4_0_to_9_0_0.sql b/src/main/resources/database/oracle/alter_8_4_0_to_9_0_0.sql
index 9eb4f474520..c78d1114031 100644
--- a/src/main/resources/database/oracle/alter_8_4_0_to_9_0_0.sql
+++ b/src/main/resources/database/oracle/alter_8_4_0_to_9_0_0.sql
@@ -319,3 +319,25 @@ alter table o_qp_item add constraint idx_qp_item_type_id foreign key (fk_type) r
 alter table o_qp_item add constraint idx_qp_item_license_id foreign key (fk_license) references o_qp_license(id);
 
 alter table o_qp_taxonomy_level add constraint idx_qp_field_2_parent_id foreign key (fk_parent_field) references o_qp_taxonomy_level(id);
+
+-- lti
+create table if not exists o_lti_outcome (
+   id number(20) not null,
+   creationdate date not null,
+   lastmodified date not null,
+   r_ressubpath varchar2(2048 char),
+   r_action varchar2(255 char) not null,
+   r_outcome_key varchar2(255 char) not null,
+   r_outcome_value varchar2(2048 char),
+   fk_resource_id number(20) not null,
+   fk_identity_id number(20) not null,
+   primary key (id)
+);
+
+alter table o_lti_outcome add constraint idx_lti_outcome_ident_id foreign key (fk_identity_id) references o_bs_identity(id);
+alter table o_lti_outcome add constraint idx_lti_outcome_rsrc_id foreign key (fk_resource_id) references o_olatresource(resource_id);
+create index idx_lti_outcome_ident_id_idx on o_lti_outcome (fk_identity_id);
+create index idx_lti_outcome_rsrc_id_idx on o_lti_outcome (fk_resource_id);
+
+-- mapper
+alter table o_mapper add (expirationdate date);
diff --git a/src/main/resources/database/oracle/setupDatabase.sql b/src/main/resources/database/oracle/setupDatabase.sql
index 64404eb2b9f..ddb6c243596 100644
--- a/src/main/resources/database/oracle/setupDatabase.sql
+++ b/src/main/resources/database/oracle/setupDatabase.sql
@@ -1123,6 +1123,7 @@ create table o_mapper (
    id number(20) not null,
    lastmodified date,
    creationdate date,
+   expirationdate date,
    mapper_uuid varchar(64 char),
    orig_session_id varchar(64 char),
    xml_config CLOB,
@@ -1244,6 +1245,19 @@ create table o_qp_license (
    primary key (id)
 );
 
+create table if not exists o_lti_outcome (
+   id number(20) not null,
+   creationdate date not null,
+   lastmodified date not null,
+   r_ressubpath varchar2(2048 char),
+   r_action varchar2(255 char) not null,
+   r_outcome_key varchar2(255 char) not null,
+   r_outcome_value varchar2(2048 char),
+   fk_resource_id number(20) not null,
+   fk_identity_id number(20) not null,
+   primary key (id)
+);
+
 --
 -- Table: o_co_db_entry
 --;
@@ -2094,6 +2108,10 @@ alter table o_qp_item add constraint idx_qp_item_license_id foreign key (fk_lice
 
 alter table o_qp_taxonomy_level add constraint idx_qp_field_2_parent_id foreign key (fk_parent_field) references o_qp_taxonomy_level(id);
 
+alter table o_lti_outcome add constraint idx_lti_outcome_ident_id foreign key (fk_identity_id) references o_bs_identity(id);
+alter table o_lti_outcome add constraint idx_lti_outcome_rsrc_id foreign key (fk_resource_id) references o_olatresource(resource_id);
+create index idx_lti_outcome_ident_id_idx on o_lti_outcome (fk_identity_id);
+create index idx_lti_outcome_rsrc_id_idx on o_lti_outcome (fk_resource_id);
 
 insert into o_stat_lastupdated (until_datetime, lastupdated) values (to_date('1999-01-01', 'YYYY-mm-dd'), to_date('1999-01-01', 'YYYY-mm-dd'));
 insert into hibernate_unique_key values ( 0 );
diff --git a/src/main/resources/database/postgresql/alter_8_4_0_to_9_0_0.sql b/src/main/resources/database/postgresql/alter_8_4_0_to_9_0_0.sql
index 0263b5775e1..d66131ae633 100644
--- a/src/main/resources/database/postgresql/alter_8_4_0_to_9_0_0.sql
+++ b/src/main/resources/database/postgresql/alter_8_4_0_to_9_0_0.sql
@@ -114,7 +114,7 @@ create table o_qp_item_type (
 );
 
 create table o_qp_license (
-   id bigint not null,
+   id int8 not null,
    creationdate timestamp not null,
    q_license varchar(256) not null,
    q_text varchar(2048),
@@ -319,4 +319,29 @@ alter table o_qp_item add constraint idx_qp_item_license_id foreign key (fk_lice
 
 alter table o_qp_taxonomy_level add constraint idx_qp_field_2_parent_id foreign key (fk_parent_field) references o_qp_taxonomy_level(id);
 
-alter table o_qp_item_type add constraint cst_unique_item_type unique (q_type);
\ No newline at end of file
+alter table o_qp_item_type add constraint cst_unique_item_type unique (q_type);
+
+
+-- lti
+create table o_lti_outcome (
+   id int8 not null,
+   creationdate timestamp not null,
+   lastmodified timestamp not null,
+   r_ressubpath varchar(2048),
+   r_action varchar(255) not null,
+   r_outcome_key varchar(255) not null,
+   r_outcome_value varchar(2048),
+   fk_resource_id int8 not null,
+   fk_identity_id int8 not null,
+   primary key (id)
+);
+
+alter table o_lti_outcome add constraint idx_lti_outcome_ident_id foreign key (fk_identity_id) references o_bs_identity(id);
+alter table o_lti_outcome add constraint idx_lti_outcome_rsrc_id foreign key (fk_resource_id) references o_olatresource(resource_id);
+create index idx_lti_outcome_ident_id_idx on o_lti_outcome (fk_identity_id);
+create index idx_lti_outcome_rsrc_id_idx on o_lti_outcome (fk_resource_id);
+
+-- mapper
+alter table o_mapper add column expirationdate timestamp;
+
+
diff --git a/src/main/resources/database/postgresql/setupDatabase.sql b/src/main/resources/database/postgresql/setupDatabase.sql
index 1e4bb97102a..f0a9dfeee26 100644
--- a/src/main/resources/database/postgresql/setupDatabase.sql
+++ b/src/main/resources/database/postgresql/setupDatabase.sql
@@ -1078,6 +1078,7 @@ create table o_mapper (
    id int8 not null,
    lastmodified timestamp,
    creationdate timestamp,
+   expirationdate timestamp,
    mapper_uuid varchar(64),
    orig_session_id varchar(64),
    xml_config TEXT,
@@ -1200,6 +1201,19 @@ create table o_qp_license (
    primary key (id)
 );
 
+create table o_lti_outcome (
+   id int8 not null,
+   creationdate timestamp not null,
+   lastmodified timestamp not null,
+   r_ressubpath varchar(2048),
+   r_action varchar(255) not null,
+   r_outcome_key varchar(255) not null,
+   r_outcome_value varchar(2048),
+   fk_resource_id int8 not null,
+   fk_identity_id int8 not null,
+   primary key (id)
+);
+
 -- user view
 create view o_bs_identity_short_v as (
    select
@@ -1973,4 +1987,9 @@ alter table o_qp_taxonomy_level add constraint idx_qp_field_2_parent_id foreign
 
 alter table o_qp_item_type add constraint cst_unique_item_type unique (q_type);
 
+alter table o_lti_outcome add constraint idx_lti_outcome_ident_id foreign key (fk_identity_id) references o_bs_identity(id);
+alter table o_lti_outcome add constraint idx_lti_outcome_rsrc_id foreign key (fk_resource_id) references o_olatresource(resource_id);
+create index idx_lti_outcome_ident_id_idx on o_lti_outcome (fk_identity_id);
+create index idx_lti_outcome_rsrc_id_idx on o_lti_outcome (fk_resource_id);
+
 insert into hibernate_unique_key values ( 0 );
diff --git a/src/test/java/org/olat/core/dispatcher/mapper/MapperDAOTest.java b/src/test/java/org/olat/core/dispatcher/mapper/MapperDAOTest.java
index 3e29a4c1965..4c8e209237b 100644
--- a/src/test/java/org/olat/core/dispatcher/mapper/MapperDAOTest.java
+++ b/src/test/java/org/olat/core/dispatcher/mapper/MapperDAOTest.java
@@ -48,7 +48,7 @@ public class MapperDAOTest extends OlatTestCase {
 		String sessionId = UUID.randomUUID().toString().substring(0, 32);
 		PersistentMapper mapper = new PersistentMapper(mapperId);
 		
-		PersistedMapper pMapper = mapperDao.persistMapper(sessionId, mapperId, mapper);
+		PersistedMapper pMapper = mapperDao.persistMapper(sessionId, mapperId, mapper, -1);
 		Assert.assertNotNull(pMapper);
 		Assert.assertNotNull(pMapper.getKey());
 		Assert.assertNotNull(pMapper.getCreationDate());
@@ -64,7 +64,7 @@ public class MapperDAOTest extends OlatTestCase {
 		//create a mapper
 		String mapperId = UUID.randomUUID().toString();
 		String sessionId = UUID.randomUUID().toString().substring(0, 32);
-		PersistedMapper pMapper = mapperDao.persistMapper(sessionId, mapperId, null);
+		PersistedMapper pMapper = mapperDao.persistMapper(sessionId, mapperId, null, -1);
 		Assert.assertNotNull(pMapper);
 		dbInstance.commitAndCloseSession();
 		
@@ -84,7 +84,7 @@ public class MapperDAOTest extends OlatTestCase {
 		for(int i=0; i<10; i++) {
 			mapperIdToDelete = UUID.randomUUID().toString();
 			String sessionId = UUID.randomUUID().toString().substring(0, 32);
-			mapperDao.persistMapper(sessionId, mapperIdToDelete, null);
+			mapperDao.persistMapper(sessionId, mapperIdToDelete, null, -1);
 		}
 		dbInstance.commitAndCloseSession();
 		
@@ -94,7 +94,7 @@ public class MapperDAOTest extends OlatTestCase {
 		//create a new mapper
 		String mapperId = UUID.randomUUID().toString();
 		String sessionId = UUID.randomUUID().toString().substring(0, 32);
-		mapperDao.persistMapper(sessionId, mapperId, null);
+		mapperDao.persistMapper(sessionId, mapperId, null, -1);
 		dbInstance.commitAndCloseSession();
 		
 		//delete old mappers
@@ -109,5 +109,43 @@ public class MapperDAOTest extends OlatTestCase {
 		PersistedMapper deletedMapper = mapperDao.loadByMapperId(mapperIdToDelete);
 		Assert.assertNull(deletedMapper);
 	}
+	
+	@Test
+	public void testDeleteMapperByMapper_expirationDate() throws Exception {
+		//create mappers
+		String mapperIdToDeleteShortLived = UUID.randomUUID().toString();
+		String sessionId1 = UUID.randomUUID().toString().substring(0, 32);
+		mapperDao.persistMapper(sessionId1, mapperIdToDeleteShortLived, null, 1);
+		
+		String mapperIdToDeleteLongLived = UUID.randomUUID().toString();
+		String sessionId2 = UUID.randomUUID().toString().substring(0, 32);
+		mapperDao.persistMapper(sessionId2, mapperIdToDeleteLongLived, null, 10000);
+		dbInstance.commitAndCloseSession();
+		
+		Calendar cal = Calendar.getInstance();
+		Thread.sleep(5000);
+		
+		//create a new mapper
+		String mapperId = UUID.randomUUID().toString();
+		String sessionId = UUID.randomUUID().toString().substring(0, 32);
+		mapperDao.persistMapper(sessionId, mapperId, null, -1);
+		dbInstance.commitAndCloseSession();
+		
+		//delete old mappers
+		cal.add(Calendar.SECOND, 3);
+		int numOfDeletedRow = mapperDao.deleteMapperByDate(cal.getTime());
+		Assert.assertTrue(numOfDeletedRow >= 1);
+
+		//load the last mapper
+		PersistedMapper loadedMapper = mapperDao.loadByMapperId(mapperId);
+		Assert.assertNotNull(loadedMapper);
+		//try to load the short lived mapper
+		PersistedMapper deletedMapper = mapperDao.loadByMapperId(mapperIdToDeleteShortLived);
+		Assert.assertNull(deletedMapper);
+		//try to load the long lived mapper
+		PersistedMapper survivorMapper = mapperDao.loadByMapperId(mapperIdToDeleteLongLived);
+		Assert.assertNotNull(survivorMapper);
+		
+	}
 
 }
diff --git a/src/test/java/org/olat/ims/lti/LTIManagerTest.java b/src/test/java/org/olat/ims/lti/LTIManagerTest.java
new file mode 100644
index 00000000000..f4e43d393de
--- /dev/null
+++ b/src/test/java/org/olat/ims/lti/LTIManagerTest.java
@@ -0,0 +1,102 @@
+/**
+ * <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.lti;
+
+import java.util.List;
+import java.util.UUID;
+
+import junit.framework.Assert;
+
+import org.junit.Test;
+import org.olat.core.commons.persistence.DB;
+import org.olat.core.id.Identity;
+import org.olat.resource.OLATResource;
+import org.olat.test.JunitTestHelper;
+import org.olat.test.OlatTestCase;
+import org.springframework.beans.factory.annotation.Autowired;
+
+/**
+ * 
+ * Initial date: 15.05.2013<br>
+ * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
+ *
+ */
+public class LTIManagerTest extends OlatTestCase {
+	
+	@Autowired
+	private DB dbInstance;
+	@Autowired
+	private LTIManager ltiManager;
+	
+	@Test
+	public void createOutcome() {
+		Identity id = JunitTestHelper.createAndPersistIdentityAsUser("lti-1-" + UUID.randomUUID().toString());
+		OLATResource resource = JunitTestHelper.createRandomResource();
+		LTIOutcome outcome = ltiManager.createOutcome(id, resource, "sub", "action", "lti-outcome", "my value");
+		dbInstance.commitAndCloseSession();
+		
+		Assert.assertNotNull(outcome);
+		Assert.assertNotNull(outcome.getCreationDate());
+		Assert.assertNotNull(outcome.getLastModified());
+		Assert.assertEquals(id, outcome.getAssessedIdentity());
+		Assert.assertEquals(resource, outcome.getResource());
+		Assert.assertEquals("sub", outcome.getResSubPath());
+		Assert.assertEquals("action", outcome.getAction());
+		Assert.assertEquals("lti-outcome", outcome.getOutcomeKey());
+		Assert.assertEquals("my value", outcome.getOutcomeValue());
+	}
+
+	@Test
+	public void loadOutcome() {
+		Identity id = JunitTestHelper.createAndPersistIdentityAsUser("lti-2-" + UUID.randomUUID().toString());
+		OLATResource resource = JunitTestHelper.createRandomResource();
+		LTIOutcome outcome = ltiManager.createOutcome(id, resource, "sub", "update", "new-outcome", "lti score value");
+		dbInstance.commitAndCloseSession();
+		
+		LTIOutcome reloadedOutcome = ltiManager.loadOutcomeByKey(outcome.getKey());
+		Assert.assertNotNull(reloadedOutcome);
+		Assert.assertNotNull(reloadedOutcome.getCreationDate());
+		Assert.assertNotNull(reloadedOutcome.getLastModified());
+		Assert.assertEquals(id, reloadedOutcome.getAssessedIdentity());
+		Assert.assertEquals(resource, reloadedOutcome.getResource());
+		Assert.assertEquals("sub", reloadedOutcome.getResSubPath());
+		Assert.assertEquals("update", outcome.getAction());
+		Assert.assertEquals("new-outcome", reloadedOutcome.getOutcomeKey());
+		Assert.assertEquals("lti score value", reloadedOutcome.getOutcomeValue());
+	}
+	
+	@Test
+	public void loadOutcomes_byIdentity() {
+		Identity id = JunitTestHelper.createAndPersistIdentityAsUser("lti-3-" + UUID.randomUUID().toString());
+		OLATResource resource = JunitTestHelper.createRandomResource();
+		LTIOutcome outcome1 = ltiManager.createOutcome(id, resource, "sub", "update", "new-outcome", "lti score value");
+		LTIOutcome outcome2 = ltiManager.createOutcome(id, resource, "sub", "delete", "new-outcome", null);
+		dbInstance.commitAndCloseSession();
+		
+		List<LTIOutcome> outcomes = ltiManager.loadOutcomes(id, resource, "sub");
+		Assert.assertNotNull(outcomes);
+		Assert.assertEquals(2, outcomes.size());
+		Assert.assertTrue(outcomes.contains(outcome1));
+		Assert.assertTrue(outcomes.contains(outcome2));
+	}
+	
+
+
+}
diff --git a/src/test/java/org/olat/test/AllTestsJunit4.java b/src/test/java/org/olat/test/AllTestsJunit4.java
index ff445d9a846..51f11679eb8 100644
--- a/src/test/java/org/olat/test/AllTestsJunit4.java
+++ b/src/test/java/org/olat/test/AllTestsJunit4.java
@@ -123,6 +123,7 @@ import org.junit.runners.Suite;
 	org.olat.modules.ims.qti.fileresource.FileResourceValidatorTest.class,
 	org.olat.ims.qti.qpool.QTIImportProcessorTest.class,
 	org.olat.ims.qti.qpool.QTIExportProcessorTest.class,
+	org.olat.ims.lti.LTIManagerTest.class,
 	org.olat.modules.webFeed.FeedManagerImplTest.class,
 	org.olat.modules.qpool.manager.MetadataConverterHelperTest.class,
 	org.olat.modules.qpool.manager.QuestionDAOTest.class,
-- 
GitLab