From 614589d72aa3560ac5f251774dcf13ae9370543d Mon Sep 17 00:00:00 2001
From: srosse <none@none>
Date: Fri, 16 Jun 2017 17:07:59 +0200
Subject: [PATCH] OO-2636: add new item in coaching tool to search the lectures

---
 .../modules/coach/ui/CoachMainController.java |  24 ++
 .../coach/ui/_i18n/LocalStrings_de.properties |   3 +-
 .../coach/ui/_i18n/LocalStrings_en.properties |   3 +-
 .../olat/modules/lecture/LectureService.java  |  11 +
 .../manager/LectureBlockRollCallDAO.java      | 278 ++++++++++++++++-
 .../lecture/manager/LectureServiceImpl.java   |  15 +
 .../model/LectureBlockIdentityStatistics.java |  28 ++
 .../LectureStatisticsSearchParameters.java    |  72 +++++
 .../lecture/ui/LecturesListController.java    | 111 +++++++
 .../lecture/ui/LecturesListDataModel.java     | 115 ++++++++
 .../lecture/ui/LecturesSearchController.java  | 106 +++++++
 .../ui/LecturesSearchFormController.java      | 279 ++++++++++++++++++
 .../lecture/ui/_content/cycle_dates.html      |   6 +
 .../ui/_content/lectures_coaching.html        |   1 +
 .../ui/_i18n/LocalStrings_de.properties       |  12 +
 .../ui/_i18n/LocalStrings_en.properties       |  12 +
 .../_spring/userPropertiesContext.xml         |  23 ++
 17 files changed, 1095 insertions(+), 4 deletions(-)
 create mode 100644 src/main/java/org/olat/modules/lecture/model/LectureBlockIdentityStatistics.java
 create mode 100644 src/main/java/org/olat/modules/lecture/model/LectureStatisticsSearchParameters.java
 create mode 100644 src/main/java/org/olat/modules/lecture/ui/LecturesListController.java
 create mode 100644 src/main/java/org/olat/modules/lecture/ui/LecturesListDataModel.java
 create mode 100644 src/main/java/org/olat/modules/lecture/ui/LecturesSearchController.java
 create mode 100644 src/main/java/org/olat/modules/lecture/ui/LecturesSearchFormController.java
 create mode 100644 src/main/java/org/olat/modules/lecture/ui/_content/cycle_dates.html
 create mode 100644 src/main/java/org/olat/modules/lecture/ui/_content/lectures_coaching.html

diff --git a/src/main/java/org/olat/modules/coach/ui/CoachMainController.java b/src/main/java/org/olat/modules/coach/ui/CoachMainController.java
index bf95fa49a00..6a0b4c87025 100644
--- a/src/main/java/org/olat/modules/coach/ui/CoachMainController.java
+++ b/src/main/java/org/olat/modules/coach/ui/CoachMainController.java
@@ -43,7 +43,10 @@ import org.olat.core.id.context.StateEntry;
 import org.olat.core.logging.activity.ThreadLocalUserActivityLogger;
 import org.olat.core.util.resource.OresHelper;
 import org.olat.core.util.tree.TreeHelper;
+import org.olat.modules.lecture.LectureModule;
+import org.olat.modules.lecture.ui.LecturesSearchController;
 import org.olat.util.logging.activity.LoggingResourceable;
+import org.springframework.beans.factory.annotation.Autowired;
 
 /**
  * 
@@ -64,6 +67,10 @@ public class CoachMainController extends MainLayoutBasicController implements Ac
 	private CourseListController courseListCtrl;
 	private StudentListController studentListCtrl;
 	private LayoutMain3ColsController columnLayoutCtr;
+	private LecturesSearchController lecturesSearchCtrl;
+	
+	@Autowired
+	private LectureModule lectureModule;
 	
 	public CoachMainController(UserRequest ureq, WindowControl control) {
 		super(ureq, control);
@@ -145,6 +152,15 @@ public class CoachMainController extends MainLayoutBasicController implements Ac
 				listenTo(courseListCtrl);
 			}
 			selectedCtrl = courseListCtrl;
+		} else if("lectures".equalsIgnoreCase(cmd)) {
+			if(lecturesSearchCtrl == null) {
+				OLATResourceable ores = OresHelper.createOLATResourceableInstance("Lectures", 0l);
+				ThreadLocalUserActivityLogger.addLoggingResourceInfo(LoggingResourceable.wrapBusinessPath(ores));
+				WindowControl bwControl = BusinessControlFactory.getInstance().createBusinessWindowControl(ores, null, getWindowControl());
+				lecturesSearchCtrl = new LecturesSearchController(ureq, bwControl, content);
+				listenTo(lecturesSearchCtrl);
+			}
+			selectedCtrl = lecturesSearchCtrl;
 		} else if("search".equalsIgnoreCase(cmd)) {
 			if(userSearchCtrl == null) {
 				OLATResourceable ores = OresHelper.createOLATResourceableInstance("Search", 0l);
@@ -191,6 +207,14 @@ public class CoachMainController extends MainLayoutBasicController implements Ac
 		courses.setAltText(translate("courses.menu.title.alt"));
 		root.addChild(courses);
 		
+		if(lectureModule.isEnabled()) {
+			GenericTreeNode lectures = new GenericTreeNode();
+			lectures.setUserObject("Lectures");
+			lectures.setTitle(translate("lectures.menu.title"));
+			lectures.setAltText(translate("courses.menu.title.alt"));
+			root.addChild(lectures);
+		}
+		
 		Roles roles = ureq.getUserSession().getRoles();
 		if(roles.isUserManager() || roles.isOLATAdmin()) {
 			GenericTreeNode search = new GenericTreeNode();
diff --git a/src/main/java/org/olat/modules/coach/ui/_i18n/LocalStrings_de.properties b/src/main/java/org/olat/modules/coach/ui/_i18n/LocalStrings_de.properties
index d590614e3fd..7bbe6356bec 100644
--- a/src/main/java/org/olat/modules/coach/ui/_i18n/LocalStrings_de.properties
+++ b/src/main/java/org/olat/modules/coach/ui/_i18n/LocalStrings_de.properties
@@ -15,7 +15,8 @@ error.search.form.too.many=Die Suche lieferte zu viele Treffer. Bitte schr\u00e4
 group.name=Name
 groups.menu.title.alt=Gruppen
 groups.menu.title=Gruppen
-
+lectures.menu.title=Lektionen
+lectures.menu.title.alt=Lektionen und Absenzmanagement
 home.link=Visitenkarte
 main.menu.title.alt=\$:site.title.alt
 main.menu.title=\$:site.title
diff --git a/src/main/java/org/olat/modules/coach/ui/_i18n/LocalStrings_en.properties b/src/main/java/org/olat/modules/coach/ui/_i18n/LocalStrings_en.properties
index 5ad2932ee51..992686f67f5 100644
--- a/src/main/java/org/olat/modules/coach/ui/_i18n/LocalStrings_en.properties
+++ b/src/main/java/org/olat/modules/coach/ui/_i18n/LocalStrings_en.properties
@@ -15,7 +15,8 @@ error.search.form.too.many=Too many search results. Please narrow down your sear
 group.name=Name
 groups.menu.title.alt=Groups
 groups.menu.title=Groups
-
+lectures.menu.title=Lectures
+lectures.menu.title.alt=Lectures and absences management
 home.link=visiting card
 main.menu.title.alt=$\:site.title.alt
 main.menu.title=$\:site.title
diff --git a/src/main/java/org/olat/modules/lecture/LectureService.java b/src/main/java/org/olat/modules/lecture/LectureService.java
index e5a80313b2a..d5a19343771 100644
--- a/src/main/java/org/olat/modules/lecture/LectureService.java
+++ b/src/main/java/org/olat/modules/lecture/LectureService.java
@@ -25,10 +25,13 @@ import org.olat.basesecurity.Group;
 import org.olat.basesecurity.IdentityRef;
 import org.olat.core.id.Identity;
 import org.olat.modules.lecture.model.LectureBlockAndRollCall;
+import org.olat.modules.lecture.model.LectureBlockIdentityStatistics;
 import org.olat.modules.lecture.model.LectureBlockStatistics;
 import org.olat.modules.lecture.model.LectureBlockWithTeachers;
+import org.olat.modules.lecture.model.LectureStatisticsSearchParameters;
 import org.olat.repository.RepositoryEntry;
 import org.olat.repository.RepositoryEntryRef;
+import org.olat.user.propertyhandlers.UserPropertyHandler;
 
 /**
  * 
@@ -351,6 +354,14 @@ public interface LectureService {
 	 */
 	public List<LectureBlockStatistics> getParticipantsLecturesStatistics(RepositoryEntry entry);
 	
+	/**
+	 * 
+	 * @param params
+	 * @return
+	 */
+	public List<LectureBlockIdentityStatistics> getLecturesStatistics(LectureStatisticsSearchParameters params,
+			List<UserPropertyHandler> userPropertyHandlers, Identity identity, boolean admin);
+	
 	/**
 	 * The list of roll calls within the specified course for the specified user
 	 * after it's first admission.
diff --git a/src/main/java/org/olat/modules/lecture/manager/LectureBlockRollCallDAO.java b/src/main/java/org/olat/modules/lecture/manager/LectureBlockRollCallDAO.java
index ff7b34a5a3a..37a602b7dd6 100644
--- a/src/main/java/org/olat/modules/lecture/manager/LectureBlockRollCallDAO.java
+++ b/src/main/java/org/olat/modules/lecture/manager/LectureBlockRollCallDAO.java
@@ -25,11 +25,15 @@ import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
+import javax.persistence.TemporalType;
+import javax.persistence.TypedQuery;
+
 import org.olat.basesecurity.GroupRoles;
 import org.olat.basesecurity.IdentityRef;
 import org.olat.core.commons.persistence.DB;
 import org.olat.core.commons.persistence.PersistenceHelper;
 import org.olat.core.id.Identity;
+import org.olat.core.util.StringHelper;
 import org.olat.modules.lecture.LectureBlock;
 import org.olat.modules.lecture.LectureBlockRef;
 import org.olat.modules.lecture.LectureBlockRollCall;
@@ -37,11 +41,14 @@ import org.olat.modules.lecture.LectureBlockStatus;
 import org.olat.modules.lecture.LectureRollCallStatus;
 import org.olat.modules.lecture.RepositoryEntryLectureConfiguration;
 import org.olat.modules.lecture.model.LectureBlockAndRollCall;
+import org.olat.modules.lecture.model.LectureBlockIdentityStatistics;
 import org.olat.modules.lecture.model.LectureBlockRollCallImpl;
 import org.olat.modules.lecture.model.LectureBlockStatistics;
+import org.olat.modules.lecture.model.LectureStatisticsSearchParameters;
 import org.olat.repository.RepositoryEntry;
 import org.olat.repository.RepositoryEntryRef;
 import org.olat.user.UserManager;
+import org.olat.user.propertyhandlers.UserPropertyHandler;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
@@ -338,6 +345,207 @@ public class LectureBlockRollCallDAO {
 		calculateAttendanceRate(statisticsList, countAuthorizedAbsenceAsAttendant);
 		return statisticsList;
 	}
+	
+	public List<LectureBlockIdentityStatistics> getStatistics(LectureStatisticsSearchParameters params,
+			List<UserPropertyHandler> userPropertyHandlers, Identity identity, boolean admin,
+			boolean absenceDefaultAuthorized, boolean countAuthorizedAbsenceAsAttendant,
+			boolean calculateAttendanceRate, double requiredAttendanceRateDefault) {
+		
+		StringBuilder sb = new StringBuilder(2048);
+		sb.append("select ident.key as participantKey, ")
+		  .append("  call.lecturesAttendedNumber as attendedLectures,")
+		  .append("  call.lecturesAbsentNumber as absentLectures,")
+		  .append("  call.absenceAuthorized as absenceAuthorized,")
+		  .append("  block.key as blockKey,")
+		  .append("  block.compulsory as compulsory,")
+		  .append("  block.plannedLecturesNumber as blockPlanned,")
+		  .append("  block.effectiveLecturesNumber as blockEffective,")
+		  .append("  block.statusString as status,")
+		  .append("  block.rollCallStatusString as rollCallStatus,")
+		  .append("  block.endDate as rollCallEndDate,")
+		  .append("  re.key as repoKey,")
+		  .append("  re.displayname as repoDisplayName,")
+		  .append("  config.overrideModuleDefault as overrideDef,")
+		  .append("  config.calculateAttendanceRate as calculateRate,")
+		  .append("  config.requiredAttendanceRate as repoConfigRate,")//rate enabled
+		  .append("  summary.firstAdmissionDate as firstAdmissionDate,")
+		  .append("  summary.requiredAttendanceRate as summaryRate");
+		for(UserPropertyHandler handler:userPropertyHandlers) {
+			sb.append(", user.").append(handler.getName()).append(" as ").append("p_").append(handler.getName());
+		} 
+		sb.append(" from lectureblock block")
+		  .append(" inner join block.entry re")
+		  .append(" inner join block.groups blockToGroup")
+		  .append(" inner join blockToGroup.group bGroup")
+		  .append(" inner join bGroup.members membership")
+		  .append(" inner join membership.identity ident")
+		  .append(" inner join ident.user user")
+		  .append(" left join lectureentryconfig as config on (re.key=config.entry.key)")
+		  .append(" left join lectureblockrollcall as call on (call.identity.key=membership.identity.key and call.lectureBlock.key=block.key)")
+		  .append(" left join lectureparticipantsummary as summary on (summary.identity.key=membership.identity.key and summary.entry.key=block.entry.key)")
+		  .append(" where membership.role='").append(GroupRoles.participant.name()).append("'");
+		if(!admin) {
+			sb.append(" and (exists (select rel from repoentrytogroup as rel, bgroupmember as membership ")
+			  .append("     where re.key=rel.entry.key and membership.group.key=rel.group.key and rel.defaultGroup=true and membership.identity.key=:identityKey")
+			  .append("     and membership.role='").append(GroupRoles.owner.name()).append("'")
+			  .append("     and re.access >= ").append(RepositoryEntry.ACC_OWNERS)
+			  .append(" ) or exists (select membership.key from bgroupmember as membership ")
+			  .append("     where block.teacherGroup.key=membership.group.key and membership.identity.key=:identityKey")
+			  .append("     and (re.access >= ").append(RepositoryEntry.ACC_USERS)
+			  .append("     or (re.access = ").append(RepositoryEntry.ACC_OWNERS).append(" and re.membersOnly=true))")
+			  .append(" ))");
+		} else {
+			sb.append(" and re.access >= ").append(RepositoryEntry.ACC_OWNERS);
+		}
+
+		if(params.getLifecycle() != null) {
+			sb.append(" and re.lifecycle.key=:lifecycleKey");
+		}
+		if(params.getStartDate() != null) {
+			sb.append(" and block.startDate>=:startDate");
+		}
+		if(params.getEndDate() != null) {
+			sb.append(" and block.endDate<=:endDate");
+		}
+		if(params.getBulkIdentifiers() != null && params.getBulkIdentifiers().size() > 0) {
+			sb.append(" and (")
+			  .append("  lower(ident.name) in (:bulkIdentifiers)")
+			  .append("  or lower(ident.externalId) in (:bulkIdentifiers)")
+			  .append("  or lower(user.email) in (:bulkIdentifiers)")
+			  .append("  or lower(user.institutionalEmail) in (:bulkIdentifiers)")
+			  .append(")");
+		}
+		
+		Map<String,Object> queryParams = new HashMap<>();
+		appendUsersStatisticsSearchParams(params, queryParams, sb);
+
+		TypedQuery<Object[]> rawQuery = dbInstance.getCurrentEntityManager()
+				.createQuery(sb.toString(), Object[].class);
+		if(StringHelper.containsNonWhitespace(params.getLogin())) {
+			rawQuery.setParameter("login", params.getLogin());
+		}
+		if(params.getLifecycle() != null) {
+			rawQuery.setParameter("lifecycleKey", params.getLifecycle().getKey());
+		}
+		if(params.getStartDate() != null) {
+			rawQuery.setParameter("startDate", params.getStartDate(), TemporalType.TIMESTAMP);
+		}
+		if(params.getEndDate() != null) {
+			rawQuery.setParameter("endDate", params.getEndDate(), TemporalType.TIMESTAMP);
+		}
+		if(params.getBulkIdentifiers() != null && params.getBulkIdentifiers().size() > 0) {
+			rawQuery.setParameter("bulkIdentifiers", params.getBulkIdentifiers());
+		}
+		for(Map.Entry<String, Object> entry:queryParams.entrySet()) {
+			rawQuery.setParameter(entry.getKey(), entry.getValue());
+		}
+		if(!admin) {
+			rawQuery.setParameter("identityKey", identity.getKey());
+		}
+
+		Date now = new Date();
+		List<Object[]> rawObjects = rawQuery.getResultList();
+		Map<Membership,LectureBlockIdentityStatistics> stats = new HashMap<>();
+		for(Object[] rawObject:rawObjects) {
+			int pos = 0;//jump roll call key
+			Long identityKey = (Long)rawObject[pos++];
+			Long lecturesAttended = PersistenceHelper.extractLong(rawObject, pos++);
+			Long lecturesAbsent = PersistenceHelper.extractLong(rawObject, pos++);
+			Boolean absenceAuthorized = (Boolean)rawObject[pos++];
+			
+			pos++;//jump block key
+			boolean compulsory = PersistenceHelper.extractBoolean(rawObject, pos++, true);
+			Long plannedLecturesNumber = PersistenceHelper.extractLong(rawObject, pos++);
+			Long effectiveLecturesNumber = PersistenceHelper.extractLong(rawObject, pos++);
+			if(effectiveLecturesNumber == null) {
+				effectiveLecturesNumber = plannedLecturesNumber;
+			}
+			String status = (String)rawObject[pos++];
+			String rollCallStatus = (String)rawObject[pos++];
+			Date rollCallEndDate = (Date)rawObject[pos++];
+			
+			//entry and config
+			Long repoKey = PersistenceHelper.extractLong(rawObject, pos++);
+			String repoDisplayname = (String)rawObject[pos++];
+			boolean overrideDefault = PersistenceHelper.extractBoolean(rawObject, pos++, false);
+			Boolean repoCalculateRate = (Boolean)rawObject[pos++];
+			Double repoRequiredRate = (Double)rawObject[pos++];
+			
+			//summary
+			Date firstAdmissionDate = (Date)rawObject[pos++];
+			Double persoRequiredRate = (Double)rawObject[pos++];
+			
+			LectureBlockIdentityStatistics entryStatistics;
+			
+			Membership memberKey = new Membership(identityKey, repoKey);
+			if(stats.containsKey(memberKey)) {
+				entryStatistics = stats.get(memberKey);
+			} else {
+				
+				//user data
+				int numOfProperties = userPropertyHandlers.size();
+				String[] identityProps = new String[numOfProperties];
+				for(int i=0; i<numOfProperties; i++) {
+					identityProps[i] = (String)rawObject[pos++];
+				}
+				
+				entryStatistics = createIdentityStatistics(identityKey, identityProps,
+						repoKey, repoDisplayname,
+						overrideDefault, repoCalculateRate,  repoRequiredRate,
+						persoRequiredRate, calculateAttendanceRate, requiredAttendanceRateDefault);
+				stats.put(memberKey, entryStatistics);
+			}
+
+			appendStatistics(entryStatistics, compulsory, status,
+					rollCallEndDate, rollCallStatus,
+					lecturesAttended, lecturesAbsent,
+					absenceAuthorized, absenceDefaultAuthorized,
+					plannedLecturesNumber, effectiveLecturesNumber,
+					firstAdmissionDate, now);
+		}
+		
+		List<LectureBlockIdentityStatistics> statisticsList = new ArrayList<>(stats.values());
+		calculateAttendanceRate(statisticsList, countAuthorizedAbsenceAsAttendant);
+		return statisticsList;
+	}
+	
+	private void appendUsersStatisticsSearchParams(LectureStatisticsSearchParameters params, Map<String,Object> queryParams, StringBuilder sb) {
+		if(StringHelper.containsNonWhitespace(params.getLogin())) {
+			String login = PersistenceHelper.makeFuzzyQueryString(params.getLogin());
+			if (login.contains("_") && dbInstance.isOracle()) {
+				//oracle needs special ESCAPE sequence to search for escaped strings
+				sb.append(" and lower(ident.name) like :login ESCAPE '\\'");
+			} else if (dbInstance.isMySQL()) {
+				sb.append(" and ident.name like :login");
+			} else {
+				sb.append(" and lower(ident.name) like :login");
+			}
+			queryParams.put("login", login);
+		}
+		
+		if(params.getUserProperties() != null && params.getUserProperties().size() > 0) {
+			Map<String,String> searchParams = new HashMap<>(params.getUserProperties());
+	
+			int count = 0;
+			for(Map.Entry<String, String> entry:searchParams.entrySet()) {
+				String propName = entry.getKey();
+				String propValue = entry.getValue();
+				String qName = "p_" + ++count;
+				
+				UserPropertyHandler handler = userManager.getUserPropertiesConfig().getPropertyHandler(propName);
+				if(dbInstance.isMySQL()) {
+					sb.append(" and user.").append(handler.getName()).append(" like :").append(qName);
+				} else {
+					sb.append(" and lower(user.").append(handler.getName()).append(") like :").append(qName);
+					if(dbInstance.isOracle()) {
+						sb.append(" escape '\\'");
+					}
+				}
+				queryParams.put(qName, PersistenceHelper.makeFuzzyQueryString(propValue));
+			}
+		}
+	}
+	
 
 	public List<LectureBlockStatistics> getStatistics(RepositoryEntry entry,
 			RepositoryEntryLectureConfiguration config,
@@ -424,7 +632,7 @@ public class LectureBlockRollCallDAO {
 		return statisticsList;
 	}
 	
-	private void calculateAttendanceRate(List<LectureBlockStatistics> statisticsList, boolean countAuthorizedAbsenceAsAttendant) {
+	private void calculateAttendanceRate(List<? extends LectureBlockStatistics> statisticsList, boolean countAuthorizedAbsenceAsAttendant) {
 		for(LectureBlockStatistics statistics:statisticsList) {
 			long totalAttendedLectures = statistics.getTotalEffectiveLectures();
 			long totalAbsentLectures = statistics.getTotalAbsentLectures();
@@ -452,6 +660,24 @@ public class LectureBlockRollCallDAO {
 			Boolean overrideDefault, Boolean repoCalculateRate, Double repoRequiredRate,
 			Double persoRequiredRate, boolean calculateAttendanceRate, double requiredAttendanceRateDefault) {
 
+		RequiredRate requiredRate = calculateRequiredRate(overrideDefault, repoCalculateRate, repoRequiredRate,
+				persoRequiredRate, calculateAttendanceRate, requiredAttendanceRateDefault);
+		return new LectureBlockStatistics(identityKey, entryKey, displayName, requiredRate.isCalculateRate(), requiredRate.getRequiredRate());
+	}
+	
+	private LectureBlockIdentityStatistics createIdentityStatistics(Long identityKey, String[] identityProps, Long entryKey, String displayName,
+			Boolean overrideDefault, Boolean repoCalculateRate, Double repoRequiredRate,
+			Double persoRequiredRate, boolean calculateAttendanceRate, double requiredAttendanceRateDefault) {
+
+		RequiredRate requiredRate = calculateRequiredRate(overrideDefault, repoCalculateRate, repoRequiredRate,
+				persoRequiredRate, calculateAttendanceRate, requiredAttendanceRateDefault);
+		return new LectureBlockIdentityStatistics(identityKey, identityProps,
+				entryKey, displayName, requiredRate.isCalculateRate(), requiredRate.getRequiredRate());
+	}
+	
+	private RequiredRate calculateRequiredRate(Boolean overrideDefault, Boolean repoCalculateRate, Double repoRequiredRate,
+			Double persoRequiredRate, boolean calculateAttendanceRate, double requiredAttendanceRateDefault) {
+		
 		final boolean calculateRate;
 		if(repoCalculateRate != null && overrideDefault != null && !overrideDefault.booleanValue()) {
 			calculateRate = repoCalculateRate.booleanValue();
@@ -469,7 +695,8 @@ public class LectureBlockRollCallDAO {
 				requiredRate = requiredAttendanceRateDefault;
 			}
 		}
-		return new LectureBlockStatistics(identityKey, entryKey, displayName, calculateRate, requiredRate);
+		
+		return new RequiredRate(calculateRate, requiredRate);
 	}
 	
 	private void appendStatistics(LectureBlockStatistics statistics, boolean compulsory, String blockStatus,
@@ -518,4 +745,51 @@ public class LectureBlockRollCallDAO {
 			statistics.addTotalPlannedLectures(plannedLecturesNumber.longValue());
 		}
 	}
+	
+	private static class Membership {
+		private final Long identityKey;
+		private final Long repoEntryKey;
+		
+		public Membership(Long identityKey, Long repoEntryKey) {
+			this.identityKey = identityKey;
+			this.repoEntryKey = repoEntryKey;
+		}
+
+		@Override
+		public int hashCode() {
+			return identityKey.hashCode() + repoEntryKey.hashCode();
+		}
+
+		@Override
+		public boolean equals(Object obj) {
+			if(this == obj) {
+				return true;
+			}
+			if(obj instanceof Membership) {
+				Membership membership = (Membership)obj;
+				return identityKey != null && identityKey.equals(membership.identityKey)
+						&& repoEntryKey != null && repoEntryKey.equals(membership.repoEntryKey);
+			}
+			return false;
+		}
+	}
+	
+	private static class RequiredRate {
+		
+		private final boolean calculateRate;
+		private final double requiredRate;
+		
+		public RequiredRate(boolean calculateRate, double requiredRate) {
+			this.calculateRate = calculateRate;
+			this.requiredRate = requiredRate;
+		}
+
+		public boolean isCalculateRate() {
+			return calculateRate;
+		}
+
+		public double getRequiredRate() {
+			return requiredRate;
+		}
+	}
 }
diff --git a/src/main/java/org/olat/modules/lecture/manager/LectureServiceImpl.java b/src/main/java/org/olat/modules/lecture/manager/LectureServiceImpl.java
index 9d1393427af..acb43e0e74e 100644
--- a/src/main/java/org/olat/modules/lecture/manager/LectureServiceImpl.java
+++ b/src/main/java/org/olat/modules/lecture/manager/LectureServiceImpl.java
@@ -66,10 +66,12 @@ import org.olat.modules.lecture.LectureService;
 import org.olat.modules.lecture.Reason;
 import org.olat.modules.lecture.RepositoryEntryLectureConfiguration;
 import org.olat.modules.lecture.model.LectureBlockAndRollCall;
+import org.olat.modules.lecture.model.LectureBlockIdentityStatistics;
 import org.olat.modules.lecture.model.LectureBlockImpl;
 import org.olat.modules.lecture.model.LectureBlockStatistics;
 import org.olat.modules.lecture.model.LectureBlockToTeacher;
 import org.olat.modules.lecture.model.LectureBlockWithTeachers;
+import org.olat.modules.lecture.model.LectureStatisticsSearchParameters;
 import org.olat.modules.lecture.model.ParticipantAndLectureSummary;
 import org.olat.modules.lecture.ui.ConfigurationHelper;
 import org.olat.modules.lecture.ui.LectureAdminController;
@@ -78,6 +80,7 @@ import org.olat.repository.RepositoryEntryRef;
 import org.olat.repository.manager.RepositoryEntryDAO;
 import org.olat.user.UserDataDeletable;
 import org.olat.user.UserManager;
+import org.olat.user.propertyhandlers.UserPropertyHandler;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
@@ -593,6 +596,18 @@ public class LectureServiceImpl implements LectureService, UserDataDeletable {
 				absenceDefaultAuthorized, countAuthorizedAbsenceAsAttendant,
 				calculateAttendanceRate, defaultRequiredAttendanceRate);
 	}
+	
+	@Override
+	public List<LectureBlockIdentityStatistics> getLecturesStatistics(LectureStatisticsSearchParameters params,
+			List<UserPropertyHandler> userPropertyHandlers, Identity identity, boolean admin) {
+		boolean calculateAttendanceRate = lectureModule.isRollCallCalculateAttendanceRateDefaultEnabled();
+		boolean absenceDefaultAuthorized = lectureModule.isAbsenceDefaultAuthorized();
+		boolean countAuthorizedAbsenceAsAttendant = lectureModule.isCountAuthorizedAbsenceAsAttendant();
+		double defaultRequiredAttendanceRate = lectureModule.getRequiredAttendanceRateDefault();
+		return lectureBlockRollCallDao.getStatistics(params, userPropertyHandlers, identity, admin,
+				absenceDefaultAuthorized, countAuthorizedAbsenceAsAttendant,
+				calculateAttendanceRate, defaultRequiredAttendanceRate);
+	}
 
 	@Override
 	public List<LectureBlockAndRollCall> getParticipantLectureBlocks(RepositoryEntryRef entry, IdentityRef participant) {
diff --git a/src/main/java/org/olat/modules/lecture/model/LectureBlockIdentityStatistics.java b/src/main/java/org/olat/modules/lecture/model/LectureBlockIdentityStatistics.java
new file mode 100644
index 00000000000..1e15fb1a0c8
--- /dev/null
+++ b/src/main/java/org/olat/modules/lecture/model/LectureBlockIdentityStatistics.java
@@ -0,0 +1,28 @@
+package org.olat.modules.lecture.model;
+
+/**
+ * 
+ * 
+ * Initial date: 16 juin 2017<br>
+ * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
+ *
+ */
+public class LectureBlockIdentityStatistics extends LectureBlockStatistics {
+	
+	private final String[] identityProps;
+	
+	public LectureBlockIdentityStatistics(Long identityKey, String[] identityProps,
+			Long repoKey, String displayName, boolean calculateRate, double requiredRate) {
+		super(identityKey, repoKey, displayName, calculateRate, requiredRate);
+		this.identityProps = identityProps;
+	}
+	
+	public String[] getIdentityProps() {
+		return identityProps;
+	}
+	
+	public String getIdentityProp(int pos) {
+		return identityProps[pos];
+	}
+
+}
diff --git a/src/main/java/org/olat/modules/lecture/model/LectureStatisticsSearchParameters.java b/src/main/java/org/olat/modules/lecture/model/LectureStatisticsSearchParameters.java
new file mode 100644
index 00000000000..374489ad7fb
--- /dev/null
+++ b/src/main/java/org/olat/modules/lecture/model/LectureStatisticsSearchParameters.java
@@ -0,0 +1,72 @@
+package org.olat.modules.lecture.model;
+
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+
+import org.olat.repository.model.RepositoryEntryLifecycle;
+
+/**
+ * 
+ * Initial date: 16 juin 2017<br>
+ * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
+ *
+ */
+public class LectureStatisticsSearchParameters {
+	
+	private String login;
+	private List<String> bulkIdentifiers;
+	private Map<String,String> userProperties;
+	
+	private Date startDate;
+	private Date endDate;
+	private RepositoryEntryLifecycle lifecycle;
+	
+	public String getLogin() {
+		return login;
+	}
+
+	public void setLogin(String login) {
+		this.login = login;
+	}
+
+	public Date getStartDate() {
+		return startDate;
+	}
+	
+	public void setStartDate(Date startDate) {
+		this.startDate = startDate;
+	}
+	
+	public Date getEndDate() {
+		return endDate;
+	}
+	
+	public void setEndDate(Date endDate) {
+		this.endDate = endDate;
+	}
+
+	public RepositoryEntryLifecycle getLifecycle() {
+		return lifecycle;
+	}
+
+	public void setLifecycle(RepositoryEntryLifecycle lifecycle) {
+		this.lifecycle = lifecycle;
+	}
+
+	public List<String> getBulkIdentifiers() {
+		return bulkIdentifiers;
+	}
+
+	public void setBulkIdentifiers(List<String> bulkIdentifiers) {
+		this.bulkIdentifiers = bulkIdentifiers;
+	}
+
+	public Map<String, String> getUserProperties() {
+		return userProperties;
+	}
+
+	public void setUserProperties(Map<String, String> userProperties) {
+		this.userProperties = userProperties;
+	}
+}
diff --git a/src/main/java/org/olat/modules/lecture/ui/LecturesListController.java b/src/main/java/org/olat/modules/lecture/ui/LecturesListController.java
new file mode 100644
index 00000000000..a8f6c270ad0
--- /dev/null
+++ b/src/main/java/org/olat/modules/lecture/ui/LecturesListController.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.modules.lecture.ui;
+
+import java.util.List;
+
+import org.olat.core.gui.UserRequest;
+import org.olat.core.gui.components.form.flexible.FormItemContainer;
+import org.olat.core.gui.components.form.flexible.elements.FlexiTableElement;
+import org.olat.core.gui.components.form.flexible.impl.FormBasicController;
+import org.olat.core.gui.components.form.flexible.impl.elements.table.DefaultFlexiColumnModel;
+import org.olat.core.gui.components.form.flexible.impl.elements.table.FlexiTableColumnModel;
+import org.olat.core.gui.components.form.flexible.impl.elements.table.FlexiTableDataModelFactory;
+import org.olat.core.gui.control.Controller;
+import org.olat.core.gui.control.WindowControl;
+import org.olat.modules.lecture.LectureModule;
+import org.olat.modules.lecture.model.LectureBlockIdentityStatistics;
+import org.olat.modules.lecture.ui.LecturesListDataModel.StatsCols;
+import org.olat.user.UserManager;
+import org.olat.user.propertyhandlers.UserPropertyHandler;
+import org.springframework.beans.factory.annotation.Autowired;
+
+/**
+ * 
+ * Initial date: 16 juin 2017<br>
+ * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
+ *
+ */
+public class LecturesListController extends FormBasicController {
+	
+	public static final int USER_PROPS_OFFSET = 500;
+	
+	private FlexiTableElement tableEl;
+	private LecturesListDataModel tableModel;
+	
+	private final String propsIdentifier;
+	private final boolean authorizedAbsenceEnabled;
+	private final List<UserPropertyHandler> userPropertyHandlers;
+	private final List<LectureBlockIdentityStatistics> statistics;
+	
+	@Autowired
+	private UserManager userManager;
+	@Autowired
+	private LectureModule lectureModule;
+	
+	public LecturesListController(UserRequest ureq, WindowControl wControl,
+			List<LectureBlockIdentityStatistics> statistics,
+			List<UserPropertyHandler> userPropertyHandlers, String propsIdentifier) {
+		super(ureq, wControl, "lectures_coaching");
+		setTranslator(userManager.getPropertyHandlerTranslator(getTranslator()));
+		this.statistics = statistics;
+		this.propsIdentifier = propsIdentifier;
+		this.userPropertyHandlers = userPropertyHandlers;
+		authorizedAbsenceEnabled = lectureModule.isAuthorizedAbsenceEnabled();
+		initForm(ureq);
+	}
+
+	@Override
+	protected void initForm(FormItemContainer formLayout, Controller listener, UserRequest ureq) {
+		FlexiTableColumnModel columnsModel = FlexiTableDataModelFactory.createFlexiTableColumnModel();
+		columnsModel.addFlexiColumnModel(new DefaultFlexiColumnModel(false, StatsCols.id));
+		
+		int colIndex = USER_PROPS_OFFSET;
+		for (int i = 0; i < userPropertyHandlers.size(); i++) {
+			UserPropertyHandler userPropertyHandler	= userPropertyHandlers.get(i);
+			boolean visible = userManager.isMandatoryUserProperty(propsIdentifier, userPropertyHandler);
+			columnsModel.addFlexiColumnModel(new DefaultFlexiColumnModel(visible, userPropertyHandler.i18nColumnDescriptorLabelKey(), colIndex++, null,
+					true, userPropertyHandler.i18nColumnDescriptorLabelKey()));
+		}
+
+		columnsModel.addFlexiColumnModel(new DefaultFlexiColumnModel(StatsCols.entry));
+		columnsModel.addFlexiColumnModel(new DefaultFlexiColumnModel(StatsCols.plannedLectures));
+		columnsModel.addFlexiColumnModel(new DefaultFlexiColumnModel(StatsCols.attendedLectures));
+		columnsModel.addFlexiColumnModel(new DefaultFlexiColumnModel(StatsCols.absentLectures));
+		if(authorizedAbsenceEnabled) {
+			columnsModel.addFlexiColumnModel(new DefaultFlexiColumnModel(StatsCols.authorizedAbsenceLectures));
+		}
+		
+		tableModel = new LecturesListDataModel(columnsModel); 
+		tableModel.setObjects(statistics);
+		tableEl = uifactory.addTableElement(getWindowControl(), "table", tableModel, 20, false, getTranslator(), formLayout);
+		tableEl.setExportEnabled(true);
+	}
+
+	@Override
+	protected void doDispose() {
+		//
+	}
+
+	@Override
+	protected void formOK(UserRequest ureq) {
+		//
+	}
+}
diff --git a/src/main/java/org/olat/modules/lecture/ui/LecturesListDataModel.java b/src/main/java/org/olat/modules/lecture/ui/LecturesListDataModel.java
new file mode 100644
index 00000000000..6e3f6a37cc6
--- /dev/null
+++ b/src/main/java/org/olat/modules/lecture/ui/LecturesListDataModel.java
@@ -0,0 +1,115 @@
+/**
+ * <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.lecture.ui;
+
+import java.util.List;
+
+import org.olat.core.commons.persistence.SortKey;
+import org.olat.core.gui.components.form.flexible.impl.elements.table.DefaultFlexiTableDataModel;
+import org.olat.core.gui.components.form.flexible.impl.elements.table.FlexiSortableColumnDef;
+import org.olat.core.gui.components.form.flexible.impl.elements.table.FlexiTableColumnModel;
+import org.olat.core.gui.components.form.flexible.impl.elements.table.SortableFlexiTableDataModel;
+import org.olat.core.gui.components.form.flexible.impl.elements.table.SortableFlexiTableModelDelegate;
+import org.olat.modules.lecture.model.LectureBlockIdentityStatistics;
+
+/**
+ * 
+ * Initial date: 16 juin 2017<br>
+ * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
+ *
+ */
+public class LecturesListDataModel extends DefaultFlexiTableDataModel<LectureBlockIdentityStatistics>
+implements SortableFlexiTableDataModel<LectureBlockIdentityStatistics>{
+	
+	public LecturesListDataModel(FlexiTableColumnModel columnModel) {
+		super(columnModel);
+	}
+
+	@Override
+	public void sort(SortKey orderBy) {
+		SortableFlexiTableModelDelegate<LectureBlockIdentityStatistics> sorter
+			= new SortableFlexiTableModelDelegate<>(orderBy, this, null);
+		List<LectureBlockIdentityStatistics> views = sorter.sort();
+		super.setObjects(views);
+	}
+
+	@Override
+	public Object getValueAt(int row, int col) {
+		LectureBlockIdentityStatistics stats = getObject(row);
+		return getValueAt(stats, col);
+	}
+
+	@Override
+	public Object getValueAt(LectureBlockIdentityStatistics row, int col) {
+		if(col >= 0 && col < StatsCols.values().length) {
+			switch(StatsCols.values()[col]) {
+				case id: return row.getIdentityKey();
+				case entry: return row.getDisplayName();
+				case plannedLectures: return positive(row.getTotalPersonalPlannedLectures());
+				case attendedLectures: return positive(row.getTotalAttendedLectures());
+				case absentLectures: return positive(row.getTotalAbsentLectures());
+				case authorizedAbsenceLectures: return positive(row.getTotalAuthorizedAbsentLectures());
+			}
+		}
+		
+		int propPos = col - LecturesListController.USER_PROPS_OFFSET;
+		return row.getIdentityProp(propPos);
+	}
+
+	private static final long positive(long pos) {
+		return pos < 0 ? 0 : pos;
+	}
+	
+	@Override
+	public DefaultFlexiTableDataModel<LectureBlockIdentityStatistics> createCopyWithEmptyList() {
+		return new LecturesListDataModel(getTableColumnModel());
+	}
+	
+	public enum StatsCols implements FlexiSortableColumnDef {
+		id("table.header.id"),
+		entry("table.header.entry"),
+		plannedLectures("table.header.planned.lectures"),
+		attendedLectures("table.header.attended.lectures"),
+		absentLectures("table.header.absent.lectures"),
+		authorizedAbsenceLectures("table.header.authorized.absence")
+		;
+		
+		private final String i18nKey;
+		
+		private StatsCols(String i18nKey) {
+			this.i18nKey = i18nKey;
+		}
+		
+		@Override
+		public String i18nHeaderKey() {
+			return i18nKey;
+		}
+
+		@Override
+		public boolean sortable() {
+			return true;
+		}
+
+		@Override
+		public String sortKey() {
+			return name();
+		}
+	}
+}
diff --git a/src/main/java/org/olat/modules/lecture/ui/LecturesSearchController.java b/src/main/java/org/olat/modules/lecture/ui/LecturesSearchController.java
new file mode 100644
index 00000000000..2b98e696d5d
--- /dev/null
+++ b/src/main/java/org/olat/modules/lecture/ui/LecturesSearchController.java
@@ -0,0 +1,106 @@
+/**
+ * <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.lecture.ui;
+
+import java.util.List;
+
+import org.olat.core.gui.UserRequest;
+import org.olat.core.gui.components.Component;
+import org.olat.core.gui.components.stack.TooledStackedPanel;
+import org.olat.core.gui.control.Controller;
+import org.olat.core.gui.control.Event;
+import org.olat.core.gui.control.WindowControl;
+import org.olat.core.gui.control.controller.BasicController;
+import org.olat.core.gui.control.generic.dtabs.Activateable2;
+import org.olat.core.id.Roles;
+import org.olat.core.id.context.ContextEntry;
+import org.olat.core.id.context.StateEntry;
+import org.olat.modules.lecture.LectureService;
+import org.olat.modules.lecture.model.LectureBlockIdentityStatistics;
+import org.olat.modules.lecture.model.LectureStatisticsSearchParameters;
+import org.olat.user.propertyhandlers.UserPropertyHandler;
+import org.springframework.beans.factory.annotation.Autowired;
+
+/**
+ * 
+ * Initial date: 16 juin 2017<br>
+ * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
+ *
+ */
+public class LecturesSearchController extends BasicController implements Activateable2 {
+	
+	private final TooledStackedPanel stackPanel;
+	
+	private LecturesListController listCtrl;
+	private LecturesSearchFormController searchForm;
+	
+	private final boolean admin;
+	
+	@Autowired
+	private LectureService lectureService;
+	
+	public LecturesSearchController(UserRequest ureq, WindowControl wControl, TooledStackedPanel stackPanel) {
+		super(ureq, wControl);
+		this.stackPanel = stackPanel;
+		Roles roles = ureq.getUserSession().getRoles();
+		admin = (roles.isUserManager() || roles.isOLATAdmin());
+		
+		searchForm = new LecturesSearchFormController(ureq, getWindowControl());
+		listenTo(searchForm);
+		putInitialPanel(searchForm.getInitialComponent());
+	}
+
+	@Override
+	protected void doDispose() {
+		//
+	}
+
+	@Override
+	public void activate(UserRequest ureq, List<ContextEntry> entries, StateEntry state) {
+		//
+	}
+
+	@Override
+	protected void event(UserRequest ureq, Component source, Event event) {
+		//
+	}
+	
+	@Override
+	protected void event(UserRequest ureq, Controller source, Event event) {
+		if(searchForm == source) {
+			if(event == Event.DONE_EVENT) {
+				doSearch(ureq);
+			}	
+		} 
+		super.event(ureq, source, event);
+	}
+	
+	private void doSearch(UserRequest ureq) {
+		LectureStatisticsSearchParameters params = searchForm.getSearchParameters();
+		List<UserPropertyHandler> userPropertyHandlers = searchForm.getUserPropertyHandlers();
+		List<LectureBlockIdentityStatistics> statistics = lectureService
+				.getLecturesStatistics(params, userPropertyHandlers, getIdentity(), admin);
+		listCtrl = new LecturesListController(ureq, getWindowControl(), statistics,
+				userPropertyHandlers, LecturesSearchFormController.PROPS_IDENTIFIER);
+		listenTo(listCtrl);
+		stackPanel.popUpToRootController(ureq);
+		stackPanel.pushController(translate("results"), listCtrl);
+	}
+}
diff --git a/src/main/java/org/olat/modules/lecture/ui/LecturesSearchFormController.java b/src/main/java/org/olat/modules/lecture/ui/LecturesSearchFormController.java
new file mode 100644
index 00000000000..5a205406cd7
--- /dev/null
+++ b/src/main/java/org/olat/modules/lecture/ui/LecturesSearchFormController.java
@@ -0,0 +1,279 @@
+/**
+ * <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.lecture.ui;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.olat.basesecurity.BaseSecurityModule;
+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.DateChooser;
+import org.olat.core.gui.components.form.flexible.elements.FormLink;
+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.control.Controller;
+import org.olat.core.gui.control.Event;
+import org.olat.core.gui.control.WindowControl;
+import org.olat.core.util.StringHelper;
+import org.olat.modules.lecture.model.LectureStatisticsSearchParameters;
+import org.olat.repository.manager.RepositoryEntryLifecycleDAO;
+import org.olat.repository.model.RepositoryEntryLifecycle;
+import org.olat.user.UserManager;
+import org.olat.user.propertyhandlers.EmailProperty;
+import org.olat.user.propertyhandlers.UserPropertyHandler;
+import org.springframework.beans.factory.annotation.Autowired;
+
+/**
+ * 
+ * 
+ * Initial date: 16 juin 2017<br>
+ * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
+ *
+ */
+public class LecturesSearchFormController extends FormBasicController {
+
+	protected static final String PROPS_IDENTIFIER = LecturesSearchFormController.class.getName();
+	private static final String[] dateKeys = new String[]{ "none", "private", "public"};
+	
+	private TextElement login;
+	private TextElement bulkEl;
+	private FormLink searchButton;
+	private DateChooser startDateEl, endDateEl;
+	private FormLayoutContainer privateDatesCont;
+	private SingleSelection dateTypesEl, publicDatesEl;
+
+	private final boolean adminProps;
+	private List<UserPropertyHandler> userPropertyHandlers;
+	private final Map<String,FormItem> propFormItems = new HashMap<>();
+	
+	@Autowired
+	private UserManager userManager;
+	@Autowired
+	private BaseSecurityModule securityModule;
+	@Autowired
+	private RepositoryEntryLifecycleDAO lifecycleDao;
+	
+	public LecturesSearchFormController(UserRequest ureq, WindowControl wControl) {
+		super(ureq, wControl);
+		setTranslator(userManager.getPropertyHandlerTranslator(getTranslator()));
+		adminProps = securityModule.isUserAllowedAdminProps(ureq.getUserSession().getRoles());
+		
+		initForm(ureq);
+		updateDatesVisibility();
+	}
+
+	@Override
+	protected void initForm(FormItemContainer formLayout, Controller listener, UserRequest ureq) {
+		login = uifactory.addTextElement("login", "search.form.login", 128, "", formLayout);
+		login.setVisible(adminProps);
+		
+		userPropertyHandlers = userManager.getUserPropertyHandlersFor(PROPS_IDENTIFIER, adminProps);
+
+		for (UserPropertyHandler userPropertyHandler : userPropertyHandlers) {
+			if (userPropertyHandler != null) {
+				FormItem fi = userPropertyHandler.addFormItem(getLocale(), null, PROPS_IDENTIFIER, false, formLayout);
+				// DO NOT validate email field => see OLAT-3324, OO-155, OO-222
+				if (userPropertyHandler instanceof EmailProperty && fi instanceof TextElement) {
+					TextElement textElement = (TextElement)fi;
+					textElement.setItemValidatorProvider(null);
+				}
+	
+				propFormItems.put(userPropertyHandler.getName(), fi);
+			}
+		}
+		
+		bulkEl = uifactory.addTextAreaElement("bulk", 4, 72, "", formLayout);
+		
+		String[] dateValues = new String[] {
+				translate("dates.none"),
+				translate("dates.private"),
+				translate("dates.public")	
+		};
+		dateTypesEl = uifactory.addRadiosVertical("dates", formLayout, dateKeys, dateValues);
+		dateTypesEl.select(dateKeys[0], true);
+		dateTypesEl.addActionListener(FormEvent.ONCHANGE);
+
+		List<RepositoryEntryLifecycle> cycles = lifecycleDao.loadPublicLifecycle();
+		List<RepositoryEntryLifecycle> filteredCycles = new ArrayList<>();
+		for(RepositoryEntryLifecycle cycle:cycles) {
+			if(cycle.getValidTo() == null) {
+				filteredCycles.add(cycle);
+			}
+		}
+		
+		String[] publicKeys = new String[filteredCycles.size()];
+		String[] publicValues = new String[filteredCycles.size()];
+		int count = 0;		
+		for(RepositoryEntryLifecycle cycle:filteredCycles) {
+				publicKeys[count] = cycle.getKey().toString();
+				
+				StringBuilder sb = new StringBuilder(32);
+				boolean labelAvailable = StringHelper.containsNonWhitespace(cycle.getLabel());
+				if(labelAvailable) {
+					sb.append(cycle.getLabel());
+				}
+				if(StringHelper.containsNonWhitespace(cycle.getSoftKey())) {
+					if(labelAvailable) sb.append(" - ");
+					sb.append(cycle.getSoftKey());
+				}
+				publicValues[count++] = sb.toString();
+		}
+		publicDatesEl = uifactory.addDropdownSingleselect("public.dates", formLayout, publicKeys, publicValues, null);
+
+		String privateDatePage = velocity_root + "/cycle_dates.html";
+		privateDatesCont = FormLayoutContainer.createCustomFormLayout("private.date", getTranslator(), privateDatePage);
+		privateDatesCont.setRootForm(mainForm);
+		privateDatesCont.setLabel("private.dates", null);
+		formLayout.add("private.date", privateDatesCont);
+		
+		startDateEl = uifactory.addDateChooser("date.start", "date.start", null, privateDatesCont);
+		startDateEl.setElementCssClass("o_sel_repo_lifecycle_validfrom");
+		endDateEl = uifactory.addDateChooser("date.end", "date.end", null, privateDatesCont);
+		endDateEl.setElementCssClass("o_sel_repo_lifecycle_validto");
+	
+		FormLayoutContainer buttonCont = FormLayoutContainer.createButtonLayout("buttons", getTranslator());
+		formLayout.add(buttonCont);
+		uifactory.addFormSubmitButton("search", buttonCont);
+	}
+	
+	private void updateDatesVisibility() {
+		if(dateTypesEl.isOneSelected()) {
+			String type = dateTypesEl.getSelectedKey();
+			if("none".equals(type)) {
+				publicDatesEl.setVisible(false);
+				privateDatesCont.setVisible(false);
+			} else if("public".equals(type)) {
+				publicDatesEl.setVisible(true);
+				privateDatesCont.setVisible(false);
+			} else if("private".equals(type)) {
+				publicDatesEl.setVisible(false);
+				privateDatesCont.setVisible(true);
+			}
+		}
+	}
+
+	@Override
+	protected void doDispose() {
+		//
+	}
+	
+	public List<UserPropertyHandler> getUserPropertyHandlers() {
+		return userPropertyHandlers;
+	}
+	
+	public LectureStatisticsSearchParameters getSearchParameters() {
+		LectureStatisticsSearchParameters params = new LectureStatisticsSearchParameters();
+
+		String type = dateTypesEl.getSelectedKey();
+		if("none".equals(type)) {
+			params.setStartDate(null);
+			params.setEndDate(null);
+			params.setLifecycle(null);
+		} else if("public".equals(type)) {
+			params.setStartDate(null);
+			params.setEndDate(null);
+			if(publicDatesEl.isOneSelected() && StringHelper.isLong(publicDatesEl.getSelectedKey())) {
+				RepositoryEntryLifecycle lifecycle = lifecycleDao.loadById(new Long(publicDatesEl.getSelectedKey()));
+				params.setLifecycle(lifecycle);
+			} else {
+				params.setLifecycle(null);
+			}
+		} else if("private".equals(type)) {
+			params.setStartDate(startDateEl.getDate());
+			params.setEndDate(endDateEl.getDate());
+			params.setLifecycle(null);
+		}
+		
+		params.setLogin(getLogin());
+		params.setBulkIdentifiers(getBulkIdentifiers());
+		params.setUserProperties(getSearchProperties());
+		return params;
+	}
+	
+	private String getLogin() {
+		return login.isVisible() && StringHelper.containsNonWhitespace(login.getValue())
+				? login.getValue() : null;
+	}
+	
+	private Map<String,String> getSearchProperties() {
+		Map<String, String> userPropertiesSearch = new HashMap<>();				
+		for (UserPropertyHandler userPropertyHandler : userPropertyHandlers) {
+			if (userPropertyHandler != null) {
+				FormItem ui = propFormItems.get(userPropertyHandler.getName());
+				String uiValue = userPropertyHandler.getStringValue(ui);
+				if(userPropertyHandler.getName().startsWith("genericCheckboxProperty")) {
+					if(!"false".equals(uiValue)) {
+						userPropertiesSearch.put(userPropertyHandler.getName(), uiValue);
+					}
+				} else if (StringHelper.containsNonWhitespace(uiValue) && !uiValue.equals("-")) {
+					userPropertiesSearch.put(userPropertyHandler.getName(), uiValue);
+				}
+			}
+		}
+		return userPropertiesSearch;
+	}
+	
+	private List<String> getBulkIdentifiers() {
+		String val = bulkEl.getValue();
+		
+		List<String> identifiers = new ArrayList<String>();
+		String[] lines = val.split("\r?\n");
+		for (int i = 0; i < lines.length; i++) {
+			String username = lines[i].trim();
+			if(username.length() > 0) {
+				identifiers.add(username);
+			}
+		}
+		return identifiers;
+	}
+	
+	@Override
+	protected void formOK(UserRequest ureq) {
+		fireEvent(ureq, Event.DONE_EVENT);
+	}
+
+	@Override
+	protected void formInnerEvent(UserRequest ureq, FormItem source, FormEvent event) {
+		if (source == dateTypesEl) {
+			updateDatesVisibility();
+		} else if (source == searchButton) {
+			if(validate()) {
+				fireEvent (ureq, Event.DONE_EVENT);
+			}		
+		}
+		super.formInnerEvent(ureq, source, event);
+	}
+
+	@Override
+	protected boolean validateFormLogic(UserRequest ureq) {
+		return validate() & super.validateFormLogic(ureq);
+	}
+	
+	private boolean validate() {
+		return true;
+	}
+}
diff --git a/src/main/java/org/olat/modules/lecture/ui/_content/cycle_dates.html b/src/main/java/org/olat/modules/lecture/ui/_content/cycle_dates.html
new file mode 100644
index 00000000000..9284e39ceb0
--- /dev/null
+++ b/src/main/java/org/olat/modules/lecture/ui/_content/cycle_dates.html
@@ -0,0 +1,6 @@
+<div class="o_date form-inline">
+	<span class="form-control-static">$r.translate("date.start")</span>
+	<div class="form-group o_sel_repo_lifecycle_validfrom">$r.render("date.start")</div>
+	<span class="form-control-static">$r.translate("date.end")</span>
+	<div class="form-group o_sel_repo_lifecycle_validto">$r.render("date.end")</div>
+</div>
\ No newline at end of file
diff --git a/src/main/java/org/olat/modules/lecture/ui/_content/lectures_coaching.html b/src/main/java/org/olat/modules/lecture/ui/_content/lectures_coaching.html
new file mode 100644
index 00000000000..bade9402acd
--- /dev/null
+++ b/src/main/java/org/olat/modules/lecture/ui/_content/lectures_coaching.html
@@ -0,0 +1 @@
+$r.render("table")
\ No newline at end of file
diff --git a/src/main/java/org/olat/modules/lecture/ui/_i18n/LocalStrings_de.properties b/src/main/java/org/olat/modules/lecture/ui/_i18n/LocalStrings_de.properties
index e570ad734a7..e78f902f115 100644
--- a/src/main/java/org/olat/modules/lecture/ui/_i18n/LocalStrings_de.properties
+++ b/src/main/java/org/olat/modules/lecture/ui/_i18n/LocalStrings_de.properties
@@ -13,6 +13,7 @@ appeal.title=Rekurs f\u00FCr\: "{0}"
 authorized.absence=Entschuldigt
 authorized.absence.reason=Begr\u00FCndung
 autoclosed=Automatisch geschlossen
+bulk=Bulk Email
 cancelled=Abgesagt
 cancel.lecture.blocks=Lektionen absagen
 closed=Geschlossen
@@ -28,6 +29,15 @@ confirm.delete.lectures=Wollen Sie wirklich diese Lektionenblock "{0}" l\u00F6sc
 confirm.delete.reason=Wollen Sie wirklich diese Begr\u00FCndung "{0}" l\u00F6schen?
 copy=Kopieren
 current.lecture=Aktueller Lektionenblock
+dates=$org.olat.repository\:cif.dates
+dates.none=$org.olat.repository\:cif.dates.none
+dates.private=$org.olat.repository\:cif.dates.private
+dates.public=$org.olat.repository\:cif.dates.public
+date.start=$org.olat.repository\:cif.date.start
+date.end=$org.olat.repository\:cif.date.end
+dates.public=$org.olat.repository\:cif.dates.public
+private.dates=$org.olat.repository\:cif.private.dates
+public.dates=$org.olat.repository\:cif.public.dates
 delete.lectures.title=Lektionenblock l\u00F6schen
 delete.title=Begr\u00FCndung l\u00F6schen
 details=Details
@@ -125,11 +135,13 @@ repo.lectures=Lektionen
 repo.lectures.block=Lektionenbl\u00F6cke
 repo.participants=Teilnehmer
 repo.settings=Konfiguration
+results=Resultaten
 rollcall=Anwesenheitskontrolle
 rollcall.comment=Bemerkung
 rollcall.status=Status Lektionenblock
 save.next=Speichern und weiter
 save.temporary=Zwischen speichern
+search.form.login=Benutzername
 start.wizard=Wizard starten
 sync.participants.calendar.enabled=Teilnehmer Kalender synchronizieren
 sync.teachers.calendar.enabled=Lehrer Kalender synchronizieren
diff --git a/src/main/java/org/olat/modules/lecture/ui/_i18n/LocalStrings_en.properties b/src/main/java/org/olat/modules/lecture/ui/_i18n/LocalStrings_en.properties
index 608fefed713..f04474f47de 100644
--- a/src/main/java/org/olat/modules/lecture/ui/_i18n/LocalStrings_en.properties
+++ b/src/main/java/org/olat/modules/lecture/ui/_i18n/LocalStrings_en.properties
@@ -13,6 +13,7 @@ appeal.title=Appeal for\: "{0}"
 authorized.absence=Authorized
 authorized.absence.reason=Reason
 autoclosed=Automatically closed
+bulk=Bulk Email
 cancelled=Cancelled
 cancel.lecture.blocks=Cancel lectures
 closed=Closed
@@ -28,6 +29,15 @@ confirm.delete.lectures=Do you really want to delete these lectures "{0}"?
 confirm.delete.reason=Do you really want to delete this reason "{0}"?
 copy=Copy
 current.lecture=Current lectures
+dates=$org.olat.repository\:cif.dates
+dates.none=$org.olat.repository\:cif.dates.none
+dates.private=$org.olat.repository\:cif.dates.private
+dates.public=$org.olat.repository\:cif.dates.public
+date.start=$org.olat.repository\:cif.date.start
+date.end=$org.olat.repository\:cif.date.end
+dates.public=$org.olat.repository\:cif.dates.public
+private.dates=$org.olat.repository\:cif.private.dates
+public.dates=$org.olat.repository\:cif.public.dates
 delete.lectures.title=Delete lectures
 delete.title=Delete reason
 details=Details
@@ -124,11 +134,13 @@ repo.lectures=Lectures
 repo.lectures.block=Lectures blocs
 repo.participants=Participants
 repo.settings=Configuration
+results=Results
 rollcall=Roll call
 rollcall.comment=Comment
 rollcall.status=Roll call status
 save.next=Save and next
 save.temporary=Quick save
+search.form.login=Username
 start.wizard=Start wizard
 sync.participants.calendar.enabled=Synchronize participants calendars
 sync.teachers.calendar.enabled=Synchronize teachers calendars
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 b21b2155678..cdcaf70b801 100644
--- a/src/main/java/org/olat/user/propertyhandlers/_spring/userPropertiesContext.xml
+++ b/src/main/java/org/olat/user/propertyhandlers/_spring/userPropertiesContext.xml
@@ -1264,6 +1264,29 @@
 					</bean>
 				</entry>
 				
+				<entry key="org.olat.modules.lecture.ui.LecturesSearchFormController">
+					<bean class="org.olat.user.propertyhandlers.UserPropertyUsageContext">
+						<property name="description" value="Properties used in the user search for lectures of the coaching tool." />
+						<property name="propertyHandlers">
+							<list>
+								<ref bean="userPropertyFirstName" />
+								<ref bean="userPropertyLastName" />	
+								<ref bean="userPropertyEmail" />
+								<ref bean="userPropertyInstitutionalName" />
+								<ref bean="userPropertyInstitutionalUserIdentifier" />
+								<ref bean="userPropertyInstitutionalEmail" />
+								<ref bean="userPropertyOrgUnit" />
+							</list>
+						</property>
+						<property name="mandatoryProperties">
+							<set>
+								<ref bean="userPropertyFirstName" />
+								<ref bean="userPropertyLastName" />
+							</set>
+						</property>					
+					</bean>
+				</entry>
+				
 				<entry key="org.olat.modules.portfolio.ui.PortfolioHomeController">
 					<bean class="org.olat.user.propertyhandlers.UserPropertyUsageContext">
 						<property name="description" value="Properties used in the portfolio v2.0." />
-- 
GitLab