Index: lams_build/lib/lams/lams.jar =================================================================== diff -u -ree2713a23c1a795eec5f782795da09918cbdf006 -r6afcecd6c2b6b9d1b985b371df9947107aa437ae Binary files differ Index: lams_learning/src/java/org/lamsfoundation/lams/learning/kumalive/KumaliveAction.java =================================================================== diff -u --- lams_learning/src/java/org/lamsfoundation/lams/learning/kumalive/KumaliveAction.java (revision 0) +++ lams_learning/src/java/org/lamsfoundation/lams/learning/kumalive/KumaliveAction.java (revision 6afcecd6c2b6b9d1b985b371df9947107aa437ae) @@ -0,0 +1,332 @@ +package org.lamsfoundation.lams.learning.kumalive; + +import java.io.IOException; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; + +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; + +import org.apache.log4j.Logger; +import org.apache.struts.action.ActionForm; +import org.apache.struts.action.ActionForward; +import org.apache.struts.action.ActionMapping; +import org.lamsfoundation.lams.gradebook.util.GradebookConstants; +import org.lamsfoundation.lams.learning.kumalive.model.Kumalive; +import org.lamsfoundation.lams.learning.kumalive.model.KumaliveRubric; +import org.lamsfoundation.lams.learning.kumalive.service.IKumaliveService; +import org.lamsfoundation.lams.security.ISecurityService; +import org.lamsfoundation.lams.usermanagement.Organisation; +import org.lamsfoundation.lams.usermanagement.Role; +import org.lamsfoundation.lams.usermanagement.dto.UserDTO; +import org.lamsfoundation.lams.util.Configuration; +import org.lamsfoundation.lams.util.ConfigurationKeys; +import org.lamsfoundation.lams.util.ExcelCell; +import org.lamsfoundation.lams.util.ExcelUtil; +import org.lamsfoundation.lams.util.FileUtil; +import org.lamsfoundation.lams.util.JsonUtil; +import org.lamsfoundation.lams.util.WebUtil; +import org.lamsfoundation.lams.web.action.LamsDispatchAction; +import org.lamsfoundation.lams.web.session.SessionManager; +import org.lamsfoundation.lams.web.util.AttributeNames; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.context.support.WebApplicationContextUtils; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import com.fasterxml.jackson.databind.node.ObjectNode; + +/** + * @author Marcin Cieslak + */ +public class KumaliveAction extends LamsDispatchAction { + + private static Logger log = Logger.getLogger(KumaliveAction.class); + + private static IKumaliveService kumaliveService; + private static ISecurityService securityService; + + public ActionForward getRubrics(ActionMapping mapping, ActionForm form, HttpServletRequest request, + HttpServletResponse response) throws IOException { + UserDTO userDTO = getUserDTO(); + Integer currentUserId = userDTO.getUserID(); + Integer organisationId = WebUtil.readIntParam(request, AttributeNames.PARAM_ORGANISATION_ID, false); + if (!Configuration.getAsBoolean(ConfigurationKeys.ALLOW_KUMALIVE)) { + String warning = "Kumalives are disabled"; + log.warn(warning); + response.sendError(HttpServletResponse.SC_FORBIDDEN, warning); + return null; + } + if (!KumaliveAction.getSecurityService().hasOrgRole(organisationId, currentUserId, + new String[] { Role.GROUP_MANAGER, Role.MONITOR }, "kumalive get rubrics", false)) { + String warning = "User " + currentUserId + " is not a monitor of organisation " + organisationId; + log.warn(warning); + response.sendError(HttpServletResponse.SC_FORBIDDEN, warning); + return null; + } + + List rubrics = KumaliveAction.getKumaliveService().getRubrics(organisationId); + ArrayNode rubricsJSON = JsonNodeFactory.instance.arrayNode(); + for (KumaliveRubric rubric : rubrics) { + rubricsJSON.add(rubric.getName()); + } + request.setAttribute("rubrics", rubricsJSON); + + return mapping.findForward("displayRubrics"); + } + + public ActionForward getReport(ActionMapping mapping, ActionForm form, HttpServletRequest request, + HttpServletResponse response) throws IOException { + UserDTO userDTO = getUserDTO(); + Integer currentUserId = userDTO.getUserID(); + Integer organisationId = WebUtil.readIntParam(request, AttributeNames.PARAM_ORGANISATION_ID, false); + if (!Configuration.getAsBoolean(ConfigurationKeys.ALLOW_KUMALIVE)) { + String warning = "Kumalives are disabled"; + log.warn(warning); + response.sendError(HttpServletResponse.SC_FORBIDDEN, warning); + return null; + } + if (!KumaliveAction.getSecurityService().hasOrgRole(organisationId, currentUserId, + new String[] { Role.GROUP_MANAGER, Role.MONITOR }, "kumalive get report", false)) { + String warning = "User " + currentUserId + " is not a monitor of organisation " + organisationId; + log.warn(warning); + response.sendError(HttpServletResponse.SC_FORBIDDEN, warning); + return null; + } + + return mapping.findForward("displayReport"); + } + + public ActionForward getReportOrganisationData(ActionMapping mapping, ActionForm form, HttpServletRequest request, + HttpServletResponse response) throws IOException { + UserDTO userDTO = getUserDTO(); + Integer currentUserId = userDTO.getUserID(); + Integer organisationId = WebUtil.readIntParam(request, AttributeNames.PARAM_ORGANISATION_ID, false); + if (!Configuration.getAsBoolean(ConfigurationKeys.ALLOW_KUMALIVE)) { + String warning = "Kumalives are disabled"; + log.warn(warning); + response.sendError(HttpServletResponse.SC_FORBIDDEN, warning); + return null; + } + if (!KumaliveAction.getSecurityService().hasOrgRole(organisationId, currentUserId, + new String[] { Role.GROUP_MANAGER, Role.MONITOR }, "kumalive get report organisation data", false)) { + String warning = "User " + currentUserId + " is not a monitor of organisation " + organisationId; + log.warn(warning); + response.sendError(HttpServletResponse.SC_FORBIDDEN, warning); + return null; + } + + int page = WebUtil.readIntParam(request, GradebookConstants.PARAM_PAGE); + int rowLimit = WebUtil.readIntParam(request, GradebookConstants.PARAM_ROWS); + String sortOrder = WebUtil.readStrParam(request, GradebookConstants.PARAM_SORD); + String sortColumn = WebUtil.readStrParam(request, GradebookConstants.PARAM_SIDX, true); + + ObjectNode resultJSON = KumaliveAction.getKumaliveService().getReportOrganisationData(organisationId, + sortColumn, !"DESC".equalsIgnoreCase(sortOrder), rowLimit, page); + writeResponse(response, LamsDispatchAction.CONTENT_TYPE_TEXT_XML, LamsDispatchAction.ENCODING_UTF8, + resultJSON.toString()); + return null; + } + + public ActionForward getReportKumaliveRubrics(ActionMapping mapping, ActionForm form, HttpServletRequest request, + HttpServletResponse response) throws IOException { + UserDTO userDTO = getUserDTO(); + Integer currentUserId = userDTO.getUserID(); + Long kumaliveId = WebUtil.readLongParam(request, "kumaliveId", false); + Kumalive kumalive = KumaliveAction.getKumaliveService().getKumalive(kumaliveId); + Organisation organisation = kumalive.getOrganisation(); + if (!Configuration.getAsBoolean(ConfigurationKeys.ALLOW_KUMALIVE)) { + String warning = "Kumalives are disabled"; + log.warn(warning); + response.sendError(HttpServletResponse.SC_FORBIDDEN, warning); + return null; + } + if (!KumaliveAction.getSecurityService().hasOrgRole(organisation.getOrganisationId(), currentUserId, + new String[] { Role.GROUP_MANAGER, Role.MONITOR }, "kumalive get report kumalive rubrics", false)) { + String warning = "User " + currentUserId + " is not a monitor of organisation " + + organisation.getOrganisationId(); + log.warn(warning); + response.sendError(HttpServletResponse.SC_FORBIDDEN, warning); + return null; + } + + ArrayNode responseJSON = JsonNodeFactory.instance.arrayNode(); + for (KumaliveRubric rubric : kumalive.getRubrics()) { + ArrayNode rubricJSON = JsonNodeFactory.instance.arrayNode(); + rubricJSON.add(rubric.getRubricId()); + rubricJSON.add(rubric.getName() == null ? "" : rubric.getName()); + responseJSON.add(rubricJSON); + } + + writeResponse(response, "text/json", LamsDispatchAction.ENCODING_UTF8, responseJSON.toString()); + return null; + } + + public ActionForward getReportKumaliveData(ActionMapping mapping, ActionForm form, HttpServletRequest request, + HttpServletResponse response) throws IOException { + UserDTO userDTO = getUserDTO(); + Integer currentUserId = userDTO.getUserID(); + Long kumaliveId = WebUtil.readLongParam(request, "kumaliveId", false); + Kumalive kumalive = KumaliveAction.getKumaliveService().getKumalive(kumaliveId); + Organisation organisation = kumalive.getOrganisation(); + if (!Configuration.getAsBoolean(ConfigurationKeys.ALLOW_KUMALIVE)) { + String warning = "Kumalives are disabled"; + log.warn(warning); + response.sendError(HttpServletResponse.SC_FORBIDDEN, warning); + return null; + } + if (!KumaliveAction.getSecurityService().hasOrgRole(organisation.getOrganisationId(), currentUserId, + new String[] { Role.GROUP_MANAGER, Role.MONITOR }, "kumalive get report kumalive data", false)) { + String warning = "User " + currentUserId + " is not a monitor of organisation " + + organisation.getOrganisationId(); + log.warn(warning); + response.sendError(HttpServletResponse.SC_FORBIDDEN, warning); + return null; + } + + String sortOrder = WebUtil.readStrParam(request, GradebookConstants.PARAM_SORD); + + ObjectNode responseJSON = KumaliveAction.getKumaliveService().getReportKumaliveData(kumaliveId, + !"DESC".equalsIgnoreCase(sortOrder)); + + writeResponse(response, "text/json", LamsDispatchAction.ENCODING_UTF8, responseJSON.toString()); + return null; + } + + public ActionForward getReportUserData(ActionMapping mapping, ActionForm form, HttpServletRequest request, + HttpServletResponse response) throws IOException { + UserDTO userDTO = getUserDTO(); + Integer currentUserId = userDTO.getUserID(); + Long kumaliveId = WebUtil.readLongParam(request, "kumaliveId", false); + Integer userId = WebUtil.readIntParam(request, "userId", false); + Kumalive kumalive = KumaliveAction.getKumaliveService().getKumalive(kumaliveId); + Organisation organisation = kumalive.getOrganisation(); + if (!Configuration.getAsBoolean(ConfigurationKeys.ALLOW_KUMALIVE)) { + String warning = "Kumalives are disabled"; + log.warn(warning); + response.sendError(HttpServletResponse.SC_FORBIDDEN, warning); + return null; + } + if (!KumaliveAction.getSecurityService().hasOrgRole(organisation.getOrganisationId(), currentUserId, + new String[] { Role.GROUP_MANAGER, Role.MONITOR }, "kumalive get report user data", false)) { + String warning = "User " + currentUserId + " is not a monitor of organisation " + + organisation.getOrganisationId(); + log.warn(warning); + response.sendError(HttpServletResponse.SC_FORBIDDEN, warning); + return null; + } + + ObjectNode responseJSON = KumaliveAction.getKumaliveService().getReportUserData(kumaliveId, userId); + + writeResponse(response, "text/json", LamsDispatchAction.ENCODING_UTF8, responseJSON.toString()); + return null; + } + + public ActionForward exportKumalives(ActionMapping mapping, ActionForm form, HttpServletRequest request, + HttpServletResponse response) throws IOException { + UserDTO userDTO = getUserDTO(); + Integer currentUserId = userDTO.getUserID(); + Integer organisationId = WebUtil.readIntParam(request, AttributeNames.PARAM_ORGANISATION_ID, true); + List kumaliveIds = null; + if (organisationId == null) { + String kumaliveIdsParam = WebUtil.readStrParam(request, "kumaliveIds", false); + + ArrayNode kumaliveIdsJSON = JsonUtil.readArray(kumaliveIdsParam); + kumaliveIds = new LinkedList(); + for (JsonNode kumaliveIdJSON : kumaliveIdsJSON) { + kumaliveIds.add(kumaliveIdJSON.asLong()); + } + + Kumalive kumalive = KumaliveAction.getKumaliveService().getKumalive(kumaliveIds.get(0)); + organisationId = kumalive.getOrganisation().getOrganisationId(); + } + + if (!Configuration.getAsBoolean(ConfigurationKeys.ALLOW_KUMALIVE)) { + String warning = "Kumalives are disabled"; + log.warn(warning); + response.sendError(HttpServletResponse.SC_FORBIDDEN, warning); + return null; + } + if (!KumaliveAction.getSecurityService().hasOrgRole(organisationId, currentUserId, + new String[] { Role.GROUP_MANAGER, Role.MONITOR }, "kumalive export", false)) { + String warning = "User " + currentUserId + " is not a monitor of organisation " + organisationId; + log.warn(warning); + response.sendError(HttpServletResponse.SC_FORBIDDEN, warning); + return null; + } + + LinkedHashMap dataToExport = null; + if (kumaliveIds == null) { + dataToExport = KumaliveAction.getKumaliveService().exportKumalives(organisationId); + } else { + dataToExport = KumaliveAction.getKumaliveService().exportKumalives(kumaliveIds); + } + String fileName = "kumalive_report.xlsx"; + fileName = FileUtil.encodeFilenameForDownload(request, fileName); + + response.setContentType("application/x-download"); + response.setHeader("Content-Disposition", "attachment;filename=" + fileName); + + // set cookie that will tell JS script that export has been finished + String downloadTokenValue = WebUtil.readStrParam(request, "downloadTokenValue"); + Cookie fileDownloadTokenCookie = new Cookie("fileDownloadToken", downloadTokenValue); + fileDownloadTokenCookie.setPath("/"); + response.addCookie(fileDownloadTokenCookie); + + ExcelUtil.createExcel(response.getOutputStream(), dataToExport, "Exported on:", true); + + return null; + } + + public ActionForward saveRubrics(ActionMapping mapping, ActionForm form, HttpServletRequest request, + HttpServletResponse response) throws IOException { + UserDTO userDTO = getUserDTO(); + Integer userId = userDTO.getUserID(); + Integer organisationId = WebUtil.readIntParam(request, AttributeNames.PARAM_ORGANISATION_ID, false); + if (!Configuration.getAsBoolean(ConfigurationKeys.ALLOW_KUMALIVE)) { + String warning = "Kumalives are disabled"; + log.warn(warning); + response.sendError(HttpServletResponse.SC_FORBIDDEN, warning); + return null; + } + if (!KumaliveAction.getSecurityService().hasOrgRole(organisationId, userId, + new String[] { Role.GROUP_MANAGER, Role.MONITOR }, "kumalive get rubrics", false)) { + String warning = "User " + userId + " is not a monitor of organisation " + organisationId; + log.warn(warning); + response.sendError(HttpServletResponse.SC_FORBIDDEN, warning); + return null; + } + + ArrayNode rubricsJSON = JsonUtil.readArray(WebUtil.readStrParam(request, "rubrics")); + KumaliveAction.getKumaliveService().saveRubrics(organisationId, rubricsJSON); + + return null; + } + + private UserDTO getUserDTO() { + HttpSession ss = SessionManager.getSession(); + return (UserDTO) ss.getAttribute(AttributeNames.USER); + } + + private static IKumaliveService getKumaliveService() { + if (kumaliveService == null) { + WebApplicationContext ctx = WebApplicationContextUtils + .getWebApplicationContext(SessionManager.getServletContext()); + kumaliveService = (IKumaliveService) ctx.getBean("kumaliveService"); + } + return kumaliveService; + } + + private static ISecurityService getSecurityService() { + if (securityService == null) { + WebApplicationContext ctx = WebApplicationContextUtils + .getRequiredWebApplicationContext(SessionManager.getServletContext()); + securityService = (ISecurityService) ctx.getBean("securityService"); + } + return securityService; + } +} \ No newline at end of file Index: lams_learning/src/java/org/lamsfoundation/lams/learning/kumalive/KumaliveWebsocketServer.java =================================================================== diff -u --- lams_learning/src/java/org/lamsfoundation/lams/learning/kumalive/KumaliveWebsocketServer.java (revision 0) +++ lams_learning/src/java/org/lamsfoundation/lams/learning/kumalive/KumaliveWebsocketServer.java (revision 6afcecd6c2b6b9d1b985b371df9947107aa437ae) @@ -0,0 +1,566 @@ +package org.lamsfoundation.lams.learning.kumalive; + +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; + +import javax.websocket.CloseReason; +import javax.websocket.CloseReason.CloseCodes; +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; + +import org.apache.commons.lang.StringUtils; +import org.apache.log4j.Logger; +import org.lamsfoundation.lams.learning.kumalive.model.Kumalive; +import org.lamsfoundation.lams.learning.kumalive.model.KumaliveRubric; +import org.lamsfoundation.lams.learning.kumalive.service.IKumaliveService; +import org.lamsfoundation.lams.security.ISecurityService; +import org.lamsfoundation.lams.usermanagement.Role; +import org.lamsfoundation.lams.usermanagement.User; +import org.lamsfoundation.lams.usermanagement.dto.UserDTO; +import org.lamsfoundation.lams.usermanagement.service.IUserManagementService; +import org.lamsfoundation.lams.util.Configuration; +import org.lamsfoundation.lams.util.ConfigurationKeys; +import org.lamsfoundation.lams.util.JsonUtil; +import org.lamsfoundation.lams.web.session.SessionManager; +import org.lamsfoundation.lams.web.util.AttributeNames; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.context.support.WebApplicationContextUtils; + +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import com.fasterxml.jackson.databind.node.ObjectNode; + +/** + * Processes messages for Kumalive. + * + * @author Marcin Cieslak + */ +@ServerEndpoint("/kumaliveWebsocket") +public class KumaliveWebsocketServer { + + private class KumaliveUser { + private UserDTO userDTO; + private Session websocket; + private boolean isTeacher; + private boolean roleTeacher; + + 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 boolean raiseHandPrompt; + private final List raisedHand = new CopyOnWriteArrayList<>(); + private Integer speaker; + private final Map learners = new ConcurrentHashMap<>(); + private final ArrayNode rubrics = JsonNodeFactory.instance.arrayNode(); + + private KumaliveDTO(Kumalive kumalive) { + this.id = kumalive.getKumaliveId(); + this.name = kumalive.getName(); + this.createdBy = kumalive.getCreatedBy().getUserDTO(); + for (KumaliveRubric rubric : kumalive.getRubrics()) { + ObjectNode rubricJSON = JsonNodeFactory.instance.objectNode(); + rubricJSON.put("id", rubric.getRubricId()); + rubricJSON.put("name", rubric.getName()); + rubrics.add(rubricJSON); + } + } + } + + private static Logger logger = Logger.getLogger(KumaliveWebsocketServer.class); + + private static IKumaliveService kumaliveService; + private static ISecurityService securityService; + private static IUserManagementService userManagementService; + // mapping org ID -> Kumalive + private static final Map kumalives = new TreeMap<>(); + + @OnOpen + public void registerUser(Session websocket) throws IOException { + Integer organisationId = Integer + .valueOf(websocket.getRequestParameterMap().get(AttributeNames.PARAM_ORGANISATION_ID).get(0)); + Integer userId = getUser(websocket).getUserId(); + if (!KumaliveWebsocketServer.getSecurityService().hasOrgRole(organisationId, userId, + 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; + logger.warn(warning); + websocket.close(new CloseReason(CloseCodes.CANNOT_ACCEPT, warning)); + } + } + + @OnClose + public void unregisterUser(Session websocket, CloseReason reason) throws IOException { + String login = websocket.getUserPrincipal().getName(); + if (login == null) { + return; + } + + Integer organisationId = Integer + .valueOf(websocket.getRequestParameterMap().get(AttributeNames.PARAM_ORGANISATION_ID).get(0)); + KumaliveDTO kumalive = kumalives.get(organisationId); + if (kumalive == null) { + return; + } + KumaliveUser user = kumalive.learners.remove(login); + if (user != null) { + Integer userId = user.userDTO.getUserID(); + if (kumalive.raisedHand != null) { + kumalive.raisedHand.remove(userId); + } + if (userId.equals(kumalive.speaker)) { + kumalive.speaker = null; + } + } + + sendRefresh(kumalive); + } + + @OnMessage + public void receiveRequest(String input, Session session) throws IOException { + if (!Configuration.getAsBoolean(ConfigurationKeys.ALLOW_KUMALIVE)) { + logger.warn("Kumalives are disabled"); + return; + } + if (StringUtils.isBlank(input)) { + return; + } + if (input.equalsIgnoreCase("ping")) { + // just a ping every few minutes + return; + } + + ObjectNode requestJSON = JsonUtil.readObject(input); + switch (JsonUtil.optString(requestJSON, "type")) { + case "start": + start(requestJSON, session); + break; + case "join": + 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; + } + } + + /** + * Fetches an existing Kumalive, creates it or tells teacher to create it + */ + private void start(ObjectNode requestJSON, Session websocket) throws IOException { + Integer organisationId = Integer + .valueOf(websocket.getRequestParameterMap().get(AttributeNames.PARAM_ORGANISATION_ID).get(0)); + KumaliveDTO kumaliveDTO = kumalives.get(organisationId); + boolean isTeacher = false; + if (kumaliveDTO == null) { + String name = JsonUtil.optString(requestJSON, "name"); + ArrayNode rubricsJSON = JsonUtil.optArray(requestJSON, "rubrics"); + String role = websocket.getRequestParameterMap().get(AttributeNames.PARAM_ROLE).get(0); + User user = getUser(websocket); + Integer userId = user.getUserId(); + isTeacher = !Role.LEARNER.equalsIgnoreCase(role) && (KumaliveWebsocketServer.getUserManagementService() + .isUserInRole(userId, organisationId, Role.GROUP_MANAGER) + || KumaliveWebsocketServer.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.getKumaliveService().startKumalive(organisationId, userId, name, + rubricsJSON, isTeacher && StringUtils.isNotBlank(name)); + if (kumalive != null) { + kumaliveDTO = new KumaliveDTO(kumalive); + kumalives.put(organisationId, kumaliveDTO); + } + } + + // tell teacher to provide a name for Kumalive and create it + // or tell learner to join + ObjectNode responseJSON = JsonNodeFactory.instance.objectNode(); + if (kumaliveDTO == null && isTeacher) { + responseJSON.put("type", "create"); + List rubrics = KumaliveWebsocketServer.getKumaliveService().getRubrics(organisationId); + if (!rubrics.isEmpty()) { + ArrayNode rubricsJSON = JsonNodeFactory.instance.arrayNode(); + for (KumaliveRubric rubric : rubrics) { + rubricsJSON.add(rubric.getName()); + } + responseJSON.set("rubrics", rubricsJSON); + } + + websocket.getBasicRemote().sendText(responseJSON.toString()); + } else { + websocket.getBasicRemote().sendText("{ \"type\" : \"join\" }"); + } + } + + /** + * Adds a learner or a teacher to Kumalive + */ + private void join(ObjectNode requestJSON, Session websocket) throws IOException { + Integer organisationId = Integer + .valueOf(websocket.getRequestParameterMap().get(AttributeNames.PARAM_ORGANISATION_ID).get(0)); + KumaliveDTO kumalive = kumalives.get(organisationId); + if (kumalive == null) { + websocket.getBasicRemote().sendText("{ \"type\" : \"start\"}"); + return; + } + + User user = getUser(websocket); + Integer userId = user.getUserId(); + String login = user.getLogin(); + boolean isTeacher = KumaliveWebsocketServer.getUserManagementService().isUserInRole(userId, organisationId, + Role.GROUP_MANAGER) + || KumaliveWebsocketServer.getUserManagementService().isUserInRole(userId, organisationId, + Role.MONITOR); + String role = websocket.getRequestParameterMap().get(AttributeNames.PARAM_ROLE).get(0); + + 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")); + } + + learner = new KumaliveUser(user, websocket, isTeacher, roleTeacher); + kumalive.learners.put(login, learner); + + sendInit(kumalive, learner); + sendRefresh(kumalive); + } + + private void sendInit(KumaliveDTO kumalive, KumaliveUser user) throws IOException { + ObjectNode responseJSON = JsonNodeFactory.instance.objectNode(); + responseJSON.put("type", "init"); + // Kumalive title + responseJSON.put("name", kumalive.name); + responseJSON.put("isTeacher", user.isTeacher); + responseJSON.put("roleTeacher", user.roleTeacher); + // teacher details + responseJSON.put("teacherId", kumalive.createdBy.getUserID()); + responseJSON.put("teacherName", kumalive.createdBy.getFirstName() + " " + kumalive.createdBy.getLastName()); + responseJSON.put("teacherPortraitUuid", kumalive.createdBy.getPortraitUuid()); + + // rubric details + responseJSON.set("rubrics", kumalive.rubrics); + + user.websocket.getBasicRemote().sendText(responseJSON.toString()); + } + + /** + * Send full Kumalive state to all learners and teachers + */ + private void sendRefresh(KumaliveDTO kumalive) throws IOException { + ObjectNode responseJSON = JsonNodeFactory.instance.objectNode(); + responseJSON.put("type", "refresh"); + + // current state of question and speaker + responseJSON.put("raiseHandPrompt", kumalive.raiseHandPrompt); + if (!kumalive.raisedHand.isEmpty()) { + responseJSON.set("raisedHand", JsonUtil.readArray(kumalive.raisedHand)); + } + responseJSON.put("speaker", kumalive.speaker); + + // each learner's details + ArrayNode learnersJSON = JsonNodeFactory.instance.arrayNode(); + ObjectNode logins = JsonNodeFactory.instance.objectNode(); + for (KumaliveUser participant : kumalive.learners.values()) { + UserDTO participantDTO = participant.userDTO; + + ObjectNode learnerJSON = JsonNodeFactory.instance.objectNode(); + learnerJSON.put("id", participantDTO.getUserID()); + learnerJSON.put("firstName", participantDTO.getFirstName()); + learnerJSON.put("lastName", participantDTO.getLastName()); + learnerJSON.put("portraitUuid", participantDTO.getPortraitUuid()); + learnerJSON.put("roleTeacher", participant.roleTeacher); + + logins.put("user" + participantDTO.getUserID(), participantDTO.getLogin()); + + learnersJSON.add(learnerJSON); + } + responseJSON.set("learners", learnersJSON); + + String learnerResponse = responseJSON.toString(); + ObjectNode 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.set("logins", logins); + teacherResponseJSON = responseJSON; + } + channel.sendText(teacherResponseJSON.toString()); + } else { + channel.sendText(learnerResponse); + } + } + } + + /** + * Tell learners that the teacher asked + */ + private void raiseHandPrompt(ObjectNode requestJSON, Session websocket) throws IOException { + 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 (!KumaliveWebsocketServer.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; + logger.warn(warning); + return; + } + + kumalive.raiseHandPrompt = true; + if (logger.isDebugEnabled()) { + logger.debug("Teacher " + userId + " asked a question in Kumalive " + kumalive.id); + } + sendRefresh(kumalive); + } + + /** + * Tell learners that the teacher finished a question + */ + private void downHandPrompt(ObjectNode requestJSON, Session websocket) throws IOException { + 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 (!KumaliveWebsocketServer.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; + logger.warn(warning); + return; + } + + kumalive.raiseHandPrompt = false; + kumalive.raisedHand.clear(); + if (logger.isDebugEnabled()) { + logger.debug("Teacher " + userId + " finished a question in Kumalive " + kumalive.id); + } + + sendRefresh(kumalive); + } + + /** + * Tell learners that a learner raised hand + */ + private void raiseHand(ObjectNode requestJSON, Session websocket) throws IOException { + 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 (!KumaliveWebsocketServer.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; + logger.warn(warning); + return; + } + + if (!kumalive.raiseHandPrompt) { + logger.warn("Raise hand prompt was not sent by teacher yet for organisation " + organisationId); + return; + } + + if (kumalive.raisedHand.contains(userId)) { + return; + } + + kumalive.raisedHand.add(userId); + if (logger.isDebugEnabled()) { + logger.debug("Learner " + userId + " raised hand in Kumalive " + kumalive.id); + } + + sendRefresh(kumalive); + } + + /** + * Tell learners that a learner put hadn down + */ + private void downHand(ObjectNode requestJSON, Session websocket) throws IOException { + 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 (!KumaliveWebsocketServer.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; + logger.warn(warning); + return; + } + + if (kumalive.raisedHand == null) { + return; + } + + kumalive.raisedHand.remove(userId); + if (logger.isDebugEnabled()) { + logger.debug("Learner " + userId + " put hand down in Kumalive " + kumalive.id); + } + + sendRefresh(kumalive); + } + + /** + * Set up a speaker or remove him + */ + private void speak(ObjectNode requestJSON, Session websocket) throws IOException { + 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 (!KumaliveWebsocketServer.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; + logger.warn(warning); + return; + } + + kumalive.speaker = JsonUtil.optInt(requestJSON, "speaker"); + sendRefresh(kumalive); + } + + /** + * Save score for a learner + */ + private void score(ObjectNode requestJSON, Session websocket) throws IOException { + Integer organisationId = Integer + .valueOf(websocket.getRequestParameterMap().get(AttributeNames.PARAM_ORGANISATION_ID).get(0)); + + User user = getUser(websocket); + Integer userId = user.getUserId(); + + if (!KumaliveWebsocketServer.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; + logger.warn(warning); + return; + } + Long rubricId = JsonUtil.optLong(requestJSON, "rubricId"); + Integer learnerId = JsonUtil.optInt(requestJSON, AttributeNames.PARAM_USER_ID); + KumaliveWebsocketServer.getKumaliveService().scoreKumalive(rubricId, learnerId, + Long.valueOf(JsonUtil.optString(requestJSON, "batch")), + Short.valueOf(JsonUtil.optString(requestJSON, "score"))); + + KumaliveDTO kumalive = kumalives.get(organisationId); + if (logger.isDebugEnabled()) { + logger.debug("Teacher " + userId + " marked rubric " + rubricId + " for learner " + learnerId + + " in Kumalive " + kumalive.id); + } + + sendRefresh(kumalive); + } + + /** + * End Kumalive + */ + private void finish(ObjectNode requestJSON, Session websocket) throws IOException { + 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 (!KumaliveWebsocketServer.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; + logger.warn(warning); + return; + } + + KumaliveWebsocketServer.getKumaliveService().finishKumalive(kumalive.id); + kumalives.remove(organisationId); + for (KumaliveUser participant : kumalive.learners.values()) { + participant.websocket.getBasicRemote().sendText("{ \"type\" : \"finish\"}"); + } + + if (logger.isDebugEnabled()) { + logger.debug("Teacher " + userId + " finished Kumalive " + kumalive.id); + } + } + + private User getUser(Session websocket) { + return KumaliveWebsocketServer.getUserManagementService() + .getUserByLogin(websocket.getUserPrincipal().getName()); + } + + private static IKumaliveService getKumaliveService() { + if (kumaliveService == null) { + WebApplicationContext ctx = WebApplicationContextUtils + .getWebApplicationContext(SessionManager.getServletContext()); + kumaliveService = (IKumaliveService) ctx.getBean("kumaliveService"); + } + return kumaliveService; + } + + private static ISecurityService getSecurityService() { + if (securityService == null) { + WebApplicationContext ctx = WebApplicationContextUtils + .getRequiredWebApplicationContext(SessionManager.getServletContext()); + securityService = (ISecurityService) ctx.getBean("securityService"); + } + return securityService; + } + + private static IUserManagementService getUserManagementService() { + if (userManagementService == null) { + WebApplicationContext ctx = WebApplicationContextUtils + .getRequiredWebApplicationContext(SessionManager.getServletContext()); + userManagementService = (IUserManagementService) ctx.getBean("userManagementService"); + } + return userManagementService; + } +} \ No newline at end of file Index: lams_learning/src/java/org/lamsfoundation/lams/learning/kumalive/service/IKumaliveService.java =================================================================== diff -u --- lams_learning/src/java/org/lamsfoundation/lams/learning/kumalive/service/IKumaliveService.java (revision 0) +++ lams_learning/src/java/org/lamsfoundation/lams/learning/kumalive/service/IKumaliveService.java (revision 6afcecd6c2b6b9d1b985b371df9947107aa437ae) @@ -0,0 +1,61 @@ +/**************************************************************** + * Copyright (C) 2005 LAMS Foundation (http://lamsfoundation.org) + * ============================================================= + * License Information: http://lamsfoundation.org/licensing/lams/2.0/ + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2.0 + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + * + * http://www.gnu.org/licenses/gpl.txt + * **************************************************************** + */ + +package org.lamsfoundation.lams.learning.kumalive.service; + +import java.util.LinkedHashMap; +import java.util.List; + +import org.lamsfoundation.lams.learning.kumalive.model.Kumalive; +import org.lamsfoundation.lams.learning.kumalive.model.KumaliveRubric; +import org.lamsfoundation.lams.util.ExcelCell; + +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; + +public interface IKumaliveService { + Kumalive getKumalive(Long id); + + Kumalive getKumaliveByOrganisation(Integer organisationId); + + Kumalive startKumalive(Integer organisationId, Integer userId, String name, ArrayNode rubrics, boolean isTeacher); + + void finishKumalive(Long id); + + void scoreKumalive(Long rubricId, Integer userId, Long batch, Short score); + + List getRubrics(Integer organisationId); + + void saveRubrics(Integer organisationId, ArrayNode rubricsJSON); + + ObjectNode getReportOrganisationData(Integer organisationId, String sortColumn, boolean isAscending, int rowLimit, + int page); + + ObjectNode getReportKumaliveData(Long kumaliveId, boolean isAscending); + + ObjectNode getReportUserData(Long kumaliveId, Integer userId); + + LinkedHashMap exportKumalives(List kumaliveIds); + + LinkedHashMap exportKumalives(Integer organisationId); +} \ No newline at end of file Index: lams_learning/src/java/org/lamsfoundation/lams/learning/kumalive/service/KumaliveService.java =================================================================== diff -u --- lams_learning/src/java/org/lamsfoundation/lams/learning/kumalive/service/KumaliveService.java (revision 0) +++ lams_learning/src/java/org/lamsfoundation/lams/learning/kumalive/service/KumaliveService.java (revision 6afcecd6c2b6b9d1b985b371df9947107aa437ae) @@ -0,0 +1,556 @@ +/**************************************************************** + * Copyright (C) 2005 LAMS Foundation (http://lamsfoundation.org) + * ============================================================= + * License Information: http://lamsfoundation.org/licensing/lams/2.0/ + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2.0 + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + * + * http://www.gnu.org/licenses/gpl.txt + * **************************************************************** + */ + +package org.lamsfoundation.lams.learning.kumalive.service; + +import java.math.RoundingMode; +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; +import java.util.Comparator; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.TreeMap; +import java.util.stream.Collectors; + +import org.apache.log4j.Logger; +import org.lamsfoundation.lams.gradebook.util.GradebookConstants; +import org.lamsfoundation.lams.gradebook.util.UserComparator; +import org.lamsfoundation.lams.learning.kumalive.dao.IKumaliveDAO; +import org.lamsfoundation.lams.learning.kumalive.model.Kumalive; +import org.lamsfoundation.lams.learning.kumalive.model.KumaliveRubric; +import org.lamsfoundation.lams.learning.kumalive.model.KumaliveScore; +import org.lamsfoundation.lams.security.ISecurityService; +import org.lamsfoundation.lams.usermanagement.Organisation; +import org.lamsfoundation.lams.usermanagement.User; +import org.lamsfoundation.lams.util.ExcelCell; +import org.lamsfoundation.lams.util.FileUtil; +import org.lamsfoundation.lams.util.MessageService; + +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import com.fasterxml.jackson.databind.node.ObjectNode; + +public class KumaliveService implements IKumaliveService { + private static Logger logger = Logger.getLogger(KumaliveService.class); + + private static final DecimalFormat DECIMAL_FORMAT = new DecimalFormat("#.##", + new DecimalFormatSymbols(Locale.ENGLISH)); + private static final ExcelCell[] EMPTY_ROW = new ExcelCell[1]; + private static final Comparator USER_COMPARATOR = new UserComparator(); + + private IKumaliveDAO kumaliveDAO; + private ISecurityService securityService; + private MessageService messageService; + + static { + DECIMAL_FORMAT.setRoundingMode(RoundingMode.HALF_UP); + } + + @Override + public Kumalive getKumalive(Long id) { + return (Kumalive) kumaliveDAO.find(Kumalive.class, id); + } + + @Override + public Kumalive getKumaliveByOrganisation(Integer organisationId) { + return kumaliveDAO.findKumalive(organisationId); + } + + /** + * Fetches or creates a Kumalive + * + * @throws JSONException + */ + @Override + public Kumalive startKumalive(Integer organisationId, Integer userId, String name, ArrayNode rubricsJSON, + boolean isTeacher) { + if (isTeacher) { + securityService.isGroupMonitor(organisationId, userId, "start kumalive", true); + } + Kumalive kumalive = getKumaliveByOrganisation(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 = new Kumalive(organisation, createdBy, name); + kumaliveDAO.insert(kumalive); + + Set rubrics = new HashSet<>(); + if (rubricsJSON == null) { + KumaliveRubric rubric = new KumaliveRubric(organisation, kumalive, (short) 0, null); + kumaliveDAO.insert(rubric); + rubrics.add(rubric); + } else { + for (Short rubricIndex = 0; rubricIndex < rubricsJSON.size(); rubricIndex++) { + String rubricName = rubricsJSON.get(rubricIndex.intValue()).asText(); + KumaliveRubric rubric = new KumaliveRubric(organisation, kumalive, rubricIndex, rubricName); + kumaliveDAO.insert(rubric); + rubrics.add(rubric); + } + } + kumalive.setRubrics(rubrics); + kumaliveDAO.update(kumalive); + + if (logger.isDebugEnabled()) { + logger.debug("Teacher " + userId + " started Kumalive " + 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 rubricId, Integer userId, Long batch, Short score) { + KumaliveRubric rubric = (KumaliveRubric) kumaliveDAO.find(KumaliveRubric.class, rubricId); + User user = (User) kumaliveDAO.find(User.class, userId); + KumaliveScore kumaliveScore = new KumaliveScore(rubric, user, batch, score); + kumaliveDAO.insert(kumaliveScore); + } + + @Override + public List getRubrics(Integer organisationId) { + return kumaliveDAO.findRubrics(organisationId); + } + + @Override + public void saveRubrics(Integer organisationId, ArrayNode rubricsJSON) { + Organisation organisation = (Organisation) kumaliveDAO.find(Organisation.class, organisationId); + kumaliveDAO.deleteByProperty(KumaliveRubric.class, "organisation", organisation); + for (Short rubricIndex = 0; rubricIndex < rubricsJSON.size(); rubricIndex++) { + String name = rubricsJSON.get(rubricIndex.intValue()).asText(); + KumaliveRubric rubric = new KumaliveRubric(organisation, null, rubricIndex, name); + kumaliveDAO.insert(rubric); + } + } + + /** + * Gets Kumalives for the given organisation packed into jqGrid JSON format + */ + @Override + public ObjectNode getReportOrganisationData(Integer organisationId, String sortColumn, boolean isAscending, + int rowLimit, int page) { + List kumalives = kumaliveDAO.findKumalives(organisationId, sortColumn, isAscending); + + // paging + int totalPages = 1; + if (rowLimit < kumalives.size()) { + totalPages = new Double( + Math.ceil(new Integer(kumalives.size()).doubleValue() / new Integer(rowLimit).doubleValue())) + .intValue(); + int firstRow = (page - 1) * rowLimit; + int lastRow = firstRow + rowLimit; + + if (lastRow > kumalives.size()) { + kumalives = kumalives.subList(firstRow, kumalives.size()); + } else { + kumalives = kumalives.subList(firstRow, lastRow); + } + } + + ObjectNode resultJSON = JsonNodeFactory.instance.objectNode(); + + resultJSON.put(GradebookConstants.ELEMENT_PAGE, page); + resultJSON.put(GradebookConstants.ELEMENT_TOTAL, totalPages); + resultJSON.put(GradebookConstants.ELEMENT_RECORDS, kumalives.size()); + + ArrayNode rowsJSON = JsonNodeFactory.instance.arrayNode(); + + // IDs are arbitrary, so generate order starting from 1 + int order = (page - 1) * rowLimit + (isAscending ? 1 : kumalives.size()); + + for (Kumalive kumalive : kumalives) { + ObjectNode rowJSON = JsonNodeFactory.instance.objectNode(); + rowJSON.put(GradebookConstants.ELEMENT_ID, kumalive.getKumaliveId()); + + ArrayNode cellJSON = JsonNodeFactory.instance.arrayNode(); + cellJSON.add(order); + cellJSON.add(kumalive.getName()); + + rowJSON.set(GradebookConstants.ELEMENT_CELL, cellJSON); + rowsJSON.add(rowJSON); + + if (isAscending) { + order++; + } else { + order--; + } + } + + resultJSON.set(GradebookConstants.ELEMENT_ROWS, rowsJSON); + + return resultJSON; + } + + /** + * Gets learners who answered to question in the given Kumalive, packed into jqGrid JSON format + */ + @Override + public ObjectNode getReportKumaliveData(Long kumaliveId, boolean isAscending) { + Kumalive kumalive = getKumalive(kumaliveId); + List rubrics = new LinkedList(); + for (KumaliveRubric rubric : kumalive.getRubrics()) { + rubrics.add(rubric.getRubricId()); + } + + // mapping learner -> rubric ID -> scores + Map>> scores = kumaliveDAO.findKumaliveScore(kumaliveId, isAscending).stream() + .collect(Collectors.groupingBy(KumaliveScore::getUser, LinkedHashMap::new, + Collectors.groupingBy(score -> score.getRubric().getRubricId(), + Collectors.mapping(KumaliveScore::getScore, Collectors.toList())))); + + ObjectNode resultJSON = JsonNodeFactory.instance.objectNode(); + resultJSON.put(GradebookConstants.ELEMENT_RECORDS, scores.size()); + + ArrayNode rowsJSON = JsonNodeFactory.instance.arrayNode(); + for (Entry>> userEntry : scores.entrySet()) { + ObjectNode rowJSON = JsonNodeFactory.instance.objectNode(); + User user = userEntry.getKey(); + rowJSON.put(GradebookConstants.ELEMENT_ID, user.getUserId()); + + ArrayNode cellJSON = JsonNodeFactory.instance.arrayNode(); + cellJSON.add(user.getFirstName() + " " + user.getLastName()); + // calculate average of scores for the given rubric + for (Long rubric : rubrics) { + Double score = null; + List attempts = userEntry.getValue().get(rubric); + if (attempts != null) { + for (Short attempt : attempts) { + if (score == null) { + score = attempt.doubleValue(); + } else { + score += attempt; + } + } + } + // format nicely + cellJSON.add(score == null ? "" : DECIMAL_FORMAT.format(score / attempts.size())); + } + + rowJSON.set(GradebookConstants.ELEMENT_CELL, cellJSON); + rowsJSON.add(rowJSON); + } + + resultJSON.set(GradebookConstants.ELEMENT_ROWS, rowsJSON); + + return resultJSON; + } + + /** + * Gets scores for the given Kumalive and learner, packed into jqGrid JSON format + */ + @Override + public ObjectNode getReportUserData(Long kumaliveId, Integer userId) { + Kumalive kumalive = getKumalive(kumaliveId); + List rubrics = new LinkedList(); + for (KumaliveRubric rubric : kumalive.getRubrics()) { + rubrics.add(rubric.getRubricId()); + } + + // mapping batch (question ID) -> rubric ID -> score + Map> scores = kumaliveDAO.findKumaliveScore(kumaliveId, userId).stream() + .collect(Collectors.groupingBy(KumaliveScore::getBatch, LinkedHashMap::new, + Collectors.toMap(score -> score.getRubric().getRubricId(), KumaliveScore::getScore))); + + ObjectNode resultJSON = JsonNodeFactory.instance.objectNode(); + resultJSON.put(GradebookConstants.ELEMENT_RECORDS, scores.size()); + + ArrayNode rowsJSON = JsonNodeFactory.instance.arrayNode(); + // just normal ordering of questions + short order = 1; + for (Entry> batchEntry : scores.entrySet()) { + ObjectNode rowJSON = JsonNodeFactory.instance.objectNode(); + rowJSON.put(GradebookConstants.ELEMENT_ID, order); + + ArrayNode cellJSON = JsonNodeFactory.instance.arrayNode(); + cellJSON.add(order); + order++; + for (Long rubric : rubrics) { + Short score = batchEntry.getValue().get(rubric); + cellJSON.add(score == null ? "" : score.toString()); + } + + rowJSON.set(GradebookConstants.ELEMENT_CELL, cellJSON); + rowsJSON.add(rowJSON); + } + + resultJSON.set(GradebookConstants.ELEMENT_ROWS, rowsJSON); + + return resultJSON; + } + + /** + * Exports to Excel all Kumalives in the given organisation. + */ + @Override + public LinkedHashMap exportKumalives(Integer organisationId) { + List kumalives = kumaliveDAO.findKumalives(organisationId, "", true); + return export(kumalives); + } + + /** + * Exports to Excel Kumalives with given IDs. + */ + @Override + public LinkedHashMap exportKumalives(List kumaliveIds) { + List kumalives = kumaliveDAO.findKumalives(kumaliveIds); + return export(kumalives); + } + + /** + * Exports to Excel given Kumalives. + */ + private LinkedHashMap export(List kumalives) { + Map>> learnerSummaries = new TreeMap<>(USER_COMPARATOR); + + Organisation organisation = kumalives.get(0).getOrganisation(); + LinkedHashMap dataToExport = new LinkedHashMap(); + + ExcelCell[][] kumalivesSheet = buildReportKumalivesSheet(kumalives, learnerSummaries); + dataToExport.put(messageService.getMessage("label.kumalive.report.sheet.header", + new Object[] { organisation.getName() }), kumalivesSheet); + + ExcelCell[][] learnersSheet = buildReportLearnersSheet(kumalives, learnerSummaries); + dataToExport.put(messageService.getMessage("label.kumalive.report.sheet.header.learners"), learnersSheet); + return dataToExport; + } + + /** + * Builds Kumalives summary sheet for the report + */ + private ExcelCell[][] buildReportKumalivesSheet(List kumalives, + Map>> learnerSummaries) { + List rows = new LinkedList(); + + // iterate over Kumalives and add them to the report + for (Kumalive kumalive : kumalives) { + ExcelCell[] kumaliveHeaderRow = new ExcelCell[1]; + kumaliveHeaderRow[0] = new ExcelCell(messageService.getMessage("label.kumalive.report.name"), true); + rows.add(kumaliveHeaderRow); + + ExcelCell[] kumaliveNameRow = new ExcelCell[1]; + kumaliveNameRow[0] = new ExcelCell(kumalive.getName(), false); + rows.add(kumaliveNameRow); + + // mapping user (sorted by name) -> batch (i.e. question ID) -> rubric -> score + TreeMap>> scores = kumaliveDAO + .findKumaliveScore(kumalive.getKumaliveId(), true).stream() + .collect(Collectors.groupingBy(KumaliveScore::getUser, + () -> new TreeMap>>(USER_COMPARATOR), + Collectors.groupingBy(KumaliveScore::getBatch, TreeMap::new, Collectors + .toMap(score -> score.getRubric().getRubricId(), KumaliveScore::getScore)))); + + if (scores.size() == 0) { + // no learners answered to question, carry on + ExcelCell[] noMarksRow = new ExcelCell[1]; + noMarksRow[0] = new ExcelCell(messageService.getMessage("label.kumalive.report.mark.none"), true); + rows.add(noMarksRow); + rows.add(EMPTY_ROW); + continue; + } + + // headers for learners + ExcelCell[] marksRow = new ExcelCell[1]; + marksRow[0] = new ExcelCell(messageService.getMessage("label.kumalive.report.mark"), true); + rows.add(marksRow); + + ExcelCell[] userHeaderRow = new ExcelCell[5 + kumalive.getRubrics().size()]; + userHeaderRow[0] = new ExcelCell(messageService.getMessage("label.kumalive.report.last.name"), true); + userHeaderRow[1] = new ExcelCell(messageService.getMessage("label.kumalive.report.first.name"), true); + userHeaderRow[2] = new ExcelCell(messageService.getMessage("label.kumalive.report.login"), true); + userHeaderRow[3] = new ExcelCell(messageService.getMessage("label.kumalive.report.attempt"), true); + // iterate over rubrics and make them columns + int userRowLength = 4; + for (KumaliveRubric rubric : kumalive.getRubrics()) { + userHeaderRow[userRowLength++] = new ExcelCell(rubric.getName(), true); + } + userHeaderRow[userRowLength] = new ExcelCell(messageService.getMessage("label.kumalive.report.time"), true); + rows.add(userHeaderRow); + + for (Entry>> learnerEntry : scores.entrySet()) { + // learner details and average mark + User learner = learnerEntry.getKey(); + ExcelCell[] userRow = new ExcelCell[userRowLength + 1]; + userRow[0] = new ExcelCell(learner.getFirstName(), false); + userRow[1] = new ExcelCell(learner.getLastName(), false); + userRow[2] = new ExcelCell(learner.getLogin(), false); + userRow[3] = new ExcelCell(messageService.getMessage("label.kumalive.report.average"), false); + rows.add(userRow); + + // build rows for each attempt (answer to a question, batch) + Short[][] resultsByRubric = new Short[learnerEntry.getValue().size()][userRowLength - 4]; + int attempt = 0; + Long[] rubricIds = new Long[kumalive.getRubrics().size()]; + for (Entry> batchEntry : scores.get(learner).entrySet()) { + ExcelCell[] attemptRow = new ExcelCell[userRowLength + 1]; + attemptRow[3] = new ExcelCell( + messageService.getMessage("label.kumalive.report.attempt") + " " + (attempt + 1), false); + int rubricIndex = 0; + Map results = batchEntry.getValue(); + for (KumaliveRubric rubric : kumalive.getRubrics()) { + rubricIds[rubricIndex] = rubric.getRubricId(); + Short result = results.get(rubric.getRubricId()); + attemptRow[rubricIndex + 4] = new ExcelCell(result == null ? null : result.intValue(), false); + // store mark to calculate average later + resultsByRubric[attempt][rubricIndex] = result; + rubricIndex++; + } + attempt++; + Date attemptDate = new Date(batchEntry.getKey() * 1000); + attemptRow[userRowLength] = new ExcelCell( + FileUtil.EXPORT_TO_SPREADSHEET_TITLE_DATE_FORMAT.format(attemptDate), false); + rows.add(attemptRow); + } + // calculate average per each rubric and update the first learner row + for (int rubricIndex = 0; rubricIndex < userRowLength - 4; rubricIndex++) { + int count = 0; + double score = 0; + for (int attemptIndex = 0; attemptIndex < resultsByRubric.length; attemptIndex++) { + Short result = resultsByRubric[attemptIndex][rubricIndex]; + if (result == null) { + continue; + } + score += result; + count++; + } + if (count > 0) { + double average = Double.valueOf(DECIMAL_FORMAT.format(score / count)); + userRow[rubricIndex + 4] = new ExcelCell(average, false); + // populate data for learners sheet + Map> learnerSummary = learnerSummaries.get(learner); + if (learnerSummary == null) { + learnerSummary = new HashMap>(); + learnerSummaries.put(learner, learnerSummary); + } + Map learnerKumaliveSummary = learnerSummary.get(kumalive.getName()); + if (learnerKumaliveSummary == null) { + learnerKumaliveSummary = new HashMap(); + learnerSummary.put(kumalive.getName(), learnerKumaliveSummary); + } + learnerKumaliveSummary.put(rubricIds[rubricIndex], average); + } + } + } + rows.add(EMPTY_ROW); + } + + return rows.toArray(new ExcelCell[][] {}); + } + + /** + * Builds Kumalives summary sheet for the report + */ + private ExcelCell[][] buildReportLearnersSheet(List kumalives, + Map>> learnerSummaries) { + List rows = new LinkedList(); + Map kumaliveNamePosition = new HashMap(); + List userHeaderRow = new LinkedList(); + userHeaderRow.add(new ExcelCell(messageService.getMessage("label.kumalive.report.last.name"), true)); + userHeaderRow.add(new ExcelCell(messageService.getMessage("label.kumalive.report.first.name"), true)); + userHeaderRow.add(new ExcelCell(messageService.getMessage("label.kumalive.report.login"), true)); + // count cells for kumalives and their rubrics + int userRowLength = 3; + for (Kumalive kumalive : kumalives) { + kumaliveNamePosition.put(kumalive.getName(), userRowLength); + // use border only on first cell in kumalive + boolean border = true; + for (KumaliveRubric rubric : kumalive.getRubrics()) { + userHeaderRow.add(new ExcelCell(rubric.getName(), true, border ? 1 : 0)); + border = false; + userRowLength++; + } + } + + // now that we know how long is the whole user row, we can build kumalive names row and add it first + ExcelCell[] kumaliveNameRow = new ExcelCell[userRowLength]; + for (Entry kumaliveNameEntry : kumaliveNamePosition.entrySet()) { + kumaliveNameRow[kumaliveNameEntry.getValue()] = new ExcelCell(kumaliveNameEntry.getKey(), true, 1); + } + rows.add(kumaliveNameRow); + rows.add(userHeaderRow.toArray(EMPTY_ROW)); + + for (Entry>> learnerSummary : learnerSummaries.entrySet()) { + ExcelCell[] userRow = new ExcelCell[userRowLength]; + User learner = learnerSummary.getKey(); + userRow[0] = new ExcelCell(learner.getFirstName(), false); + userRow[1] = new ExcelCell(learner.getLastName(), false); + userRow[2] = new ExcelCell(learner.getLogin(), false); + for (Kumalive kumalive : kumalives) { + Map learnerKumaliveSummary = learnerSummary.getValue().get(kumalive.getName()); + if (learnerKumaliveSummary == null) { + continue; + } + int position = kumaliveNamePosition.get(kumalive.getName()); + boolean border = true; + for (KumaliveRubric rubric : kumalive.getRubrics()) { + Double average = learnerKumaliveSummary.get(rubric.getRubricId()); + if (average != null) { + userRow[position] = new ExcelCell(average, false, border ? 1 : 0); + } + border = false; + position++; + } + } + rows.add(userRow); + } + + return rows.toArray(new ExcelCell[][] {}); + } + + public void setSecurityService(ISecurityService securityService) { + this.securityService = securityService; + } + + public void setKumaliveDAO(IKumaliveDAO kumaliveDAO) { + this.kumaliveDAO = kumaliveDAO; + } + + public void setMessageService(MessageService messageService) { + this.messageService = messageService; + } +} \ No newline at end of file Index: lams_tool_lamc/src/java/org/lamsfoundation/lams/tool/mc/service/McService.java =================================================================== diff -u -r68a00ed36298398bdd7646b440a122d373bd75a1 -r6afcecd6c2b6b9d1b985b371df9947107aa437ae --- lams_tool_lamc/src/java/org/lamsfoundation/lams/tool/mc/service/McService.java (.../McService.java) (revision 68a00ed36298398bdd7646b440a122d373bd75a1) +++ lams_tool_lamc/src/java/org/lamsfoundation/lams/tool/mc/service/McService.java (.../McService.java) (revision 6afcecd6c2b6b9d1b985b371df9947107aa437ae) @@ -51,9 +51,6 @@ import org.apache.poi.ss.usermodel.CellStyle; import org.apache.poi.ss.usermodel.Font; import org.apache.poi.ss.usermodel.IndexedColors; -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.contentrepository.client.IToolContentHandler; import org.lamsfoundation.lams.gradebook.service.IGradebookService; import org.lamsfoundation.lams.learning.service.ILearnerService; @@ -111,6 +108,10 @@ import org.lamsfoundation.lams.web.util.AttributeNames; import org.springframework.dao.DataAccessException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; + /** * * The POJO implementation of Mc service. All business logics of MCQ tool are implemented in this class. It translate @@ -602,7 +603,8 @@ for (McUsrAttempt attempt : finalizedUserAttempts) { Integer displayOrder = attempt.getMcQueContent().getDisplayOrder(); int arrayIndex = (displayOrder != null) && (displayOrder.intValue() > 0) - ? displayOrder.intValue() - 1 : 1; + ? displayOrder.intValue() - 1 + : 1; if (userMarks[arrayIndex] == null) { // We get the mark for the attempt if the answer is correct and we don't allow @@ -675,7 +677,7 @@ e); } } - + @Override public List getLearnerMarksByContentId(Long toolContentId) { return mcUsrAttemptDAO.getLearnerMarksByContentId(toolContentId); @@ -1560,7 +1562,7 @@ public ToolOutput getToolOutput(String name, Long toolSessionId, Long learnerId) { return mcOutputFactory.getToolOutput(name, this, toolSessionId, learnerId); } - + @Override public List getToolOutputs(String name, Long toolContentId) { return mcOutputFactory.getToolOutputs(name, this, toolContentId); @@ -1619,10 +1621,10 @@ public boolean isGroupedActivity(long toolContentID) { return toolService.isGroupedActivity(toolContentID); } - + @Override public void auditLogStartEditingActivityInMonitor(long toolContentID) { - toolService.auditLogStartEditingActivityInMonitor(toolContentID); + toolService.auditLogStartEditingActivityInMonitor(toolContentID); } @Override @@ -1826,32 +1828,41 @@ for (McUsrAttempt item : attempts) { Date newDate = item.getAttemptTime(); if (newDate != null) { - if (startDate == null || newDate.before(startDate)) + if (startDate == null || newDate.before(startDate)) { startDate = newDate; - if (endDate == null || newDate.after(endDate)) + } + if (endDate == null || newDate.after(endDate)) { endDate = newDate; + } } } - if (learner.isResponseFinalised()) + if (learner.isResponseFinalised()) { return new ToolCompletionStatus(ToolCompletionStatus.ACTIVITY_COMPLETED, startDate, endDate); - else + } else { return new ToolCompletionStatus(ToolCompletionStatus.ACTIVITY_ATTEMPTED, startDate, null); + } } @Override public LeaderResultsDTO getLeaderResultsDTOForLeaders(Long contentId) { LeaderResultsDTO newDto = new LeaderResultsDTO(contentId); Object[] markStats = mcUserDAO.getStatsMarksForLeaders(contentId); - if ( markStats != null ) { - newDto.setMinMark(markStats[0] != null ? NumberUtil.formatLocalisedNumber((Float)markStats[0], (Locale)null, 2) : "0.00"); - newDto.setAvgMark(markStats[1] != null ? NumberUtil.formatLocalisedNumber((Float)markStats[1], (Locale)null, 2) : "0.00"); - newDto.setMaxMark(markStats[2] != null ? NumberUtil.formatLocalisedNumber((Float)markStats[2], (Locale)null, 2) : "0.00"); - newDto.setNumberGroupsLeaderFinished((Integer)markStats[3]); + if (markStats != null) { + newDto.setMinMark( + markStats[0] != null ? NumberUtil.formatLocalisedNumber((Float) markStats[0], (Locale) null, 2) + : "0.00"); + newDto.setAvgMark( + markStats[1] != null ? NumberUtil.formatLocalisedNumber((Float) markStats[1], (Locale) null, 2) + : "0.00"); + newDto.setMaxMark( + markStats[2] != null ? NumberUtil.formatLocalisedNumber((Float) markStats[2], (Locale) null, 2) + : "0.00"); + newDto.setNumberGroupsLeaderFinished((Integer) markStats[3]); } return newDto; } - + @SuppressWarnings("unchecked") @Override public List getSessionDtos(Long contentId, boolean includeStatistics) { @@ -1872,11 +1883,14 @@ Object[] markStats = mcUserDAO.getStatsMarksBySession(session.getMcSessionId()); if (markStats != null) { sessionDto.setMinMark(markStats[0] != null - ? NumberUtil.formatLocalisedNumber((Float) markStats[0], (Locale) null, 2) : "0.00"); + ? NumberUtil.formatLocalisedNumber((Float) markStats[0], (Locale) null, 2) + : "0.00"); sessionDto.setAvgMark(markStats[1] != null - ? NumberUtil.formatLocalisedNumber((Float) markStats[1], (Locale) null, 2) : "0.00"); + ? NumberUtil.formatLocalisedNumber((Float) markStats[1], (Locale) null, 2) + : "0.00"); sessionDto.setMaxMark(markStats[2] != null - ? NumberUtil.formatLocalisedNumber((Float) markStats[2], (Locale) null, 2) : "0.00"); + ? NumberUtil.formatLocalisedNumber((Float) markStats[2], (Locale) null, 2) + : "0.00"); } } @@ -1885,7 +1899,7 @@ } return sessionDtos; } - + @Override public List getMarksArray(Long sessionId) { return mcUserDAO.getRawUserMarksBySession(sessionId); @@ -1900,16 +1914,15 @@ /** * Rest call to create a new Multiple Choice content. Required fields in toolContentJSON: "title", "instructions", - * "questions". The questions entry should be JSONArray containing JSON objects, which in turn must contain - * "questionText", "displayOrder" (Integer) and a JSONArray "answers". The answers entry should be JSONArray + * "questions". The questions entry should be ArrayNode containing JSON objects, which in turn must contain + * "questionText", "displayOrder" (Integer) and a ArrayNode "answers". The answers entry should be ArrayNode * containing JSON objects, which in turn must contain "answerText", "displayOrder" (Integer), "correct" (Boolean). * * Retries are controlled by lockWhenFinished, which defaults to true (no retries). */ @SuppressWarnings("unchecked") @Override - public void createRestToolContent(Integer userID, Long toolContentID, JSONObject toolContentJSON) - throws JSONException { + public void createRestToolContent(Integer userID, Long toolContentID, ObjectNode toolContentJSON) { McContent mcq = new McContent(); Date updateDate = new Date(); @@ -1920,37 +1933,38 @@ mcq.setDefineLater(false); mcq.setMcContentId(toolContentID); - mcq.setTitle(toolContentJSON.getString(RestTags.TITLE)); - mcq.setInstructions(toolContentJSON.getString(RestTags.INSTRUCTIONS)); + mcq.setTitle(JsonUtil.optString(toolContentJSON, RestTags.TITLE)); + mcq.setInstructions(JsonUtil.optString(toolContentJSON, RestTags.INSTRUCTIONS)); - mcq.setRetries(JsonUtil.opt(toolContentJSON, "allowRetries", Boolean.FALSE)); + mcq.setRetries(JsonUtil.optBoolean(toolContentJSON, "allowRetries", Boolean.FALSE)); mcq.setUseSelectLeaderToolOuput( - JsonUtil.opt(toolContentJSON, RestTags.USE_SELECT_LEADER_TOOL_OUTPUT, Boolean.FALSE)); - mcq.setReflect(JsonUtil.opt(toolContentJSON, RestTags.REFLECT_ON_ACTIVITY, Boolean.FALSE)); - mcq.setReflectionSubject(JsonUtil.opt(toolContentJSON, RestTags.REFLECT_INSTRUCTIONS, "")); - mcq.setQuestionsSequenced(JsonUtil.opt(toolContentJSON, "questionsSequenced", Boolean.FALSE)); - mcq.setRandomize(JsonUtil.opt(toolContentJSON, "randomize", Boolean.FALSE)); - mcq.setShowReport(JsonUtil.opt(toolContentJSON, "showReport", Boolean.FALSE)); - mcq.setDisplayAnswers(JsonUtil.opt(toolContentJSON, "displayAnswers", Boolean.FALSE)); - mcq.setShowMarks(JsonUtil.opt(toolContentJSON, "showMarks", Boolean.FALSE)); - mcq.setPrefixAnswersWithLetters(JsonUtil.opt(toolContentJSON, "prefixAnswersWithLetters", Boolean.TRUE)); - mcq.setPassMark(JsonUtil.opt(toolContentJSON, "passMark", 0)); + JsonUtil.optBoolean(toolContentJSON, RestTags.USE_SELECT_LEADER_TOOL_OUTPUT, Boolean.FALSE)); + mcq.setReflect(JsonUtil.optBoolean(toolContentJSON, RestTags.REFLECT_ON_ACTIVITY, Boolean.FALSE)); + mcq.setReflectionSubject(JsonUtil.optString(toolContentJSON, RestTags.REFLECT_INSTRUCTIONS, "")); + mcq.setQuestionsSequenced(JsonUtil.optBoolean(toolContentJSON, "questionsSequenced", Boolean.FALSE)); + mcq.setRandomize(JsonUtil.optBoolean(toolContentJSON, "randomize", Boolean.FALSE)); + mcq.setShowReport(JsonUtil.optBoolean(toolContentJSON, "showReport", Boolean.FALSE)); + mcq.setDisplayAnswers(JsonUtil.optBoolean(toolContentJSON, "displayAnswers", Boolean.FALSE)); + mcq.setShowMarks(JsonUtil.optBoolean(toolContentJSON, "showMarks", Boolean.FALSE)); + mcq.setPrefixAnswersWithLetters(JsonUtil.optBoolean(toolContentJSON, "prefixAnswersWithLetters", Boolean.TRUE)); + mcq.setPassMark(JsonUtil.optInt(toolContentJSON, "passMark", 0)); // submissionDeadline is set in monitoring createMc(mcq); // Questions - JSONArray questions = toolContentJSON.getJSONArray(RestTags.QUESTIONS); - for (int i = 0; i < questions.length(); i++) { - JSONObject questionData = (JSONObject) questions.get(i); - McQueContent question = new McQueContent(questionData.getString(RestTags.QUESTION_TEXT), - questionData.getInt(RestTags.DISPLAY_ORDER), 1, "", mcq, null, new HashSet()); + ArrayNode questions = JsonUtil.optArray(toolContentJSON, RestTags.QUESTIONS); + for (JsonNode questionData : questions) { + McQueContent question = new McQueContent(JsonUtil.optString(questionData, RestTags.QUESTION_TEXT), + JsonUtil.optInt(questionData, RestTags.DISPLAY_ORDER), 1, "", mcq, null, + new HashSet()); - JSONArray optionsData = questionData.getJSONArray(RestTags.ANSWERS); - for (int j = 0; j < optionsData.length(); j++) { - JSONObject optionData = (JSONObject) optionsData.get(j); - question.getMcOptionsContents().add(new McOptsContent(optionData.getInt(RestTags.DISPLAY_ORDER), - optionData.getBoolean(RestTags.CORRECT), optionData.getString(RestTags.ANSWER_TEXT), question)); + ArrayNode optionsData = JsonUtil.optArray(questionData, RestTags.ANSWERS); + for (JsonNode optionData : optionsData) { + question.getMcOptionsContents() + .add(new McOptsContent(JsonUtil.optInt(optionData, RestTags.DISPLAY_ORDER), + JsonUtil.optBoolean(optionData, RestTags.CORRECT), + JsonUtil.optString(optionData, RestTags.ANSWER_TEXT), question)); } saveOrUpdateMcQueContent(question); } Index: lams_tool_lamc/src/java/org/lamsfoundation/lams/tool/mc/web/action/McMonitoringAction.java =================================================================== diff -u -r68a00ed36298398bdd7646b440a122d373bd75a1 -r6afcecd6c2b6b9d1b985b371df9947107aa437ae --- lams_tool_lamc/src/java/org/lamsfoundation/lams/tool/mc/web/action/McMonitoringAction.java (.../McMonitoringAction.java) (revision 68a00ed36298398bdd7646b440a122d373bd75a1) +++ lams_tool_lamc/src/java/org/lamsfoundation/lams/tool/mc/web/action/McMonitoringAction.java (.../McMonitoringAction.java) (revision 6afcecd6c2b6b9d1b985b371df9947107aa437ae) @@ -37,14 +37,10 @@ import org.apache.commons.lang.StringEscapeUtils; import org.apache.commons.lang.StringUtils; -import org.apache.log4j.Logger; import org.apache.struts.action.ActionForm; import org.apache.struts.action.ActionForward; import org.apache.struts.action.ActionMapping; import org.apache.struts.action.ActionRedirect; -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.notebook.model.NotebookEntry; import org.lamsfoundation.lams.notebook.service.CoreNotebookConstants; import org.lamsfoundation.lams.tool.exception.ToolException; @@ -62,18 +58,21 @@ import org.lamsfoundation.lams.tool.mc.service.IMcService; import org.lamsfoundation.lams.tool.mc.service.McServiceProxy; import org.lamsfoundation.lams.util.DateUtil; +import org.lamsfoundation.lams.util.JsonUtil; import org.lamsfoundation.lams.util.MessageService; import org.lamsfoundation.lams.util.WebUtil; import org.lamsfoundation.lams.web.action.LamsDispatchAction; import org.lamsfoundation.lams.web.session.SessionManager; import org.lamsfoundation.lams.web.util.AttributeNames; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import com.fasterxml.jackson.databind.node.ObjectNode; + /** * * @author Ozgur Demirtas */ public class McMonitoringAction extends LamsDispatchAction { - private static Logger logger = Logger.getLogger(McMonitoringAction.class.getName()); - /** * displayAnswers */ @@ -82,11 +81,11 @@ IMcService mcService = McServiceProxy.getMcService(getServlet().getServletContext()); String strToolContentID = request.getParameter(AttributeNames.PARAM_TOOL_CONTENT_ID); String contentFolderID = WebUtil.readStrParam(request, AttributeNames.PARAM_CONTENT_FOLDER_ID); - + McContent mcContent = mcService.getMcContent(new Long(strToolContentID)); mcContent.setDisplayAnswers(new Boolean(true)); mcService.updateMc(mcContent); - + // use redirect to prevent resubmition of the same request ActionRedirect redirect = new ActionRedirect(mapping.findForwardConfig("monitoringStarterRedirect")); redirect.addParameter(McAppConstants.TOOL_CONTENT_ID, strToolContentID); @@ -173,7 +172,8 @@ /** * Set Submission Deadline - * @throws IOException + * + * @throws IOException */ public ActionForward setSubmissionDeadline(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws IOException { @@ -214,14 +214,14 @@ * @throws IOException */ public ActionForward setActivityEvaluation(ActionMapping mapping, ActionForm form, HttpServletRequest request, - HttpServletResponse response) throws JSONException, IOException { + HttpServletResponse response) throws IOException { IMcService service = McServiceProxy.getMcService(getServlet().getServletContext()); Long contentID = WebUtil.readLongParam(request, AttributeNames.PARAM_TOOL_CONTENT_ID); String activityEvaluation = WebUtil.readStrParam(request, McAppConstants.ATTR_ACTIVITY_EVALUATION); service.setActivityEvaluation(contentID, activityEvaluation); - JSONObject responseJSON = new JSONObject(); + ObjectNode responseJSON = JsonNodeFactory.instance.objectNode(); responseJSON.put("success", "true"); response.setContentType("application/json;charset=utf-8"); response.getWriter().print(new String(responseJSON.toString())); @@ -270,7 +270,7 @@ * Return paged users for jqGrid. */ public ActionForward getPagedUsers(ActionMapping mapping, ActionForm form, HttpServletRequest request, - HttpServletResponse response) throws JSONException, IOException { + HttpServletResponse response) throws IOException { IMcService mcService = McServiceProxy.getMcService(getServlet().getServletContext()); Long sessionId = WebUtil.readLongParam(request, AttributeNames.PARAM_TOOL_SESSION_ID); @@ -314,38 +314,38 @@ int totalPages = new Double( Math.ceil(new Integer(countVisitLogs).doubleValue() / new Integer(rowLimit).doubleValue())).intValue(); - JSONArray rows = new JSONArray(); + ArrayNode rows = JsonNodeFactory.instance.arrayNode(); int i = 1; for (McUserMarkDTO userDto : userDtos) { - JSONArray visitLogData = new JSONArray(); + ArrayNode visitLogData = JsonNodeFactory.instance.arrayNode(); Long userUid = Long.parseLong(userDto.getQueUsrId()); - visitLogData.put(userUid); - visitLogData.put(userDto.getUserId()); + visitLogData.add(userUid); + visitLogData.add(userDto.getUserId()); String fullName = StringEscapeUtils.escapeHtml(userDto.getFullName()); if (groupLeader != null && groupLeader.getUid().equals(userUid)) { fullName += " (" + mcService.getLocalizedMessage("label.monitoring.group.leader") + ")"; } - visitLogData.put(fullName); + visitLogData.add(fullName); Long totalMark = (userDto.getTotalMark() == null) ? 0 : userDto.getTotalMark(); - visitLogData.put(totalMark); + visitLogData.add(totalMark); + + visitLogData.add(userDto.getPortraitId()); - visitLogData.put(userDto.getPortraitId()); - - JSONObject userRow = new JSONObject(); + ObjectNode userRow = JsonNodeFactory.instance.objectNode(); userRow.put("id", i++); - userRow.put("cell", visitLogData); + userRow.set("cell", visitLogData); - rows.put(userRow); + rows.add(userRow); } - JSONObject responseJSON = new JSONObject(); + ObjectNode responseJSON = JsonNodeFactory.instance.objectNode(); responseJSON.put("total", totalPages); responseJSON.put("page", page); responseJSON.put("records", countVisitLogs); - responseJSON.put("rows", rows); + responseJSON.set("rows", rows); response.setContentType("application/json;charset=utf-8"); response.getWriter().write(responseJSON.toString()); @@ -366,48 +366,49 @@ return null; } - + /** * Get the mark summary with data arranged in bands. Can be displayed graphically or in a table. */ public ActionForward getMarkChartData(ActionMapping mapping, ActionForm form, HttpServletRequest request, - HttpServletResponse res) throws IOException, ServletException, JSONException { + HttpServletResponse res) throws IOException, ServletException { IMcService mcService = McServiceProxy.getMcService(getServlet().getServletContext()); Long contentID = WebUtil.readLongParam(request, AttributeNames.PARAM_TOOL_CONTENT_ID); McContent mcContent = mcService.getMcContent(contentID); List results = null; - - if ( mcContent != null ) { - if ( mcContent.isUseSelectLeaderToolOuput() ) { + + if (mcContent != null) { + if (mcContent.isUseSelectLeaderToolOuput()) { results = mcService.getMarksArrayForLeaders(contentID); } else { Long sessionID = WebUtil.readLongParam(request, AttributeNames.PARAM_TOOL_SESSION_ID); results = mcService.getMarksArray(sessionID); } } - - JSONObject responseJSON = new JSONObject(); - if ( results != null ) - responseJSON.put("data", results); - else - responseJSON.put("data", new Float[0]); + ObjectNode responseJSON = JsonNodeFactory.instance.objectNode(); + if (results != null) { + responseJSON.set("data", JsonUtil.readArray(results)); + } else { + responseJSON.set("data", JsonUtil.readArray(new Float[0])); + } + res.setContentType("application/json;charset=utf-8"); res.getWriter().write(responseJSON.toString()); return null; } - + public ActionForward statistic(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) { IMcService mcService = McServiceProxy.getMcService(getServlet().getServletContext()); Long contentID = WebUtil.readLongParam(request, AttributeNames.PARAM_TOOL_CONTENT_ID); request.setAttribute(AttributeNames.PARAM_TOOL_CONTENT_ID, contentID); McContent mcContent = mcService.getMcContent(contentID); - if ( mcContent != null ) { - if ( mcContent.isUseSelectLeaderToolOuput() ) { + if (mcContent != null) { + if (mcContent.isUseSelectLeaderToolOuput()) { LeaderResultsDTO leaderDto = mcService.getLeaderResultsDTOForLeaders(contentID); request.setAttribute("leaderDto", leaderDto); } else { @@ -416,7 +417,7 @@ } request.setAttribute("useSelectLeaderToolOutput", mcContent.isUseSelectLeaderToolOuput()); } - + // prepare toolOutputDefinitions and activityEvaluation List toolOutputDefinitions = new ArrayList(); toolOutputDefinitions.add(McAppConstants.OUTPUT_NAME_LEARNER_MARK); @@ -428,5 +429,4 @@ return mapping.findForward(McAppConstants.STATISTICS); } - }