From bb1a0bb478a116a97ac4239808731cea7a6690e4 Mon Sep 17 00:00:00 2001
From: srosse <none@none>
Date: Mon, 12 Dec 2011 09:18:55 +0100
Subject: [PATCH] FXOLAT-358: merge members BB (from VCRP)

---
 .../olat/course/nodes/MembersCourseNode.java  | 102 ++++
 .../MembersCourseNodeConfiguration.java       |  72 +++
 .../MembersCourseNodeEditController.java      |  77 +++
 .../MembersCourseNodeRunController.java       | 447 ++++++++++++++++++
 .../nodes/members/_content/memberList.html    |  13 +
 .../nodes/members/_content/members.html       |  16 +
 .../members/_i18n/LocalStrings_de.properties  |  12 +
 .../members/_i18n/LocalStrings_en.properties  |  12 +
 .../members/_spring/membersCourseContext.xml  |  13 +
 .../resources/serviceconfig/olat.properties   |   3 +-
 .../webapp/static/themes/default/all/olat.css |   9 +
 11 files changed, 775 insertions(+), 1 deletion(-)
 create mode 100644 src/main/java/org/olat/course/nodes/MembersCourseNode.java
 create mode 100644 src/main/java/org/olat/course/nodes/members/MembersCourseNodeConfiguration.java
 create mode 100644 src/main/java/org/olat/course/nodes/members/MembersCourseNodeEditController.java
 create mode 100644 src/main/java/org/olat/course/nodes/members/MembersCourseNodeRunController.java
 create mode 100644 src/main/java/org/olat/course/nodes/members/_content/memberList.html
 create mode 100644 src/main/java/org/olat/course/nodes/members/_content/members.html
 create mode 100644 src/main/java/org/olat/course/nodes/members/_i18n/LocalStrings_de.properties
 create mode 100644 src/main/java/org/olat/course/nodes/members/_i18n/LocalStrings_en.properties
 create mode 100644 src/main/java/org/olat/course/nodes/members/_spring/membersCourseContext.xml

diff --git a/src/main/java/org/olat/course/nodes/MembersCourseNode.java b/src/main/java/org/olat/course/nodes/MembersCourseNode.java
new file mode 100644
index 00000000000..a007f0c0aff
--- /dev/null
+++ b/src/main/java/org/olat/course/nodes/MembersCourseNode.java
@@ -0,0 +1,102 @@
+/**
+ * <a href="http://www.openolat.org">
+ * OpenOLAT - Online Learning and Training</a><br>
+ * <p>
+ * Licensed under the Apache License, Version 2.0 (the "License"); <br>
+ * you may not use this file except in compliance with the License.<br>
+ * You may obtain a copy of the License at the
+ * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a>
+ * <p>
+ * Unless required by applicable law or agreed to in writing,<br>
+ * software distributed under the License is distributed on an "AS IS" BASIS, <br>
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br>
+ * See the License for the specific language governing permissions and <br>
+ * limitations under the License.
+ * <p>
+ * Initial code contributed and copyrighted by<br>
+ * frentix GmbH, http://www.frentix.com
+ * <p>
+ */
+package org.olat.course.nodes;
+
+import java.util.List;
+
+import org.olat.core.gui.UserRequest;
+import org.olat.core.gui.control.Controller;
+import org.olat.core.gui.control.WindowControl;
+import org.olat.core.gui.control.generic.tabbable.TabbableController;
+import org.olat.core.util.Util;
+import org.olat.course.ICourse;
+import org.olat.course.editor.CourseEditorEnv;
+import org.olat.course.editor.NodeEditController;
+import org.olat.course.editor.StatusDescription;
+import org.olat.course.nodes.info.InfoCourseNodeEditController;
+import org.olat.course.nodes.members.MembersCourseNodeEditController;
+import org.olat.course.nodes.members.MembersCourseNodeRunController;
+import org.olat.course.run.navigation.NodeRunConstructionResult;
+import org.olat.course.run.userview.NodeEvaluation;
+import org.olat.course.run.userview.UserCourseEnvironment;
+import org.olat.repository.RepositoryEntry;
+
+
+/**
+ * 
+ * Description:<br>
+ *  The course node show all members of the course
+ * 
+ * <P>
+ * Initial Date:  11 mars 2011 <br>
+ * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
+ */
+public class MembersCourseNode extends AbstractAccessableCourseNode {
+	
+	private static final long serialVersionUID = -8404722446386415061L;
+	
+	public static final String TYPE = "cmembers";
+	
+	public MembersCourseNode() {
+		super(TYPE);
+		updateModuleConfigDefaults(true);
+	}
+
+	@Override
+	public RepositoryEntry getReferencedRepositoryEntry() {
+		return null;
+	}
+
+	@Override
+	public boolean needsReferenceToARepositoryEntry() {
+		return false;
+	}
+
+	@Override
+	public StatusDescription isConfigValid() {
+		return StatusDescription.NOERROR;
+	}
+	
+	@Override
+	public StatusDescription[] isConfigValid(CourseEditorEnv cev) {
+		oneClickStatusCache = null;
+		String translatorStr = Util.getPackageName(InfoCourseNodeEditController.class);
+		List<StatusDescription> statusDescs =isConfigValidWithTranslator(cev, translatorStr, getConditionExpressions());
+		oneClickStatusCache = StatusDescriptionHelper.sort(statusDescs);
+		return oneClickStatusCache;
+	}
+
+	@Override
+	public TabbableController createEditController(UserRequest ureq, WindowControl wControl, ICourse course, UserCourseEnvironment euce) {
+		MembersCourseNodeEditController childTabCntrllr = new MembersCourseNodeEditController(ureq, wControl, this, course, euce);
+		CourseNode chosenNode = course.getEditorTreeModel().getCourseNode(euce.getCourseEditorEnv().getCurrentCourseNodeId());
+		return new NodeEditController(ureq, wControl, course.getEditorTreeModel(), course, chosenNode, course.getCourseEnvironment()
+				.getCourseGroupManager(), euce, childTabCntrllr);
+	}
+
+	@Override
+	public NodeRunConstructionResult createNodeRunConstructionResult(UserRequest ureq, WindowControl wControl,
+			UserCourseEnvironment userCourseEnv, NodeEvaluation ne, String nodecmd) {
+		
+		MembersCourseNodeRunController infoCtrl = new MembersCourseNodeRunController(ureq, wControl, this, userCourseEnv);
+		Controller titledCtrl = TitledWrapperHelper.getWrapper(ureq, wControl, infoCtrl, this, "o_cmembers_icon");
+		return new NodeRunConstructionResult(titledCtrl);
+	}
+}
\ No newline at end of file
diff --git a/src/main/java/org/olat/course/nodes/members/MembersCourseNodeConfiguration.java b/src/main/java/org/olat/course/nodes/members/MembersCourseNodeConfiguration.java
new file mode 100644
index 00000000000..fc714f815e7
--- /dev/null
+++ b/src/main/java/org/olat/course/nodes/members/MembersCourseNodeConfiguration.java
@@ -0,0 +1,72 @@
+/**
+ * <a href="http://www.openolat.org">
+ * OpenOLAT - Online Learning and Training</a><br>
+ * <p>
+ * Licensed under the Apache License, Version 2.0 (the "License"); <br>
+ * you may not use this file except in compliance with the License.<br>
+ * You may obtain a copy of the License at the
+ * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a>
+ * <p>
+ * Unless required by applicable law or agreed to in writing,<br>
+ * software distributed under the License is distributed on an "AS IS" BASIS, <br>
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br>
+ * See the License for the specific language governing permissions and <br>
+ * limitations under the License.
+ * <p>
+ * Initial code contributed and copyrighted by<br>
+ * frentix GmbH, http://www.frentix.com
+ * <p>
+ */
+package org.olat.course.nodes.members;
+
+import java.util.Locale;
+
+import org.olat.core.gui.translator.Translator;
+import org.olat.core.util.Util;
+import org.olat.course.nodes.AbstractCourseNodeConfiguration;
+import org.olat.course.nodes.CourseNode;
+import org.olat.course.nodes.CourseNodeConfiguration;
+import org.olat.course.nodes.MembersCourseNode;
+
+/**
+ * 
+ * Description:<br>
+ * Node configuration for the course building block. Nothing to do.
+ * 
+ * <P>
+ * Initial Date:  11 mars 2011 <br>
+ * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
+ */
+public class MembersCourseNodeConfiguration extends AbstractCourseNodeConfiguration implements CourseNodeConfiguration {
+	
+	private MembersCourseNodeConfiguration() {
+		super();
+	}
+
+	@Override
+	public String getAlias() {
+		return "cmembers";
+	}
+
+	@Override
+	public CourseNode getInstance() {
+		return new MembersCourseNode();
+	}
+
+	@Override
+	public String getLinkText(Locale locale) {
+		Translator fallback = Util.createPackageTranslator(CourseNodeConfiguration.class, locale);
+		Translator translator = Util.createPackageTranslator(MembersCourseNodeConfiguration.class, locale, fallback);
+		return translator.translate("title_info");
+	}
+
+	@Override
+	public String getIconCSSClass() {
+		return "o_cmembers_icon";
+	}
+
+	@Override
+	public String getLinkCSSClass() {
+		return null;
+	}
+}
diff --git a/src/main/java/org/olat/course/nodes/members/MembersCourseNodeEditController.java b/src/main/java/org/olat/course/nodes/members/MembersCourseNodeEditController.java
new file mode 100644
index 00000000000..1bfab3af79f
--- /dev/null
+++ b/src/main/java/org/olat/course/nodes/members/MembersCourseNodeEditController.java
@@ -0,0 +1,77 @@
+/**
+ * <a href="http://www.openolat.org">
+ * OpenOLAT - Online Learning and Training</a><br>
+ * <p>
+ * Licensed under the Apache License, Version 2.0 (the "License"); <br>
+ * you may not use this file except in compliance with the License.<br>
+ * You may obtain a copy of the License at the
+ * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a>
+ * <p>
+ * Unless required by applicable law or agreed to in writing,<br>
+ * software distributed under the License is distributed on an "AS IS" BASIS, <br>
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br>
+ * See the License for the specific language governing permissions and <br>
+ * limitations under the License.
+ * <p>
+ * Initial code contributed and copyrighted by<br>
+ * frentix GmbH, http://www.frentix.com
+ * <p>
+ */
+package org.olat.course.nodes.members;
+
+import org.olat.core.gui.UserRequest;
+import org.olat.core.gui.components.Component;
+import org.olat.core.gui.components.tabbedpane.TabbedPane;
+import org.olat.core.gui.control.ControllerEventListener;
+import org.olat.core.gui.control.Event;
+import org.olat.core.gui.control.WindowControl;
+import org.olat.core.gui.control.generic.tabbable.ActivateableTabbableDefaultController;
+import org.olat.course.ICourse;
+import org.olat.course.nodes.MembersCourseNode;
+import org.olat.course.run.userview.UserCourseEnvironment;
+
+/**
+ * 
+ * Description:<br>
+ * Edit panel for the members course building block. Nothing to do.
+ * 
+ * <P>
+ * Initial Date:  11 mars 2011 <br>
+ * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
+ */
+public class MembersCourseNodeEditController extends ActivateableTabbableDefaultController implements ControllerEventListener {
+	private static final String[] paneKeys = {};
+	
+	private TabbedPane myTabbedPane;
+
+	public MembersCourseNodeEditController(UserRequest ureq, WindowControl wControl, MembersCourseNode courseNode, ICourse course,
+			UserCourseEnvironment euce) {
+		super(ureq,wControl);
+
+	}
+	
+	@Override
+	protected void doDispose() {
+		//
+	}
+	
+	@Override
+	public String[] getPaneKeys() {
+		return paneKeys;
+	}
+	
+	@Override
+	public TabbedPane getTabbedPane() {
+		return myTabbedPane;
+	}
+
+	@Override
+	public void addTabs(TabbedPane tabbedPane) {
+		myTabbedPane = tabbedPane;
+	}
+
+	@Override
+	protected void event(UserRequest ureq, Component source, Event event) {
+		//
+	}
+}
diff --git a/src/main/java/org/olat/course/nodes/members/MembersCourseNodeRunController.java b/src/main/java/org/olat/course/nodes/members/MembersCourseNodeRunController.java
new file mode 100644
index 00000000000..3e0e58dc4ce
--- /dev/null
+++ b/src/main/java/org/olat/course/nodes/members/MembersCourseNodeRunController.java
@@ -0,0 +1,447 @@
+/**
+ * <a href="http://www.openolat.org">
+ * OpenOLAT - Online Learning and Training</a><br>
+ * <p>
+ * Licensed under the Apache License, Version 2.0 (the "License"); <br>
+ * you may not use this file except in compliance with the License.<br>
+ * You may obtain a copy of the License at the
+ * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a>
+ * <p>
+ * Unless required by applicable law or agreed to in writing,<br>
+ * software distributed under the License is distributed on an "AS IS" BASIS, <br>
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br>
+ * See the License for the specific language governing permissions and <br>
+ * limitations under the License.
+ * <p>
+ * Initial code contributed and copyrighted by<br>
+ * frentix GmbH, http://www.frentix.com
+ * <p>
+ */
+package org.olat.course.nodes.members;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.olat.NewControllerFactory;
+import org.olat.basesecurity.BaseSecurity;
+import org.olat.basesecurity.BaseSecurityManager;
+import org.olat.core.dispatcher.mapper.Mapper;
+import org.olat.core.gui.UserRequest;
+import org.olat.core.gui.components.form.flexible.FormItem;
+import org.olat.core.gui.components.form.flexible.FormItemContainer;
+import org.olat.core.gui.components.form.flexible.elements.FormLink;
+import org.olat.core.gui.components.form.flexible.impl.FormBasicController;
+import org.olat.core.gui.components.form.flexible.impl.FormEvent;
+import org.olat.core.gui.components.form.flexible.impl.FormLayoutContainer;
+import org.olat.core.gui.components.link.Link;
+import org.olat.core.gui.control.Controller;
+import org.olat.core.gui.control.Event;
+import org.olat.core.gui.control.WindowControl;
+import org.olat.core.gui.control.generic.closablewrapper.CloseableModalController;
+import org.olat.core.gui.media.MediaResource;
+import org.olat.core.id.Identity;
+import org.olat.core.id.User;
+import org.olat.core.id.UserConstants;
+import org.olat.core.id.context.BusinessControl;
+import org.olat.core.id.context.BusinessControlFactory;
+import org.olat.core.util.StringHelper;
+import org.olat.core.util.mail.ContactList;
+import org.olat.core.util.mail.ContactMessage;
+import org.olat.course.CourseFactory;
+import org.olat.course.ICourse;
+import org.olat.course.groupsandrights.CourseGroupManager;
+import org.olat.course.nodes.CourseNode;
+import org.olat.course.run.userview.UserCourseEnvironment;
+import org.olat.modules.co.ContactFormController;
+import org.olat.repository.RepositoryEntry;
+import org.olat.repository.RepositoryManager;
+import org.olat.user.DisplayPortraitManager;
+
+/**
+ * 
+ * Description:<br>
+ * The run controller show the list of members of the course
+ * 
+ * <P>
+ * Initial Date:  11 mars 2011 <br>
+ * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
+ */
+public class MembersCourseNodeRunController extends FormBasicController {
+
+	private final RepositoryManager rm ;
+	private final BaseSecurity securityManager;
+	private final UserCourseEnvironment userCourseEnv;
+	private final DisplayPortraitManager portraitManager;
+	private final String avatarBaseURL;
+
+	private FormLink ownersEmailLink;
+	private FormLink coachesEmailLink;
+	private FormLink participantsEmailLink;
+	private List<FormLink> memberLinks = new ArrayList<FormLink>();
+	private List<FormLink> emailLinks = new ArrayList<FormLink>();
+	
+	private List<FormLink> ownerLinks;
+	private List<FormLink> coachesLinks;
+	private List<FormLink> participantsLinks;
+	
+	private ContactFormController emailController;
+	private CloseableModalController cmc;
+	
+	public MembersCourseNodeRunController(UserRequest ureq, WindowControl wControl, CourseNode courseNode, UserCourseEnvironment userCourseEnv) {
+		super(ureq, wControl, "members");
+		
+		avatarBaseURL = registerCacheableMapper("avatars-members", new AvatarMapper());
+		
+		rm = RepositoryManager.getInstance();
+		securityManager = BaseSecurityManager.getInstance();
+		this.userCourseEnv = userCourseEnv;
+		portraitManager = DisplayPortraitManager.getInstance();
+
+		initForm(ureq);
+	}
+
+	@Override
+	protected void initForm(FormItemContainer formLayout, Controller listener, UserRequest ureq) {
+
+		CourseGroupManager cgm = userCourseEnv.getCourseEnvironment().getCourseGroupManager();
+		Long courseResId = userCourseEnv.getCourseEnvironment().getCourseResourceableId();
+		ICourse course = CourseFactory.loadCourse(courseResId);
+		RepositoryEntry courseRepositoryEntry = rm.lookupRepositoryEntry(course, true);
+		List<Identity> owners = securityManager.getIdentitiesOfSecurityGroup(courseRepositoryEntry.getOwnerGroup());
+		List<Identity> coaches = cgm.getCoachesFromLearningGroup(null);
+		//fxdiff VCRP-1,2: access control of resources
+		coaches.addAll(cgm.getCoaches());
+		List<Identity> participants = cgm.getParticipantsFromLearningGroup(null);
+		participants.addAll(cgm.getParticipants());
+		Comparator<Identity> idComparator = new IdentityComparator();
+		Collections.sort(owners, idComparator);
+		Collections.sort(coaches, idComparator);
+		Collections.sort(participants, idComparator);
+		
+		boolean canEmail =  canEmail(owners, coaches);
+		if(canEmail) {
+			ownersEmailLink = uifactory.addFormLink("owners-email", "members.email", null, formLayout, Link.LINK);
+			ownersEmailLink.setCustomEnabledLinkCSS("b_small_icon o_cmembers_mail");
+			coachesEmailLink = uifactory.addFormLink("coaches-email", "members.email", null, formLayout, Link.LINK);
+			coachesEmailLink.setCustomEnabledLinkCSS("b_small_icon o_cmembers_mail");
+			participantsEmailLink = uifactory.addFormLink("participants-email", "members.email", null, formLayout, Link.LINK);
+			participantsEmailLink.setCustomEnabledLinkCSS("b_small_icon o_cmembers_mail");
+			
+			formLayout.add("owners-email", ownersEmailLink);
+			formLayout.add("coaches-email", coachesEmailLink);
+			formLayout.add("participants-email", participantsEmailLink);
+		}
+
+		Set<Long> duplicateCatcher = new HashSet<Long>();
+		ownerLinks = initFormMemberList("owners", owners, duplicateCatcher, formLayout, canEmail);
+		coachesLinks = initFormMemberList("coaches", coaches, duplicateCatcher, formLayout, canEmail);
+		participantsLinks = initFormMemberList("participants", participants, duplicateCatcher, formLayout, canEmail);
+		
+		if(formLayout instanceof FormLayoutContainer) {
+			FormLayoutContainer layoutCont = (FormLayoutContainer)formLayout;
+			layoutCont.contextPut("hasOwners", new Boolean(!ownerLinks.isEmpty()));
+			layoutCont.contextPut("hasCoaches", new Boolean(!coachesLinks.isEmpty()));
+			layoutCont.contextPut("hasParticipants", new Boolean(!participantsLinks.isEmpty()));
+		}
+	}
+	
+	private boolean canEmail(List<Identity> owners, List<Identity> coaches) {
+		for(Identity owner:owners) {
+			if(owner.equalsByPersistableKey(getIdentity())) {
+				return true;
+			}
+		}
+		
+		for(Identity coach:coaches) {
+			if(coach.equalsByPersistableKey(getIdentity())) {
+				return true;
+			}
+		}
+		return false;
+	}
+	
+	private List<FormLink> initFormMemberList(String name, List<Identity> ids, Set<Long> duplicateCatcher, FormItemContainer formLayout, boolean withEmail) {
+		String page = velocity_root + "/memberList.html";
+		
+		FormLayoutContainer container = FormLayoutContainer.createCustomFormLayout(name, getTranslator(), page);
+		formLayout.add(name, container);
+		container.setRootForm(mainForm);
+
+		List<FormLink> links = createMemberLinks(ids, duplicateCatcher, container, withEmail);
+		container.contextPut("memberLinks", links);
+		container.contextPut("avatarBaseURL", avatarBaseURL);
+		return links;
+	}
+	
+	protected List<FormLink> createMemberLinks(List<Identity> identities, Set<Long> duplicateCatcher, FormLayoutContainer formLayout, boolean withEmail) {
+		List<FormLink> idLinks = new ArrayList<FormLink>();
+		for(Identity identity:identities) {
+			if(duplicateCatcher.contains(identity.getKey())) continue;
+			
+			Member member = createMember(identity);
+			FormLink idLink = uifactory.addFormLink("id_" + identity.getKey(), member.getFullName(), null, formLayout, Link.NONTRANSLATED);
+			idLink.setUserObject(member);
+			idLinks.add(idLink);
+			formLayout.add(idLink.getComponent().getComponentName(), idLink);
+			memberLinks.add(idLink);
+			
+			if(withEmail) {
+				FormLink emailLink = uifactory.addFormLink("mail_" + identity.getKey(), member.getFullName(), null, formLayout, Link.NONTRANSLATED);
+				emailLink.setUserObject(member);
+				emailLink.setCustomEnabledLinkCSS("b_small_icon o_cmembers_mail");
+				formLayout.add(emailLink.getComponent().getComponentName(), emailLink);
+				emailLinks.add(emailLink);
+				member.setEmailLink(emailLink);
+			}
+			duplicateCatcher.add(identity.getKey());
+		}
+		return idLinks;
+	}
+	
+	protected Member createMember(Identity identity) {
+		User user = identity.getUser();
+		String firstname = user.getProperty(UserConstants.FIRSTNAME, null);
+		String lastname = user.getProperty(UserConstants.LASTNAME, null);
+		MediaResource rsrc = portraitManager.getPortrait(identity, DisplayPortraitManager.PORTRAIT_SMALL_FILENAME);
+		
+		String portraitCssClass = null;
+		String gender = identity.getUser().getProperty(UserConstants.GENDER, Locale.ENGLISH);
+		if (gender.equalsIgnoreCase("male")) {
+			portraitCssClass = DisplayPortraitManager.DUMMY_MALE_SMALL_CSS_CLASS;
+		} else if (gender.equalsIgnoreCase("female")) {
+			portraitCssClass = DisplayPortraitManager.DUMMY_FEMALE_SMALL_CSS_CLASS;
+		} else {
+			portraitCssClass = DisplayPortraitManager.DUMMY_SMALL_CSS_CLASS;
+		}
+		
+		Member member = new Member(identity.getKey(), identity, firstname, lastname, rsrc != null, portraitCssClass);
+		return member;
+	}
+	
+	@Override
+	protected void doDispose() {
+		//
+	}
+
+	@Override
+	protected void formOK(UserRequest ureq) {
+		//
+	}
+
+	@Override
+	protected void formInnerEvent(UserRequest ureq, FormItem source, FormEvent event) {
+		if(memberLinks.contains(source)) {
+			FormLink memberLink = (FormLink)source;
+			Member member = (Member)memberLink.getUserObject();
+			openHomePage(member.getIdentity(), ureq);
+		} else if (emailLinks.contains(source)) {
+			FormLink emailLink = (FormLink)source;
+			Member member = (Member)emailLink.getUserObject();
+			ContactList memberList = new ContactList(translate("members.to", new String[]{member.getFullName(), this.userCourseEnv.getCourseEnvironment().getCourseTitle()}));
+			memberList.add(member.getIdentity());
+			sendEmailToMember(memberList, ureq);
+		} else if (source == coachesEmailLink) {
+			ContactList coachList = new ContactList(translate("coaches.to", new String[]{this.userCourseEnv.getCourseEnvironment().getCourseTitle()}));
+			for(FormLink coachLink:coachesLinks) {
+				Member member = (Member)coachLink.getUserObject();
+				coachList.add(member.getIdentity());
+			}
+			sendEmailToMember(coachList, ureq);
+		} else if (source == ownersEmailLink) {
+			ContactList ownerList = new ContactList(translate("owners.to", new String[]{this.userCourseEnv.getCourseEnvironment().getCourseTitle()}));
+			for(FormLink ownerLink:ownerLinks) {
+				Member member = (Member)ownerLink.getUserObject();
+				ownerList.add(member.getIdentity());
+			}
+			sendEmailToMember(ownerList, ureq);
+		} else if (source == participantsEmailLink) {
+			ContactList participantList = new ContactList(translate("participants.to", new String[]{this.userCourseEnv.getCourseEnvironment().getCourseTitle()}));
+			for(FormLink participantLink:participantsLinks) {
+				Member member = (Member)participantLink.getUserObject();
+				participantList.add(member.getIdentity());
+			}
+			sendEmailToMember(participantList, ureq);
+		}
+	}
+	
+	@Override
+	protected void event(UserRequest ureq, Controller source, Event event) {
+		if(source == cmc) {
+			removeAsListenerAndDispose(emailController);
+			removeAsListenerAndDispose(cmc);
+			emailController = null;
+			cmc = null;
+		} else if (source == emailController) {
+			cmc.deactivate();
+			removeAsListenerAndDispose(emailController);
+			removeAsListenerAndDispose(cmc);
+			emailController = null;
+			cmc = null;
+		}
+		super.event(ureq, source, event);
+	}
+
+	protected void sendEmailToMember(ContactList contactList, UserRequest ureq) {
+		if (contactList.getEmailsAsStrings().size() > 0) {
+			removeAsListenerAndDispose(emailController);
+			
+			ContactMessage cmsg = new ContactMessage(ureq.getIdentity());
+			cmsg.addEmailTo(contactList);
+			
+			emailController = new ContactFormController(ureq, getWindowControl(), false, true, false, false, cmsg);
+			listenTo(emailController);
+			
+			removeAsListenerAndDispose(cmc);
+			String title = translate("members.email.title");
+			cmc = new CloseableModalController(getWindowControl(), translate("close"), emailController.getInitialComponent(), true, title);
+			listenTo(cmc);
+			
+			cmc.activate();			
+		}
+	}
+	
+	protected void openHomePage(Identity member, UserRequest ureq) {
+		String url = "[Identity:" + member.getKey() + "]";
+		BusinessControl bc = BusinessControlFactory.getInstance().createFromString(url);
+	  WindowControl bwControl = BusinessControlFactory.getInstance().createBusinessWindowControl(bc, getWindowControl());
+	  NewControllerFactory.getInstance().launch(ureq, bwControl);
+	}
+	
+	public class AvatarMapper implements Mapper {
+
+		@Override
+		public MediaResource handle(String relPath, HttpServletRequest request) {
+			if(relPath != null && relPath.endsWith("/portrait_small.jpg")) {
+				if(relPath.startsWith("/")) {
+					relPath = relPath.substring(1, relPath.length());
+				}
+				
+				int endKeyIndex = relPath.indexOf('/');
+				if(endKeyIndex > 0) {
+					String idKey = relPath.substring(0, endKeyIndex);
+					Long key = Long.parseLong(idKey);
+					for(FormLink memberLink:memberLinks) {
+						Member m = (Member)memberLink.getUserObject();
+						if(m.getIdentity().getKey().equals(key)) {
+							return portraitManager.getPortrait(m.getIdentity(), DisplayPortraitManager.PORTRAIT_SMALL_FILENAME);
+						}
+					}
+				}
+			}
+			return null;
+		}
+	}
+	
+	public class Member {
+		private final String firstName;
+		private final String lastName;
+		private final Long key;
+		private Identity identity;
+		private boolean portrait;
+		private String portraitCssClass;
+		private FormLink emailLink;
+		
+		public Member(Long key, Identity identity, String firstName, String lastName, boolean portrait, String portraitCssClass) {
+			this.firstName = firstName;
+			this.lastName = lastName;
+			this.identity = identity;
+			this.key = key;
+			this.portrait = portrait;
+			this.portraitCssClass = portraitCssClass;
+		}
+
+		public String getFirstName() {
+			return firstName;
+		}
+
+		public String getLastName() {
+			return lastName;
+		}
+		
+		public String getPortraitCssClass() {
+			return portraitCssClass;
+		}
+
+		public Identity getIdentity() {
+			return identity;
+		}
+		
+		public boolean isPortraitAvailable() {
+			return portrait; 
+		}
+
+		public FormLink getEmailLink() {
+			return emailLink;
+		}
+
+		public void setEmailLink(FormLink emailLink) {
+			this.emailLink = emailLink;
+		}
+
+		public String getFullName() {
+			StringBuilder sb = new StringBuilder();
+			if(StringHelper.containsNonWhitespace(lastName)) {
+				sb.append(lastName);
+			}
+			if(StringHelper.containsNonWhitespace(firstName)) {
+				if(sb.length() > 0) {
+					sb.append(' ');
+				}
+				sb.append(firstName);
+			}
+			return sb.toString(); 
+		}
+
+		public Long getKey() {
+			return key;
+		}
+		
+		@Override
+		public int hashCode() {
+			return key.hashCode();
+		}
+		
+		@Override
+		public boolean equals(Object obj) {
+			if(this == obj) {
+				return true;
+			}
+			if(obj instanceof Member) {
+				Member member = (Member)obj;
+				return key != null && key.equals(member.key);
+			}
+			return false;
+		}
+	}
+	
+	public class IdentityComparator implements Comparator<Identity> {
+
+		@Override
+		public int compare(Identity id1, Identity id2) {
+			if(id1 == null) return -1;
+			if(id2 == null) return 1;
+			
+			String l1 = id1.getUser().getProperty(UserConstants.LASTNAME, null);
+			String l2 = id2.getUser().getProperty(UserConstants.LASTNAME, null);
+			if(l1 == null) return -1;
+			if(l2 == null) return 1;
+			
+			int result = l1.compareToIgnoreCase(l2);
+			if(result == 0) {
+				String f1 = id1.getUser().getProperty(UserConstants.FIRSTNAME, null);
+				String f2 = id2.getUser().getProperty(UserConstants.FIRSTNAME, null);
+				if(f1 == null) return -1;
+				if(f2 == null) return 1;
+				result = f1.compareToIgnoreCase(f2);
+			}
+			return result;
+		}
+	}
+}
diff --git a/src/main/java/org/olat/course/nodes/members/_content/memberList.html b/src/main/java/org/olat/course/nodes/members/_content/memberList.html
new file mode 100644
index 00000000000..dcccc800190
--- /dev/null
+++ b/src/main/java/org/olat/course/nodes/members/_content/memberList.html
@@ -0,0 +1,13 @@
+#foreach($memberLink in $memberLinks)
+	<div class="o_cmember">
+		#if($memberLink.getUserObject().isPortraitAvailable())
+			<img class="o_cmember_portrait" src="$avatarBaseURL/$memberLink.getUserObject().getKey()/portrait_small.jpg" width="50"/>
+		#else
+			<img class="o_cmember_portrait $memberLink.getUserObject().getPortraitCssClass()" src="$r.staticLink("images/transparent.gif")" alt="user portrait" width="50" height="50" />
+		#end
+		$r.render($memberLink.getComponent().getComponentName())
+		#if($r.available($memberLink.getUserObject().getEmailLink().getComponent().getComponentName()))
+			$r.render($memberLink.getUserObject().getEmailLink().getComponent().getComponentName())
+		#end
+	</div>
+#end
diff --git a/src/main/java/org/olat/course/nodes/members/_content/members.html b/src/main/java/org/olat/course/nodes/members/_content/members.html
new file mode 100644
index 00000000000..0e2225c205f
--- /dev/null
+++ b/src/main/java/org/olat/course/nodes/members/_content/members.html
@@ -0,0 +1,16 @@
+<div class="o_cmembers">
+	#if($hasOwners)
+		<h4>$r.translate("members.owners") #if($r.available("owners-email")) $r.render("owners-email") #end</h4>
+		$r.render("owners")
+	#end
+	#if($hasCoaches)
+		<h4>$r.translate("members.coaches") #if($hasCoaches) $r.render("coaches-email") #end</h4>
+		$r.render("coaches")
+	#end
+	<h4>$r.translate("members.participants") #if($hasParticipants && $r.available("participants-email")) $r.render("participants-email") #end</h4>
+	#if($hasParticipants)
+		$r.render("participants")
+	#else
+		$r.translate("members.noParticipants.message")
+	#end
+</div>
diff --git a/src/main/java/org/olat/course/nodes/members/_i18n/LocalStrings_de.properties b/src/main/java/org/olat/course/nodes/members/_i18n/LocalStrings_de.properties
new file mode 100644
index 00000000000..1e48ced3616
--- /dev/null
+++ b/src/main/java/org/olat/course/nodes/members/_i18n/LocalStrings_de.properties
@@ -0,0 +1,12 @@
+#Mon Mar 02 09:54:04 CET 2009
+title_info=Teilnehmerliste
+pane.tab.accessibility=Zugang
+members.owners=Kursadministrator
+members.coaches=Betreuer
+members.participants=Teilnehmer
+members.noParticipants.message=Diesem Kurs sind keine Lerngruppen mit Teilnehmern zugeordnet. Erstellen Sie eine Lerngruppe im Gruppenwerkzeug und fügen Sie dort Teilnehmer hinzu.
+members.email.title=E-Mail versenden
+members.to=Teilnehmer "{0}" von Kurs "{1}"
+owners.to=Aministratoren von Kurs "{0}"
+coaches.to=Betreuer von Kurs "{0}"
+participants.to=Teilnehmer von Kurs "{0}"
\ No newline at end of file
diff --git a/src/main/java/org/olat/course/nodes/members/_i18n/LocalStrings_en.properties b/src/main/java/org/olat/course/nodes/members/_i18n/LocalStrings_en.properties
new file mode 100644
index 00000000000..49fa915987d
--- /dev/null
+++ b/src/main/java/org/olat/course/nodes/members/_i18n/LocalStrings_en.properties
@@ -0,0 +1,12 @@
+#Mon May 16 17:20:06 CEST 2011
+members.coaches=Coach
+members.email.title=Send e-mail
+members.noParticipants.message=This course has no learning groups with participants. Create a learning group in the group management tool and add a member there.
+members.owners=Course administrator
+members.participants=Participant
+members.to="{0}" of course "{1}"
+owners.to=administrators of course "{0}"
+coaches.to=coaches of course "{0}"
+participants.to=participants of course "{0}"
+pane.tab.accessibility=Access
+title_info=Participant list
diff --git a/src/main/java/org/olat/course/nodes/members/_spring/membersCourseContext.xml b/src/main/java/org/olat/course/nodes/members/_spring/membersCourseContext.xml
new file mode 100644
index 00000000000..496ec57edba
--- /dev/null
+++ b/src/main/java/org/olat/course/nodes/members/_spring/membersCourseContext.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<beans xmlns="http://www.springframework.org/schema/beans"
+	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+	xsi:schemaLocation="
+  http://www.springframework.org/schema/beans 
+  http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">
+  
+  <bean id="membersCourseNodeConf" class="org.olat.course.nodes.members.MembersCourseNodeConfiguration" scope="prototype">
+		<property name="enabled" value="${course.node.members.enabled}" />
+		<property name="order"   value="190" />
+  </bean>
+
+</beans>
\ No newline at end of file
diff --git a/src/main/resources/serviceconfig/olat.properties b/src/main/resources/serviceconfig/olat.properties
index b88ad72003f..ef42f087ed2 100644
--- a/src/main/resources/serviceconfig/olat.properties
+++ b/src/main/resources/serviceconfig/olat.properties
@@ -634,12 +634,13 @@ method.free.enabled=true
 # Course building blocks, every course building block can be disabled by adding a property here and reference it in
 # appropriate spring config file (by default are course bb are enabled)
 ########################################
-course.node.linklist.enabled=false
+course.node.linklist.enabled=true
 course.node.checklist.enabled=false
 course.node.dateenrollment.enabled=false
 course.node.basiclti.enabled=true
 course.node.portfolio.enabled=true
 course.node.infomessage.enabled=true
+course.node.members.enabled=true
 course.node.vc.enabled=false
 course.node.vitero.enabled=false
 
diff --git a/src/main/webapp/static/themes/default/all/olat.css b/src/main/webapp/static/themes/default/all/olat.css
index 9befeffcaba..fe2b6375ec1 100644
--- a/src/main/webapp/static/themes/default/all/olat.css
+++ b/src/main/webapp/static/themes/default/all/olat.css
@@ -614,6 +614,15 @@ s
 	.o_infomsg_icon { background-image: url(../images/olat/infomessage.png); }
 	.o_infomsg_create_button { position:absolute; top:0; right:250px; }
 	
+	/* MEMBERS BB */
+	div.o_cmembers {}
+	div.o_cmembers * { vertical-align:middle; }
+	div.o_cmembers div.o_cmember { float:left; width:30%; margin: 5px 5px 5px 0; padding:5px; background-color:#EBEBEB; border:1px solid #ddd; border-radius:5px; -moz-border-radius:5px; -webkit-border-radius:5px; -webkit-box-shadow: 0 1px 2px #d3d3d3; -moz-box-shadow: 0 1px 2px #d3d3d3; -o-box-shadow: 0 1px 2px #d3d3d3; box-shadow: 0 1px 2px #d3d3d3; }
+	div.o_cmembers div.o_cmember img.o_cmember_portrait { margin-right:5px; border:1px solid #ddd; background-color:white; background-position:50% 50%; background-repeat:no-repeat; }
+	div.o_cmembers a.o_cmembers_mail { float:none; margin-left:5px; padding-left:20px; background-image:url(../images/olat/email.png); }
+	div.o_cmembers a.o_cmembers_mail span { display:none; }
+	div.o_cmembers h4 { padding: 7px 0 0 0; clear:both;}
+	
 	/* LINK LIST */
 	div.o_ll_container ul li { list-style: none; margin: 1em; }
 	div.o_ll_container ul li div { font-style: italic;}
-- 
GitLab