Index: lams_central/conf/language/lams/ApplicationResources.properties =================================================================== diff -u -r34bc1c178bd5ada01543d5b4637487322d3ff565 -r2584207dba753ac8e61c06022694b202597775d9 --- lams_central/conf/language/lams/ApplicationResources.properties (.../ApplicationResources.properties) (revision 34bc1c178bd5ada01543d5b4637487322d3ff565) +++ lams_central/conf/language/lams/ApplicationResources.properties (.../ApplicationResources.properties) (revision 2584207dba753ac8e61c06022694b202597775d9) @@ -91,6 +91,8 @@ index.participate =Participate index.dummymonitor =Dummy Monitor index.kumalive =Kumalive +index.kumalive.tooltip =Enter a live lesson +index.kumalive.enter.learner =Enter as a learner title.import.result =Import tool content result title.import =Import tool content title.import.instruction =Please choose LAMS sequence to import. Index: lams_central/src/java/org/lamsfoundation/lams/web/DisplayGroupAction.java =================================================================== diff -u -r34bc1c178bd5ada01543d5b4637487322d3ff565 -r2584207dba753ac8e61c06022694b202597775d9 --- lams_central/src/java/org/lamsfoundation/lams/web/DisplayGroupAction.java (.../DisplayGroupAction.java) (revision 34bc1c178bd5ada01543d5b4637487322d3ff565) +++ lams_central/src/java/org/lamsfoundation/lams/web/DisplayGroupAction.java (.../DisplayGroupAction.java) (revision 2584207dba753ac8e61c06022694b202597775d9) @@ -106,7 +106,7 @@ request.setAttribute("tools", getLearningDesignService().getToolDTOs(false, false, request.getRemoteUser())); } - + //set whether organisation is favorite boolean isFavorite = service.isOrganisationFavorite(orgId, user.getUserId()); iob.setFavorite(isFavorite); @@ -158,8 +158,6 @@ String name = org.getEnableSingleActivityLessons() ? "index.addlesson.single" : "index.addlesson"; links.add(new IndexLinkBean(name, "javascript:showAddLessonDialog(" + org.getOrganisationId() + ")", "fa fa-fw fa-plus", null)); - links.add(new IndexLinkBean("index.kumalive", "javascript:showKumaliveDialog(" + org.getOrganisationId() + ")", - "fa fa-fw fa-bolt", null)); } moreLinks.add(new IndexLinkBean("index.searchlesson", "javascript:showSearchLessonDialog(" + org.getOrganisationId() + ")", "fa fa-fw fa-search", @@ -172,7 +170,7 @@ "javascript:showNotificationsDialog(" + org.getOrganisationId() + ",null)", "fa fa-fw fa-bullhorn", "index.emailnotifications.tooltip")); } - + // Adding lesson sorting link if (roles.contains(Role.ROLE_GROUP_MANAGER) || roles.contains(Role.ROLE_MONITOR)) { @@ -206,10 +204,17 @@ // Adding gradebook course monitor links if enabled if (roles.contains(Role.ROLE_GROUP_MANAGER) || roles.contains(Role.ROLE_GROUP_ADMIN)) { String link = "javascript:showGradebookCourseDialog(" + org.getOrganisationId() + ")"; - moreLinks.add(new IndexLinkBean("index.coursegradebook.subgroup", link, "fa fa-fw fa-list-ol", null)); + moreLinks.add( + new IndexLinkBean("index.coursegradebook.subgroup", link, "fa fa-fw fa-list-ol", null)); } } } + + links.add(new IndexLinkBean( + roles.contains(Role.ROLE_GROUP_MANAGER) || roles.contains(Role.ROLE_MONITOR) ? "index.kumalive.teacher" + : "index.kumalive", + "javascript:showKumaliveDialog(" + org.getOrganisationId() + ")", "fa fa-fw fa-bolt", "index.kumalive.tooltip")); + orgBean.setLinks(links); orgBean.setMoreLinks(moreLinks); @@ -289,12 +294,6 @@ } } - // getting the organisation - Organisation org = (Organisation) DisplayGroupAction.service.findById(Organisation.class, orgId); - - // Getting the parent organisation if applicable - Organisation parent = org.getParentOrganisation(); - // iterate through user's lessons where they are staff (or simply through all lessons in case of Group_Manager), // and add staff links to the beans in the map. Integer userRole = (roles.contains(Role.ROLE_GROUP_MANAGER)) ? Role.ROLE_GROUP_MANAGER : Role.ROLE_MONITOR; @@ -332,8 +331,8 @@ // Add delete lesson option if (isGroupManagerOrMonitor) { String removeLessonLink = "javascript:removeLesson(" + bean.getId() + ")"; - lessonLinks.addFirst(new IndexLinkBean("index.remove.lesson", removeLessonLink, - "fa fa-fw fa-trash-o", "index.remove.lesson.tooltip")); + lessonLinks.addFirst(new IndexLinkBean("index.remove.lesson", removeLessonLink, "fa fa-fw fa-trash-o", + "index.remove.lesson.tooltip")); } if (lessonLinks.size() > 0) { Index: lams_central/web/groupHeader.jsp =================================================================== diff -u -r906f0b619098cd3c9d542033e716c117533d6e78 -r2584207dba753ac8e61c06022694b202597775d9 --- lams_central/web/groupHeader.jsp (.../groupHeader.jsp) (revision 906f0b619098cd3c9d542033e716c117533d6e78) +++ lams_central/web/groupHeader.jsp (.../groupHeader.jsp) (revision 2584207dba753ac8e61c06022694b202597775d9) @@ -30,7 +30,10 @@ - "> + + "> + + @@ -47,13 +50,34 @@ + + + "> + + + + + + + + + + + + + + + + + title="" > - "> + "> + Index: lams_central/web/includes/javascript/main.js =================================================================== diff -u -r34bc1c178bd5ada01543d5b4637487322d3ff565 -r2584207dba753ac8e61c06022694b202597775d9 --- lams_central/web/includes/javascript/main.js (.../main.js) (revision 34bc1c178bd5ada01543d5b4637487322d3ff565) +++ lams_central/web/includes/javascript/main.js (.../main.js) (revision 2584207dba753ac8e61c06022694b202597775d9) @@ -534,7 +534,7 @@ }, true); } -function showKumaliveDialog(orgID) { +function showKumaliveDialog(orgID, role) { showDialog("dialogKumalive", { 'data' : { 'orgID' : orgID @@ -547,7 +547,7 @@ // load contents after opening the dialog $('iframe', dialog) .attr('src', LAMS_URL - + '/learning/kumalive.jsp?organisationID=' + dialog.data('orgID')); + + '/learning/kumalive.jsp?organisationID=' + dialog.data('orgID') + '&role=' + role); } }); } Index: lams_learning/conf/language/lams/ApplicationResources.properties =================================================================== diff -u -r34bc1c178bd5ada01543d5b4637487322d3ff565 -r2584207dba753ac8e61c06022694b202597775d9 --- lams_learning/conf/language/lams/ApplicationResources.properties (.../ApplicationResources.properties) (revision 34bc1c178bd5ada01543d5b4637487322d3ff565) +++ lams_learning/conf/language/lams/ApplicationResources.properties (.../ApplicationResources.properties) (revision 2584207dba753ac8e61c06022694b202597775d9) @@ -108,5 +108,26 @@ label.group.confirm.header =Confirm group selection label.group.confirm.button =Confirm label.group.confirm.areyoujoining =Are you joining - +label.kumalive.title =Kumalive: +label.kumalive.wait.start =Wait for a teacher to start Kumalive +label.kumalive.name.enter =Enter a name for a new Kumalive +button.kumalive.create =Create +button.kumalive.finish.kumalive =Finish Kumalive +button.kumalive.ask =Ask a question +button.kumalive.finish.question =Finish question +button.kumalive.raise =Raise hand +button.kumalive.putdown =Put hand down +label.kumalive.raised.hands =Raised hands +label.kumalive.learners =Learners +label.kumalive.teacher =Teacher +label.kumalive.finish.speak =Finish speaking +label.kumalive.mark =Mark learner: +label.kumalive.mark.great =Great! +label.kumalive.mark.ok =Just OK +label.kumalive.mark.bad =Not so good +label.kumalive.mark.cancel =Do not mark +message.kumalive.finish.kumalive =Kumalive is finished. The window will close. +message.kumalive.finish.kumalive.confirm =Are you sure you want to finish this Kumalive? +message.kumalive.speak.not.raised.hand =Do you want to make a speaker a learner who did not raise hand? +label.kumalive.closed =You have got no connection with Kumalive. Try closing this window and opening it again. #======= End labels: Exported 102 labels for en AU ===== Index: lams_learning/src/java/org/lamsfoundation/lams/learning/kumalive/KumaliveWebsocketServer.java =================================================================== diff -u -r34bc1c178bd5ada01543d5b4637487322d3ff565 -r2584207dba753ac8e61c06022694b202597775d9 --- lams_learning/src/java/org/lamsfoundation/lams/learning/kumalive/KumaliveWebsocketServer.java (.../KumaliveWebsocketServer.java) (revision 34bc1c178bd5ada01543d5b4637487322d3ff565) +++ lams_learning/src/java/org/lamsfoundation/lams/learning/kumalive/KumaliveWebsocketServer.java (.../KumaliveWebsocketServer.java) (revision 2584207dba753ac8e61c06022694b202597775d9) @@ -1,6 +1,8 @@ package org.lamsfoundation.lams.learning.kumalive; import java.io.IOException; +import java.util.LinkedList; +import java.util.List; import java.util.Map; import java.util.TreeMap; import java.util.concurrent.ConcurrentHashMap; @@ -10,6 +12,7 @@ import javax.websocket.OnClose; import javax.websocket.OnMessage; import javax.websocket.OnOpen; +import javax.websocket.RemoteEndpoint.Basic; import javax.websocket.Session; import javax.websocket.server.ServerEndpoint; @@ -18,6 +21,7 @@ import org.apache.tomcat.util.json.JSONArray; import org.apache.tomcat.util.json.JSONException; import org.apache.tomcat.util.json.JSONObject; +import org.lamsfoundation.lams.learning.kumalive.model.Kumalive; import org.lamsfoundation.lams.learning.service.ILearnerService; import org.lamsfoundation.lams.security.ISecurityService; import org.lamsfoundation.lams.usermanagement.Role; @@ -30,42 +34,49 @@ import org.springframework.web.context.support.WebApplicationContextUtils; /** + * Processes messages for Kumalive + * * @author Marcin Cieslak */ @ServerEndpoint("/kumaliveWebsocket") public class KumaliveWebsocketServer { - private class KumaliveLearner { + private class KumaliveUser { private UserDTO userDTO; private Session websocket; + private boolean isTeacher; + private boolean roleTeacher; - private KumaliveLearner(User user, Session websocket) { + private KumaliveUser(User user, Session websocket, boolean isTeacher, boolean roleTeacher) { this.userDTO = user.getUserDTO(); this.websocket = websocket; + this.isTeacher = isTeacher; + this.roleTeacher = roleTeacher; } } private class KumaliveDTO { private Long id; private String name; private UserDTO createdBy; - private final Map learners = new ConcurrentHashMap<>(); + private boolean raiseHandPrompt; + private List raisedHand; + private Integer speaker; + private final Map learners = new ConcurrentHashMap<>(); - private KumaliveDTO(Long id, String name, UserDTO createdBy) { - this.id = id; - this.name = name; - this.createdBy = createdBy; + private KumaliveDTO(Kumalive kumalive) { + this.id = kumalive.getKumaliveId(); + this.name = kumalive.getName(); + this.createdBy = kumalive.getCreatedBy().getUserDTO(); } } private static Logger log = Logger.getLogger(KumaliveWebsocketServer.class); private static ILearnerService learnerService; - private static ISecurityService securityService; - private static IUserManagementService userManagementService; - + // mapping org ID -> Kumalive private static final Map kumalives = new TreeMap<>(); @OnOpen @@ -74,18 +85,16 @@ .valueOf(websocket.getRequestParameterMap().get(AttributeNames.PARAM_ORGANISATION_ID).get(0)); Integer userId = getUser(websocket).getUserId(); if (!getSecurityService().hasOrgRole(organisationId, userId, - new String[] { Role.GROUP_MANAGER, Role.MONITOR, Role.LEARNER }, "join kumalive", false)) { + new String[] { Role.GROUP_MANAGER, Role.MONITOR, Role.LEARNER }, "register on kumalive", false)) { + // prevent unauthorised user from accessing Kumalive String warning = "User " + userId + " is not a monitor nor a learner of organisation " + organisationId; log.warn(warning); websocket.close(new CloseReason(CloseCodes.CANNOT_ACCEPT, warning)); } } - /** - * Removes Learner websocket from the collection. - */ @OnClose - public void unregisterUser(Session websocket, CloseReason reason) { + public void unregisterUser(Session websocket, CloseReason reason) throws JSONException, IOException { String login = websocket.getUserPrincipal().getName(); if (login == null) { return; @@ -97,8 +106,9 @@ if (kumalive == null) { return; } - kumalive.learners.remove(login); + + sendRefresh(kumalive); } @OnMessage @@ -114,27 +124,71 @@ JSONObject requestJSON = new JSONObject(input); switch (requestJSON.getString("type")) { case "start": - startKumalive(requestJSON, session); + start(requestJSON, session); break; case "join": - joinKumalive(requestJSON, session); + join(requestJSON, session); break; + case "raiseHandPrompt": + raiseHandPrompt(requestJSON, session); + break; + case "downHandPrompt": + downHandPrompt(requestJSON, session); + break; + case "raiseHand": + raiseHand(requestJSON, session); + break; + case "downHand": + downHand(requestJSON, session); + break; + case "speak": + speak(requestJSON, session); + break; + case "score": + score(requestJSON, session); + break; + case "finish": + finish(requestJSON, session); + break; } } - private void startKumalive(JSONObject requestJSON, Session websocket) throws JSONException, IOException { + /** + * Fetches an existing Kumalive, creates it or tells teacher to create it + */ + private void start(JSONObject requestJSON, Session websocket) throws JSONException, IOException { Integer organisationId = Integer .valueOf(websocket.getRequestParameterMap().get(AttributeNames.PARAM_ORGANISATION_ID).get(0)); - String name = requestJSON.getString("name"); - User user = getUser(websocket); - Long kumaliveId = KumaliveWebsocketServer.getLearnerService().startKumalive(organisationId, user.getUserId(), - name); - kumalives.put(organisationId, new KumaliveDTO(kumaliveId, name, user.getUserDTO())); + KumaliveDTO kumaliveDTO = kumalives.get(organisationId); + boolean isTeacher = false; + if (kumaliveDTO == null) { + String name = requestJSON.optString("name"); + String role = websocket.getRequestParameterMap().get(AttributeNames.PARAM_ROLE).get(0); + User user = getUser(websocket); + Integer userId = user.getUserId(); + isTeacher = !Role.LEARNER.equalsIgnoreCase(role) + && (getUserManagementService().isUserInRole(userId, organisationId, Role.GROUP_MANAGER) + || getUserManagementService().isUserInRole(userId, organisationId, Role.MONITOR)); + // if it kumalive does not exists and the user is not a teacher or he did not provide a name yet, + // kumalive will not get created + Kumalive kumalive = KumaliveWebsocketServer.getLearnerService().startKumalive(organisationId, userId, name, + isTeacher && StringUtils.isNotBlank(name)); + if (kumalive != null) { + kumaliveDTO = new KumaliveDTO(kumalive); + kumalives.put(organisationId, kumaliveDTO); + } + } - websocket.getBasicRemote().sendText("{ \"type\" : \"join\" }"); + // tell teacher to provide a name for Kumalive and create it + // or tell learner to join + websocket.getBasicRemote() + .sendText("{ \"type\" : \"" + (kumaliveDTO == null && isTeacher ? "create" : "join") + "\" }"); } - private void joinKumalive(JSONObject requestJSON, Session websocket) throws JSONException, IOException { + /** + * Adds a learner or a teacher to Kumalive + */ + private void join(JSONObject requestJSON, Session websocket) throws JSONException, IOException { Integer organisationId = Integer .valueOf(websocket.getRequestParameterMap().get(AttributeNames.PARAM_ORGANISATION_ID).get(0)); KumaliveDTO kumalive = kumalives.get(organisationId); @@ -145,50 +199,262 @@ User user = getUser(websocket); Integer userId = user.getUserId(); - boolean isMonitor = getUserManagementService().isUserInRole(userId, organisationId, Role.GROUP_MANAGER) + String login = user.getLogin(); + boolean isTeacher = getUserManagementService().isUserInRole(userId, organisationId, Role.GROUP_MANAGER) || getUserManagementService().isUserInRole(userId, organisationId, Role.MONITOR); + String role = websocket.getRequestParameterMap().get(AttributeNames.PARAM_ROLE).get(0); - Map learners = kumalive.learners; - String login = user.getLogin(); - if (learners.containsKey(login)) { - KumaliveLearner learner = learners.get(login); + KumaliveUser learner = kumalive.learners.get(login); + boolean roleTeacher = isTeacher && !Role.LEARNER.equalsIgnoreCase(role) + && ("teacher".equalsIgnoreCase(role) || learner == null || learner.roleTeacher); + if (learner != null && !learner.websocket.getId().equals(websocket.getId())) { + // only one websocket per user learner.websocket.close( new CloseReason(CloseCodes.NOT_CONSISTENT, "Another websocket for same user was estabilished")); } - KumaliveLearner learner = new KumaliveLearner(user, websocket); - learners.put(login, learner); + learner = new KumaliveUser(user, websocket, isTeacher, roleTeacher); + kumalive.learners.put(login, learner); + + sendRefresh(kumalive); + } + + /** + * Send full Kumalive state to all learners and teachers + */ + private void sendRefresh(KumaliveDTO kumalive) throws JSONException, IOException { JSONObject responseJSON = new JSONObject(); responseJSON.put("type", "refresh"); + // Kumalive title responseJSON.put("name", kumalive.name); + + // teacher details responseJSON.put("teacherId", kumalive.createdBy.getUserID()); responseJSON.put("teacherName", kumalive.createdBy.getFirstName() + " " + kumalive.createdBy.getLastName()); responseJSON.put("teacherPortraitUuid", kumalive.createdBy.getPortraitUuid()); - - if (isMonitor) { - responseJSON.put("isTeacher", true); + + // current state of question and speaker + responseJSON.put("raiseHandPrompt", kumalive.raiseHandPrompt); + if (kumalive.raisedHand != null) { + responseJSON.put("raisedHand", new JSONArray(kumalive.raisedHand)); } - + responseJSON.put("speaker", kumalive.speaker); + + // each learner's details JSONArray learnersJSON = new JSONArray(); - for (KumaliveLearner participant : learners.values()) { - JSONObject learnerJSON = new JSONObject(); + JSONObject logins = new JSONObject(); + for (KumaliveUser participant : kumalive.learners.values()) { UserDTO participantDTO = participant.userDTO; + + JSONObject learnerJSON = new JSONObject(); learnerJSON.put("id", participantDTO.getUserID()); learnerJSON.put("firstName", participantDTO.getFirstName()); learnerJSON.put("lastName", participantDTO.getLastName()); learnerJSON.put("portraitUuid", participantDTO.getPortraitUuid()); - if (isMonitor) { - learnerJSON.put("login", participantDTO.getLogin()); - } + learnerJSON.put("roleTeacher", participant.roleTeacher); + learnersJSON.put(learnerJSON); } responseJSON.put("learners", learnersJSON); - for (KumaliveLearner participant : learners.values()) { - participant.websocket.getBasicRemote().sendText(responseJSON.toString()); + String learnerResponse = responseJSON.toString(); + JSONObject teacherResponseJSON = null; + + // send refresh to everyone + for (KumaliveUser participant : kumalive.learners.values()) { + Basic channel = participant.websocket.getBasicRemote(); + if (participant.isTeacher) { + // send extra information to teachers + if (teacherResponseJSON == null) { + responseJSON.put("isTeacher", true); + responseJSON.put("logins", logins); + teacherResponseJSON = responseJSON; + } + responseJSON.put("roleTeacher", participant.roleTeacher); + channel.sendText(teacherResponseJSON.toString()); + } else { + channel.sendText(learnerResponse); + } } } + /** + * Tell learners that the teacher asked + */ + private void raiseHandPrompt(JSONObject requestJSON, Session websocket) throws IOException, JSONException { + Integer organisationId = Integer + .valueOf(websocket.getRequestParameterMap().get(AttributeNames.PARAM_ORGANISATION_ID).get(0)); + KumaliveDTO kumalive = kumalives.get(organisationId); + + User user = getUser(websocket); + Integer userId = user.getUserId(); + + if (!getSecurityService().hasOrgRole(organisationId, userId, new String[] { Role.GROUP_MANAGER, Role.MONITOR }, + "kumalive raise hand prompt", false)) { + String warning = "User " + userId + " is not a monitor of organisation " + organisationId; + log.warn(warning); + return; + } + + kumalive.raiseHandPrompt = true; + sendRefresh(kumalive); + } + + /** + * Tell learners that the teacher finished a question + */ + private void downHandPrompt(JSONObject requestJSON, Session websocket) throws IOException, JSONException { + Integer organisationId = Integer + .valueOf(websocket.getRequestParameterMap().get(AttributeNames.PARAM_ORGANISATION_ID).get(0)); + KumaliveDTO kumalive = kumalives.get(organisationId); + + User user = getUser(websocket); + Integer userId = user.getUserId(); + + if (!getSecurityService().hasOrgRole(organisationId, userId, new String[] { Role.GROUP_MANAGER, Role.MONITOR }, + "kumalive down hand prompt", false)) { + String warning = "User " + userId + " is not a monitor of organisation " + organisationId; + log.warn(warning); + return; + } + + kumalive.raiseHandPrompt = false; + kumalive.raisedHand = null; + sendRefresh(kumalive); + } + + /** + * Tell learners that a learner raised hand + */ + private void raiseHand(JSONObject requestJSON, Session websocket) throws IOException, JSONException { + Integer organisationId = Integer + .valueOf(websocket.getRequestParameterMap().get(AttributeNames.PARAM_ORGANISATION_ID).get(0)); + KumaliveDTO kumalive = kumalives.get(organisationId); + + User user = getUser(websocket); + Integer userId = user.getUserId(); + + if (!getSecurityService().hasOrgRole(organisationId, userId, + new String[] { Role.GROUP_MANAGER, Role.MONITOR, Role.LEARNER }, "kumalive raise hand", false)) { + String warning = "User " + userId + " is not a monitor nor a learner of organisation " + organisationId; + log.warn(warning); + return; + } + + if (!kumalive.raiseHandPrompt) { + log.warn("Raise hand prompt was not sent by teacher yet for organisation " + organisationId); + return; + } + + if (kumalive.raisedHand == null) { + kumalive.raisedHand = new LinkedList<>(); + } else if (kumalive.raisedHand.contains(userId)) { + return; + } + + kumalive.raisedHand.add(userId); + sendRefresh(kumalive); + } + + /** + * Tell learners that a learner put hadn down + */ + private void downHand(JSONObject requestJSON, Session websocket) throws IOException, JSONException { + Integer organisationId = Integer + .valueOf(websocket.getRequestParameterMap().get(AttributeNames.PARAM_ORGANISATION_ID).get(0)); + KumaliveDTO kumalive = kumalives.get(organisationId); + + User user = getUser(websocket); + Integer userId = user.getUserId(); + + if (!getSecurityService().hasOrgRole(organisationId, userId, + new String[] { Role.GROUP_MANAGER, Role.MONITOR, Role.LEARNER }, "kumalive down hand", false)) { + String warning = "User " + userId + " is not a monitor nor a learner of organisation " + organisationId; + log.warn(warning); + return; + } + + if (kumalive.raisedHand == null) { + return; + } + + kumalive.raisedHand.remove(userId); + if (kumalive.raisedHand.isEmpty()) { + kumalive.raisedHand = null; + } + sendRefresh(kumalive); + } + + /** + * Set up a speaker or remove him + */ + private void speak(JSONObject requestJSON, Session websocket) throws IOException, JSONException { + Integer organisationId = Integer + .valueOf(websocket.getRequestParameterMap().get(AttributeNames.PARAM_ORGANISATION_ID).get(0)); + KumaliveDTO kumalive = kumalives.get(organisationId); + + User user = getUser(websocket); + Integer userId = user.getUserId(); + + if (!getSecurityService().hasOrgRole(organisationId, userId, new String[] { Role.GROUP_MANAGER, Role.MONITOR }, + "kumalive speak", false)) { + String warning = "User " + userId + " is not a monitor of organisation " + organisationId; + log.warn(warning); + return; + } + + kumalive.speaker = requestJSON.optInt("speaker"); + sendRefresh(kumalive); + } + + /** + * Save score for a learner + */ + private void score(JSONObject requestJSON, Session websocket) throws IOException, JSONException { + Integer organisationId = Integer + .valueOf(websocket.getRequestParameterMap().get(AttributeNames.PARAM_ORGANISATION_ID).get(0)); + KumaliveDTO kumalive = kumalives.get(organisationId); + + User user = getUser(websocket); + Integer userId = user.getUserId(); + + if (!getSecurityService().hasOrgRole(organisationId, userId, new String[] { Role.GROUP_MANAGER, Role.MONITOR }, + "kumalive score", false)) { + String warning = "User " + userId + " is not a monitor of organisation " + organisationId; + log.warn(warning); + return; + } + + KumaliveWebsocketServer.getLearnerService().scoreKumalive(kumalive.id, + requestJSON.getInt(AttributeNames.PARAM_USER_ID), Short.valueOf(requestJSON.getString("score"))); + sendRefresh(kumalive); + } + + /** + * End Kumalive + */ + private void finish(JSONObject requestJSON, Session websocket) throws IOException, JSONException { + Integer organisationId = Integer + .valueOf(websocket.getRequestParameterMap().get(AttributeNames.PARAM_ORGANISATION_ID).get(0)); + KumaliveDTO kumalive = kumalives.get(organisationId); + + User user = getUser(websocket); + Integer userId = user.getUserId(); + + if (!getSecurityService().hasOrgRole(organisationId, userId, new String[] { Role.GROUP_MANAGER, Role.MONITOR }, + "kumalive finish", false)) { + String warning = "User " + userId + " is not a monitor of organisation " + organisationId; + log.warn(warning); + return; + } + + KumaliveWebsocketServer.getLearnerService().finishKumalive(kumalive.id); + kumalives.remove(organisationId); + for (KumaliveUser participant : kumalive.learners.values()) { + participant.websocket.getBasicRemote().sendText("{ \"type\" : \"finish\"}"); + } + } + private User getUser(Session websocket) { return getUserManagementService().getUserByLogin(websocket.getUserPrincipal().getName()); } Index: lams_learning/src/java/org/lamsfoundation/lams/learning/kumalive/dao/IKumaliveDAO.java =================================================================== diff -u -refe322b3756d88ff1fb7d149a3f4e267065eca40 -r2584207dba753ac8e61c06022694b202597775d9 --- lams_learning/src/java/org/lamsfoundation/lams/learning/kumalive/dao/IKumaliveDAO.java (.../IKumaliveDAO.java) (revision efe322b3756d88ff1fb7d149a3f4e267065eca40) +++ lams_learning/src/java/org/lamsfoundation/lams/learning/kumalive/dao/IKumaliveDAO.java (.../IKumaliveDAO.java) (revision 2584207dba753ac8e61c06022694b202597775d9) @@ -24,6 +24,8 @@ package org.lamsfoundation.lams.learning.kumalive.dao; import org.lamsfoundation.lams.dao.IBaseDAO; +import org.lamsfoundation.lams.learning.kumalive.model.Kumalive; public interface IKumaliveDAO extends IBaseDAO { + Kumalive findByOrganisationId(Integer organisationId); } \ No newline at end of file Index: lams_learning/src/java/org/lamsfoundation/lams/learning/kumalive/dao/hibernate/KumaliveDAO.java =================================================================== diff -u -refe322b3756d88ff1fb7d149a3f4e267065eca40 -r2584207dba753ac8e61c06022694b202597775d9 --- lams_learning/src/java/org/lamsfoundation/lams/learning/kumalive/dao/hibernate/KumaliveDAO.java (.../KumaliveDAO.java) (revision efe322b3756d88ff1fb7d149a3f4e267065eca40) +++ lams_learning/src/java/org/lamsfoundation/lams/learning/kumalive/dao/hibernate/KumaliveDAO.java (.../KumaliveDAO.java) (revision 2584207dba753ac8e61c06022694b202597775d9) @@ -23,10 +23,22 @@ package org.lamsfoundation.lams.learning.kumalive.dao.hibernate; +import java.util.List; + import org.lamsfoundation.lams.dao.hibernate.LAMSBaseDAO; import org.lamsfoundation.lams.learning.kumalive.dao.IKumaliveDAO; +import org.lamsfoundation.lams.learning.kumalive.model.Kumalive; import org.springframework.stereotype.Repository; @Repository public class KumaliveDAO extends LAMSBaseDAO implements IKumaliveDAO { + private static final String FIND_BY_ORGANISATION = "FROM " + Kumalive.class.getName() + + " AS k WHERE k.organisation.organisationId = ? AND k.finished = 0"; + + @Override + @SuppressWarnings("unchecked") + public Kumalive findByOrganisationId(Integer organisationId) { + List result = (List) doFind(FIND_BY_ORGANISATION, organisationId); + return result.isEmpty() ? null : result.get(0); + } } \ No newline at end of file Index: lams_learning/src/java/org/lamsfoundation/lams/learning/kumalive/model/Kumalive.java =================================================================== diff -u -r34bc1c178bd5ada01543d5b4637487322d3ff565 -r2584207dba753ac8e61c06022694b202597775d9 --- lams_learning/src/java/org/lamsfoundation/lams/learning/kumalive/model/Kumalive.java (.../Kumalive.java) (revision 34bc1c178bd5ada01543d5b4637487322d3ff565) +++ lams_learning/src/java/org/lamsfoundation/lams/learning/kumalive/model/Kumalive.java (.../Kumalive.java) (revision 2584207dba753ac8e61c06022694b202597775d9) @@ -38,6 +38,9 @@ private String name; private Boolean finished = false; private Map scores; + + public Kumalive(){ + } public Kumalive(Organisation organisation, User createdBy, String name) { this.organisation = organisation; Index: lams_learning/src/java/org/lamsfoundation/lams/learning/learningApplicationContext.xml =================================================================== diff -u -r34bc1c178bd5ada01543d5b4637487322d3ff565 -r2584207dba753ac8e61c06022694b202597775d9 --- lams_learning/src/java/org/lamsfoundation/lams/learning/learningApplicationContext.xml (.../learningApplicationContext.xml) (revision 34bc1c178bd5ada01543d5b4637487322d3ff565) +++ lams_learning/src/java/org/lamsfoundation/lams/learning/learningApplicationContext.xml (.../learningApplicationContext.xml) (revision 2584207dba753ac8e61c06022694b202597775d9) @@ -76,6 +76,8 @@ PROPAGATION_REQUIRED PROPAGATION_REQUIRED,readOnly PROPAGATION_REQUIRES_NEW + PROPAGATION_REQUIRED + PROPAGATION_REQUIRED Index: lams_learning/src/java/org/lamsfoundation/lams/learning/service/ILearnerService.java =================================================================== diff -u -r34bc1c178bd5ada01543d5b4637487322d3ff565 -r2584207dba753ac8e61c06022694b202597775d9 --- lams_learning/src/java/org/lamsfoundation/lams/learning/service/ILearnerService.java (.../ILearnerService.java) (revision 34bc1c178bd5ada01543d5b4637487322d3ff565) +++ lams_learning/src/java/org/lamsfoundation/lams/learning/service/ILearnerService.java (.../ILearnerService.java) (revision 2584207dba753ac8e61c06022694b202597775d9) @@ -27,6 +27,7 @@ import java.util.List; import org.lamsfoundation.lams.learning.command.model.Command; +import org.lamsfoundation.lams.learning.kumalive.model.Kumalive; import org.lamsfoundation.lams.learning.web.bean.ActivityPositionDTO; import org.lamsfoundation.lams.tool.ToolOutput; @@ -61,5 +62,9 @@ List getCommandsForLesson(Long lessonId, Date laterThan); - Long startKumalive(Integer organisationId, Integer userId, String name); + Kumalive startKumalive(Integer organisationId, Integer userId, String name, boolean isTeacher); + + void finishKumalive(Long id); + + void scoreKumalive(Long id, Integer userId, Short score); } Index: lams_learning/src/java/org/lamsfoundation/lams/learning/service/LearnerService.java =================================================================== diff -u -r34bc1c178bd5ada01543d5b4637487322d3ff565 -r2584207dba753ac8e61c06022694b202597775d9 --- lams_learning/src/java/org/lamsfoundation/lams/learning/service/LearnerService.java (.../LearnerService.java) (revision 34bc1c178bd5ada01543d5b4637487322d3ff565) +++ lams_learning/src/java/org/lamsfoundation/lams/learning/service/LearnerService.java (.../LearnerService.java) (revision 2584207dba753ac8e61c06022694b202597775d9) @@ -93,7 +93,6 @@ import org.lamsfoundation.lams.tool.exception.ToolException; import org.lamsfoundation.lams.tool.service.ILamsCoreToolService; import org.lamsfoundation.lams.usermanagement.Organisation; -import org.lamsfoundation.lams.usermanagement.Role; import org.lamsfoundation.lams.usermanagement.User; import org.lamsfoundation.lams.usermanagement.service.IUserManagementService; @@ -1411,16 +1410,51 @@ return toolSession == null ? null : getActivityPosition(toolSession.getToolActivity().getActivityId()); } + /** + * Fetches or creates a Kumalive + */ @Override - public Long startKumalive(Integer organisationId, Integer userId, String name) { - securityService.isGroupMonitor(organisationId, userId, "start kumalive", true); + public Kumalive startKumalive(Integer organisationId, Integer userId, String name, boolean isTeacher) { + if (isTeacher) { + securityService.isGroupMonitor(organisationId, userId, "start kumalive", true); + } + Kumalive kumalive = kumaliveDAO.findByOrganisationId(organisationId); + if (kumalive == null) { + if (!isTeacher) { + return null; + } + } else { + return kumalive; + } + Organisation organisation = (Organisation) kumaliveDAO.find(Organisation.class, organisationId); User createdBy = (User) kumaliveDAO.find(User.class, userId); - Kumalive kumalive = new Kumalive(organisation, createdBy, name); + kumalive = new Kumalive(organisation, createdBy, name); kumaliveDAO.insert(kumalive); - return kumalive.getKumaliveId(); + return kumalive; } + /** + * Ends Kumalive + */ + @Override + public void finishKumalive(Long id) { + Kumalive kumalive = (Kumalive) kumaliveDAO.find(Kumalive.class, id); + kumalive.setFinished(true); + kumaliveDAO.update(kumalive); + } + + /** + * Save Kumalive score + */ + @Override + public void scoreKumalive(Long id, Integer userId, Short score) { + Kumalive kumalive = (Kumalive) kumaliveDAO.find(Kumalive.class, id); + User user = (User) kumaliveDAO.find(User.class, userId); + kumalive.getScores().put(user, score); + kumaliveDAO.update(kumalive); + } + private boolean isActivityLast(Activity activity) { Transition transition = activity.getTransitionFrom(); while (transition != null) { Index: lams_learning/web/css/kumalive.css =================================================================== diff -u -r34bc1c178bd5ada01543d5b4637487322d3ff565 -r2584207dba753ac8e61c06022694b202597775d9 --- lams_learning/web/css/kumalive.css (.../kumalive.css) (revision 34bc1c178bd5ada01543d5b4637487322d3ff565) +++ lams_learning/web/css/kumalive.css (.../kumalive.css) (revision 2584207dba753ac8e61c06022694b202597775d9) @@ -1,20 +1,54 @@ -#raiseHandContainer { +#initDiv { + padding-top: 35vh; + text-align: center; +} + +#createKumaliveDiv, #closedDiv { display: none; - margin-bottom : 10px; - border-bottom: thin black solid; + padding-top: 35vh; + text-align: center; } +#closedDiv { + font-weight: bold; +} + +#createKumaliveDiv input { + width: 60%; +} + table { + display: none; table-layout: fixed; } +#learnersCell { + height: 100vh; + width: 100%; + vertical-align: top; +} + #actionCell { - width: 400px; + height: 100%; border-left: thin black solid; vertical-align: top; } +#actionCell > * { + width: 350px; +} +#actionCell button { + width: 70%; + margin-top: 10px; +} + +#raiseHandContainer { + display: none; + margin-bottom : 10px; + border-bottom: thin black solid; +} + .learner { width: 80px; height: 120px; @@ -43,18 +77,21 @@ left: 40px; width: 0; height: 0; + font-size: 0; } .profilePictureShown { top: 0; left: 0; width: 80px; height: 80px; + font-size: 6em; } .name { overflow: hidden; text-overflow: ellipsis; + padding-top: 5px; } .learner .name { @@ -63,24 +100,63 @@ .speaker { - width: 200px; height: 300px; - margin: auto; text-align: center; } .speaker .profilePicture{ width: 200px; height: 200px; + font-size: 200px; + margin: auto; } .speaker .name { font-size: 18px; } +#teacher { + display: none; +} + #raiseHandPrompt { display: none; text-align: center; - padding-top: 100px; + padding-top: 20px; +} + +#raiseHandPrompt i { + font-size: 140px; +} + +.score { + display: none; + padding-top: 20px; + padding-left: 15px; +} + +.score span { + font-weight: bold; +} + +.score i { + font-size: 75px; + margin-right: 15px; cursor: pointer; +} + +.scoreGood { + color: green; +} + +.scoreNeutral { + color: darkOrange; +} + +.scoreBad { + color: red; +} + +.scoreNone { + color: gray; } \ No newline at end of file Index: lams_learning/web/includes/javascript/kumalive.js =================================================================== diff -u -r34bc1c178bd5ada01543d5b4637487322d3ff565 -r2584207dba753ac8e61c06022694b202597775d9 --- lams_learning/web/includes/javascript/kumalive.js (.../kumalive.js) (revision 34bc1c178bd5ada01543d5b4637487322d3ff565) +++ lams_learning/web/includes/javascript/kumalive.js (.../kumalive.js) (revision 2584207dba753ac8e61c06022694b202597775d9) @@ -1,169 +1,566 @@ -$(document).ready(function(){ - $('#teacher').click(raiseHandPromptShow); -}); - -var kumaliveWebsocket = new WebSocket(LEARNING_URL.replace('http', 'ws') + 'kumaliveWebsocket?organisationID=' + orgId), +var kumaliveWebsocket = new WebSocket(LEARNING_URL.replace('http', 'ws') + + 'kumaliveWebsocket?organisationID=' + orgId + '&role=' +role), + // is the user a learenr or a teacher + roleTeacher = false, + // was the initial set up run initialised = false, - learnerDivTemplate = $('').addClass('learner') + // is a refresh already running, so next messages need to wait + refreshing = false, + // refresh message awaiting processing + queuedMessage = null, + // is there a learner speaking right now + speakerId = null, + // learners with no profile picture will get an icon with one of these colours + learnerColors = ['#001f3f', '#FF851B', '#85144b', '#111111', '#3D9970', '#0074D9', '#FF4136'], + // index of user icon colour currently used + learnerColorIndex = 1, + // template of a HTML structure of a learner + learnerDivTemplate = $('').addClass('learner changing') .append($('').addClass('profilePictureWrapper').append($('').addClass('profilePicture profilePictureHidden'))) - .append($('').addClass('name')); + .append($('').addClass('name')), + REFRESH_DELAY = 1000; +/** + * Fetches existing Kumalive session + */ kumaliveWebsocket.onopen = function(e) { kumaliveWebsocket.send(JSON.stringify({ - 'type' : 'join' + 'type' : 'start' })); }; + +/** + * Display information to an user when he gets disconnected + */ kumaliveWebsocket.onclose = function(e){ - $('body').text("Websocket closed"); + $('body > *').hide(); + $('#closedDiv').show(); }; +/** + * Process a message from server. + */ kumaliveWebsocket.onmessage = function(e){ // read JSON object var message = JSON.parse(e.data), - type = message.type, - container = $('#learnersContainer'); + type = message.type; + // check what is this message about switch(type) { case 'start' : { + // user tried to join a Kumalive which is not started yet + // try to start it, if user is a teacher + // otherwise just wait for a teacher kumaliveWebsocket.send(JSON.stringify({ 'type' : 'start', - 'name' : 'random name' + 'role' : role })); } break; + case 'create' : { + // user is a teacher and will now create a new Kumalive + + // hide splash screen + $('#initDiv').hide(); + // show name input + var createDiv = $('#createKumaliveDiv').show(), + createButton = createDiv.children('button').click(create).prop('disabled', true); + createDiv.children('input').focus().keyup(function(){ + // name can not be empty + createButton.prop('disabled', !$(this).val()); + }) + } + break; case 'join' : { + // server tell user to join Kumalive, so user obeys kumaliveWebsocket.send(JSON.stringify({ 'type' : 'join' })); } break; case 'refresh': { if (!initialised) { - $('#dialogKumaliveLabel', window.parent.document).text('Kumalive: ' + message.name); - $('#teacher .profilePicture').css('background-image', - 'url(' + LAMS_URL + 'download?preferDownload=false&uuid=' + message.teacherPortraitUuid + ')'); - $('#teacher .name').text(message.teacherName); - initialised = true; + // it is the first refresh message ever + init(message); } - for (var i = 0;i<30;i++) { - $.each(message.learners, function(index, learner){ - var learnerDiv = learnerDivTemplate.clone().data('id', learner.id).appendTo(container); - $('.profilePicture', learnerDiv).css('background-image', - 'url(' + LAMS_URL + 'download?preferDownload=false&uuid=' + learner.portraitUuid + ')'); - $('.name', learnerDiv).text(learner.firstName + ' ' + learner.lastName + (i % 2 ? 'asdfassafasfsdafd' : '')); - if (message.isTeacher) { - learnerDiv.attr('title', learner.login); - } - learnerFadeIn(learnerDiv); - if (i % 30 == 0) { - setTimeout(function(){ - learnerFadeOut(learnerDiv); - }, 4000); - } - if (i % 30 == 15) { - setTimeout(function(){ - raiseHand(learnerDiv); - }, 7000); - } - if (i % 30 > 17) { - setTimeout(function(){ - raiseHand(learnerDiv); - }, (i % 30) * 500); - } - }); + if (refreshing) { + // set current message as the next one to be processed + queuedMessage = message; + } else { + // no refresh is running, so process current message + processRefresh(message); } } break; + case 'finish' : { + // tell user that Kumalive is finished and close the dialog + window.alert(LABELS.FINISH_KUMALIVE_MESSAGE); + window.parent.closeDialog('dialogKumalive'); + } + break; } }; -function learnerFadeIn(learnerDiv) { - var nameDiv = $('.name', learnerDiv).css('color', 'green'); - learnerDiv.show(); - $('.profilePicture', learnerDiv).switchClass('profilePictureHidden', 'profilePictureShown', 1000, function(){ - nameDiv.css('color', 'initial'); - }); +/** + * Initialise basic Kumalive information when first refresh message arrives + */ +function init(message) { + initialised = true; + roleTeacher = message.isTeacher && message.roleTeacher; + + // hide all buttons and enable ones appropriate for the role + $('table button').hide(); + if (roleTeacher) { + $('#raiseHandPromptButton').click(raiseHandPrompt); + $('#downHandPromptButton').click(downHandPrompt); + $('#score i').click(score); + $('#finishButton').click(finish).show(); + } else { + $('#raiseHandButton').click(raiseHand); + $('#downHandButton').click(downHand); + } + + // set dialog name + $('#dialogKumaliveLabel', window.parent.document).text(LABELS.KUMALIVE_TITLE + ' ' + message.name); + // set teacher portrait and name + if (message.teacherPortraitUuid) { + $('#actionCell #teacher .profilePicture').css('background-image', + 'url(' + LAMS_URL + 'download?preferDownload=false&uuid=' + message.teacherPortraitUuid + ')'); + } else { + $('#actionCell #teacher .profilePicture').addClass('fa fa-user-circle-o'); + } + $('#teacher .name').text(message.teacherName); + + // show proper work screen + $('#initDiv').hide(); + $('table').show(); } -function learnerFadeOut(learnerDiv) { - var nameDiv = $('.name', learnerDiv).css('color', 'red'); +/** + * Main function for processing refresh messages + */ +function processRefresh(message) { + // block other refresh messages from running until this one is processed + refreshing = true; - $('.profilePicture', learnerDiv).switchClass('profilePictureShown', 'profilePictureHidden', 1000, function(){ - nameDiv.remove(); - learnerDiv.animate({ - 'width' : 'toggle' - }, 1000, function(){ - learnerDiv.remove(); + // if an element is now being changed and it takes a while, + // try processing the same message again after a second + var repeat = toggleRaiseHandPrompt(message); + repeat |= processParticipants(message); + repeat |= processRaisedHand(message); + repeat |= toggleSpeak(message); + + if (repeat || queuedMessage) { + setTimeout(function() { + // get the newest message + nextMessage = queuedMessage || message; + queuedMessage = null; + processRefresh(nextMessage); + }, REFRESH_DELAY); + } else { + refreshing = false; + } +} + +/** + * Show whether a question is currently asked + */ +function toggleRaiseHandPrompt(message) { + var raiseHandPrompt = $('#raiseHandPrompt'); + if (message.raiseHandPrompt) { + if (roleTeacher) { + // show button for finishing the question + $('#downHandPromptButton').show(); + } + if (!message.speaker) { + // no learner is currently speaking, so show "hand up" icon + $('#teacher').slideUp(function(){ + raiseHandPrompt.slideDown(); + }); + } + } else if (!message.speaker) { + if (roleTeacher){ + // allow teacher to ask a question + $('#raiseHandPromptButton').show(); + } + + // no question is asked at the moment + raiseHandPrompt.slideUp(function(){ + $('#teacher').slideDown(); }); + } +} + +/** + * Add/removes current learners + */ +function processParticipants(message) { + var learnersContainer = $('#learnersContainer'), + currentLearnerIds = [], + // should refresh be repeated? + result = false; + + $.each(message.learners, function(index, learner){ + if (learner.roleTeacher) { + // do not add teachers to learners container + return true; + } + currentLearnerIds.push(+learner.id); + + // check if a learner already exists + var learnerDiv = $('.learner[userId="' + learner.id + '"]', learnersContainer); + if (learnerDiv.length > 0) { + if (learnerDiv.is('.changing')) { + // maybe he exists, but is fading out? See in the next run + result = true; + } + return true; + } + + // build a new learner + learnerDiv = learnerDivTemplate.clone() + .attr('userId', learner.id) + .appendTo(learnersContainer); + var profilePicture = $('.profilePicture', learnerDiv); + // use profiel picture or a coloured icon + if (learner.portraitUuid) { + profilePicture.css('background-image', + 'url(' + LAMS_URL + 'download?preferDownload=false&uuid=' + learner.portraitUuid + ')'); + } else { + profilePicture.addClass('fa fa-user-circle-o').css('color', learnerColors[learnerColorIndex]); + learnerColorIndex = (learnerColorIndex + 1) % learnerColors.length; + } + $('.name', learnerDiv).text(learner.firstName + ' ' + learner.lastName); + + if (roleTeacher) { + // teacher can see logins and chooses who speaks + learnerDiv.attr('title', learner.login) + .css('cursor', 'pointer') + .click(speak); + } + learnerFadeIn(learnerDiv); }); + + // remove learners who left + $('.learner', learnersContainer).each(function(){ + var learnerDiv = $(this), + userId = +learnerDiv.attr('userId'); + if (currentLearnerIds.indexOf(userId) < 0) { + // remove both from learners container and "raised hand" container + learnerFadeOut(learnerDiv); + learnerFadeOut($('#raiseHandContainer .learner[userId="' + userId + '"]')); + } + }); + + return result; } -function raiseHand(learnerDiv) { +/** + * Add/remove learners who raised hand + */ +function processRaisedHand(message) { var raiseHandContainer = $('#raiseHandContainer'), - firstHand = raiseHandContainer.children('.learner').length == 0; - if (firstHand) { - raiseHandContainer.css({ - 'display' : 'none', - }) + // should refresh be repeated? + result = false, + raisedHand = false; + + // are there any learners who raised hand? + if (message.raisedHand) { + // remove learners who raised hand before and now they put it down + $('.learner', raiseHandContainer).each(function(){ + var raisedHandDiv = $(this), + learnerId = +raisedHandDiv.attr('userId'); + if (message.raisedHand.indexOf(learnerId) < 0) { + learnerFadeOut(raisedHandDiv); + } + }); + + // add learners who raised hand + $.each(message.raisedHand, function() { + // if the user has already raised hand, do nothing + var raisedHandDiv = $('.learner[userId="' + this + '"]', raiseHandContainer); + if (raisedHandDiv.length > 0) { + return true; + } + if (userId == this) { + raisedHand = true; + } + + var learnerDiv = $('#learnersContainer .learner[userId="' + this + '"]'); + if (learnerDiv.hasClass('changing')){ + result = true; + return true; + } + + // create a new raised hand learner + var targetLearnerDiv = learnerDiv.addClass('changing').clone(true).css({ + 'visibility' : 'hidden' + }).appendTo(raiseHandContainer); + + raiseHandContainer.slideDown(function(){ + // animate learner's profile picture + var targetOffset = $('.profilePicture', targetLearnerDiv).offset(), + profilePicture = $('.profilePicture', learnerDiv), + transitionCopy = profilePicture.clone() + .css({ + 'position' : 'fixed' + }) + .appendTo('body') + .offset(profilePicture.offset()) + .animate({ + 'left' : targetOffset.left, + 'top' : targetOffset.top + }, 1000, function(){ + targetLearnerDiv.css('visibility', 'visible'); + transitionCopy.remove(); + learnerDiv.removeClass('changing'); + targetLearnerDiv.removeClass('changing'); + }); + }); + }); + } else { + // hide raised hand container if no learner raised hand + raiseHandContainer.slideUp(function() { + raiseHandContainer.children('.learner').remove(); + }); } - var targetLearnerDiv = learnerDiv.clone(true).css({ - 'visibility' : 'hidden', - 'cursor' : 'pointer' - }).click(learnerSpeak) - .appendTo(raiseHandContainer); - if (firstHand) { - raiseHandContainer.slideDown(500); + // show buttons for raising/putting down hand + if (!roleTeacher) { + if (raisedHand) { + $('#raiseHandButton').hide(); + $('#downHandButton').show(); + } else { + $('#raiseHandButton').show(); + $('#downHandButton').hide(); + } } - var targetOffset = $('.profilePicture', targetLearnerDiv).offset(), - profilePicture = $('.profilePicture', learnerDiv), - transitionCopy = profilePicture.clone().appendTo('body') - .css({ - 'position' : 'fixed' - }).offset(profilePicture.offset()) - .animate({ - 'left' : targetOffset.left, - 'top' : targetOffset.top - }, 1000, function(){ - targetLearnerDiv.css('visibility', 'visible'); - transitionCopy.remove(); - }); + return result; } -function raiseHandPromptShow() { - $('#teacher').slideUp(function(){ - $('#raiseHandPrompt').slideDown(); - }); -} - -function learnerSpeak() { - var learnerDiv = $(this), - id = learnerDiv.data('id'), - speaker = $('').addClass('speaker').css({ - 'margin-top' : '20px', - 'visibility' : 'hidden' - }).appendTo('#actionCell'), - targetProfilePicture = $('.profilePicture', learnerDiv).clone().removeClass('profilePictureShown').appendTo(speaker); - $('.name', learnerDiv).clone().appendTo(speaker); +/** + * Set current learner speaker + */ +function toggleSpeak(message) { + if (message.speaker) { + speakerId = message.speaker; + + var learnerDiv = $('#raiseHandContainer .learner[userId="' + speakerId + '"]'); + if (learnerDiv.length == 0) { + learnerDiv = $('#learnersContainer .learner[userId="' + speakerId + '"]'); + } + // if current learner is in a process of raising hand, + // run the refresh again and only then set him as a speaker + if (learnerDiv.hasClass('changing')) { + return true; + } + } + + var speaker = $('#actionCell .speaker').not('#teacher'); + if (!message.speaker) { + if (speaker.length > 0) { + speaker.slideUp(function(){ + // no speaker anymore + // show scoring buttons for a teacher + speaker.remove(); + if (roleTeacher) { + if ($('#actionCell .score[userId="' + speakerId + '"]').length == 0) { + $('#score').clone(true).attr({ + 'id' : null, + 'userId' : speakerId + }).appendTo('#actionCell') + .slideDown() + .find('span') + .text($('#learnersContainer .learner[userId="' + speakerId + '"] .name').text()); + } + speakerId = null; + } else if (message.raiseHandPrompt) { + $('#raiseHandPrompt').slideDown(); + } else { + $('#teacher').slideDown(); + } + }); + } + return; + } + + if (speaker.length > 0){ + if (speaker.attr('userId') == message.speaker) { + return; + } + speaker.remove(); + } + + // prepare room for speaker + $('#teacher').slideUp(); $('#raiseHandPrompt').slideUp(function(){ - var targetOffset = targetProfilePicture.offset(); + speaker = $('').addClass('speaker') + .attr('userId', speakerId) + .css({ + 'margin-top' : '20px', + 'visibility' : 'hidden' + }) + .prependTo('#actionCell'); + + // create speaker HTML element + $('.name', learnerDiv).clone().appendTo(speaker); + $('').addClass('btn btn-default').click(stopSpeak).text(LABELS.SPEAK_FINISH).appendTo(speaker); + + var targetProfilePicture = $('.profilePicture', learnerDiv).clone() + .removeClass('profilePictureShown') + .prependTo(speaker), + targetOffset = targetProfilePicture.offset(), profilePicture = $('.profilePicture', learnerDiv), transitionCopy = profilePicture.clone().appendTo('body') .css({ 'position' : 'fixed' }).offset(profilePicture.offset()) + // animate moving speaker from learners to right panel .animate({ 'left' : targetOffset.left, 'top' : targetOffset.top, 'width' : '200px', - 'height' : '200px' + 'height' : '200px', + 'font-size' : '200px' }, 1000, function(){ speaker.css('visibility', 'visible'); transitionCopy.remove(); }); + }); +} +/** + * Animate learner arrival + */ +function learnerFadeIn(learnerDiv) { + var nameDiv = $('.name', learnerDiv).css('color', 'green'); + learnerDiv.show(); + + $('.profilePicture', learnerDiv).switchClass('profilePictureHidden', 'profilePictureShown', 1000, function(){ + nameDiv.css('color', 'initial'); + learnerDiv.removeClass('changing'); }); +} +/** + * Animate learner departure + */ +function learnerFadeOut(learnerDiv) { + if (learnerDiv.length == 0) { + return; + } + learnerDiv.addClass('changing'); + var nameDiv = $('.name', learnerDiv).css('color', 'red'); + + $('.profilePicture', learnerDiv).switchClass('profilePictureShown', 'profilePictureHidden', 1000, function(){ + nameDiv.remove(); + learnerDiv.animate({ + 'width' : 'toggle' + }, 1000, function(){ + learnerDiv.remove(); + }); + }); +} + +function raiseHandPrompt() { + kumaliveWebsocket.send(JSON.stringify({ + 'type' : 'raiseHandPrompt' + })); +} + +function downHandPrompt() { + kumaliveWebsocket.send(JSON.stringify({ + 'type' : 'downHandPrompt' + })); +} + +function raiseHand() { + kumaliveWebsocket.send(JSON.stringify({ + 'type' : 'raiseHand' + })); +} + +function downHand() { + kumaliveWebsocket.send(JSON.stringify({ + 'type' : 'downHand' + })); +} + +/** + * Set a learner as a speaker + */ +function speak() { + var speakerId = $(this).attr('userId'); + // the learner did not raise a hand; is the teacher sure to set him as a speaker? + if ($('#raiseHandContainer .learner[userId="' + speakerId + '"]').length == 0 + && !confirm(LABELS.SPEAK_CONFIRM)){ + return; + } + + kumaliveWebsocket.send(JSON.stringify({ + 'type' : 'speak', + 'speaker' : speakerId + })); +} + +function stopSpeak() { + kumaliveWebsocket.send(JSON.stringify({ + 'type' : 'speak' + })); +} + +/** + * Show scoring buttons + */ +function score(){ + var button = $(this), + container = button.parent(), + score = null; + if (button.is('.scoreGood')) { + score = 2; + } else if (button.is('.scoreNeutral')) { + score = 1; + } else if (button.is('.scoreBad')) { + score = 0; + } + + if (score !== null) { + kumaliveWebsocket.send(JSON.stringify({ + 'type' : 'score', + 'userID' : container.attr('userId'), + 'score' : score + })); + } + + container.slideUp(function(){ + container.remove(); + }); +} + + +/** + * Create a new Kumalive + */ +function create(){ + var container = $('#createKumaliveDiv').hide(), + name = $('input', container).val(); + kumaliveWebsocket.send(JSON.stringify({ + 'type' : 'start', + 'role' : role, + 'name' : name + })); +} + + +/** + * End Kumalive + */ +function finish(){ + if (confirm(LABELS.FINISH_KUMALIVE_CONFIRM)) { + kumaliveWebsocket.send(JSON.stringify({ + 'type' : 'finish' + })); + } } \ No newline at end of file Index: lams_learning/web/kumalive.jsp =================================================================== diff -u -r34bc1c178bd5ada01543d5b4637487322d3ff565 -r2584207dba753ac8e61c06022694b202597775d9 --- lams_learning/web/kumalive.jsp (.../kumalive.jsp) (revision 34bc1c178bd5ada01543d5b4637487322d3ff565) +++ lams_learning/web/kumalive.jsp (.../kumalive.jsp) (revision 2584207dba753ac8e61c06022694b202597775d9) @@ -3,7 +3,6 @@ <%@ taglib uri="tags-lams" prefix="lams"%> <%@ taglib uri="tags-fmt" prefix="fmt"%> <%@ taglib uri="tags-core" prefix="c"%> -<%@ taglib uri="tags-function" prefix="fn"%> @@ -17,40 +16,78 @@ + + + + + + + + + + + + + + - + - Raised hand + - Learners + + - Teacher + + + - Raise hand - + + + + + + + " class="scoreGood fa fa-smile-o"> + " class="scoreNeutral fa fa-meh-o"> + " class="scoreBad fa fa-frown-o"> + " class="scoreNone fa fa-times"> +