diff --git a/pom.xml b/pom.xml index a8be5bc61a72cebc941aae02daa512f670075492..d4703f9331616dbcff4f9552074e4e8681bdb518 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 d2310dfe2db1b98f314718001af42b1743e41978..ddc4e5aafc67e8e4e9dcc13a3766e64801674ca8 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 09cfe2eb396d0a0d4e58f9cb02f0422ebd108400..0b2db1bc748322c531b8aa808cac2afe7e7bc63c 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 4ffcc9f2a591702e9515f720f14aac187fc01640..eaa7dd60cb1a15ea382ee28d3c66bd0829d9f493 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 36b959083653fb7350b26d1c8e9f0a6b687cf333..556079f676d2e40019e93adad7086fd2be0d2f51 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 e8cd8370e4cea0eb2cd08d7bb648d0d63fa67e19..9951f38cec854a8d4d6a96e7a40945b534a62429 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 6adc1fb9d1bf7b2edf1322ac2be430224870821a..0000000000000000000000000000000000000000 --- 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 bf2d89ee0214b196d0613580c1d63bc6edbed5d0..0a4a5afb92394bfce6be05761b9b581c332732f1 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 91aaf73bbf6e8348c748ad1b0c252e543b71ddb1..adad4f745a76268e61011266bfdf188349cf023d 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 ba9935b62d2c196e7d3c82a3d2aeb31d86390e7c..6e900c4fb66a4514609f2e67a9676fb8e0aebe9a 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 8d69839b8a24485d03bf777f3d01d3d1c8c58e31..f9c4600497588649fbdcd4550cd26b4cbb732224 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 54d07fbbb909fd00b27fcff0e38e6e0d86bda044..ae94108cfdd5c6c5f82790cae84c5772cd5bf6f3 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 0000000000000000000000000000000000000000..1a12b7b046423c921a98cee34d285ac694cfa99c --- /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 44ce7746feb31dbedab02cf61b3c2a097b0ef584..3544cc2af137c75a961061076a2b92420128d2dc 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 48752cf6a99cbe04cb918876bade4f943a0241c2..d00b92ab490674afd7be70d39790d7633065784a 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 0000000000000000000000000000000000000000..3dc224cbf2eda71d623f4d273b9f14e7c69686c3 --- /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 7937bfd3f1b8803d3a70c7e184a424937f51079e..6546512c851cc0f22decde22c598afabb401501b 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 ca0c0dad1aeee862c49692b0ff671cf7ab90a6f4..86a407e1096564c994f361875ce5cc3d55db3c6a 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 0000000000000000000000000000000000000000..1271117eb581ffa1ac3fbb6d8c8b42b823c40241 --- /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 f615d698944dd5b5406d58c6a0e9c90c2adcabf4..d56cb11cda81a8a2426fe958793780e36218a58f 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 0000000000000000000000000000000000000000..fa42526c5b46b40c469d366323375aaf10a9beee --- /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 eb0c9977fa49fcb31bf6fbd5ab8a84d8402c37de..9abe81df8db2ac850f2f4f723887bcdb01ba1328 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 27c4c3d4462a645479266f6718baec5041246072..ed0a5d1b3036b362c977471319b84d47ca5651b5 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 80a1843a9de506ff1c7f738ae1779a7a34f1ba4f..8ce69d1192936b7823984b609e54d794fe414d1a 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 0000000000000000000000000000000000000000..a47212cbefa1c963143466bee5c152e489cf1c68 --- /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 b7aae99bbde4b716167def2b50b4521ea2a23363..0000000000000000000000000000000000000000 --- 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 0000000000000000000000000000000000000000..45843e5e4094bfdd73d136d2f6e19c6458344385 --- /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 0000000000000000000000000000000000000000..88ec60a65e184679e902d33a138e06e279e32e88 --- /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 0000000000000000000000000000000000000000..f31696ba61f7375809a027466617f420d899dcb7 --- /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 0000000000000000000000000000000000000000..93a978bd8c76fa0fc4615cc96880ed77a65ede75 --- /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 0000000000000000000000000000000000000000..9b37d5c71468acf9d96b78d70bd82073092a4fa0 --- /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 0000000000000000000000000000000000000000..3f3053e6001c148046941c64e40b25d9f9766b81 --- /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 0000000000000000000000000000000000000000..75958fff3acdedb9e1e36ed707b94f441e26b896 --- /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 0000000000000000000000000000000000000000..f4661804ae7170258b6eadb1afeb8e66a7c809fb --- /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 0000000000000000000000000000000000000000..693bd93465943d127c1105b2ed5de72957794a53 --- /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 0000000000000000000000000000000000000000..eb2d274839a72f268cc99927bfcb83cd2b117688 --- /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 0000000000000000000000000000000000000000..f1663d4735d8e5f1fd171e623a205fa69629e2a5 --- /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 0000000000000000000000000000000000000000..4b5d91258babb4b3f2c9f9b39948d3d8439483ce --- /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 33f35d53806114c6c60ff093fe165037ae9fddb3..df8fab0fff8fb4cf9ee30df7df7746db0cb03d17 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 08b8b211b8b2c2166b8388c88d17868bd994128d..9becbcf28b178b2409dc79c02a8155dbf2dbfa18 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 4d5b2e3444ad0798e7dcf87c5706ddb72f318fd4..ca2583c97f244867a7a9430b8628a460583da91c 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 0bb3a179e1bc0e2161762866898cbe4c8089d523..6bff1cff0abae29e878f56b88393d56dcccee434 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 91ee4abe45625c22fb1c6c8758324d812ccef6ff..fa5c54164328631e7440b63e06ba1ec80c0af5af 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 9eb4f4745206e239ed35d442f887308446c198b2..c78d111403193c7f13c4d146220cfd74acce115f 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 64404eb2b9fdab3d6cd96a2ae971ae24952195b9..ddb6c243596fd1262fdf1826b801a59bbba7cd8e 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 0263b5775e1d0aac2a479473b6e0ee6827b380df..d66131ae6334171fd42eb4a1448bb5a67888fcda 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 1e4bb97102ab610c920e6171ffc2b0db19194466..f0a9dfeee2628cfd38b521065eaa61dc926b9c8c 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 3e29a4c196554b67bc4dfe8ae1067595b1e1a966..4c8e209237b717e32a2210b340f7301f41fb4043 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 0000000000000000000000000000000000000000..f4e43d393deaef2ffab61ecde0e327853a4c6ffa --- /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 ff445d9a846dd3b9acf8358215152c2c3e910d5f..51f11679eb8e53e808e1bd6c1dcaba3a70f79f1f 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,