From b19c054e1b0b74ca9ed97ca72d4cba664ba4419e Mon Sep 17 00:00:00 2001
From: srosse <stephane.rosse@frentix.com>
Date: Tue, 21 Aug 2018 12:08:36 +0200
Subject: [PATCH] OO-3286: add a configuration to decorate the WebDAV course
 listing with informations of the curriculums

---
 .../commons/services/webdav/WebDAVModule.java |  18 ++
 .../webdav/ui/WebDAVAdminController.java      |  34 +++-
 .../ui/_i18n/LocalStrings_de.properties       |   1 +
 .../ui/_i18n/LocalStrings_en.properties       |   1 +
 .../course/CoursefolderWebDAVMergeSource.java | 177 +++++++++++++-----
 .../modules/curriculum/CurriculumService.java |   4 +
 .../manager/CurriculumElementDAO.java         |   1 -
 .../CurriculumRepositoryEntryRelationDAO.java |  35 ++++
 .../manager/CurriculumServiceImpl.java        |  11 ++
 .../model/CurriculumElementWebDAVInfos.java   | 100 ++++++++++
 .../olat/repository/RepositoryManager.java    |   4 +-
 .../RepositorySearchController.java           |   2 +-
 .../manager/RepositoryUserDataManager.java    |   2 +-
 .../RepositoryEntriesWebService.java          |   2 +-
 .../repository/RepositoryManagerTest.java     |   6 +-
 15 files changed, 338 insertions(+), 60 deletions(-)
 create mode 100644 src/main/java/org/olat/modules/curriculum/model/CurriculumElementWebDAVInfos.java

diff --git a/src/main/java/org/olat/core/commons/services/webdav/WebDAVModule.java b/src/main/java/org/olat/core/commons/services/webdav/WebDAVModule.java
index 6915f57a8e5..a60d92bfa9c 100644
--- a/src/main/java/org/olat/core/commons/services/webdav/WebDAVModule.java
+++ b/src/main/java/org/olat/core/commons/services/webdav/WebDAVModule.java
@@ -48,6 +48,7 @@ public class WebDAVModule extends AbstractSpringModule implements ConfigOnOff {
 	private static final String LEARNERS_BOOKMARKS_COURSE = "webdav.learners.bookmarks.courses";
 	private static final String LEARNERS_PARTICIPATING_COURSES = "webdav.learners.participating.courses";
 	private static final String PREPEND_COURSE_REFERENCE_TO_TITLE = "webdav.prepend.course.reference.to.title";
+	private static final String CURRICULUM_ELEMENTS_FOLDERS_ENABLED = "webdav.curriculumelements.folders.enabled";
 	
 	@Autowired
 	private List<WebDAVProvider> webdavProviders;
@@ -60,6 +61,8 @@ public class WebDAVModule extends AbstractSpringModule implements ConfigOnOff {
 	private boolean digestAuthenticationEnabled;
 	@Value("${webdav.termsfolders.enabled:true}")
 	private boolean termsFoldersEnabled;
+	@Value("${webdav.curriculumelements.folders.enabled:false}")
+	private boolean curriculumElementFoldersEnabled;
 	@Value("${webdav.prepend.course.reference.to.title:false}")
 	private boolean prependCourseReferenceToTitle;
 
@@ -99,6 +102,11 @@ public class WebDAVModule extends AbstractSpringModule implements ConfigOnOff {
 			termsFoldersEnabled = "true".equals(termsFoldersEnabledObj);
 		}
 		
+		String curriculumElementsFoldersEnabledObj = getStringPropertyValue(CURRICULUM_ELEMENTS_FOLDERS_ENABLED, true);
+		if(StringHelper.containsNonWhitespace(curriculumElementsFoldersEnabledObj)) {
+			curriculumElementFoldersEnabled = "true".equals(curriculumElementsFoldersEnabledObj);
+		}
+		
 		String learnersBookmarksCourseObj = getStringPropertyValue(LEARNERS_BOOKMARKS_COURSE, true);
 		if(StringHelper.containsNonWhitespace(learnersBookmarksCourseObj)) {
 			enableLearnersBookmarksCourse = "true".equals(learnersBookmarksCourseObj);
@@ -159,6 +167,16 @@ public class WebDAVModule extends AbstractSpringModule implements ConfigOnOff {
 		setStringProperty(TERMS_FOLDERS_ENABLED, enabledStr, true);
 	}
 
+
+	public boolean isCurriculumElementFoldersEnabled() {
+		return curriculumElementFoldersEnabled;
+	}
+
+	public void setCurriculumElementFoldersEnabled(boolean enabled) {
+		this.curriculumElementFoldersEnabled = enabled;
+		setStringProperty(CURRICULUM_ELEMENTS_FOLDERS_ENABLED, Boolean.toString(enabled), true);
+	}
+
 	public boolean isEnableLearnersBookmarksCourse() {
 		return enableLearnersBookmarksCourse;
 	}
diff --git a/src/main/java/org/olat/core/commons/services/webdav/ui/WebDAVAdminController.java b/src/main/java/org/olat/core/commons/services/webdav/ui/WebDAVAdminController.java
index f531c4d0fc4..55f9c1fd03b 100644
--- a/src/main/java/org/olat/core/commons/services/webdav/ui/WebDAVAdminController.java
+++ b/src/main/java/org/olat/core/commons/services/webdav/ui/WebDAVAdminController.java
@@ -37,8 +37,16 @@ import org.olat.core.gui.control.WindowControl;
  */
 public class WebDAVAdminController extends FormBasicController {
 	
-	private MultipleSelectionElement enableModuleEl, enableLinkEl, enableDigestEl, enableTermsFoldersEl,
-			learnersAsParticipantEl, learnersBookmarkEl, prependReferenceEl;
+	private static final String[] onKeys = new String[]{"xx"};
+	
+	private MultipleSelectionElement enableModuleEl;
+	private MultipleSelectionElement enableLinkEl;
+	private MultipleSelectionElement enableDigestEl;
+	private MultipleSelectionElement enableTermsFoldersEl;
+	private MultipleSelectionElement enableCurriculumElementFoldersEl;
+	private MultipleSelectionElement learnersAsParticipantEl;
+	private MultipleSelectionElement learnersBookmarkEl;
+	private MultipleSelectionElement prependReferenceEl;
 	
 	private final WebDAVModule webDAVModule;
 	
@@ -58,40 +66,45 @@ public class WebDAVAdminController extends FormBasicController {
 
 		boolean enabled = webDAVModule.isEnabled();
 		String[] values = new String[] { getTranslator().translate("webdav.on") };
-		enableModuleEl = uifactory.addCheckboxesHorizontal("webdavModule", "webdav.module", formLayout, new String[]{"xx"}, values);
+		enableModuleEl = uifactory.addCheckboxesHorizontal("webdavModule", "webdav.module", formLayout, onKeys, values);
 		enableModuleEl.select("xx", enabled);
 		enableModuleEl.addActionListener(FormEvent.ONCHANGE);
 		
-		enableLinkEl = uifactory.addCheckboxesHorizontal("webdavLink", "webdav.link", formLayout, new String[]{"xx"}, values);
+		enableLinkEl = uifactory.addCheckboxesHorizontal("webdavLink", "webdav.link", formLayout, onKeys, values);
 		enableLinkEl.select("xx", webDAVModule.isLinkEnabled());
 		enableLinkEl.addActionListener(FormEvent.ONCHANGE);
 		enableLinkEl.setEnabled(enabled);
 		
-		enableDigestEl = uifactory.addCheckboxesHorizontal("webdavDigest", "webdav.digest", formLayout, new String[]{"xx"}, values);
+		enableDigestEl = uifactory.addCheckboxesHorizontal("webdavDigest", "webdav.digest", formLayout, onKeys, values);
 		enableDigestEl.select("xx", webDAVModule.isDigestAuthenticationEnabled());
 		enableDigestEl.addActionListener(FormEvent.ONCHANGE);
 		enableDigestEl.setEnabled(enabled);
 		
 		uifactory.addSpacerElement("spacer1", formLayout, false);
 		
-		enableTermsFoldersEl = uifactory.addCheckboxesHorizontal("webdavTermsFolders", "webdav.termsfolders", formLayout, new String[]{"xx"}, values);
+		enableTermsFoldersEl = uifactory.addCheckboxesHorizontal("webdavTermsFolders", "webdav.termsfolders", formLayout, onKeys, values);
 		enableTermsFoldersEl.select("xx", webDAVModule.isTermsFoldersEnabled());
 		enableTermsFoldersEl.addActionListener(FormEvent.ONCHANGE);
 		enableTermsFoldersEl.setEnabled(enabled);
 		
-		prependReferenceEl = uifactory.addCheckboxesHorizontal("webdavPrepend", "webdav.prepend.reference", formLayout, new String[]{"xx"}, values);
+		enableCurriculumElementFoldersEl = uifactory.addCheckboxesHorizontal("webdavCurriculumsElementsFolders", "webdav.curriculumelementsfolders", formLayout, onKeys, values);
+		enableCurriculumElementFoldersEl.select("xx", webDAVModule.isCurriculumElementFoldersEnabled());
+		enableCurriculumElementFoldersEl.addActionListener(FormEvent.ONCHANGE);
+		enableCurriculumElementFoldersEl.setEnabled(enabled);
+		
+		prependReferenceEl = uifactory.addCheckboxesHorizontal("webdavPrepend", "webdav.prepend.reference", formLayout, onKeys, values);
 		prependReferenceEl.select("xx", webDAVModule.isPrependCourseReferenceToTitle());
 		prependReferenceEl.addActionListener(FormEvent.ONCHANGE);
 		prependReferenceEl.setEnabled(enabled);
 
 		uifactory.addSpacerElement("spacer2", formLayout, false);
 		
-		learnersAsParticipantEl = uifactory.addCheckboxesHorizontal("learnersParticipants", "webdav.for.learners.participants", formLayout, new String[]{"xx"}, values);
+		learnersAsParticipantEl = uifactory.addCheckboxesHorizontal("learnersParticipants", "webdav.for.learners.participants", formLayout, onKeys, values);
 		learnersAsParticipantEl.select("xx", webDAVModule.isEnableLearnersParticipatingCourses());
 		learnersAsParticipantEl.addActionListener(FormEvent.ONCHANGE);
 		learnersAsParticipantEl.setEnabled(enabled);
 		
-		learnersBookmarkEl = uifactory.addCheckboxesHorizontal("learnerBookmarks", "webdav.for.learners.bookmarks", formLayout, new String[]{"xx"}, values);
+		learnersBookmarkEl = uifactory.addCheckboxesHorizontal("learnerBookmarks", "webdav.for.learners.bookmarks", formLayout, onKeys, values);
 		learnersBookmarkEl.select("xx", webDAVModule.isEnableLearnersBookmarksCourse());
 		learnersBookmarkEl.addActionListener(FormEvent.ONCHANGE);
 		learnersBookmarkEl.setEnabled(enabled);
@@ -121,6 +134,9 @@ public class WebDAVAdminController extends FormBasicController {
 		} else if(source == enableTermsFoldersEl) {
 			boolean enabled = enableTermsFoldersEl.isAtLeastSelected(1);
 			webDAVModule.setTermsFoldersEnabled(enabled);
+		} else if(source == enableCurriculumElementFoldersEl) {
+			boolean enabled = enableCurriculumElementFoldersEl.isAtLeastSelected(1);
+			webDAVModule.setCurriculumElementFoldersEnabled(enabled);
 		} else if(source == learnersAsParticipantEl) {
 			boolean enabled = learnersAsParticipantEl.isAtLeastSelected(1);
 			webDAVModule.setEnableLearnersParticipatingCourses(enabled);
diff --git a/src/main/java/org/olat/core/commons/services/webdav/ui/_i18n/LocalStrings_de.properties b/src/main/java/org/olat/core/commons/services/webdav/ui/_i18n/LocalStrings_de.properties
index 84b12d8bde0..b2dde162710 100644
--- a/src/main/java/org/olat/core/commons/services/webdav/ui/_i18n/LocalStrings_de.properties
+++ b/src/main/java/org/olat/core/commons/services/webdav/ui/_i18n/LocalStrings_de.properties
@@ -3,6 +3,7 @@ admin.menu.title=WebDAV
 admin.menu.title.alt=WebDAV Zugang
 admin.webdav.description=Mit Hilfe von WebDAV k\u00F6nnen Sie OpenOLAT Ordner auf Ihrem lokalen Desktop wie lokale Ordner anzeigen und verwenden. Konfigurieren Sie ob diese Funktion allen Benutzern Systemweit zur Verf\u00FCgung stehen soll. Bitte lesen sie die Kontexthilfe.
 core.webdav=WebDAV
+webdav.curriculumelementsfolders=Kurse nach Curriculumelementen gruppieren
 webdav.digest=Digest Authentication bei HTTP Zugang verwenden 
 webdav.for.learners.bookmarks=Zugriff f\u00FCr Studenten / Betreuer Favoriten
 webdav.for.learners.participants=Zugriff f\u00FCr Studenten / Betreuer Kurse
diff --git a/src/main/java/org/olat/core/commons/services/webdav/ui/_i18n/LocalStrings_en.properties b/src/main/java/org/olat/core/commons/services/webdav/ui/_i18n/LocalStrings_en.properties
index 47a1aee8a22..b17b91bc651 100644
--- a/src/main/java/org/olat/core/commons/services/webdav/ui/_i18n/LocalStrings_en.properties
+++ b/src/main/java/org/olat/core/commons/services/webdav/ui/_i18n/LocalStrings_en.properties
@@ -3,6 +3,7 @@ admin.menu.title=WebDAV
 admin.menu.title.alt=WebDAV access
 admin.webdav.description=Using WebDAV you can mount and use OpenOLAT folders on your local desktop as if they were local folders. Enable this feature to make it accessable to all users of your platform. Please read the context help.
 core.webdav=WebDAV
+webdav.curriculumelementsfolders=Group courses by curriculum elements
 webdav.digest=Digest Authentication for HTTP access
 webdav.for.learners.bookmarks=Enable access for courses that users marked as favorite
 webdav.for.learners.participants=Enable access for courses where user is participant or coach
diff --git a/src/main/java/org/olat/course/CoursefolderWebDAVMergeSource.java b/src/main/java/org/olat/course/CoursefolderWebDAVMergeSource.java
index aa85cde2fb3..8f8beb9029e 100644
--- a/src/main/java/org/olat/course/CoursefolderWebDAVMergeSource.java
+++ b/src/main/java/org/olat/course/CoursefolderWebDAVMergeSource.java
@@ -36,6 +36,8 @@ import org.olat.core.util.StringHelper;
 import org.olat.core.util.vfs.NamedContainerImpl;
 import org.olat.core.util.vfs.VFSContainer;
 import org.olat.core.util.vfs.VirtualContainer;
+import org.olat.modules.curriculum.CurriculumService;
+import org.olat.modules.curriculum.model.CurriculumElementWebDAVInfos;
 import org.olat.repository.RepositoryEntry;
 import org.olat.repository.RepositoryEntryStatusEnum;
 import org.olat.repository.RepositoryManager;
@@ -53,49 +55,52 @@ class CoursefolderWebDAVMergeSource extends WebDAVMergeSource {
 	
 	private final WebDAVModule webDAVModule;
 	private final RepositoryManager repositoryManager;
+	private final CurriculumService curriculumService;
 	
 	public CoursefolderWebDAVMergeSource(IdentityEnvironment identityEnv) {
 		super(identityEnv.getIdentity());
 		this.identityEnv = identityEnv;
 		webDAVModule = CoreSpringFactory.getImpl(WebDAVModule.class);
 		repositoryManager = CoreSpringFactory.getImpl(RepositoryManager.class);
+		curriculumService = CoreSpringFactory.getImpl(CurriculumService.class);
 	}
 	
 	@Override
 	protected List<VFSContainer> loadMergedContainers() {
 		List<VFSContainer> containers = new ArrayList<>();
 		
-		Map<String, VFSContainer> terms = null;
-		VirtualContainer noTermContainer = null;
+		Map<String, VFSContainer> terms = new HashMap<>();
+		VirtualContainer noTermContainer = new VirtualContainer("_other");
 		VirtualContainer finishedContainer = null;
 		
-		boolean useTerms = webDAVModule.isTermsFoldersEnabled();
-		if (useTerms) {
-			// prepare no-terms folder for all resources without semester term info or private date
-			terms = new HashMap<>();
-			noTermContainer = new VirtualContainer("_other");
-		} else {
+		boolean useSemestersTerms = webDAVModule.isTermsFoldersEnabled();
+		boolean useCurriculumElementsTerms = webDAVModule.isCurriculumElementFoldersEnabled();
+		boolean prependReference = webDAVModule.isPrependCourseReferenceToTitle();
+		
+		NamingAndGrouping namingAndGrouping = new NamingAndGrouping(useSemestersTerms, useCurriculumElementsTerms);
+		if(useCurriculumElementsTerms) {
+			namingAndGrouping.setCurriculumElementInfos(getCurriculumElementWebDAVInfosMap());
+		}
+		if(!useSemestersTerms && !useCurriculumElementsTerms) {
 			finishedContainer = new VirtualContainer("_finished");
 		}
-		boolean prependReference = webDAVModule.isPrependCourseReferenceToTitle();
 		
-		UniqueNames container = new UniqueNames();
-		List<RepositoryEntry> editorEntries = repositoryManager.queryByOwner(getIdentity(), "CourseModule");
-		appendCourses(editorEntries, true, containers, useTerms, terms, noTermContainer, finishedContainer, prependReference, container);
+		List<RepositoryEntry> editorEntries = repositoryManager.queryByOwner(getIdentity(), true, "CourseModule");
+		appendCourses(editorEntries, true, containers, terms, noTermContainer, finishedContainer, prependReference, namingAndGrouping);
 		
 		//add courses as participant and coaches
 		if(webDAVModule.isEnableLearnersParticipatingCourses()) {
 			List<RepositoryEntry> entries = repositoryManager.getLearningResourcesAsParticipantAndCoach(getIdentity(), "CourseModule");
-			appendCourses(entries, false, containers, useTerms, terms, noTermContainer, finishedContainer, prependReference, container);
+			appendCourses(entries, false, containers, terms, noTermContainer, finishedContainer, prependReference, namingAndGrouping);
 		}
 		
 		//add bookmarked courses
 		if(webDAVModule.isEnableLearnersBookmarksCourse()) {
 			List<RepositoryEntry> bookmarkedEntries = repositoryManager.getLearningResourcesAsBookmark(getIdentity(), identityEnv.getRoles(), "CourseModule", 0, -1);
-			appendCourses(bookmarkedEntries, false, containers, useTerms, terms, noTermContainer, finishedContainer, prependReference, container);
+			appendCourses(bookmarkedEntries, false, containers, terms, noTermContainer, finishedContainer, prependReference, namingAndGrouping);
 		}
 
-		if (useTerms) {
+		if (useSemestersTerms || useCurriculumElementsTerms) {
 			// add no-terms folder if any have been found
 			if (!noTermContainer.getItems().isEmpty()) {
 				addContainerToList(noTermContainer, containers);
@@ -107,52 +112,85 @@ class CoursefolderWebDAVMergeSource extends WebDAVMergeSource {
 		return containers;
 	}
 	
+	private Map<Long,List<CurriculumElementWebDAVInfos>> getCurriculumElementWebDAVInfosMap() {
+		List<CurriculumElementWebDAVInfos> infos = curriculumService.getCurriculumElementInfosForWebDAV(getIdentity());
+		Map<Long,List<CurriculumElementWebDAVInfos>> infoMap = new HashMap<>();
+		for(CurriculumElementWebDAVInfos info:infos) {
+			List<CurriculumElementWebDAVInfos> repoInfos = infoMap
+					.computeIfAbsent(info.getRepositoryEntryKey(), i -> new ArrayList<>());
+			if(!repoInfos.contains(info)) {
+				repoInfos.add(info);
+			}
+		}
+		return infoMap;
+	}
+	
 	private void appendCourses(List<RepositoryEntry> courseEntries, boolean editor, List<VFSContainer> containers,
-			boolean useTerms, Map<String, VFSContainer> terms, VirtualContainer noTermContainer, VirtualContainer finishedContainer,
-			boolean prependReference, UniqueNames container) {	
+			Map<String, VFSContainer> terms, VirtualContainer noTermContainer, VirtualContainer finishedContainer,
+			boolean prependReference, NamingAndGrouping namingAndGrouping) {	
 		
 		// Add all found repo entries to merge source
 		int count = 0;
 		for (RepositoryEntry re:courseEntries) {
-			if(container.isDuplicate(re)) {
+			if(namingAndGrouping.isDuplicate(re)) {
 				continue;
 			}
-			
-			String displayName = re.getDisplayname();
-			if(prependReference && StringHelper.containsNonWhitespace(re.getExternalRef())) {
-				displayName = re.getExternalRef() + " " + displayName;
-			}
-			String courseTitle = RequestUtil.normalizeFilename(displayName);
-			
+
 			if(finishedContainer != null && re.getEntryStatus() == RepositoryEntryStatusEnum.closed) {
-				String name = container.getFinishedUniqueName(courseTitle);
+				String courseTitle = getCourseTitle(re, prependReference);
+				String name = namingAndGrouping.getFinishedUniqueName(courseTitle);
 				NamedContainerImpl cfContainer = new CoursefolderWebDAVNamedContainer(name, re, editor ? null : identityEnv);
 				finishedContainer.getItems().add(cfContainer);
-			} else if (useTerms) {
+			} else if (namingAndGrouping.isUseSemesterTerms() || namingAndGrouping.isUseCurriculumElementsTerms()) {
 				RepositoryEntryLifecycle lc = re.getLifecycle();
-				if (lc != null && !lc.isPrivateCycle()) {
+				
+				boolean termed = false;
+				if (namingAndGrouping.isUseSemesterTerms() && lc != null && !lc.isPrivateCycle()) {
 					// when a semester term info is found, add it to corresponding term folder
 					String termSoftKey = lc.getSoftKey();
-					VFSContainer termContainer = terms.get(termSoftKey);
-					if (termContainer == null) {
-						// folder for this semester term does not yet exist, create one and add to map
-						String normalizedKey = RequestUtil.normalizeFilename(termSoftKey);
-						termContainer = new VirtualContainer(normalizedKey);
-						terms.put(termSoftKey, termContainer);
-						addContainerToList(termContainer, containers);
-					}
-					
-					String name = container.getTermUniqueName(termSoftKey, courseTitle);
+					VFSContainer termContainer = terms.computeIfAbsent(termSoftKey, term -> {
+						String normalizedKey = RequestUtil.normalizeFilename(term);
+						VirtualContainer container = new VirtualContainer(normalizedKey);
+						addContainerToList(container, containers);
+						return container;
+					});
+
+					String courseTitle = getCourseTitle(re, prependReference);
+					String name = namingAndGrouping.getTermUniqueName(termSoftKey, courseTitle);
 					NamedContainerImpl cfContainer = new CoursefolderWebDAVNamedContainer(name, re, editor ? null : identityEnv);
 					termContainer.getItems().add(cfContainer);
-				} else {
+					termed = true;
+				}
+				
+				if(namingAndGrouping.isUseCurriculumElementsTerms() && namingAndGrouping.hasCurriculumElements(re)) {
+					List<CurriculumElementWebDAVInfos> elements = namingAndGrouping.getCurriculumElementInfos().get(re.getKey());
+					for(CurriculumElementWebDAVInfos element:elements) {
+						String termSoftKey = getTermSoftKey(element);
+						VFSContainer termContainer = terms.computeIfAbsent(termSoftKey, term -> {
+							String normalizedKey = RequestUtil.normalizeFilename(term);
+							VirtualContainer container = new VirtualContainer(normalizedKey);
+							addContainerToList(container, containers);
+							return container;
+						});	
+
+						String courseTitle = getCourseTitle(re, false);
+						String name = namingAndGrouping.getTermUniqueName(termSoftKey, courseTitle);
+						NamedContainerImpl cfContainer = new CoursefolderWebDAVNamedContainer(name, re, editor ? null : identityEnv);
+						termContainer.getItems().add(cfContainer);
+						termed = true;
+					}
+				}
+				
+				if(!termed) {
 					// no semester term found, add to no-term folder
-					String name = container.getNoTermUniqueName(courseTitle);
+					String courseTitle = getCourseTitle(re, prependReference);
+					String name = namingAndGrouping.getNoTermUniqueName(courseTitle);
 					NamedContainerImpl cfContainer = new CoursefolderWebDAVNamedContainer(name, re, editor ? null : identityEnv);
 					noTermContainer.getItems().add(cfContainer);
 				}
 			} else {
-				String name = container.getContainersUniqueName(courseTitle);
+				String courseTitle = getCourseTitle(re, prependReference);
+				String name = namingAndGrouping.getContainersUniqueName(courseTitle);
 				NamedContainerImpl cfContainer = new CoursefolderWebDAVNamedContainer(name, re, editor ? null : identityEnv);
 				addContainerToList(cfContainer, containers);
 			}
@@ -162,7 +200,35 @@ class CoursefolderWebDAVMergeSource extends WebDAVMergeSource {
 		}
 	}
 	
-	private static class UniqueNames {
+	private String getCourseTitle(RepositoryEntry re, boolean prependReference) {
+		String displayName = re.getDisplayname();
+		if(prependReference && StringHelper.containsNonWhitespace(re.getExternalRef())) {
+			displayName = re.getExternalRef() + " " + displayName;
+		}
+		return RequestUtil.normalizeFilename(displayName);
+		
+	}
+	
+	private String getTermSoftKey(CurriculumElementWebDAVInfos element) {
+		StringBuilder sb = new StringBuilder();
+		if(StringHelper.containsNonWhitespace(element.getParentCurriculumElementDisplayName())) {
+			if(StringHelper.containsNonWhitespace(element.getParentCurriculumElementIdentifier())) {
+				sb.append(element.getParentCurriculumElementIdentifier()).append(" ");
+			}
+			sb.append(element.getParentCurriculumElementDisplayName());
+		} else if(StringHelper.containsNonWhitespace(element.getCurriculumElementDisplayName())) {
+			if(StringHelper.containsNonWhitespace(element.getCurriculumElementIdentifier())) {
+				sb.append(element.getCurriculumElementIdentifier()).append(" ");
+			}
+			sb.append(element.getCurriculumElementDisplayName());
+		}
+		return sb.toString();
+	}
+	
+	private static class NamingAndGrouping {
+		
+		private final boolean useSemesterTerms;
+		private final boolean useCurriculumElementsTerms;
 		
 		private final Set<RepositoryEntry> duplicates = new HashSet<>();
 		private final Set<String> containers = new HashSet<>();
@@ -170,6 +236,33 @@ class CoursefolderWebDAVMergeSource extends WebDAVMergeSource {
 		private final Set<String> finishedContainer = new HashSet<>();
 		private final Map<String,Set<String>> termContainers = new HashMap<>();
 		
+		private Map<Long,List<CurriculumElementWebDAVInfos>> curriculumElementInfos;
+		
+		public NamingAndGrouping(boolean useSemesterTerms, boolean useCurriculumElementsTerms) {
+			this.useSemesterTerms = useSemesterTerms;
+			this.useCurriculumElementsTerms = useCurriculumElementsTerms;
+		}
+		
+		public boolean isUseSemesterTerms() {
+			return useSemesterTerms;
+		}
+
+		public boolean isUseCurriculumElementsTerms() {
+			return useCurriculumElementsTerms;
+		}
+
+		public Map<Long, List<CurriculumElementWebDAVInfos>> getCurriculumElementInfos() {
+			return curriculumElementInfos;
+		}
+
+		public void setCurriculumElementInfos(Map<Long, List<CurriculumElementWebDAVInfos>> curriculumElementInfos) {
+			this.curriculumElementInfos = curriculumElementInfos;
+		}
+		
+		public boolean hasCurriculumElements(RepositoryEntry re) {
+			return curriculumElementInfos != null && curriculumElementInfos.containsKey(re.getKey());
+		}
+
 		public boolean isDuplicate(RepositoryEntry re) {
 			boolean duplicate = duplicates.contains(re);
 			if(!duplicate) {
diff --git a/src/main/java/org/olat/modules/curriculum/CurriculumService.java b/src/main/java/org/olat/modules/curriculum/CurriculumService.java
index 1688ae639ce..bd61ee50391 100644
--- a/src/main/java/org/olat/modules/curriculum/CurriculumService.java
+++ b/src/main/java/org/olat/modules/curriculum/CurriculumService.java
@@ -31,6 +31,7 @@ import org.olat.core.id.Roles;
 import org.olat.modules.curriculum.model.CurriculumElementInfos;
 import org.olat.modules.curriculum.model.CurriculumElementMembershipChange;
 import org.olat.modules.curriculum.model.CurriculumElementRepositoryEntryViews;
+import org.olat.modules.curriculum.model.CurriculumElementWebDAVInfos;
 import org.olat.modules.curriculum.model.CurriculumInfos;
 import org.olat.modules.curriculum.model.CurriculumMember;
 import org.olat.modules.curriculum.model.CurriculumSearchParameters;
@@ -391,5 +392,8 @@ public interface CurriculumService {
 	
 	
 	public List<CurriculumElementRepositoryEntryViews> getCurriculumElements(Identity identity, Roles roles, CurriculumRef curriculum);
+	
+
+	public List<CurriculumElementWebDAVInfos> getCurriculumElementInfosForWebDAV(IdentityRef identity);
 
 }
diff --git a/src/main/java/org/olat/modules/curriculum/manager/CurriculumElementDAO.java b/src/main/java/org/olat/modules/curriculum/manager/CurriculumElementDAO.java
index 3201368282b..a1722cecf3a 100644
--- a/src/main/java/org/olat/modules/curriculum/manager/CurriculumElementDAO.java
+++ b/src/main/java/org/olat/modules/curriculum/manager/CurriculumElementDAO.java
@@ -342,7 +342,6 @@ public class CurriculumElementDAO {
 				.getResultList();
 	}
 	
-	
 	public List<CurriculumElementMembership> getMembershipInfos(CurriculumRef curriculum, Collection<CurriculumElement> elements, Identity... identities) {
 		StringBuilder sb = new StringBuilder(256);
 		sb.append("select el.key, membership from curriculumelement el")
diff --git a/src/main/java/org/olat/modules/curriculum/manager/CurriculumRepositoryEntryRelationDAO.java b/src/main/java/org/olat/modules/curriculum/manager/CurriculumRepositoryEntryRelationDAO.java
index e342f4c791a..1dbce1248aa 100644
--- a/src/main/java/org/olat/modules/curriculum/manager/CurriculumRepositoryEntryRelationDAO.java
+++ b/src/main/java/org/olat/modules/curriculum/manager/CurriculumRepositoryEntryRelationDAO.java
@@ -28,6 +28,7 @@ import java.util.stream.Collectors;
 
 import javax.persistence.TypedQuery;
 
+import org.olat.basesecurity.IdentityRef;
 import org.olat.core.commons.persistence.DB;
 import org.olat.core.commons.persistence.QueryBuilder;
 import org.olat.core.id.Identity;
@@ -36,6 +37,7 @@ import org.olat.modules.curriculum.CurriculumElementRef;
 import org.olat.modules.curriculum.CurriculumElementStatus;
 import org.olat.modules.curriculum.CurriculumRef;
 import org.olat.modules.curriculum.CurriculumRoles;
+import org.olat.modules.curriculum.model.CurriculumElementWebDAVInfos;
 import org.olat.repository.RepositoryEntry;
 import org.olat.repository.RepositoryEntryRef;
 import org.olat.repository.RepositoryEntryStatusEnum;
@@ -83,6 +85,39 @@ public class CurriculumRepositoryEntryRelationDAO {
 			.getResultList();
 	}
 	
+	public List<CurriculumElementWebDAVInfos> getCurriculumElementInfosForWebDAV(IdentityRef identity, List<String> roles) {
+		StringBuilder sb = new StringBuilder(256);
+		sb.append("select rel.entry.key, el.key, el.displayName, el.identifier,")
+		  .append(" parentEl.key, parentEl.displayName, parentEl.identifier")
+		  .append(" from curriculumelement as el")
+		  .append(" inner join el.group as bGroup")
+		  .append(" inner join bGroup.members as memberships")
+		  .append(" left join el.parent as parentEl")
+		  .append(" inner join repoentrytogroup as rel on (bGroup.key=rel.group.key)")
+		  .append(" where memberships.identity.key=:identityKey and memberships.role in (:roles)")
+		  .append(" group by rel.entry.key, el.key, el.displayName, el.identifier, parentEl.key, parentEl.displayName, parentEl.identifier");
+		
+		List<Object[]> rawObjects = dbInstance.getCurrentEntityManager()
+				.createQuery(sb.toString(), Object[].class)
+				.setParameter("identityKey", identity.getKey())
+				.setParameter("roles", roles)
+				.getResultList();
+		List<CurriculumElementWebDAVInfos> infos = new ArrayList<>(rawObjects.size());
+		for(Object[] rawObject:rawObjects) {
+			Long repositoryEntryKey = (Long)rawObject[0];
+			Long curriculumElementKey = (Long)rawObject[1];
+			String curriculumElementDisplayName = (String)rawObject[2];
+			String curriculumElementIdentifier = (String)rawObject[3];
+			Long parentCurriculumElementKey = (Long)rawObject[4];
+			String parentCurriculumElementDisplayName = (String)rawObject[5];
+			String parentCurriculumElementIdentifier = (String)rawObject[6];	
+			infos.add(new CurriculumElementWebDAVInfos(repositoryEntryKey,
+					curriculumElementKey, curriculumElementDisplayName, curriculumElementIdentifier,
+					parentCurriculumElementKey, parentCurriculumElementDisplayName, parentCurriculumElementIdentifier));
+		}
+		return infos;
+	}
+	
 	public List<CurriculumElement> getCurriculumElements(RepositoryEntryRef entry, Identity identity, Collection<CurriculumRoles> roles) {
 		StringBuilder sb = new StringBuilder(256);
 		sb.append("select el from curriculumelement as el")
diff --git a/src/main/java/org/olat/modules/curriculum/manager/CurriculumServiceImpl.java b/src/main/java/org/olat/modules/curriculum/manager/CurriculumServiceImpl.java
index f1f698b1de3..a6851af33b6 100644
--- a/src/main/java/org/olat/modules/curriculum/manager/CurriculumServiceImpl.java
+++ b/src/main/java/org/olat/modules/curriculum/manager/CurriculumServiceImpl.java
@@ -30,6 +30,7 @@ import java.util.Map;
 import java.util.Set;
 
 import org.olat.basesecurity.GroupMembershipInheritance;
+import org.olat.basesecurity.GroupRoles;
 import org.olat.basesecurity.IdentityRef;
 import org.olat.basesecurity.OrganisationRoles;
 import org.olat.basesecurity.manager.GroupDAO;
@@ -56,6 +57,7 @@ import org.olat.modules.curriculum.model.CurriculumElementImpl;
 import org.olat.modules.curriculum.model.CurriculumElementInfos;
 import org.olat.modules.curriculum.model.CurriculumElementMembershipChange;
 import org.olat.modules.curriculum.model.CurriculumElementRepositoryEntryViews;
+import org.olat.modules.curriculum.model.CurriculumElementWebDAVInfos;
 import org.olat.modules.curriculum.model.CurriculumInfos;
 import org.olat.modules.curriculum.model.CurriculumMember;
 import org.olat.modules.curriculum.model.CurriculumSearchParameters;
@@ -460,6 +462,15 @@ public class CurriculumServiceImpl implements CurriculumService {
 		return curriculumElementToTaxonomyLevelDao.getCurriculumElements(level);
 	}
 
+	@Override
+	public List<CurriculumElementWebDAVInfos> getCurriculumElementInfosForWebDAV(IdentityRef identity) {
+		List<String> roles = new ArrayList<>();
+		roles.add(GroupRoles.owner.name());
+		roles.add(GroupRoles.coach.name());
+		roles.add(GroupRoles.participant.name());
+		return curriculumRepositoryEntryRelationDao.getCurriculumElementInfosForWebDAV(identity, roles);
+	}
+
 	@Override
 	public List<CurriculumElementRepositoryEntryViews> getCurriculumElements(Identity identity, Roles roles, CurriculumRef curriculum) {
 		if(curriculum == null) return Collections.emptyList();
diff --git a/src/main/java/org/olat/modules/curriculum/model/CurriculumElementWebDAVInfos.java b/src/main/java/org/olat/modules/curriculum/model/CurriculumElementWebDAVInfos.java
new file mode 100644
index 00000000000..b2d133a2ba1
--- /dev/null
+++ b/src/main/java/org/olat/modules/curriculum/model/CurriculumElementWebDAVInfos.java
@@ -0,0 +1,100 @@
+/**
+ * <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.modules.curriculum.model;
+
+/**
+ * 
+ * Initial date: 21 août 2018<br>
+ * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
+ *
+ */
+public class CurriculumElementWebDAVInfos {
+	
+	private final Long repositoryEntryKey;
+	
+	private final Long curriculumElementKey;
+	private final String curriculumElementDisplayName;
+	private final String curriculumElementIdentifier;
+	
+	private final Long parentCurriculumElementKey;
+	private final String parentCurriculumElementDisplayName;
+	private final String parentCurriculumElementIdentifier;
+	
+	public CurriculumElementWebDAVInfos(Long repositoryEntryKey,
+			Long curriculumElementKey, String curriculumElementDisplayName, String curriculumElementIdentifier,
+			Long parentCurriculumElementKey, String parentCurriculumElementDisplayName,String parentCurriculumElementIdentifier) {
+		this.repositoryEntryKey = repositoryEntryKey;
+		this.curriculumElementKey = curriculumElementKey;
+		this.curriculumElementDisplayName = curriculumElementDisplayName;
+		this.curriculumElementIdentifier = curriculumElementIdentifier;
+		this.parentCurriculumElementKey = parentCurriculumElementKey;
+		this.parentCurriculumElementDisplayName = parentCurriculumElementDisplayName;
+		this.parentCurriculumElementIdentifier = parentCurriculumElementIdentifier;
+	}
+
+	public Long getRepositoryEntryKey() {
+		return repositoryEntryKey;
+	}
+
+	public Long getCurriculumElementKey() {
+		return curriculumElementKey;
+	}
+
+	public String getCurriculumElementDisplayName() {
+		return curriculumElementDisplayName;
+	}
+
+	public String getCurriculumElementIdentifier() {
+		return curriculumElementIdentifier;
+	}
+
+	public Long getParentCurriculumElementKey() {
+		return parentCurriculumElementKey;
+	}
+
+	public String getParentCurriculumElementDisplayName() {
+		return parentCurriculumElementDisplayName;
+	}
+
+	public String getParentCurriculumElementIdentifier() {
+		return parentCurriculumElementIdentifier;
+	}
+	
+	@Override
+	public int hashCode() {
+		return (repositoryEntryKey == null ? 7645925 : repositoryEntryKey.hashCode())
+				+ (curriculumElementKey == null ? -4785 : curriculumElementKey.hashCode());
+	}
+
+	@Override
+	public boolean equals(Object obj) {
+		if(this == obj) {
+			return true;
+		}
+		if(obj instanceof CurriculumElementWebDAVInfos) {
+			CurriculumElementWebDAVInfos infos = (CurriculumElementWebDAVInfos)obj;
+			return repositoryEntryKey != null && repositoryEntryKey.equals(infos.repositoryEntryKey)
+					&& curriculumElementKey != null && curriculumElementKey.equals(infos.curriculumElementKey);
+		}
+		return false;
+	}
+	
+	
+}
diff --git a/src/main/java/org/olat/repository/RepositoryManager.java b/src/main/java/org/olat/repository/RepositoryManager.java
index 1f45d67cae6..dc0cfaee3d9 100644
--- a/src/main/java/org/olat/repository/RepositoryManager.java
+++ b/src/main/java/org/olat/repository/RepositoryManager.java
@@ -888,14 +888,14 @@ public class RepositoryManager {
 	 * @param limitType
 	 * @return Results
 	 */
-	public List<RepositoryEntry> queryByOwner(IdentityRef identity, String... limitTypes) {
+	public List<RepositoryEntry> queryByOwner(IdentityRef identity, boolean follow, String... limitTypes) {
 		if (identity == null) throw new AssertException("identity can not be null!");
 		QueryBuilder sb = new QueryBuilder(400);
 		sb.append("select v from repositoryentry v")
 		  .append(" inner join fetch v.olatResource as res")
 		  .append(" inner join fetch v.statistics as statistics")
 		  .append(" left join fetch v.lifecycle as lifecycle")
-		  .append(" inner join v.groups as relGroup on relGroup.defaultGroup=true")
+		  .append(" inner join v.groups as relGroup ").append(" on relGroup.defaultGroup=true", !follow)
 		  .append(" inner join relGroup.group as baseGroup")
 		  .append(" inner join baseGroup.members as membership on membership.role='").append(GroupRoles.owner.name()).append("'")
 		  .append(" where membership.identity.key=:identityKey and v.status ").in(RepositoryEntryStatusEnum.preparationToClosed());
diff --git a/src/main/java/org/olat/repository/controllers/RepositorySearchController.java b/src/main/java/org/olat/repository/controllers/RepositorySearchController.java
index 62b77cb68c4..55d9e746fb9 100644
--- a/src/main/java/org/olat/repository/controllers/RepositorySearchController.java
+++ b/src/main/java/org/olat/repository/controllers/RepositorySearchController.java
@@ -339,7 +339,7 @@ public class RepositorySearchController extends BasicController implements Activ
 	
 	private void doSearchByOwnerLimitTypeInternal(Identity owner, String[] limitTypes, boolean updateFilters) {
 		searchType = SearchType.byOwner;
-		List<RepositoryEntry> entries = repositoryManager.queryByOwner(owner, limitTypes);
+		List<RepositoryEntry> entries = repositoryManager.queryByOwner(owner, true, limitTypes);
 		filterRepositoryEntries(entries);
 		if(updateFilters) {
 			updateFilters(entries, owner);
diff --git a/src/main/java/org/olat/repository/manager/RepositoryUserDataManager.java b/src/main/java/org/olat/repository/manager/RepositoryUserDataManager.java
index 439422587b6..f41fd62e17a 100644
--- a/src/main/java/org/olat/repository/manager/RepositoryUserDataManager.java
+++ b/src/main/java/org/olat/repository/manager/RepositoryUserDataManager.java
@@ -85,7 +85,7 @@ public class RepositoryUserDataManager implements UserDataDeletable, UserDataExp
 	public void deleteUserData(Identity identity, String newDeletedUserName) {
 		// Remove as owner
 		Identity adminIdentity = deletionModule.getAdminUserIdentity();
-		List<RepositoryEntry> ownedRepoEntries = repositoryManager.queryByOwner(identity);
+		List<RepositoryEntry> ownedRepoEntries = repositoryManager.queryByOwner(identity, false);
 		for (RepositoryEntry repositoryEntry: ownedRepoEntries) {
 			repositoryService.removeRole(identity, repositoryEntry, GroupRoles.owner.name());
 			if (adminIdentity != null && repositoryService.countMembers(repositoryEntry, GroupRoles.owner.name()) == 0) {
diff --git a/src/main/java/org/olat/restapi/repository/RepositoryEntriesWebService.java b/src/main/java/org/olat/restapi/repository/RepositoryEntriesWebService.java
index e228c0c6230..97bbc2c98b9 100644
--- a/src/main/java/org/olat/restapi/repository/RepositoryEntriesWebService.java
+++ b/src/main/java/org/olat/restapi/repository/RepositoryEntriesWebService.java
@@ -256,7 +256,7 @@ public class RepositoryEntriesWebService {
 			Roles roles = getRoles(httpRequest);
 			
 			if(myEntries) {
-				List<RepositoryEntry> lstRepos = rm.queryByOwner(identity, restrictedType ? new String[] {type} : null);
+				List<RepositoryEntry> lstRepos = rm.queryByOwner(identity, true, restrictedType ? new String[] {type} : null);
 				boolean restrictedName = !name.equals("*");
 				boolean restrictedAuthor = !author.equals("*");
 				if(restrictedName || restrictedAuthor) {
diff --git a/src/test/java/org/olat/repository/RepositoryManagerTest.java b/src/test/java/org/olat/repository/RepositoryManagerTest.java
index c0ca1d2def5..7725cc2ea70 100644
--- a/src/test/java/org/olat/repository/RepositoryManagerTest.java
+++ b/src/test/java/org/olat/repository/RepositoryManagerTest.java
@@ -234,12 +234,12 @@ public class RepositoryManagerTest extends OlatTestCase {
 		repositoryEntryRelationDao.addRole(participant, re, GroupRoles.participant.name());
 		dbInstance.commitAndCloseSession();
 		
-		List<RepositoryEntry> entries = repositoryManager.queryByOwner(owner);
+		List<RepositoryEntry> entries = repositoryManager.queryByOwner(owner, true);
 		Assert.assertNotNull(entries);
 		Assert.assertEquals(1, entries.size());
 		Assert.assertTrue(entries.contains(re));
 		
-		List<RepositoryEntry> partEntries = repositoryManager.queryByOwner(participant);
+		List<RepositoryEntry> partEntries = repositoryManager.queryByOwner(participant, true);
 		Assert.assertNotNull(partEntries);
 		Assert.assertEquals(0, partEntries.size());
 	}
@@ -253,7 +253,7 @@ public class RepositoryManagerTest extends OlatTestCase {
 		repositoryEntryRelationDao.addRole(id, re, GroupRoles.owner.name());
 		dbInstance.commitAndCloseSession();
 		
-		List<RepositoryEntry> entries = repositoryManager.queryByOwner(id);
+		List<RepositoryEntry> entries = repositoryManager.queryByOwner(id, true);
 		Assert.assertNotNull(entries);
 		Assert.assertEquals(1, entries.size());
 		Assert.assertTrue(entries.contains(re));
-- 
GitLab