Index: lams_build/lib/lams/lams.jar =================================================================== diff -u -rd27ed028b0e16c263776418b7bce22099fed4eed -r46bcb6ea758a272250071d1a571d008c6745e593 Binary files differ Index: lams_central/conf/language/lams/ApplicationResources.properties =================================================================== diff -u -racf3f31918ff9535015d4c4a51afa0a3d86a8f9c -r46bcb6ea758a272250071d1a571d008c6745e593 --- lams_central/conf/language/lams/ApplicationResources.properties (.../ApplicationResources.properties) (revision acf3f31918ff9535015d4c4a51afa0a3d86a8f9c) +++ lams_central/conf/language/lams/ApplicationResources.properties (.../ApplicationResources.properties) (revision 46bcb6ea758a272250071d1a571d008c6745e593) @@ -659,4 +659,23 @@ ckeditor.math.arrows =arrows ckeditor.math.combining =combining +heading.comments=Comments +label.hidden=Comment Hidden +label.reply =Reply +label.hide=Hide +label.show=Show +label.post =Post +label.refresh = Refresh Comments +error.cannot.redisplay.please.refresh =Your changes have been saved but cannot be redisplayed. Please select refresh to reload the comments. +error.please.refresh =An error has occurred. Your post may not have been saved. Please select refresh to start again. +label.showhide.prompt =Show/Hide Replies +message.complete.or.cancel.reply =Please complete or cancel the current reply before starting a new reply. +message.complete.or.cancel.edit =Please complete or cancel the current edit before starting a new edit. +label.show.more.messages =More posts +label.likes=Likes +label.comment.body.validation=The comment must be between 1 and 5000 characters long. +label.edited=Edited +label.like=Like +label.dislike=Dislike +label.no.comments=No Comments #======= End labels: Exported 439 labels for en AU ===== Index: lams_central/conf/language/lams/ApplicationResources_en_AU.properties =================================================================== diff -u -rd27ed028b0e16c263776418b7bce22099fed4eed -r46bcb6ea758a272250071d1a571d008c6745e593 --- lams_central/conf/language/lams/ApplicationResources_en_AU.properties (.../ApplicationResources_en_AU.properties) (revision d27ed028b0e16c263776418b7bce22099fed4eed) +++ lams_central/conf/language/lams/ApplicationResources_en_AU.properties (.../ApplicationResources_en_AU.properties) (revision 46bcb6ea758a272250071d1a571d008c6745e593) @@ -649,4 +649,23 @@ label.select.groups =Select groups +heading.comments=Comments +label.hidden=Comment Hidden +label.reply =Reply +label.hide=Hide +label.show=Show +label.post =Post +label.refresh = Refresh Comments +error.cannot.redisplay.please.refresh =Your changes have been saved but cannot be redisplayed. Please select refresh to reload the comments. +error.please.refresh =An error has occurred. Your post may not have been saved. Please select refresh to start again. +label.showhide.prompt =Show/Hide Replies +message.complete.or.cancel.reply =Please complete or cancel the current reply before starting a new reply. +message.complete.or.cancel.edit =Please complete or cancel the current edit before starting a new edit. +label.show.more.messages =More posts +label.likes=Likes +label.comment.body.validation=The comment must be between 1 and 5000 characters long. +label.edited=Edited +label.like=Like +label.dislike=Dislike +label.no.comments=No Comments #======= End labels: Exported 439 labels for en AU ===== Index: lams_central/conf/xdoclet/struts-actions.xml =================================================================== diff -u -rad752d4543e8954afc126a6ca72c5a3a692f1172 -r46bcb6ea758a272250071d1a571d008c6745e593 --- lams_central/conf/xdoclet/struts-actions.xml (.../struts-actions.xml) (revision ad752d4543e8954afc126a6ca72c5a3a692f1172) +++ lams_central/conf/xdoclet/struts-actions.xml (.../struts-actions.xml) (revision 46bcb6ea758a272250071d1a571d008c6745e593) @@ -10,4 +10,101 @@ > - --> \ No newline at end of file + --> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file Index: lams_central/src/java/org/lamsfoundation/lams/comments/web/CommentAction.java =================================================================== diff -u --- lams_central/src/java/org/lamsfoundation/lams/comments/web/CommentAction.java (revision 0) +++ lams_central/src/java/org/lamsfoundation/lams/comments/web/CommentAction.java (revision 46bcb6ea758a272250071d1a571d008c6745e593) @@ -0,0 +1,620 @@ +/**************************************************************** + * Copyright (C) 2008 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 + * **************************************************************** + */ + +/* $Id$ */ +package org.lamsfoundation.lams.comments.web; + +import java.io.IOException; +import java.util.Iterator; +import java.util.List; + +import javax.servlet.ServletException; +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.Action; +import org.apache.struts.action.ActionForm; +import org.apache.struts.action.ActionForward; +import org.apache.struts.action.ActionMapping; +import org.apache.tomcat.util.json.JSONException; +import org.apache.tomcat.util.json.JSONObject; +import org.lamsfoundation.lams.comments.Comment; +import org.lamsfoundation.lams.comments.CommentLike; +import org.lamsfoundation.lams.comments.dto.CommentDTO; +import org.lamsfoundation.lams.comments.service.ICommentService; +import org.lamsfoundation.lams.comments.util.CommentConstants; +import org.lamsfoundation.lams.security.ISecurityService; +import org.lamsfoundation.lams.tool.GroupedToolSession; +import org.lamsfoundation.lams.tool.ToolAccessMode; +import org.lamsfoundation.lams.tool.service.ILamsCoreToolService; +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.MessageService; +import org.lamsfoundation.lams.util.WebUtil; +import org.lamsfoundation.lams.web.session.SessionManager; +import org.lamsfoundation.lams.web.util.AttributeNames; +import org.lamsfoundation.lams.web.util.SessionMap; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.context.support.WebApplicationContextUtils; + +/** + * @author Fiona Malikoff + */ +public class CommentAction extends Action { + + private static Logger log = Logger.getLogger(CommentAction.class); + + private static IUserManagementService userService; + private static ICommentService commentService; + private static ILamsCoreToolService coreToolService; + private static ISecurityService securityService; + + @Override + public final ActionForward execute(ActionMapping mapping, ActionForm form, HttpServletRequest request, + HttpServletResponse response) throws Exception { + String param = mapping.getParameter(); + if (param.equals("init")) { + return init(mapping, form, request, response); + } + if (param.equals("viewTopic")) { + return viewTopic(mapping, form, request, response); + } + if (param.equals("viewTopicThread")) { + return viewTopicThread(mapping, form, request, response); + } + + if ( param.equals("newComment")) { + return newComment(mapping, form, request, response); + } + + if ( param.equals("newReplyTopic")) { + return newReplyTopic(mapping, form, request, response); + } + if (param.equals("replyTopicInline")) { + return replyTopicInline(mapping, form, request, response); + } + + if ( param.equals("editTopic")) { + return editTopic(mapping, form, request, response); + } + if (param.equals("updateTopicInline")) { + return updateTopicInline(mapping, form, request, response); + } + + if (param.equals("like")) { + return updateLikeCount(mapping, form, request, response, true); + } + if (param.equals("dislike")) { + return updateLikeCount(mapping, form, request, response, false); + } + if (param.equals("hide")) { + return hideComment(mapping, form, request, response, false); + } + + return mapping.findForward("error"); + } + + /** + * Display the comments for a given external id (usually tool session id). The session comments will be + * arranged by Tree structure and loaded thread by thread (with paging). + * + * @param mapping + * @param form + * @param request + * @param response + * @return + * @throws ServletException + */ + @SuppressWarnings("unchecked") + private ActionForward init(ActionMapping mapping, ActionForm form, HttpServletRequest request, + HttpServletResponse response) throws ServletException { + + // initial Session Map + String sessionMapID = request.getParameter(CommentConstants.ATTR_SESSION_MAP_ID); + SessionMap sessionMap; + + Long externalId; + int externalType; + String externalSignature; + String mode; + + // refresh forum page, not initial enter + if (sessionMapID != null) { + sessionMap = (SessionMap) request.getSession().getAttribute(sessionMapID); + externalId = (Long) sessionMap.get(CommentConstants.ATTR_EXTERNAL_ID); + externalType = (Integer) sessionMap.get(CommentConstants.ATTR_EXTERNAL_TYPE); + externalSignature = (String) sessionMap.get(CommentConstants.ATTR_EXTERNAL_SIG); + mode = (String) sessionMap.get(AttributeNames.ATTR_MODE); + + } else { + sessionMap = new SessionMap(); + request.getSession().setAttribute(sessionMap.getSessionID(), sessionMap); + + externalId = WebUtil.readLongParam(request, CommentConstants.ATTR_EXTERNAL_ID); + externalType = WebUtil.readIntParam(request, CommentConstants.ATTR_EXTERNAL_TYPE); + externalSignature = WebUtil.readStrParam(request, CommentConstants.ATTR_EXTERNAL_SIG); + sessionMap.put(CommentConstants.ATTR_EXTERNAL_ID, externalId); + sessionMap.put(CommentConstants.ATTR_EXTERNAL_TYPE, externalType); + sessionMap.put(CommentConstants.ATTR_EXTERNAL_SIG, externalSignature); + + mode = request.getParameter(AttributeNames.ATTR_MODE); + sessionMap.put(AttributeNames.ATTR_MODE, mode != null ? mode : ToolAccessMode.LEARNER.toString()); + } + + User user = getCurrentUser(request); + if ( externalType != CommentConstants.TYPE_TOOL ) + throwException("Unknown comment type ", user.getLogin(), externalId, externalType, externalSignature ); + + Comment rootComment = getCommentService().createOrGetRoot(externalId, externalType, externalSignature, user); + sessionMap.put(CommentConstants.ATTR_ROOT_COMMENT_UID, rootComment.getUid()); + return viewTopicImpl(mapping, form, request, response, sessionMap); + } + + private void throwException(String msg, String loginName, Long externalId, Integer externalType, String externalSignature) throws ServletException { + String error = msg+" User "+loginName+" "+externalId+":"+externalType+":"+externalSignature; + log.error(error); + throw new ServletException(error); + } + + private boolean learnerInToolSession(Long toolSessionId, User user) { + GroupedToolSession toolSession = (GroupedToolSession) getCoreToolService().getToolSessionById(toolSessionId); + return toolSession.getSessionGroup().getUsers().contains(user); + } + + private boolean monitorInToolSession(Long toolSessionId, User user) { + GroupedToolSession toolSession = (GroupedToolSession) getCoreToolService().getToolSessionById(toolSessionId); + return getSecurityService().isLessonMonitor(toolSession.getLesson().getLessonId(), user.getUserId(), "Comment Monitoring Tasks", false); + } + + /** + * Display the comments for a given external id (usually tool session id). The session comments will be + * arranged by Tree structure and loaded thread by thread (with paging). + * + * @param mapping + * @param form + * @param request + * @param response + * @return + */ + @SuppressWarnings("unchecked") + private ActionForward viewTopic(ActionMapping mapping, ActionForm form, HttpServletRequest request, + HttpServletResponse response) { + + String sessionMapID = WebUtil.readStrParam(request, CommentConstants.ATTR_SESSION_MAP_ID); + SessionMap sessionMap = (SessionMap) request.getSession().getAttribute(sessionMapID); + return viewTopicImpl(mapping, form, request, response, sessionMap); + + } + + private ActionForward viewTopicImpl(ActionMapping mapping, ActionForm form, HttpServletRequest request, + HttpServletResponse response, SessionMap sessionMap) { + + Long externalId = (Long) sessionMap.get(CommentConstants.ATTR_EXTERNAL_ID); + Integer externalType = (Integer) sessionMap.get(CommentConstants.ATTR_EXTERNAL_TYPE); + String externalSignature = (String) sessionMap.get(CommentConstants.ATTR_EXTERNAL_SIG); + + commentService = getCommentService(); + + // get user and check they are in the tool session.... + HttpSession ss = SessionManager.getSession(); + UserDTO user = (UserDTO) ss.getAttribute(AttributeNames.USER); + + Long lastMsgSeqId = WebUtil.readLongParam(request, CommentConstants.PAGE_LAST_ID, true); + Integer pageSize = WebUtil.readIntParam(request, CommentConstants.PAGE_SIZE, true); + + setupViewTopicPagedDTOList(request, externalId, externalType, externalSignature, sessionMap, user, lastMsgSeqId, pageSize); + + return mapping.findForward("success"); + } + + @SuppressWarnings("unchecked") + private SessionMap getSessionMap(HttpServletRequest request) { + String sessionMapId = WebUtil.readStrParam(request, CommentConstants.ATTR_SESSION_MAP_ID); + return (SessionMap) request.getSession().getAttribute(sessionMapId); + } + + private void setupViewTopicPagedDTOList(HttpServletRequest request, Long externalId, Integer externalType, String externalSignature, + SessionMap sessionMap, UserDTO user, Long lastMsgSeqId, Integer pageSize) { + + // get root topic list + List msgDtoList = commentService.getTopicThread(externalId, externalType, externalSignature, lastMsgSeqId, pageSize, user.getUserID()); + updateMessageFlag(msgDtoList, user.getUserID()); + request.setAttribute(CommentConstants.ATTR_COMMENT_THREAD, msgDtoList); + + // transfer SessionMapID as well + request.setAttribute(CommentConstants.ATTR_SESSION_MAP_ID, sessionMap.getSessionID()); + } + + /** + * Display the messages for a particular thread in a particular topic. Returns all messages for this thread - does not need paging. + * + * @param mapping + * @param form + * @param request + * @param response + * @return + */ + private ActionForward viewTopicThread(ActionMapping mapping, ActionForm form, HttpServletRequest request, + HttpServletResponse response) { + + commentService = getCommentService(); + + Long highlightMessageUid = WebUtil.readLongParam(request, CommentConstants.ATTR_COMMENT_ID, true); + SessionMap sessionMap = getSessionMap(request); + + // get forum user and forum + UserDTO user = (UserDTO) SessionManager.getSession().getAttribute(AttributeNames.USER); + Long threadId = WebUtil.readLongParam(request, CommentConstants.ATTR_THREAD_ID, true); + List msgDtoList = commentService.getThread(threadId, user.getUserID()); + updateMessageFlag(msgDtoList, user.getUserID()); + request.setAttribute(CommentConstants.ATTR_COMMENT_THREAD, msgDtoList); + + if ( highlightMessageUid != null ) { + request.setAttribute(CommentConstants.ATTR_COMMENT_ID, highlightMessageUid); + } + // transfer SessionMapID as well + request.setAttribute(CommentConstants.ATTR_SESSION_MAP_ID, sessionMap.getSessionID()); + + // don't want to try to scroll as this is a single thread, completely displayed. + request.setAttribute(CommentConstants.ATTR_NO_MORE_PAGES, true); + + return mapping.findForward("success"); + } + + /** + * This method will set the author and voted (has done like/dislike) flag in message DTO + * + * @param msgDtoList + */ + private void updateMessageFlag(List msgDtoList, Integer currUserId) { + Iterator iter = msgDtoList.iterator(); + while (iter.hasNext()) { + CommentDTO dto = iter.next(); + Comment comment = dto.getComment(); + if (comment.getCreatedBy() != null + && currUserId.equals(comment.getCreatedBy().getUserId())) { + dto.setIsAuthor(true); + } else { + dto.setIsAuthor(false); + } + } + } + + /** + * Create a new comment (not a reply) + * + * @param mapping + * @param form + * @param request + * @param response + * @return + * @throws InterruptedException + * @throws JSONException + * @throws IOException + * @throws ServletException + */ + private synchronized ActionForward newComment(ActionMapping mapping, ActionForm form, HttpServletRequest request, + HttpServletResponse response) throws InterruptedException, JSONException, IOException, ServletException { + + SessionMap sessionMap = getSessionMap(request); + Long externalId = (Long) sessionMap.get(CommentConstants.ATTR_EXTERNAL_ID); + Integer externalType = (Integer) sessionMap.get(CommentConstants.ATTR_EXTERNAL_TYPE); + String externalSignature = (String) sessionMap.get(CommentConstants.ATTR_EXTERNAL_SIG); + + String commentText = request.getParameter(CommentConstants.ATTR_BODY); + if ( commentText != null ) + commentText = commentText.trim(); + + JSONObject JSONObject; + + if ( ! validateText(commentText) ) { + JSONObject = getFailedValidationJSON(); + + } else { + + commentService = getCommentService(); + + User user = getCurrentUser(request); + if ( ! learnerInToolSession(externalId, user) ) + throwException("New comment: User does not have the rights to access the comments. ", user.getLogin(), externalId, externalType, externalSignature ); + + Comment rootSeq = commentService.getRoot(externalId, externalType, externalSignature); + + // save message into database + Comment newComment = commentService.createReply(rootSeq, commentText, user); + + JSONObject = new JSONObject(); + JSONObject.put(CommentConstants.ATTR_COMMENT_ID, newComment.getUid()); + JSONObject.put(CommentConstants.ATTR_THREAD_ID, newComment.getThreadComment().getUid()); + JSONObject.put(CommentConstants.ATTR_SESSION_MAP_ID, sessionMap.getSessionID()); + JSONObject.put(CommentConstants.ATTR_PARENT_COMMENT_ID, newComment.getParent().getUid()); + + } + response.setContentType("application/json;charset=utf-8"); + response.getWriter().print(JSONObject); + return null; + } + + /** + * Display replay topic page. Message form subject will include parent topics same subject. + * + * @param mapping + * @param form + * @param request + * @param response + * @return + */ + private ActionForward newReplyTopic(ActionMapping mapping, ActionForm form, HttpServletRequest request, + HttpServletResponse response) { + + request.setAttribute( CommentConstants.ATTR_SESSION_MAP_ID, request.getParameter(CommentConstants.ATTR_SESSION_MAP_ID) ); + request.setAttribute( CommentConstants.ATTR_PARENT_COMMENT_ID, request.getParameter(CommentConstants.ATTR_PARENT_COMMENT_ID) ); + return mapping.findForward("success"); + } + /** + * Create a reply to a parent topic. + * + * @param mapping + * @param form + * @param request + * @param response + * @return + * @throws InterruptedException + * @throws JSONException + * @throws IOException + * @throws ServletException + */ + private synchronized ActionForward replyTopicInline(ActionMapping mapping, ActionForm form, HttpServletRequest request, + HttpServletResponse response) throws InterruptedException, JSONException, IOException, ServletException { + + SessionMap sessionMap = getSessionMap(request); + Long externalId = (Long) sessionMap.get(CommentConstants.ATTR_EXTERNAL_ID); + Integer externalType = (Integer) sessionMap.get(CommentConstants.ATTR_EXTERNAL_TYPE); + String externalSignature = (String) sessionMap.get(CommentConstants.ATTR_EXTERNAL_SIG); + + Long parentId = WebUtil.readLongParam(request, CommentConstants.ATTR_PARENT_COMMENT_ID); + String commentText = WebUtil.readStrParam(request, CommentConstants.ATTR_BODY, true); + + if ( commentText != null ) + commentText = commentText.trim(); + + JSONObject JSONObject; + + if ( ! validateText(commentText) ) { + JSONObject = getFailedValidationJSON(); + + } else { + + commentService = getCommentService(); + User user = getCurrentUser(request); + if ( ! learnerInToolSession(externalId, user) ) + throwException("New comment: User does not have the rights to access the comments. ", user.getLogin(), externalId, externalType, externalSignature ); + + // save message into database + Comment newComment = commentService.createReply(parentId, commentText.trim(), user); + + JSONObject = new JSONObject(); + JSONObject.put(CommentConstants.ATTR_COMMENT_ID, newComment.getUid()); + JSONObject.put(CommentConstants.ATTR_THREAD_ID, newComment.getThreadComment().getUid()); + JSONObject.put(CommentConstants.ATTR_SESSION_MAP_ID, sessionMap.getSessionID()); + JSONObject.put(CommentConstants.ATTR_PARENT_COMMENT_ID, newComment.getParent().getUid()); + + } + + response.setContentType("application/json;charset=utf-8"); + response.getWriter().print(JSONObject); + return null; + } + + private boolean validateText(String commentText) { + return commentText != null & commentText.length() > 0 && commentText.length() < CommentConstants.MAX_BODY_LENGTH; + } + + private JSONObject getFailedValidationJSON() throws JSONException { + MessageService msgService = getCommentService().getMessageService(); + JSONObject JSONObject = new JSONObject(); + JSONObject.put(CommentConstants.ATTR_ERR_MESSAGE, msgService.getMessage(CommentConstants.KEY_BODY_VALIDATION)); + return JSONObject; + } + + /** + * Display a editable form for a topic in order to update it. + * + * @param mapping + * @param form + * @param request + * @param response + * @return + * @throws PersistenceException + */ + public ActionForward editTopic(ActionMapping mapping, ActionForm form, HttpServletRequest request, + HttpServletResponse response) { + + Long commentId = WebUtil.readLongParam(request, CommentConstants.ATTR_COMMENT_ID); + CommentDTO comment = getCommentService().getComment(commentId); + + SessionMap sessionMap = getSessionMap(request); + sessionMap.put(CommentConstants.ATTR_COMMENT_ID, commentId); + request.setAttribute(CommentConstants.ATTR_COMMENT_ID, commentId); + request.setAttribute(CommentConstants.ATTR_COMMENT, comment); + request.setAttribute(CommentConstants.ATTR_SESSION_MAP_ID, sessionMap.getSessionID()); + return mapping.findForward("success"); + } + + /** + * Update a topic. + * @throws ServletException + */ + public ActionForward updateTopicInline(ActionMapping mapping, ActionForm form, HttpServletRequest request, + HttpServletResponse response) throws JSONException, IOException, ServletException { + + commentService = getCommentService(); + SessionMap sessionMap = getSessionMap(request); + Long commentId = WebUtil.readLongParam(request, CommentConstants.ATTR_COMMENT_ID); + + Long externalId = (Long) sessionMap.get(CommentConstants.ATTR_EXTERNAL_ID); + Integer externalType = (Integer) sessionMap.get(CommentConstants.ATTR_EXTERNAL_TYPE); + String externalSignature = (String) sessionMap.get(CommentConstants.ATTR_EXTERNAL_SIG); + + String commentText = request.getParameter(CommentConstants.ATTR_BODY); + if ( commentText != null ) + commentText = commentText.trim(); + + JSONObject JSONObject; + + if ( ! validateText(commentText) ) { + JSONObject = getFailedValidationJSON(); + + } else { + + CommentDTO originalComment = commentService.getComment(commentId); + + boolean monitoringMode = ToolAccessMode.TEACHER.equals(WebUtil.getToolAccessMode((String)sessionMap.get(AttributeNames.ATTR_MODE))); + User user = getCurrentUser(request); + if ( ! originalComment.getComment().getCreatedBy().equals(user) && ! ( monitoringMode && monitorInToolSession(externalId, user) ) ) + throwException("Update comment: User does not have the rights to update the comment "+commentId+". ", user.getLogin(), externalId, externalType, externalSignature ); + + Comment updatedComment = commentService.updateComment(commentId, commentText, user, monitoringMode); + + JSONObject = new JSONObject(); + JSONObject.put(CommentConstants.ATTR_COMMENT_ID, commentId); + JSONObject.put(CommentConstants.ATTR_SESSION_MAP_ID, sessionMap.getSessionID()); + JSONObject.put(CommentConstants.ATTR_THREAD_ID, updatedComment.getThreadComment().getUid()); + JSONObject.put(CommentConstants.ATTR_PARENT_COMMENT_ID, updatedComment.getParent().getUid()); + + } + + response.setContentType("application/json;charset=utf-8"); + response.getWriter().print(JSONObject); + return null; + } + + /** + * Update the likes/dislikes + * @throws ServletException + */ + private synchronized ActionForward updateLikeCount(ActionMapping mapping, ActionForm form, HttpServletRequest request, + HttpServletResponse response, boolean isLike) throws InterruptedException, JSONException, IOException, ServletException { + + SessionMap sessionMap = getSessionMap(request); + Long messageUid = WebUtil.readLongParam(request, CommentConstants.ATTR_COMMENT_ID); + Long externalId = (Long) sessionMap.get(CommentConstants.ATTR_EXTERNAL_ID); + + commentService = getCommentService(); + + User user = getCurrentUser(request); + if ( ! learnerInToolSession(externalId, user) ) + throwException("Update comment: User does not have the rights to like/dislike the comment "+messageUid+". ", user.getLogin(), externalId, + (Integer) sessionMap.get(CommentConstants.ATTR_EXTERNAL_TYPE), (String) sessionMap.get(CommentConstants.ATTR_EXTERNAL_SIG) ); + + boolean added = commentService.addLike(messageUid, user, isLike ? CommentLike.LIKE : CommentLike.DISLIKE); + + JSONObject JSONObject = new JSONObject(); + JSONObject.put(CommentConstants.ATTR_COMMENT_ID, messageUid); + JSONObject.put(CommentConstants.ATTR_STATUS, added); + response.setContentType("application/json;charset=utf-8"); + response.getWriter().print(JSONObject); + return null; + } + + /** + * Update hide flag + * @throws ServletException + */ + private synchronized ActionForward hideComment(ActionMapping mapping, ActionForm form, HttpServletRequest request, + HttpServletResponse response, boolean isLike) throws InterruptedException, JSONException, IOException, ServletException { + + SessionMap sessionMap = getSessionMap(request); + Long commentId = WebUtil.readLongParam(request, CommentConstants.ATTR_COMMENT_ID); + Long externalId = (Long) sessionMap.get(CommentConstants.ATTR_EXTERNAL_ID); + boolean status = WebUtil.readBooleanParam(request, CommentConstants.ATTR_HIDE_FLAG); + + commentService = getCommentService(); + + boolean monitoringMode = ToolAccessMode.TEACHER.equals(WebUtil.getToolAccessMode((String)sessionMap.get(AttributeNames.ATTR_MODE))); + User user = getCurrentUser(request); + if ( ! monitoringMode || ! monitorInToolSession(externalId, user) ) + throwException("Update comment: User does not have the rights to hide the comment "+commentId+". ", user.getLogin(), externalId, + (Integer) sessionMap.get(CommentConstants.ATTR_EXTERNAL_TYPE), (String) sessionMap.get(CommentConstants.ATTR_EXTERNAL_SIG) ); + + Comment updatedComment = commentService.hideComment(commentId, user, status); + + JSONObject JSONObject = new JSONObject(); + JSONObject.put(CommentConstants.ATTR_COMMENT_ID, updatedComment.getUid()); + JSONObject.put(CommentConstants.ATTR_SESSION_MAP_ID, sessionMap.getSessionID()); + JSONObject.put(CommentConstants.ATTR_THREAD_ID, updatedComment.getThreadComment().getUid()); + JSONObject.put(CommentConstants.ATTR_PARENT_COMMENT_ID, updatedComment.getParent().getUid()); + + response.setContentType("application/json;charset=utf-8"); + response.getWriter().print(JSONObject); + return null; + } + + /** + * Get login user information from system level session. + */ + private User getCurrentUser(HttpServletRequest request) { + UserDTO user = (UserDTO) SessionManager.getSession().getAttribute(AttributeNames.USER); + return getUserService().getUserByLogin(user.getLogin()); + } + + private IUserManagementService getUserService() { + if (CommentAction.userService == null) { + WebApplicationContext ctx = WebApplicationContextUtils.getRequiredWebApplicationContext(getServlet() + .getServletContext()); + CommentAction.userService = (IUserManagementService) ctx.getBean("userManagementService"); + } + return CommentAction.userService; + } + private ICommentService getCommentService() { + if (CommentAction.commentService == null) { + WebApplicationContext ctx = WebApplicationContextUtils.getRequiredWebApplicationContext(getServlet() + .getServletContext()); + CommentAction.commentService = (ICommentService) ctx.getBean("commentService"); + } + return CommentAction.commentService; + } + + private ILamsCoreToolService getCoreToolService() { + if (CommentAction.coreToolService == null) { + WebApplicationContext ctx = WebApplicationContextUtils.getRequiredWebApplicationContext(getServlet() + .getServletContext()); + CommentAction.coreToolService = (ILamsCoreToolService) ctx.getBean("lamsCoreToolService"); + } + return CommentAction.coreToolService; + } + + private ISecurityService getSecurityService() { + if (CommentAction.securityService == null) { + WebApplicationContext ctx = WebApplicationContextUtils.getRequiredWebApplicationContext(getServlet() + .getServletContext()); + CommentAction.securityService = (ISecurityService) ctx.getBean("securityService"); + } + return CommentAction.securityService; + } + + + +} \ No newline at end of file Index: lams_central/src/java/org/lamsfoundation/lams/comments/web/CommentForm.java =================================================================== diff -u --- lams_central/src/java/org/lamsfoundation/lams/comments/web/CommentForm.java (revision 0) +++ lams_central/src/java/org/lamsfoundation/lams/comments/web/CommentForm.java (revision 46bcb6ea758a272250071d1a571d008c6745e593) @@ -0,0 +1,102 @@ +/**************************************************************** + * 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 + * **************************************************************** + */ + +/* $Id$ */ +package org.lamsfoundation.lams.comments.web; + +import org.apache.commons.lang.StringUtils; +import org.apache.log4j.Logger; +import org.apache.struts.action.ActionErrors; +import org.apache.struts.action.ActionMapping; +import org.apache.struts.action.ActionMessage; +import org.apache.struts.validator.ValidatorForm; +import org.lamsfoundation.lams.comments.Comment; + +/** + * Comment Form. + */ +public class CommentForm extends ValidatorForm { + private static final long serialVersionUID = -9054365604649146734L; + private static Logger logger = Logger.getLogger(CommentForm.class.getName()); + + protected Comment comment; + protected String sessionMapID; + protected Long parentId; + protected Long topicId; + + public CommentForm() { + comment = new Comment(); + } + + /** + * MessageForm validation method from STRUCT interface. + * + */ + @Override + public ActionErrors validate(ActionMapping mapping, javax.servlet.http.HttpServletRequest request) { + ActionErrors errors = new ActionErrors(); + try { + if (StringUtils.isBlank(comment.getBody())) { + ActionMessage error = new ActionMessage("error.body.required"); + errors.add("message.body", error); + } + } catch (Exception e) { + CommentForm.logger.error("", e); + } + return errors; + } + + // -------------------------get/set methods---------------- + public void setComment(Comment comment) { + this.comment = comment; + } + + public Comment getComment() { + return comment; + } + + public void setParentId(Long parentId) { + this.parentId = parentId; + } + + public Long getParentId() { + return this.parentId; + } + + public void setTopicId(Long topicId) { + this.topicId = topicId; + } + + public Long getTopicId() { + return this.topicId; + } + + public String getSessionMapID() { + return sessionMapID; + } + + public void setSessionMapID(String sessionMapID) { + this.sessionMapID = sessionMapID; + } + +} Index: lams_central/web/WEB-INF/tags/Comments.tag =================================================================== diff -u --- lams_central/web/WEB-INF/tags/Comments.tag (revision 0) +++ lams_central/web/WEB-INF/tags/Comments.tag (revision 46bcb6ea758a272250071d1a571d008c6745e593) @@ -0,0 +1,31 @@ +<%@ taglib uri="tags-core" prefix="c"%> +<%@ taglib uri="tags-lams" prefix="lams"%> +<%@ taglib uri="tags-function" prefix="fn" %> + +<%@ attribute name="toolSessionId" required="true" rtexprvalue="true"%> +<%@ attribute name="toolSignature" required="true" rtexprvalue="true"%> +<%@ attribute name="height" required="false" rtexprvalue="true"%> +<%@ attribute name="width" required="false" rtexprvalue="true"%> +<%@ attribute name="mode" required="false" rtexprvalue="true"%> + + + + + + + + + + + + &mode=${mode} + + + + + \ No newline at end of file Index: lams_central/web/WEB-INF/tlds/lams/lams.tld =================================================================== diff -u -r606581007abbdaee5483777096f32ffac1c3e3eb -r46bcb6ea758a272250071d1a571d008c6745e593 --- lams_central/web/WEB-INF/tlds/lams/lams.tld (.../lams.tld) (revision 606581007abbdaee5483777096f32ffac1c3e3eb) +++ lams_central/web/WEB-INF/tlds/lams/lams.tld (.../lams.tld) (revision 46bcb6ea758a272250071d1a571d008c6745e593) @@ -481,5 +481,9 @@ head /WEB-INF/tags/Head.tag + + Comments + /WEB-INF/tags/Comments.tag + Index: lams_central/web/comments/comments.jsp =================================================================== diff -u --- lams_central/web/comments/comments.jsp (revision 0) +++ lams_central/web/comments/comments.jsp (revision 46bcb6ea758a272250071d1a571d008c6745e593) @@ -0,0 +1,76 @@ + + +<%@ taglib uri="tags-lams" prefix="lams"%> +<%@ taglib uri="tags-core" prefix="c"%> +<%@ taglib uri="tags-html" prefix="html"%> +<%@ taglib uri="tags-fmt" prefix="fmt"%> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + <%@ include file="new.jsp"%> + + + + + + + + + + + + <%@ include file="topicview.jsp"%> + + + + + + + Index: lams_central/web/comments/edit.jsp =================================================================== diff -u --- lams_central/web/comments/edit.jsp (revision 0) +++ lams_central/web/comments/edit.jsp (revision 46bcb6ea758a272250071d1a571d008c6745e593) @@ -0,0 +1,80 @@ +<%@ taglib uri="tags-lams" prefix="lams"%> +<%@ taglib uri="tags-core" prefix="c"%> +<%@ taglib uri="tags-html" prefix="html"%> +<%@ taglib uri="tags-fmt" prefix="fmt"%> + +<%@ page import="org.lamsfoundation.lams.comments.util.CommentConstants"%> + + + + +"> + ${comment.comment.body} + + + + + + + + + + + + + + + + + + Index: lams_central/web/comments/msgview.jsp =================================================================== diff -u --- lams_central/web/comments/msgview.jsp (revision 0) +++ lams_central/web/comments/msgview.jsp (revision 46bcb6ea758a272250071d1a571d008c6745e593) @@ -0,0 +1,132 @@ +<%-- commentDto, commentUid, msgLevel needs to be in the session elsewhere --%> + + + + + + + + + + + + + + + highlight + + + + + + + px;"> + + + + + + + + + + + + + () + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Reply + · + + + + + + + Edit + · + + + + + + + + + + + + + + + + + + + + + · + + + ${commentDto.comment.likeCount} + + + + + " + onclick="javascript:likeEntry(${commentDto.comment.uid});" id="msglikebutton${commentDto.comment.uid}"> + " + onclick="javascript:dislikeEntry(${commentDto.comment.uid});" id="msgdislikebutton${commentDto.comment.uid}"> + + + " + id="msglikebutton${commentDto.comment.uid}"> + + + " + id="msgdislikebutton${commentDto.comment.uid}"> + + + + + + + + + Index: lams_central/web/comments/msgviewwrapper.jsp =================================================================== diff -u --- lams_central/web/comments/msgviewwrapper.jsp (revision 0) +++ lams_central/web/comments/msgviewwrapper.jsp (revision 46bcb6ea758a272250071d1a571d008c6745e593) @@ -0,0 +1,25 @@ + +<%@ taglib uri="tags-lams" prefix="lams"%> +<%@ taglib uri="tags-core" prefix="c"%> +<%@ taglib uri="tags-html" prefix="html"%> +<%@ taglib uri="tags-fmt" prefix="fmt"%> + +<%-- Wraps up msgview.jsp for returning a single message - called when an edit is performed. It needs to do all the setup that topicview.jsp normally does. --%> + + + + + + + + <%@ include file="msgview.jsp"%> + + Index: lams_central/web/comments/new.jsp =================================================================== diff -u --- lams_central/web/comments/new.jsp (revision 0) +++ lams_central/web/comments/new.jsp (revision 46bcb6ea758a272250071d1a571d008c6745e593) @@ -0,0 +1,91 @@ +<%@ taglib uri="tags-lams" prefix="lams"%> +<%@ taglib uri="tags-core" prefix="c"%> +<%@ taglib uri="tags-html" prefix="html"%> +<%@ taglib uri="tags-fmt" prefix="fmt"%> + +<%@ page import="org.lamsfoundation.lams.comments.util.CommentConstants"%> + + + + +"> + + + + + + + + + + + + + + + + + + Index: lams_central/web/comments/reply.jsp =================================================================== diff -u --- lams_central/web/comments/reply.jsp (revision 0) +++ lams_central/web/comments/reply.jsp (revision 46bcb6ea758a272250071d1a571d008c6745e593) @@ -0,0 +1,76 @@ +<%@ taglib uri="tags-lams" prefix="lams"%> +<%@ taglib uri="tags-core" prefix="c"%> +<%@ taglib uri="tags-html" prefix="html"%> +<%@ taglib uri="tags-fmt" prefix="fmt"%> + +<%@ page import="org.lamsfoundation.lams.comments.util.CommentConstants"%> + + + + +"> + + + + + + + + + + + + + + + + Index: lams_central/web/comments/topicview.jsp =================================================================== diff -u --- lams_central/web/comments/topicview.jsp (revision 0) +++ lams_central/web/comments/topicview.jsp (revision 46bcb6ea758a272250071d1a571d008c6745e593) @@ -0,0 +1,166 @@ +<%@ page import="org.lamsfoundation.lams.comments.util.CommentConstants"%> + + + + + + + + +expandable:true,initialState:'expanded', + expanderTemplate:' ${prompt}', + stringCollapse:'${hide}',stringExpand:'${show}', + clickableNodeNames:true,indent:${indent}, + onNodeInitialized:function() { + if (this.level() >= 2) { + this.collapse(); + } + } + + + + + + + + + + + + + + + <%-- same test & command appears at bottom of script --%> + + + + + + + + + + + + + + + + <%@ include file="msgview.jsp"%> + + + + + + + + + + + + + + + + + " class="button"> + + Index: lams_central/web/comments/topicviewwrapper.jsp =================================================================== diff -u --- lams_central/web/comments/topicviewwrapper.jsp (revision 0) +++ lams_central/web/comments/topicviewwrapper.jsp (revision 46bcb6ea758a272250071d1a571d008c6745e593) @@ -0,0 +1,19 @@ + + + + +<%@ taglib uri="tags-lams" prefix="lams"%> +<%@ taglib uri="tags-core" prefix="c"%> +<%@ taglib uri="tags-html" prefix="html"%> +<%@ taglib uri="tags-fmt" prefix="fmt"%> + + + + + + + + +<%@ include file="topicview.jsp"%> + + Index: lams_central/web/css/defaultHTML_learner.css =================================================================== diff -u -r53d60d5d44af50b85d6ed3ed5ce4941f2bc1b611 -r46bcb6ea758a272250071d1a571d008c6745e593 --- lams_central/web/css/defaultHTML_learner.css (.../defaultHTML_learner.css) (revision 53d60d5d44af50b85d6ed3ed5ce4941f2bc1b611) +++ lams_central/web/css/defaultHTML_learner.css (.../defaultHTML_learner.css) (revision 46bcb6ea758a272250071d1a571d008c6745e593) @@ -616,4 +616,52 @@ /* jQuery tablesorter/pager/filter defaults */ .tablesorter .disabled { display: none +} + +/* Comments Widget */ +iframe.commentFrame{ + border: 0 none; + margin: 0; + padding: 0; + overflow: hidden; +} + + +.comment-author { + color:#0087e5; + font-size:11px; + margin-bottom:2px; +} + +.comment-date { + font-size:11px; + margin-bottom:2px; +} + +table.comment { + width:100%; + margin-left:0px; + padding-top:0px; + margin-bottom:5px; + text-align:left; +} + +table.comment td { + padding:0px; + padding-left:0px; + font-size:11px; + vertical-align:top; +} +a.comment { + color: #47bc23; + text-decoration: none; + border-bottom: none; +} +textarea.comment { + width: 99%; + height: 60px; + margin-bottom:10px; +} +div.comment-entry { + width: 98%; } \ No newline at end of file Index: lams_central/web/css/defaultHTML_learner_mobile.css =================================================================== diff -u -r53d60d5d44af50b85d6ed3ed5ce4941f2bc1b611 -r46bcb6ea758a272250071d1a571d008c6745e593 --- lams_central/web/css/defaultHTML_learner_mobile.css (.../defaultHTML_learner_mobile.css) (revision 53d60d5d44af50b85d6ed3ed5ce4941f2bc1b611) +++ lams_central/web/css/defaultHTML_learner_mobile.css (.../defaultHTML_learner_mobile.css) (revision 46bcb6ea758a272250071d1a571d008c6745e593) @@ -1,19 +1,71 @@ +/* +************************************************* +LAMS 2.0 +Date modified: 22/09/2006 + +************************************************* +General page styles +********************************************** */ + +* { + margin:0; + padding:0; +} + body { - font-size: 16px; + font-size: 11px; font-family: verdana, arial, helvetica, sans-serif; } -body.large-font {font-size: 16px;} +body.stripes { + background: url('../images/css/diagonal_bg.gif'); +} -h2 {/*color:#47bc23; */ - font-size:17px; +a { + color: #47bc23; + text-decoration: none; + border-bottom: 1px dotted #47bc23; +} + +a:hover { + color: #47bc23; + text-decoration: none; + border-bottom: 1px solid #47bc23; +} + +/* CKEditor buttons */ +a.cke_button { + border-bottom: none; +} +a.cke_combo_button{ + border-bottom: 1px solid #e1e1e1; +} + +.row { + voice-family: "\"}\""; /* hide the following rules from IE5 */ + voice-family: inherit; + overflow: hidden; /* CSS 'clearing hack' - works on all except for IE5, that's why the IE5 box model hack above */ +} + +* html .row { + height: 1%; +} /* part of the CSS 'clearing hack' */ + + +h1 {color:#0087e5; + font-size:15px; + margin-bottom:5px; + } + +h2 {color:#47bc23; + font-size:14px; padding-top:10px; - margin-bottom:15px; } + margin-bottom:5px; } span.h2font {color:#47bc23; font-size:14px; } -h3 {/*color:#47bc23; */ +h3 {color:#47bc23; font-size:13px; margin-top:10px; margin-bottom:5px; } @@ -26,78 +78,142 @@ padding:0; } -h5 {/*color:#47bc23; */ +h5 {color:#47bc23; font-size:14px; margin-right:40px; margin-bottom:8px; } - + +/*defines when instructions are given and places a blue arrow before the text */ +p.instructions { + color: #0087e5; + margin-bottom:0; + margin-top:0; + background: url('../images/css/blue_arrow_right.gif') no-repeat 0 0px; + padding-left: 30px; + padding-bottom: 10px; + font-size:13px; +} + p { margin-right: 30px; + margin-bottom: 10px; margin-top:10px; -} - -.regular-li { font-size: 14px; font-weight: normal; } +} -/**********************layout of the table with the alternate color for the table heading cell *********************/ +b.error {color:#cc0000;} + +ul { + margin-left: 25px; + margin-right: 30px; +} + +ul li { list-style-type:circle;} + +ol { + margin-left: 25px; + margin-right: 30px; +} + +li.no-list-type { + list-style-type: none; +} +/*layout of the table with the alternate color for the table heading cell */ table.alternative-color { width:100%; margin-left:0px; padding-top:0px; margin-bottom:15px; + margin-top:10px; text-align:left; } table.alternative-color th { - - font-size: 12px; + background: url('../images/css/green_bg.jpg') repeat-x; height: 30px; + color: #47bc23; + padding-left: 15px; + border-left: 1px solid #cacdd1; + border-right: 1px solid #fff; + font-size: 12px; } table.alternative-color td { - padding:8px 5px; - font-size: 12px; + padding:5px; + padding-left: 20px; + font-size: 11px; border-bottom: 1px solid #efefef; - vertical-align: middle; + vertical-align: top; background:url('../images/css/greyfade_bg.jpg') repeat-x 3px 0px } table.alternative-color td.first, table.alternative-color th.first { border-left: none; - padding-left: 2px; + padding-left: 20px; } -table.alternative-color .ui-btn-up-c { -font-weight: normal; cursor: default; + +/* jquery tablesorter pager plugin */ +table.tablesorter-admin td { + text-align: center; + padding:5px; + padding-left: 20px; + font-size: 11px; + border-bottom: 1px solid #efefef; + vertical-align: top; + background:url('../images/css/greyfade_bg.jpg') repeat-x 3px 0px } -table.table-content-padding-left th {padding-left: 8px; } -table.table-content-padding-left td {padding-left: 10px; } +table.tablesorter-admin th { + background-color: #e7f3cf; + height: 30px; + color: #47bc23; + padding-left: 15px; + border-top: 1px solid #cacdd1; + border-left: 1px solid #cacdd1; + border-right: 1px solid #fff; + font-size: 12px; +} +table.tablesorter-admin thead tr .header { + background-image: url(../images/css/bg.gif); + background-repeat: no-repeat; + background-position: center right; + cursor: pointer; + width: auto; + padding-right: 15px; +} -b.error {color:#cc0000;} +table.tablesorter-admin thead tr .headerSortUp { + background-image: url(../images/css/asc.gif); +} -.float-left {float:left;} +table.tablesorter-admin thead tr .headerSortDown { + background-image: url(../images/css/desc.gif); +} -.float-right {float:right;} - -/*******************************layout of the forum tables with the colored table heading cell ********************/ +/*layout of the forum tables with the colored table heading cell */ table.forum { - width:93%; + width:100%; + margin-left:0px; + padding-top:5px; margin-bottom:10px; background:url('../images/css/greyfade_bg.jpg') repeat-x 3px 49px; text-align:left; border-bottom:1px solid #efefef;} table.forum th { - height:20px; - padding-top:5px; + background: url('../images/css/green_bg_forum.jpg') repeat-x #f0f5df; + height:26px; + padding-top:1px; + color:#47bc23; padding-left:15px; border-left:1px solid #cacdd1; font-size:12px; vertical-align:middle; } table.forum td.posted-by { + background: url('../images/css/forum_postedby_bg.jpg') repeat-x; height:20px; padding-left:15px; padding-top:0px; @@ -108,23 +224,76 @@ } table.forum td { + padding:10px; padding-left:20px; font-size:11px; vertical-align:top; } + +/*layout of an inserted image */ +.image-border { + border:1px solid #e2e2e2; + padding:3px; + background-color:#fff; + margin:5px 10px; +} -.shading-bg {padding:5px; padding-left:10px; padding-top:10px; margin-top:10px; margin-bottom:10px; width:99%; border-top:1px solid #ccc; background:url('../images/css/greyfade_bg.jpg') repeat-x 3px 1px} +/*layout of the table with form elements */ -.shading-bg td {padding:5px;} +table { + width:100%; + text-align:left; + } -.big-space-top { - padding-top: 30px; +td { + + font-size:11px; + vertical-align:top; } -.space-top { - padding-top: 20px; +td p{ + padding:10px; + font-size:11px; + line-height:18px; } +/*layout of button in table */ +td p.button{ + margin:0px; + padding:10px 10px 10px 0px; + font-size:11px; + font-family: verdana, arial, helvetica, sans-serif; +} + +/* */ /*layout of form styles */ +.field-name{ + color: #0087e5; + font-weight: bold; + text-align: left; + padding-bottom:10px; +} + +.field-name-alternative-color { + color: #47bc23; + font-weight: bold; + text-align: left; +} + +input { + border: 1px solid #c1c1c1; + padding: 2px; +} + +textarea { + border: 1px solid #c1c1c1; + padding: 2px; +} + +.noBorder { + border: none; +} + +/*misc styles*/ .space-right { margin-right: 10px; } @@ -137,21 +306,20 @@ margin-bottom: 40px; } + .space-bottom-top { padding-bottom: 20px; padding-top: 20px; } .small-space-bottom { - margin-bottom: 10px; + margin-bottom: 20px; } -.float-left {float:left;} - -.float-right {float:right;} - .small-space-top {padding-top:6px;} +.space-top {padding-top:20px;} + .title-space-top {padding-top:12px;} hr {border:0px; @@ -162,9 +330,7 @@ .warning { color: #cc0000; padding:8px 10px 10px 40px; - margin-top: 25px; - margin-left: auto; - margin-right: auto; + margin: 25px auto 10px; text-align: left; font-weight: normal; background: url('../images/css/warning.gif') no-repeat #ffeae0 10px 8px; @@ -210,16 +376,292 @@ border: 1px solid #C0C0C0; } -.ui-finishbtn-right { float: right;padding-right: 40px; min-height: 30px;} +.group-box { + padding:8px 10px 10px 40px; + margin-top: 20px; + margin-left: 0px; + margin-right: 0px; + font-weight: normal; + border: 1px solid #ccc; +} -.button-inside {font-size: 16px;} +/************************************************* +Page layout +********************************************** */ -#qaQuestions {padding-top: 45px; padding-bottom: 20px;} -#qaAnswers {padding-top: 20px; padding-bottom: 20px;} -#qaQuestionsSequential{padding-top: 30px; padding-bottom: 0px;} -.finishButtonDiv {text-align: right; margin: 5px 13px;} +#content { + margin-top:20px; + margin-left:auto; + margin-right:auto; + margin-bottom:30px; + width:85%; + height:100%; + border:1px solid #d4d8da; + background-color:#fff; + padding:20px 25px; + } + +#footer { + clear:inherit; + height:20px;} + +.float-left {float:left;} + +.float-right {float:right;} + + + +/* +************************************************* + +Buttons + +********************************************** */ + +.left-buttons {float:left;} + +.right-buttons {float:right;} + +/*layout of a general button*/ +#content a.button { + background: url('../images/css/btn_off.gif') repeat-x 0px 0px; + color: #333; + border: 0px none; + padding:5px 20px 6px 20px; + text-decoration: none; +} + +/* style to disable input buttons */ +#content a.button .disabled { + color: #999; + background: url('../images/css/btn_off.gif') repeat-x 0px 0px; + border: 0px none; + padding:5px 20px 6px 20px; + } + +#content a:hover.button { + background: url('../images/css/btn_over.gif') repeat-x 0px 0px; + color: #349018; + border: 0px none; + padding:5px 20px 6px 20px; + text-decoration: none; +} + +#content a.button { + background: url('../images/css/btn_off.gif') repeat-x 0px 0px; + color: #333; + border: 0px none; + padding:5px 20px 6px 20px; + text-decoration: none; +} + +input.disabled { + color: #999; + background: url('../images/css/btn_off.gif') repeat-x 0px 0px; + border: 0px none; + padding:5px 20px 6px 20px; + font-size:11px; +} + +/*layout of a general button*/ +a.button { + background: url('../images/css/btn_off.gif') repeat-x 0px 0px; + color: #333; + border: 0px none; + padding:5px 20px 6px 20px; + text-decoration: none; +} + +a:hover.button { + background: url('../images/css/btn_over.gif') repeat-x 0px 0px; + color: #349018; + border: 0px none; + padding:5px 20px 6px 20px; + text-decoration: none; +} + + +/*layout of adding a topic button */ +a.button-add-item { + background: url('../images/css/btn_add_off.gif') no-repeat 0px 20px; + color: #333; + border: 0px none; + padding:25px 15px 25px 25px; + text-decoration: none; +} + +a:hover.button-add-item { + background: url('../images/css/btn_add_over.gif') no-repeat 0px 20px; + color: #349018; + border: 0px none; + padding:25px 15px 25px 25px; + text-decoration: none; +} + + +/*layout of adding a topic button */ +#content a.button-add-item { + background: url('../images/css/btn_add_off.gif') no-repeat 0px 20px; + color: #333; + border: 0px none; + padding:25px 15px 25px 25px; +} + +#content a:hover.button-add-item { + background: url('../images/css/btn_add_over.gif') no-repeat 0px 20px; + color: #349018; + border: 0px none; + padding:25px 15px 25px 25px; +} + +/*layout of html editor button */ +#content a.button-html-editor { + background: url('../images/css/btn_htmled_off.gif') no-repeat 0px 20px; + color: #666; + border: 0px none; + padding:25px 15px 25px 28px; +} + +#content a:hover.button-html-editor { + background: url('../images/css/btn_htmled_off.gif') no-repeat 0px 20px; + color: #333; + border: 0px none; + padding:25px 15px 25px 28px; +} + +/* Hovering submit button */ +input.button { + background: url('../images/css/btn_off.gif') repeat-x 0px 0px; + color: #333; + border: 0px none; + padding:5px 20px 6px 20px; + text-decoration: none; + font-size: 11px; + font-family: verdana, arial, helvetica, sans-serif; +} + +input:hover.button { + background: url('../images/css/btn_over.gif') repeat-x 0px 0px; + color: #349018; + border: 0px none; + padding:5px 20px 6px 20px; + text-decoration: none; + font-size: 11px; + font-family: verdana, arial, helvetica, sans-serif; + cursor: pointer; +} + +.editForm { + padding: 0 0 5px 18px; + background: url('../images/css/report_edit.png') no-repeat; + +} + +.close { + padding: 0 0 5px 18px; + background: url('../images/css/cancel.png') no-repeat; + +} + +.import { + padding: 0 0 5px 18px; + background: url('../images/css/import.png') no-repeat; + +} + + +.nextActivity { + padding: 0px 20px 0px 0px; + background: url('../images/css/next.png') no-repeat; + background-position: right; + +} + +.text-area {width:99%} + +.shading-bg {padding:5px; padding-left:10px; padding-top:10px; margin-top:10px; margin-bottom:10px; width:99%; border-top:1px solid #ccc; background:url('../images/css/greyfade_bg.jpg') repeat-x 3px 1px} + +.shading-bg td {padding:5px;} + +.last-item {border-top:1px solid #ccc; height:20px;} + +.indent {margin-left:20px;} + +.middle {vertical-align:middle;} + +.help {text-align:left; margin-left:35px; cursor: pointer;} + +.help-no-tabs {float:right; cursor:pointer;} + +.align-right{ +text-align:right; +} +.align-left{ +text-align:left; +} + + +.customDialogButton { + position: absolute; + top: 0; + padding: 5px 20px 5px 20px; + vertical-align: center; + font-weight: bolder; + border: thin solid #2E6E9E !important; + color: #222222 !important; + background-color: #D0E5F5;" +} + /* jQuery tablesorter/pager/filter defaults */ .tablesorter .disabled { display: none +} + +/* Comments Widget */ +iframe.commentFrame{ + border: 0 none; + margin: 0; + padding: 0; + overflow: hidden; +} + + +.comment-author { + color:#0087e5; + font-size:11px; + margin-bottom:2px; +} + +.comment-date { + font-size:11px; + margin-bottom:2px; +} + +table.comment { + width:100%; + margin-left:0px; + padding-top:0px; + margin-bottom:5px; + text-align:left; +} + +table.comment td { + padding:0px; + padding-left:0px; + font-size:11px; + vertical-align:top; +} +a.comment { + color: #47bc23; + text-decoration: none; + border-bottom: none; +} +textarea.comment { + width: 99%; + height: 60px; + margin-bottom:10px; +} +div.comment-entry { + width: 98%; } \ No newline at end of file Index: lams_central/web/css/defaultHTML_rtl_learner.css =================================================================== diff -u -r53d60d5d44af50b85d6ed3ed5ce4941f2bc1b611 -r46bcb6ea758a272250071d1a571d008c6745e593 --- lams_central/web/css/defaultHTML_rtl_learner.css (.../defaultHTML_rtl_learner.css) (revision 53d60d5d44af50b85d6ed3ed5ce4941f2bc1b611) +++ lams_central/web/css/defaultHTML_rtl_learner.css (.../defaultHTML_rtl_learner.css) (revision 46bcb6ea758a272250071d1a571d008c6745e593) @@ -65,7 +65,6 @@ span.h2font {color:#47bc23; font-size:14px; } - h3 {color:#47bc23; font-size:13px; margin-top:10px; @@ -78,14 +77,20 @@ margin-bottom:10px; padding:0; } + +h5 {color:#47bc23; + font-size:14px; + margin-right:40px; + margin-bottom:8px; + } /*defines when instructions are given and places a blue arrow before the text */ p.instructions { color: #0087e5; margin-bottom:0; margin-top:0; - background: url('../images/css/blue_arrow_left.gif') no-repeat 0 0px; - padding-right: 30px; + background: url('../images/css/blue_arrow_right.gif') no-repeat 0 0px; + padding-left: 30px; padding-bottom: 10px; font-size:13px; } @@ -99,54 +104,52 @@ b.error {color:#cc0000;} ul { - margin-right: 25px; - margin-left: 30px; + margin-left: 25px; + margin-right: 30px; } ul li { list-style-type:circle;} ol { - margin-right: 25px; - margin-left: 30px; + margin-left: 25px; + margin-right: 30px; } -ul li { list-style-type:circle;} - li.no-list-type { list-style-type: none; } /*layout of the table with the alternate color for the table heading cell */ table.alternative-color { width:100%; - margin-right:0px; + margin-left:0px; padding-top:0px; margin-bottom:15px; margin-top:10px; - text-align:right; + text-align:left; } table.alternative-color th { background: url('../images/css/green_bg.jpg') repeat-x; height: 30px; color: #47bc23; - padding-right: 15px; - border-right: 1px solid #cacdd1; - border-left: 1px solid #fff; + padding-left: 15px; + border-left: 1px solid #cacdd1; + border-right: 1px solid #fff; font-size: 12px; } table.alternative-color td { padding:5px; - padding-right: 20px; + padding-left: 20px; font-size: 11px; border-bottom: 1px solid #efefef; vertical-align: top; background:url('../images/css/greyfade_bg.jpg') repeat-x 3px 0px } table.alternative-color td.first, table.alternative-color th.first { - border-right: none; - padding-right: 20px; + border-left: none; + padding-left: 20px; } /* jquery tablesorter pager plugin */ @@ -191,38 +194,38 @@ /*layout of the forum tables with the colored table heading cell */ table.forum { width:100%; - margin-right:0px; + margin-left:0px; padding-top:5px; margin-bottom:10px; background:url('../images/css/greyfade_bg.jpg') repeat-x 3px 49px; - text-align:right; + text-align:left; border-bottom:1px solid #efefef;} table.forum th { background: url('../images/css/green_bg_forum.jpg') repeat-x #f0f5df; height:26px; padding-top:1px; color:#47bc23; - padding-right:15px; - border-right:1px solid #cacdd1; + padding-left:15px; + border-left:1px solid #cacdd1; font-size:12px; vertical-align:middle; } table.forum td.posted-by { background: url('../images/css/forum_postedby_bg.jpg') repeat-x; height:20px; - padding-right:15px; + padding-left:15px; padding-top:0px; font-size:10px; vertical-align:top; - border-right:1px solid #cacdd1; + border-left:1px solid #cacdd1; color:#666; } table.forum td { padding:10px; - padding-right:20px; + padding-left:20px; font-size:11px; vertical-align:top; } @@ -239,7 +242,7 @@ table { width:100%; - text-align:right; + text-align:left; } td { @@ -266,14 +269,14 @@ .field-name{ color: #0087e5; font-weight: bold; - text-align: right; + text-align: left; padding-bottom:10px; } .field-name-alternative-color { color: #47bc23; font-weight: bold; - text-align: right; + text-align: left; } input { @@ -292,11 +295,11 @@ /*misc styles*/ .space-right { - margin-left: 10px; + margin-right: 10px; } .space-left { - margin-right: 10px; + margin-left: 10px; } .space-bottom { @@ -324,36 +327,57 @@ height: 1px; } -/* Warning message style */ .warning { color: #cc0000; - padding:8px 40px 10px 5px; - margin-top: 25px; - margin-left: auto; - margin-right: auto; - text-align: right; + padding:8px 10px 10px 40px; + margin: 25px auto 10px; + text-align: left; font-weight: normal; - background: url('../images/css/warning.gif') no-repeat #ffeae0 right 10px; + background: url('../images/css/warning.gif') no-repeat #ffeae0 10px 8px; width: 70%; border: 1px solid #E82A28; } - .info { color: #000000; - padding:8px 40px 10px 5px; + padding:8px 10px 10px 40px; margin-top: 25px; margin-left: auto; margin-right: auto; - text-align: right; + text-align: left; font-weight: normal; - background: url('../images/css/edit.gif') no-repeat #d8e4f1 right 10px; + background: url('../images/css/edit.gif') no-repeat #d8e4f1 10px 8px; width: 70%; border: 1px solid #3c78b5; } +.error { + color: #000000; + margin-left: auto; + margin-right: auto; + text-align: left; + font-weight: normal; +} +input.error { + background-color: bisque; + padding:8px 2px 10px; +} +label.error { + margin-left: 10px; + background-color: #EFEFEF; + padding:8px 10px 10px; +} + +#errorMessages { + padding:8px 10px 10px 40px; + margin-top: 25px; + background-color: #ffffce; + width: 70%; + border: 1px solid #C0C0C0; +} + .group-box { - padding:8px 40px 10px 10px; + padding:8px 10px 10px 40px; margin-top: 20px; margin-left: 0px; margin-right: 0px; @@ -382,21 +406,22 @@ height:20px;} -.float-left {float:right;} +.float-left {float:left;} -.float-right {float:left;} +.float-right {float:right;} + /* ************************************************* Buttons ********************************************** */ -.left-buttons {float:right;} +.left-buttons {float:left;} -.right-buttons {float:left;} +.right-buttons {float:right;} /*layout of a general button*/ #content a.button { @@ -407,7 +432,7 @@ text-decoration: none; } -/* style to disbable input buttons */ +/* style to disable input buttons */ #content a.button .disabled { color: #999; background: url('../images/css/btn_off.gif') repeat-x 0px 0px; @@ -423,6 +448,14 @@ text-decoration: none; } +#content a.button { + background: url('../images/css/btn_off.gif') repeat-x 0px 0px; + color: #333; + border: 0px none; + padding:5px 20px 6px 20px; + text-decoration: none; +} + input.disabled { color: #999; background: url('../images/css/btn_off.gif') repeat-x 0px 0px; @@ -451,50 +484,50 @@ /*layout of adding a topic button */ a.button-add-item { - background: url('../images/css/btn_add_off.gif') no-repeat right -20px; + background: url('../images/css/btn_add_off.gif') no-repeat 0px 20px; color: #333; border: 0px none; - padding:25px 25px 25px 15px; + padding:25px 15px 25px 25px; text-decoration: none; } a:hover.button-add-item { - background: url('../images/css/btn_add_over.gif') no-repeat right -20px; + background: url('../images/css/btn_add_over.gif') no-repeat 0px 20px; color: #349018; border: 0px none; - padding:25px 25px 25px 15px; + padding:25px 15px 25px 25px; text-decoration: none; } /*layout of adding a topic button */ #content a.button-add-item { - background: url('../images/css/btn_add_off.gif') no-repeat right -20px; + background: url('../images/css/btn_add_off.gif') no-repeat 0px 20px; color: #333; border: 0px none; - padding:25px 25px 25px 15px; + padding:25px 15px 25px 25px; } #content a:hover.button-add-item { - background: url('../images/css/btn_add_over.gif') no-repeat right -20px; + background: url('../images/css/btn_add_over.gif') no-repeat 0px 20px; color: #349018; border: 0px none; - padding:25px 25px 25px 15px; + padding:25px 15px 25px 25px; } /*layout of html editor button */ #content a.button-html-editor { - background: url('../images/css/btn_htmled_off.gif') no-repeat right -20px; + background: url('../images/css/btn_htmled_off.gif') no-repeat 0px 20px; color: #666; border: 0px none; - padding:25px 28px 25px 15px; + padding:25px 15px 25px 28px; } #content a:hover.button-html-editor { - background: url('../images/css/btn_htmled_off.gif') no-repeat right -20px; + background: url('../images/css/btn_htmled_off.gif') no-repeat 0px 20px; color: #333; border: 0px none; - padding:25px 28px 25px 15px; + padding:25px 15px 25px 28px; } /* Hovering submit button */ @@ -516,32 +549,119 @@ text-decoration: none; font-size: 11px; font-family: verdana, arial, helvetica, sans-serif; + cursor: pointer; } +.editForm { + padding: 0 0 5px 18px; + background: url('../images/css/report_edit.png') no-repeat; + +} + +.close { + padding: 0 0 5px 18px; + background: url('../images/css/cancel.png') no-repeat; + +} + +.import { + padding: 0 0 5px 18px; + background: url('../images/css/import.png') no-repeat; + +} + + +.nextActivity { + padding: 0px 20px 0px 0px; + background: url('../images/css/next.png') no-repeat; + background-position: right; + +} + .text-area {width:99%} -.shading-bg {padding:5px; padding-right:10px; padding-top:10px; margin-top:10px; margin-bottom:10px; width:99%; border-top:1px solid #ccc; background:url('../images/css/greyfade_bg.jpg') repeat-x 3px -1px} +.shading-bg {padding:5px; padding-left:10px; padding-top:10px; margin-top:10px; margin-bottom:10px; width:99%; border-top:1px solid #ccc; background:url('../images/css/greyfade_bg.jpg') repeat-x 3px 1px} .shading-bg td {padding:5px;} .last-item {border-top:1px solid #ccc; height:20px;} -.indent {margin-right:20px;} +.indent {margin-left:20px;} .middle {vertical-align:middle;} .help {text-align:left; margin-left:35px; cursor: pointer;} -.help-no-tabs {float:left; cursor:pointer;} +.help-no-tabs {float:right; cursor:pointer;} .align-right{ -text-align:left; +text-align:right; } .align-left{ -text-align:right; +text-align:left; } + +.customDialogButton { + position: absolute; + top: 0; + padding: 5px 20px 5px 20px; + vertical-align: center; + font-weight: bolder; + border: thin solid #2E6E9E !important; + color: #222222 !important; + background-color: #D0E5F5;" +} + /* jQuery tablesorter/pager/filter defaults */ .tablesorter .disabled { display: none +} + +/* Comments Widget */ +iframe.commentFrame{ + border: 0 none; + margin: 0; + padding: 0; + overflow: hidden; +} + + +.comment-author { + color:#0087e5; + font-size:11px; + margin-bottom:2px; +} + +.comment-date { + font-size:11px; + margin-bottom:2px; +} + +table.comment { + width:100%; + margin-left:0px; + padding-top:0px; + margin-bottom:5px; + text-align:left; +} + +table.comment td { + padding:0px; + padding-left:0px; + font-size:11px; + vertical-align:top; +} +a.comment { + color: #47bc23; + text-decoration: none; + border-bottom: none; +} +textarea.comment { + width: 99%; + height: 60px; + margin-bottom:10px; +} +div.comment-entry { + width: 98%; } \ No newline at end of file Index: lams_central/web/css/jquery.treetable.css =================================================================== diff -u --- lams_central/web/css/jquery.treetable.css (revision 0) +++ lams_central/web/css/jquery.treetable.css (revision 46bcb6ea758a272250071d1a571d008c6745e593) @@ -0,0 +1,28 @@ +table.treetable span.indenter { + display: inline-block; + margin: 0; + padding: 0; + text-align: right; + + /* Disable text selection of nodes (for better D&D UX) */ + user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -o-user-select: none; + -webkit-user-select: none; + + /* Force content-box box model for indenter (Bootstrap compatibility) */ + -webkit-box-sizing: content-box; + -moz-box-sizing: content-box; + box-sizing: content-box; + + width: 150px; +} + +table.treetable span.indenter a { + background-position: left center; + background-repeat: no-repeat; + display: inline-block; + text-decoration: none; + width: 150px; +} Index: lams_central/web/css/jquery.treetable.lams.css =================================================================== diff -u --- lams_central/web/css/jquery.treetable.lams.css (revision 0) +++ lams_central/web/css/jquery.treetable.lams.css (revision 46bcb6ea758a272250071d1a571d008c6745e593) @@ -0,0 +1,44 @@ +table.treetable span.indenter { + text-align: left; + width: 250px; +} + +table.treetable tr.collapsed span.indenter a { + background-image: url(data:image/png;base64,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); +} + +table.treetable tr.expanded span.indenter a { + background-image: url(data:image/png;base64,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); +} + +table.treetable tr span.indenter a { + outline: none; /* Expander shows outline after upgrading to 3.0 (#141) */ + border-bottom: none; /* Hide the dotted line underneath */ + width: 250px; +} + +table.treetable tr.collapsed.selected span.indenter a { + background-image: url(data:image/png;base64,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); +} + +table.treetable tr.expanded.selected span.indenter a { + background-image: url(data:image/png;base64,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); +} + +table.treetable tr.accept { + background-color: #a3bce4; + color: #fff +} + +table.treetable tr.collapsed.accept td span.indenter a { + background-image: url(data:image/x-png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAACXBIWXMAAAsTAAALEwEAmpwYAAAKT2lDQ1BQaG90b3Nob3AgSUNDIHByb2ZpbGUAAHjanVNnVFPpFj333vRCS4iAlEtvUhUIIFJCi4AUkSYqIQkQSoghodkVUcERRUUEG8igiAOOjoCMFVEsDIoK2AfkIaKOg6OIisr74Xuja9a89+bN/rXXPues852zzwfACAyWSDNRNYAMqUIeEeCDx8TG4eQuQIEKJHAAEAizZCFz/SMBAPh+PDwrIsAHvgABeNMLCADATZvAMByH/w/qQplcAYCEAcB0kThLCIAUAEB6jkKmAEBGAYCdmCZTAKAEAGDLY2LjAFAtAGAnf+bTAICd+Jl7AQBblCEVAaCRACATZYhEAGg7AKzPVopFAFgwABRmS8Q5ANgtADBJV2ZIALC3AMDOEAuyAAgMADBRiIUpAAR7AGDIIyN4AISZABRG8lc88SuuEOcqAAB4mbI8uSQ5RYFbCC1xB1dXLh4ozkkXKxQ2YQJhmkAuwnmZGTKBNA/g88wAAKCRFRHgg/P9eM4Ors7ONo62Dl8t6r8G/yJiYuP+5c+rcEAAAOF0ftH+LC+zGoA7BoBt/qIl7gRoXgugdfeLZrIPQLUAoOnaV/Nw+H48PEWhkLnZ2eXk5NhKxEJbYcpXff5nwl/AV/1s+X48/Pf14L7iJIEyXYFHBPjgwsz0TKUcz5IJhGLc5o9H/LcL//wd0yLESWK5WCoU41EScY5EmozzMqUiiUKSKcUl0v9k4t8s+wM+3zUAsGo+AXuRLahdYwP2SycQWHTA4vcAAPK7b8HUKAgDgGiD4c93/+8//UegJQCAZkmScQAAXkQkLlTKsz/HCAAARKCBKrBBG/TBGCzABhzBBdzBC/xgNoRCJMTCQhBCCmSAHHJgKayCQiiGzbAdKmAv1EAdNMBRaIaTcA4uwlW4Dj1wD/phCJ7BKLyBCQRByAgTYSHaiAFiilgjjggXmYX4IcFIBBKLJCDJiBRRIkuRNUgxUopUIFVIHfI9cgI5h1xGupE7yAAygvyGvEcxlIGyUT3UDLVDuag3GoRGogvQZHQxmo8WoJvQcrQaPYw2oefQq2gP2o8+Q8cwwOgYBzPEbDAuxsNCsTgsCZNjy7EirAyrxhqwVqwDu4n1Y8+xdwQSgUXACTYEd0IgYR5BSFhMWE7YSKggHCQ0EdoJNwkDhFHCJyKTqEu0JroR+cQYYjIxh1hILCPWEo8TLxB7iEPENyQSiUMyJ7mQAkmxpFTSEtJG0m5SI+ksqZs0SBojk8naZGuyBzmULCAryIXkneTD5DPkG+Qh8lsKnWJAcaT4U+IoUspqShnlEOU05QZlmDJBVaOaUt2ooVQRNY9aQq2htlKvUYeoEzR1mjnNgxZJS6WtopXTGmgXaPdpr+h0uhHdlR5Ol9BX0svpR+iX6AP0dwwNhhWDx4hnKBmbGAcYZxl3GK+YTKYZ04sZx1QwNzHrmOeZD5lvVVgqtip8FZHKCpVKlSaVGyovVKmqpqreqgtV81XLVI+pXlN9rkZVM1PjqQnUlqtVqp1Q61MbU2epO6iHqmeob1Q/pH5Z/YkGWcNMw09DpFGgsV/jvMYgC2MZs3gsIWsNq4Z1gTXEJrHN2Xx2KruY/R27iz2qqaE5QzNKM1ezUvOUZj8H45hx+Jx0TgnnKKeX836K3hTvKeIpG6Y0TLkxZVxrqpaXllirSKtRq0frvTau7aedpr1Fu1n7gQ5Bx0onXCdHZ4/OBZ3nU9lT3acKpxZNPTr1ri6qa6UbobtEd79up+6Ynr5egJ5Mb6feeb3n+hx9L/1U/W36p/VHDFgGswwkBtsMzhg8xTVxbzwdL8fb8VFDXcNAQ6VhlWGX4YSRudE8o9VGjUYPjGnGXOMk423GbcajJgYmISZLTepN7ppSTbmmKaY7TDtMx83MzaLN1pk1mz0x1zLnm+eb15vft2BaeFostqi2uGVJsuRaplnutrxuhVo5WaVYVVpds0atna0l1rutu6cRp7lOk06rntZnw7Dxtsm2qbcZsOXYBtuutm22fWFnYhdnt8Wuw+6TvZN9un2N/T0HDYfZDqsdWh1+c7RyFDpWOt6azpzuP33F9JbpL2dYzxDP2DPjthPLKcRpnVOb00dnF2e5c4PziIuJS4LLLpc+Lpsbxt3IveRKdPVxXeF60vWdm7Obwu2o26/uNu5p7ofcn8w0nymeWTNz0MPIQ+BR5dE/C5+VMGvfrH5PQ0+BZ7XnIy9jL5FXrdewt6V3qvdh7xc+9j5yn+M+4zw33jLeWV/MN8C3yLfLT8Nvnl+F30N/I/9k/3r/0QCngCUBZwOJgUGBWwL7+Hp8Ib+OPzrbZfay2e1BjKC5QRVBj4KtguXBrSFoyOyQrSH355jOkc5pDoVQfujW0Adh5mGLw34MJ4WHhVeGP45wiFga0TGXNXfR3ENz30T6RJZE3ptnMU85ry1KNSo+qi5qPNo3ujS6P8YuZlnM1VidWElsSxw5LiquNm5svt/87fOH4p3iC+N7F5gvyF1weaHOwvSFpxapLhIsOpZATIhOOJTwQRAqqBaMJfITdyWOCnnCHcJnIi/RNtGI2ENcKh5O8kgqTXqS7JG8NXkkxTOlLOW5hCepkLxMDUzdmzqeFpp2IG0yPTq9MYOSkZBxQqohTZO2Z+pn5mZ2y6xlhbL+xW6Lty8elQfJa7OQrAVZLQq2QqboVFoo1yoHsmdlV2a/zYnKOZarnivN7cyzytuQN5zvn//tEsIS4ZK2pYZLVy0dWOa9rGo5sjxxedsK4xUFK4ZWBqw8uIq2Km3VT6vtV5eufr0mek1rgV7ByoLBtQFr6wtVCuWFfevc1+1dT1gvWd+1YfqGnRs+FYmKrhTbF5cVf9go3HjlG4dvyr+Z3JS0qavEuWTPZtJm6ebeLZ5bDpaql+aXDm4N2dq0Dd9WtO319kXbL5fNKNu7g7ZDuaO/PLi8ZafJzs07P1SkVPRU+lQ27tLdtWHX+G7R7ht7vPY07NXbW7z3/T7JvttVAVVN1WbVZftJ+7P3P66Jqun4lvttXa1ObXHtxwPSA/0HIw6217nU1R3SPVRSj9Yr60cOxx++/p3vdy0NNg1VjZzG4iNwRHnk6fcJ3/ceDTradox7rOEH0x92HWcdL2pCmvKaRptTmvtbYlu6T8w+0dbq3nr8R9sfD5w0PFl5SvNUyWna6YLTk2fyz4ydlZ19fi753GDborZ752PO32oPb++6EHTh0kX/i+c7vDvOXPK4dPKy2+UTV7hXmq86X23qdOo8/pPTT8e7nLuarrlca7nuer21e2b36RueN87d9L158Rb/1tWeOT3dvfN6b/fF9/XfFt1+cif9zsu72Xcn7q28T7xf9EDtQdlD3YfVP1v+3Njv3H9qwHeg89HcR/cGhYPP/pH1jw9DBY+Zj8uGDYbrnjg+OTniP3L96fynQ89kzyaeF/6i/suuFxYvfvjV69fO0ZjRoZfyl5O/bXyl/erA6xmv28bCxh6+yXgzMV70VvvtwXfcdx3vo98PT+R8IH8o/2j5sfVT0Kf7kxmTk/8EA5jz/GMzLdsAAAAgY0hSTQAAeiUAAICDAAD5/wAAgOkAAHUwAADqYAAAOpgAABdvkl/FRgAAAFpJREFUeNpi/P//PwMlgHHADWD4//8/NtyAQxwD45KAAQdKDfj//////fgMIsYAZIMw1DKREFwODAwM/4kNRKq64AADA4MjFDOQ6gKyY4HodMA49PMCxQYABgAVYHsjyZ1x7QAAAABJRU5ErkJggg==); +} + +table.treetable tr.expanded.accept td span.indenter a { + background-image: url(data:image/x-png;base64,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); +} + +div.highlight { + border:2px solid #cacdd1; +} + Index: lams_central/web/images/dislike.gif =================================================================== diff -u Binary files differ Index: lams_central/web/images/dislike_disabled.gif =================================================================== diff -u Binary files differ Index: lams_central/web/images/like.gif =================================================================== diff -u Binary files differ Index: lams_central/web/images/like_disabled.gif =================================================================== diff -u Binary files differ Index: lams_central/web/includes/javascript/comments.js =================================================================== diff -u --- lams_central/web/includes/javascript/comments.js (revision 0) +++ lams_central/web/includes/javascript/comments.js (revision 46bcb6ea758a272250071d1a571d008c6745e593) @@ -0,0 +1,43 @@ + function resizeIframe() { + // parent.resizeCommentFrame(document.body.scrollHeight); Don't use at present - breaks the scrolling + } + + function highlightMessage() { + $('.highlight').filter($('table')).css('background','none'); + $('.highlight').filter($('div')).effect('highlight', {color: "#fcf0ad"}, 6000); + $('.highlight').removeClass('highlight'); + } + + function validateBodyText(bodyText, maxLength, validationMessage) { + bodyText = bodyText.trim(); + if ( bodyText.length==0 || bodyText.length>maxLength ) { + alert(validationMessage); + return false; + } else { + return true; + } + } + + function reloadThread(response) { + var threadDiv = document.getElementById('thread'+response.threadUid); + var threadUid = response.threadUid; + var commentUid = response.commentUid; + + if ( commentUid ) { + if ( ! threadDiv) { + alert(''); + } else { + var loadString = "viewTopicThread.do?&sessionMapID=" + response.sessionMapID + "&threadUid=" + threadUid+"&commentUid="+commentUid; + $(threadDiv).load(loadString, function() { + $('#msg'+commentUid).focus(); + highlightMessage(); + }); + resizeIframe(); + } + } else if ( response.errMessage ) { + // No valid id? Something failed. Assume it is a response message coming back. + alert(response.errMessage); + } else { + alert(''); + } + } Index: lams_central/web/includes/javascript/jquery.jscroll.js =================================================================== diff -u --- lams_central/web/includes/javascript/jquery.jscroll.js (revision 0) +++ lams_central/web/includes/javascript/jquery.jscroll.js (revision 46bcb6ea758a272250071d1a571d008c6745e593) @@ -0,0 +1,209 @@ +/*! + * jScroll - jQuery Plugin for Infinite Scrolling / Auto-Paging - v2.2.4 + * http://jscroll.com/ + * + * Copyright 2011-2013, Philip Klauzinski + * http://klauzinski.com/ + * Dual licensed under the MIT and GPL Version 2 licenses. + * http://jscroll.com/#license + * http://www.opensource.org/licenses/mit-license.php + * http://www.gnu.org/licenses/gpl-2.0.html + * + * @author Philip Klauzinski + * @requires jQuery v1.4.3+ + */ +(function($) { + + // Define the jscroll namespace and default settings + $.jscroll = { + defaults: { + debug: false, + autoTrigger: true, + autoTriggerUntil: false, + loadingHtml: 'Loading...', + padding: 0, + nextSelector: 'a:last', + contentSelector: '', + pagingSelector: '', + callback: false + } + }; + + // Constructor + var jScroll = function($e, options) { + + // Private vars + var _data = $e.data('jscroll'), + _userOptions = (typeof options === 'function') ? { callback: options } : options, + _options = $.extend({}, $.jscroll.defaults, _userOptions, _data || {}), + _isWindow = ($e.css('overflow-y') === 'visible'), + _$next = $e.find(_options.nextSelector).first(), + _$window = $(window), + _$body = $('body'), + _$scroll = _isWindow ? _$window : $e, + _nextHref = $.trim(_$next.attr('href') + ' ' + _options.contentSelector); + + // Initialization + $e.data('jscroll', $.extend({}, _data, {initialized: true, waiting: false, nextHref: _nextHref})); + _wrapInnerContent(); + _preloadImage(); + _setBindings(); + + // Private methods + + // Check if a loading image is defined and preload + function _preloadImage() { + var src = $(_options.loadingHtml).filter('img').attr('src'); + if (src) { + var image = new Image(); + image.src = src; + } + } + + // Wrapper inner content, if it isn't already + function _wrapInnerContent() { + if (!$e.find('.jscroll-inner').length) { + $e.contents().wrapAll(''); + } + } + + // Find the next link's parent, or add one, and hide it + function _nextWrap($next) { + if (_options.pagingSelector) { + var $parent = $next.closest(_options.pagingSelector).hide(); + } else { + var $parent = $next.parent().not('.jscroll-inner,.jscroll-added').addClass('jscroll-next-parent').hide(); + if (!$parent.length) { + $next.wrap('').parent().hide(); + } + } + } + + // Remove the jscroll behavior and data from an element + function _destroy() { + return _$scroll.unbind('.jscroll') + .removeData('jscroll') + .find('.jscroll-inner').children().unwrap() + .filter('.jscroll-added').children().unwrap(); + } + + // Observe the scroll event for when to trigger the next load + function _observe() { + _wrapInnerContent(); + var $inner = $e.find('div.jscroll-inner').first(), + data = $e.data('jscroll'), + borderTopWidth = parseInt($e.css('borderTopWidth')), + borderTopWidthInt = isNaN(borderTopWidth) ? 0 : borderTopWidth, + iContainerTop = parseInt($e.css('paddingTop')) + borderTopWidthInt, + iTopHeight = _isWindow ? _$scroll.scrollTop() : $e.offset().top, + innerTop = $inner.length ? $inner.offset().top : 0, + iTotalHeight = Math.ceil(iTopHeight - innerTop + _$scroll.height() + iContainerTop); + + if (!data.waiting && iTotalHeight + _options.padding >= $inner.outerHeight()) { + //data.nextHref = $.trim(data.nextHref + ' ' + _options.contentSelector); + _debug('info', 'jScroll:', $inner.outerHeight() - iTotalHeight, 'from bottom. Loading next request...'); + return _load(); + } + } + + // Check if the href for the next set of content has been set + function _checkNextHref(data) { + data = data || $e.data('jscroll'); + if (!data || !data.nextHref) { + _debug('warn', 'jScroll: nextSelector not found - destroying'); + _destroy(); + return false; + } else { + _setBindings(); + return true; + } + } + + function _setBindings() { + var $next = $e.find(_options.nextSelector).first(); + if (_options.autoTrigger && (_options.autoTriggerUntil === false || _options.autoTriggerUntil > 0)) { + _nextWrap($next); + if (_$body.height() <= _$window.height()) { + _observe(); + } + _$scroll.unbind('.jscroll').bind('scroll.jscroll', function() { + return _observe(); + }); + if (_options.autoTriggerUntil > 0) { + _options.autoTriggerUntil--; + } + } else { + _$scroll.unbind('.jscroll'); + $next.bind('click.jscroll', function() { + _nextWrap($next); + _load(); + return false; + }); + } + } + + // Load the next set of content, if available + function _load() { + var $inner = $e.find('div.jscroll-inner').first(), + data = $e.data('jscroll'); + + data.waiting = true; + $inner.append('') + .children('.jscroll-added').last() + .html('' + _options.loadingHtml + ''); + + return $e.animate({scrollTop: $inner.outerHeight()}, 0, function() { + $inner.find('div.jscroll-added').last().load(data.nextHref, function(r, status, xhr) { + if (status === 'error') { + return _destroy(); + } + var $next = $(this).find(_options.nextSelector).first(); + data.waiting = false; + data.nextHref = $next.attr('href') ? $.trim($next.attr('href') + ' ' + _options.contentSelector) : false; + $('.jscroll-next-parent', $e).remove(); // Remove the previous next link now that we have a new one + _checkNextHref(); + if (_options.callback) { + _options.callback.call(this); + } + _debug('dir', data); + }); + }); + } + + // Safe console debug - http://klauzinski.com/javascript/safe-firebug-console-in-javascript + function _debug(m) { + if (_options.debug && typeof console === 'object' && (typeof m === 'object' || typeof console[m] === 'function')) { + if (typeof m === 'object') { + var args = []; + for (var sMethod in m) { + if (typeof console[sMethod] === 'function') { + args = (m[sMethod].length) ? m[sMethod] : [m[sMethod]]; + console[sMethod].apply(console, args); + } else { + console.log.apply(console, args); + } + } + } else { + console[m].apply(console, Array.prototype.slice.call(arguments, 1)); + } + } + } + + // Expose API methods via the jQuery.jscroll namespace, e.g. $('sel').jscroll.method() + $.extend($e.jscroll, { + destroy: _destroy + }); + return $e; + }; + + // Define the jscroll plugin method and loop + $.fn.jscroll = function(m) { + return this.each(function() { + var $this = $(this), + data = $this.data('jscroll'); + // Instantiate jScroll on this element if it hasn't been already + if (data && data.initialized) return; + var jscroll = new jScroll($this, m); + }); + }; +})(jQuery); \ No newline at end of file Index: lams_central/web/includes/javascript/jquery.treetable.js =================================================================== diff -u --- lams_central/web/includes/javascript/jquery.treetable.js (revision 0) +++ lams_central/web/includes/javascript/jquery.treetable.js (revision 46bcb6ea758a272250071d1a571d008c6745e593) @@ -0,0 +1,634 @@ +/* + * jQuery treetable Plugin 3.2.0 + * http://ludo.cubicphuse.nl/jquery-treetable + * + * Copyright 2013, Ludo van den Boom + * Dual licensed under the MIT or GPL Version 2 licenses. + */ +(function($) { + var Node, Tree, methods; + + Node = (function() { + function Node(row, tree, settings) { + var parentId; + + this.row = row; + this.tree = tree; + this.settings = settings; + + // TODO Ensure id/parentId is always a string (not int) + this.id = this.row.data(this.settings.nodeIdAttr); + + // TODO Move this to a setParentId function? + parentId = this.row.data(this.settings.parentIdAttr); + if (parentId != null && parentId !== "") { + this.parentId = parentId; + } + + this.treeCell = $(this.row.children(this.settings.columnElType)[this.settings.column]); + this.expander = $(this.settings.expanderTemplate); + this.indenter = $(this.settings.indenterTemplate); + this.children = []; + this.initialized = false; + + // Modified Fiona Malikoff 6 Mar 2015 - we want to put the twister triangle after the message not before. + // Changed prepend to append. + // this.treeCell.prepend(this.indenter); + this.treeCell.append(this.indenter); + + } + + Node.prototype.addChild = function(child) { + return this.children.push(child); + }; + + Node.prototype.ancestors = function() { + var ancestors, node; + node = this; + ancestors = []; + while (node = node.parentNode()) { + ancestors.push(node); + } + return ancestors; + }; + + Node.prototype.collapse = function() { + if (this.collapsed()) { + return this; + } + + this.row.removeClass("expanded").addClass("collapsed"); + + this._hideChildren(); + this.expander.attr("title", this.settings.stringExpand); + + if (this.initialized && this.settings.onNodeCollapse != null) { + this.settings.onNodeCollapse.apply(this); + } + + return this; + }; + + Node.prototype.collapsed = function() { + return this.row.hasClass("collapsed"); + }; + + // TODO destroy: remove event handlers, expander, indenter, etc. + + Node.prototype.expand = function() { + if (this.expanded()) { + return this; + } + + this.row.removeClass("collapsed").addClass("expanded"); + + if (this.initialized && this.settings.onNodeExpand != null) { + this.settings.onNodeExpand.apply(this); + } + + if ($(this.row).is(":visible")) { + this._showChildren(); + } + + this.expander.attr("title", this.settings.stringCollapse); + + return this; + }; + + Node.prototype.expanded = function() { + return this.row.hasClass("expanded"); + }; + + Node.prototype.hide = function() { + this._hideChildren(); + this.row.hide(); + return this; + }; + + Node.prototype.isBranchNode = function() { + if(this.children.length > 0 || this.row.data(this.settings.branchAttr) === true) { + return true; + } else { + return false; + } + }; + + Node.prototype.updateBranchLeafClass = function(){ + this.row.removeClass('branch'); + this.row.removeClass('leaf'); + this.row.addClass(this.isBranchNode() ? 'branch' : 'leaf'); + }; + + Node.prototype.level = function() { + return this.ancestors().length; + }; + + Node.prototype.parentNode = function() { + if (this.parentId != null) { + return this.tree[this.parentId]; + } else { + return null; + } + }; + + Node.prototype.removeChild = function(child) { + var i = $.inArray(child, this.children); + return this.children.splice(i, 1) + }; + + Node.prototype.render = function() { + var handler, + settings = this.settings, + target; + + if (settings.expandable === true && this.isBranchNode()) { + handler = function(e) { + $(this).parents("table").treetable("node", $(this).parents("tr").data(settings.nodeIdAttr)).toggle(); + return e.preventDefault(); + }; + + this.indenter.html(this.expander); + target = settings.clickableNodeNames === true ? this.treeCell : this.expander; + + target.off("click.treetable").on("click.treetable", handler); + target.off("keydown.treetable").on("keydown.treetable", function(e) { + if (e.keyCode == 13) { + handler.apply(this, [e]); + } + }); + } + + this.indenter[0].style.paddingLeft = "" + (this.level() * settings.indent) + "px"; + + return this; + }; + + Node.prototype.reveal = function() { + if (this.parentId != null) { + this.parentNode().reveal(); + } + return this.expand(); + }; + + Node.prototype.setParent = function(node) { + if (this.parentId != null) { + this.tree[this.parentId].removeChild(this); + } + this.parentId = node.id; + this.row.data(this.settings.parentIdAttr, node.id); + return node.addChild(this); + }; + + Node.prototype.show = function() { + if (!this.initialized) { + this._initialize(); + } + this.row.show(); + if (this.expanded()) { + this._showChildren(); + } + return this; + }; + + Node.prototype.toggle = function() { + if (this.expanded()) { + this.collapse(); + } else { + this.expand(); + } + return this; + }; + + Node.prototype._hideChildren = function() { + var child, _i, _len, _ref, _results; + _ref = this.children; + _results = []; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + child = _ref[_i]; + _results.push(child.hide()); + } + return _results; + }; + + Node.prototype._initialize = function() { + var settings = this.settings; + + this.render(); + + if (settings.expandable === true && settings.initialState === "collapsed") { + this.collapse(); + } else { + this.expand(); + } + + if (settings.onNodeInitialized != null) { + settings.onNodeInitialized.apply(this); + } + + return this.initialized = true; + }; + + Node.prototype._showChildren = function() { + var child, _i, _len, _ref, _results; + _ref = this.children; + _results = []; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + child = _ref[_i]; + _results.push(child.show()); + } + return _results; + }; + + return Node; + })(); + + Tree = (function() { + function Tree(table, settings) { + this.table = table; + this.settings = settings; + this.tree = {}; + + // Cache the nodes and roots in simple arrays for quick access/iteration + this.nodes = []; + this.roots = []; + } + + Tree.prototype.collapseAll = function() { + var node, _i, _len, _ref, _results; + _ref = this.nodes; + _results = []; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + node = _ref[_i]; + _results.push(node.collapse()); + } + return _results; + }; + + Tree.prototype.expandAll = function() { + var node, _i, _len, _ref, _results; + _ref = this.nodes; + _results = []; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + node = _ref[_i]; + _results.push(node.expand()); + } + return _results; + }; + + Tree.prototype.findLastNode = function (node) { + if (node.children.length > 0) { + return this.findLastNode(node.children[node.children.length - 1]); + } else { + return node; + } + }; + + Tree.prototype.loadRows = function(rows) { + var node, row, i; + + if (rows != null) { + for (i = 0; i < rows.length; i++) { + row = $(rows[i]); + + if (row.data(this.settings.nodeIdAttr) != null) { + node = new Node(row, this.tree, this.settings); + this.nodes.push(node); + this.tree[node.id] = node; + + if (node.parentId != null && this.tree[node.parentId]) { + this.tree[node.parentId].addChild(node); + } else { + this.roots.push(node); + } + } + } + } + + for (i = 0; i < this.nodes.length; i++) { + node = this.nodes[i].updateBranchLeafClass(); + } + + return this; + }; + + Tree.prototype.move = function(node, destination) { + // Conditions: + // 1: +node+ should not be inserted as a child of +node+ itself. + // 2: +destination+ should not be the same as +node+'s current parent (this + // prevents +node+ from being moved to the same location where it already + // is). + // 3: +node+ should not be inserted in a location in a branch if this would + // result in +node+ being an ancestor of itself. + var nodeParent = node.parentNode(); + if (node !== destination && destination.id !== node.parentId && $.inArray(node, destination.ancestors()) === -1) { + node.setParent(destination); + this._moveRows(node, destination); + + // Re-render parentNode if this is its first child node, and therefore + // doesn't have the expander yet. + if (node.parentNode().children.length === 1) { + node.parentNode().render(); + } + } + + if(nodeParent){ + nodeParent.updateBranchLeafClass(); + } + if(node.parentNode()){ + node.parentNode().updateBranchLeafClass(); + } + node.updateBranchLeafClass(); + return this; + }; + + Tree.prototype.removeNode = function(node) { + // Recursively remove all descendants of +node+ + this.unloadBranch(node); + + // Remove node from DOM () + node.row.remove(); + + // Remove node from parent children list + if (node.parentId != null) { + node.parentNode().removeChild(node); + } + + // Clean up Tree object (so Node objects are GC-ed) + delete this.tree[node.id]; + this.nodes.splice($.inArray(node, this.nodes), 1); + + return this; + } + + Tree.prototype.render = function() { + var root, _i, _len, _ref; + _ref = this.roots; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + root = _ref[_i]; + + // Naming is confusing (show/render). I do not call render on node from + // here. + root.show(); + } + return this; + }; + + Tree.prototype.sortBranch = function(node, sortFun) { + // First sort internal array of children + node.children.sort(sortFun); + + // Next render rows in correct order on page + this._sortChildRows(node); + + return this; + }; + + Tree.prototype.unloadBranch = function(node) { + // Use a copy of the children array to not have other functions interfere + // with this function if they manipulate the children array + // (eg removeNode). + var children = node.children.slice(0), + i; + + for (i = 0; i < children.length; i++) { + this.removeNode(children[i]); + } + + // Reset node's collection of children + node.children = []; + + node.updateBranchLeafClass(); + + return this; + }; + + Tree.prototype._moveRows = function(node, destination) { + var children = node.children, i; + + node.row.insertAfter(destination.row); + node.render(); + + // Loop backwards through children to have them end up on UI in correct + // order (see #112) + for (i = children.length - 1; i >= 0; i--) { + this._moveRows(children[i], node); + } + }; + + // Special _moveRows case, move children to itself to force sorting + Tree.prototype._sortChildRows = function(parentNode) { + return this._moveRows(parentNode, parentNode); + }; + + return Tree; + })(); + + // jQuery Plugin + methods = { + init: function(options, force) { + var settings; + + settings = $.extend({ + branchAttr: "ttBranch", + clickableNodeNames: false, + column: 0, + columnElType: "td", // i.e. 'td', 'th' or 'td,th' + expandable: false, + expanderTemplate: " ", + indent: 19, + indenterTemplate: "", + initialState: "collapsed", + nodeIdAttr: "ttId", // maps to data-tt-id + parentIdAttr: "ttParentId", // maps to data-tt-parent-id + stringExpand: "Expand", + stringCollapse: "Collapse", + + // Events + onInitialized: null, + onNodeCollapse: null, + onNodeExpand: null, + onNodeInitialized: null + }, options); + + return this.each(function() { + var el = $(this), tree; + + if (force || el.data("treetable") === undefined) { + tree = new Tree(this, settings); + tree.loadRows(this.rows).render(); + + el.addClass("treetable").data("treetable", tree); + + if (settings.onInitialized != null) { + settings.onInitialized.apply(tree); + } + } + + return el; + }); + }, + + destroy: function() { + return this.each(function() { + return $(this).removeData("treetable").removeClass("treetable"); + }); + }, + + collapseAll: function() { + this.data("treetable").collapseAll(); + return this; + }, + + collapseNode: function(id) { + var node = this.data("treetable").tree[id]; + + if (node) { + node.collapse(); + } else { + throw new Error("Unknown node '" + id + "'"); + } + + return this; + }, + + expandAll: function() { + this.data("treetable").expandAll(); + return this; + }, + + expandNode: function(id) { + var node = this.data("treetable").tree[id]; + + if (node) { + if (!node.initialized) { + node._initialize(); + } + + node.expand(); + } else { + throw new Error("Unknown node '" + id + "'"); + } + + return this; + }, + + loadBranch: function(node, rows) { + var settings = this.data("treetable").settings, + tree = this.data("treetable").tree; + + // TODO Switch to $.parseHTML + rows = $(rows); + + if (node == null) { // Inserting new root nodes + this.append(rows); + } else { + var lastNode = this.data("treetable").findLastNode(node); + rows.insertAfter(lastNode.row); + } + + this.data("treetable").loadRows(rows); + + // Make sure nodes are properly initialized + rows.filter("tr").each(function() { + tree[$(this).data(settings.nodeIdAttr)].show(); + }); + + if (node != null) { + // Re-render parent to ensure expander icon is shown (#79) + node.render().expand(); + } + + return this; + }, + + move: function(nodeId, destinationId) { + var destination, node; + + node = this.data("treetable").tree[nodeId]; + destination = this.data("treetable").tree[destinationId]; + this.data("treetable").move(node, destination); + + return this; + }, + + node: function(id) { + return this.data("treetable").tree[id]; + }, + + removeNode: function(id) { + var node = this.data("treetable").tree[id]; + + if (node) { + this.data("treetable").removeNode(node); + } else { + throw new Error("Unknown node '" + id + "'"); + } + + return this; + }, + + reveal: function(id) { + var node = this.data("treetable").tree[id]; + + if (node) { + node.reveal(); + } else { + throw new Error("Unknown node '" + id + "'"); + } + + return this; + }, + + sortBranch: function(node, columnOrFunction) { + var settings = this.data("treetable").settings, + prepValue, + sortFun; + + columnOrFunction = columnOrFunction || settings.column; + sortFun = columnOrFunction; + + if ($.isNumeric(columnOrFunction)) { + sortFun = function(a, b) { + var extractValue, valA, valB; + + extractValue = function(node) { + var val = node.row.find("td:eq(" + columnOrFunction + ")").text(); + // Ignore trailing/leading whitespace and use uppercase values for + // case insensitive ordering + return $.trim(val).toUpperCase(); + } + + valA = extractValue(a); + valB = extractValue(b); + + if (valA < valB) return -1; + if (valA > valB) return 1; + return 0; + }; + } + + this.data("treetable").sortBranch(node, sortFun); + return this; + }, + + unloadBranch: function(node) { + this.data("treetable").unloadBranch(node); + return this; + } + }; + + $.fn.treetable = function(method) { + if (methods[method]) { + return methods[method].apply(this, Array.prototype.slice.call(arguments, 1)); + } else if (typeof method === 'object' || !method) { + return methods.init.apply(this, arguments); + } else { + return $.error("Method " + method + " does not exist on jQuery.treetable"); + } + }; + + // Expose classes to world + this.TreeTable || (this.TreeTable = {}); + this.TreeTable.Node = Node; + this.TreeTable.Tree = Tree; +})(jQuery); Index: lams_common/conf/hibernate/mappings/org/lamsfoundation/lams/comments/Comment.hbm.xml =================================================================== diff -u --- lams_common/conf/hibernate/mappings/org/lamsfoundation/lams/comments/Comment.hbm.xml (revision 0) +++ lams_common/conf/hibernate/mappings/org/lamsfoundation/lams/comments/Comment.hbm.xml (revision 46bcb6ea758a272250071d1a571d008c6745e593) @@ -0,0 +1,143 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Index: lams_common/conf/hibernate/mappings/org/lamsfoundation/lams/comments/CommentLike.hbm.xml =================================================================== diff -u --- lams_common/conf/hibernate/mappings/org/lamsfoundation/lams/comments/CommentLike.hbm.xml (revision 0) +++ lams_common/conf/hibernate/mappings/org/lamsfoundation/lams/comments/CommentLike.hbm.xml (revision 46bcb6ea758a272250071d1a571d008c6745e593) @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + Index: lams_common/conf/hibernate/mappings/org/lamsfoundation/lams/comments/CommentSession.hbm.xml =================================================================== diff -u --- lams_common/conf/hibernate/mappings/org/lamsfoundation/lams/comments/CommentSession.hbm.xml (revision 0) +++ lams_common/conf/hibernate/mappings/org/lamsfoundation/lams/comments/CommentSession.hbm.xml (revision 46bcb6ea758a272250071d1a571d008c6745e593) @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + Index: lams_common/src/java/org/lamsfoundation/lams/beanRefContext.xml =================================================================== diff -u -re14819ccf4e922a6175186b39012255b834f37cf -r46bcb6ea758a272250071d1a571d008c6745e593 --- lams_common/src/java/org/lamsfoundation/lams/beanRefContext.xml (.../beanRefContext.xml) (revision e14819ccf4e922a6175186b39012255b834f37cf) +++ lams_common/src/java/org/lamsfoundation/lams/beanRefContext.xml (.../beanRefContext.xml) (revision 46bcb6ea758a272250071d1a571d008c6745e593) @@ -38,6 +38,7 @@ /org/lamsfoundation/lams/applicationContext.xml /org/lamsfoundation/lams/toolApplicationContext.xml + /org/lamsfoundation/lams/comments/commentsContext.xml /org/lamsfoundation/lams/contentrepository/applicationContext.xml /org/lamsfoundation/lams/lesson/lessonApplicationContext.xml /org/lamsfoundation/lams/learning/learningApplicationContext.xml Index: lams_common/src/java/org/lamsfoundation/lams/comments/Comment.java =================================================================== diff -u --- lams_common/src/java/org/lamsfoundation/lams/comments/Comment.java (revision 0) +++ lams_common/src/java/org/lamsfoundation/lams/comments/Comment.java (revision 46bcb6ea758a272250071d1a571d008c6745e593) @@ -0,0 +1,240 @@ +/**************************************************************** + * 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 + * **************************************************************** + */ + +/* $Id$ */ +package org.lamsfoundation.lams.comments; + +import java.util.Date; + +import org.apache.commons.lang.builder.EqualsBuilder; +import org.apache.commons.lang.builder.HashCodeBuilder; +import org.apache.commons.lang.builder.ToStringBuilder; +import org.lamsfoundation.lams.usermanagement.User; + +/** + * @author Fiona Malikoff + * + */ +public class Comment implements Cloneable { + + // message types - initially just one, the tool type. + public static final int EXTERNAL_TYPE_TOOL = 1; // externalId will be a toolSessionId + + private Long uid; + + private String body; + + private Date created; + private User createdBy; + private Date updated; + private User updatedBy; + + private Date lastReplyDate; + private int replyNumber; + private boolean hideFlag; + + private Comment rootComment; + private Comment threadComment; + private short commentLevel; + + private Comment parent; + private CommentSession session; + + /** Read only fields - calculated when loaded from the database */ + private Integer likeCount; + private Integer vote; + + public Comment() { + } + + /** + * Updates the modification data for this entity. + */ + public void updateModificationData(User user) { + long now = System.currentTimeMillis(); + if (created == null) { + this.setCreated(new Date(now)); + this.setCreatedBy(user); + } + this.setUpdated(new Date(now)); + this.setUpdatedBy(user); + } + + public boolean equals(Object o) { + if (this == o) + return true; + if (!(o instanceof Comment)) + return false; + + Comment genericEntity = (Comment) o; + + // uses same attributes to determine equality as + // ConditionTopicComparator.compare() + return new EqualsBuilder().append(this.body, genericEntity.getBody()) + .append(this.replyNumber, genericEntity.getReplyNumber()) + .append(this.createdBy, genericEntity.getCreatedBy()) + .append(this.updatedBy, genericEntity.getUpdatedBy()).isEquals(); + } + + public int hashCode() { + return new HashCodeBuilder().append(uid).append(body).append(created).append(updated).append(createdBy) + .append(updatedBy).toHashCode(); + } + + // ********************************************************** + // get/set methods + // ********************************************************** + + public Long getUid() { + return uid; + } + + public void setUid(Long uid) { + this.uid = uid; + } + + public String getBody() { + return body; + } + + public void setBody(String body) { + this.body = body; + } + + public Date getCreated() { + return created; + } + + public void setCreated(Date created) { + this.created = created; + } + + public User getCreatedBy() { + return createdBy; + } + + public void setCreatedBy(User createdBy) { + this.createdBy = createdBy; + } + + public Date getUpdated() { + return updated; + } + + public void setUpdated(Date updated) { + this.updated = updated; + } + + public User getUpdatedBy() { + return updatedBy; + } + + public void setUpdatedBy(User updatedBy) { + this.updatedBy = updatedBy; + } + + public Date getLastReplyDate() { + return lastReplyDate; + } + + public void setLastReplyDate(Date lastReplyDate) { + this.lastReplyDate = lastReplyDate; + } + + public int getReplyNumber() { + return replyNumber; + } + + public void setReplyNumber(int replyNumber) { + this.replyNumber = replyNumber; + } + + public boolean isHideFlag() { + return hideFlag; + } + + public void setHideFlag(boolean hideFlag) { + this.hideFlag = hideFlag; + } + + public Comment getParent() { + return parent; + } + + public void setParent(Comment parent) { + this.parent = parent; + } + + public CommentSession getSession() { + return session; + } + + public void setSession(CommentSession session) { + this.session = session; + } + + public Comment getRootComment() { + return rootComment; + } + + public void setRootComment(Comment rootComment) { + this.rootComment = rootComment; + } + + public Comment getThreadComment() { + return threadComment; + } + + public void setThreadComment(Comment threadComment) { + this.threadComment = threadComment; + } + + public short getCommentLevel() { + return commentLevel; + } + + public void setCommentLevel(short commentLevel) { + this.commentLevel = commentLevel; + } + + public Integer getLikeCount() { + return likeCount; + } + + public void setLikeCount(Integer likeCount) { + this.likeCount = likeCount; + } + + public Integer getVote() { + return vote; + } + + public void setVote(Integer vote) { + this.vote = vote; + } + + public String toString() { + return new ToStringBuilder(this).append("uid", uid).append("body", body).toString(); + } + +} Index: lams_common/src/java/org/lamsfoundation/lams/comments/CommentLike.java =================================================================== diff -u --- lams_common/src/java/org/lamsfoundation/lams/comments/CommentLike.java (revision 0) +++ lams_common/src/java/org/lamsfoundation/lams/comments/CommentLike.java (revision 46bcb6ea758a272250071d1a571d008c6745e593) @@ -0,0 +1,74 @@ +/**************************************************************** + * 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 + * **************************************************************** + */ + +/* $Id$ */ + +package org.lamsfoundation.lams.comments; + +/** + * @author Fiona.Malikoff + * + */ +public class CommentLike { + + public static int LIKE = 1; + public static int DISLIKE = -1; + + private Long uid; + private Comment comment; + private Integer userId; + private int vote; + + public Long getUid() { + return uid; + } + + public void setUid(Long uid) { + this.uid = uid; + } + + public Comment getComment() { + return comment; + } + + public void setComment(Comment comment) { + this.comment = comment; + } + + public Integer getUserId() { + return userId; + } + + public void setUserId(Integer userId) { + this.userId = userId; + } + + public int getVote() { + return vote; + } + + public void setVote(int vote) { + this.vote = vote; + } + +} Index: lams_common/src/java/org/lamsfoundation/lams/comments/CommentSession.java =================================================================== diff -u --- lams_common/src/java/org/lamsfoundation/lams/comments/CommentSession.java (revision 0) +++ lams_common/src/java/org/lamsfoundation/lams/comments/CommentSession.java (revision 46bcb6ea758a272250071d1a571d008c6745e593) @@ -0,0 +1,73 @@ +/**************************************************************** + * 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 + * **************************************************************** + */ + +/* $Id$ */ +package org.lamsfoundation.lams.comments; + +/** + * @author Fiona Malikoff + * + */ +public class CommentSession implements Cloneable { + + private Long uid; + private Long externalId; + private Integer externalIdType; + private String externalSignature; + + public CommentSession() { + } + + public Long getUid() { + return uid; + } + + public void setUid(Long uid) { + this.uid = uid; + } + + public Long getExternalId() { + return externalId; + } + + public void setExternalId(Long externalId) { + this.externalId = externalId; + } + + public Integer getExternalIdType() { + return externalIdType; + } + + public void setExternalIdType(Integer externalIdType) { + this.externalIdType = externalIdType; + } + + public String getExternalSignature() { + return externalSignature; + } + + public void setExternalSignature(String externalSignature) { + this.externalSignature = externalSignature; + } + + +} Index: lams_common/src/java/org/lamsfoundation/lams/comments/commentsContext.xml =================================================================== diff -u --- lams_common/src/java/org/lamsfoundation/lams/comments/commentsContext.xml (revision 0) +++ lams_common/src/java/org/lamsfoundation/lams/comments/commentsContext.xml (revision 46bcb6ea758a272250071d1a571d008c6745e593) @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + PROPAGATION_REQUIRED + PROPAGATION_REQUIRED + PROPAGATION_REQUIRED + PROPAGATION_REQUIRED + PROPAGATION_REQUIRED,readOnly + + + + Index: lams_common/src/java/org/lamsfoundation/lams/comments/dao/ICommentDAO.java =================================================================== diff -u --- lams_common/src/java/org/lamsfoundation/lams/comments/dao/ICommentDAO.java (revision 0) +++ lams_common/src/java/org/lamsfoundation/lams/comments/dao/ICommentDAO.java (revision 46bcb6ea758a272250071d1a571d008c6745e593) @@ -0,0 +1,29 @@ +package org.lamsfoundation.lams.comments.dao; + +import java.util.List; +import java.util.SortedSet; + +import org.lamsfoundation.lams.comments.Comment; + +public interface ICommentDAO { + + /* Standard DAO call */ + public abstract void saveOrUpdate(Comment comment); + + public abstract void update(Comment comment); + + public abstract Comment getById(Long commentId); + + /** + * Get the root (dummy / top level) topic in a Session. + * + * @param sessionId + * @return + */ + public abstract Comment getRootTopic(Long externalId, Integer externalIdType, String externalSignature); + + public abstract SortedSet getThreadByThreadId(Long threadCommentId, Integer userId); + + public abstract SortedSet getNextThreadByThreadId(final Long rootTopicId, final Long previousThreadMessageId, Integer numberOfThreads, Integer userId); + +} \ No newline at end of file Index: lams_common/src/java/org/lamsfoundation/lams/comments/dao/ICommentLikeDAO.java =================================================================== diff -u --- lams_common/src/java/org/lamsfoundation/lams/comments/dao/ICommentLikeDAO.java (revision 0) +++ lams_common/src/java/org/lamsfoundation/lams/comments/dao/ICommentLikeDAO.java (revision 46bcb6ea758a272250071d1a571d008c6745e593) @@ -0,0 +1,7 @@ +package org.lamsfoundation.lams.comments.dao; + + +public interface ICommentLikeDAO { + + boolean addLike( Long commentUid, Integer userId, Integer vote); +} \ No newline at end of file Index: lams_common/src/java/org/lamsfoundation/lams/comments/dao/ICommentSessionDAO.java =================================================================== diff -u --- lams_common/src/java/org/lamsfoundation/lams/comments/dao/ICommentSessionDAO.java (revision 0) +++ lams_common/src/java/org/lamsfoundation/lams/comments/dao/ICommentSessionDAO.java (revision 46bcb6ea758a272250071d1a571d008c6745e593) @@ -0,0 +1,8 @@ +package org.lamsfoundation.lams.comments.dao; + +import org.lamsfoundation.lams.comments.CommentSession; + +public interface ICommentSessionDAO { + + void save( CommentSession session); +} \ No newline at end of file Index: lams_common/src/java/org/lamsfoundation/lams/comments/dao/hibernate/CommentDAO.java =================================================================== diff -u --- lams_common/src/java/org/lamsfoundation/lams/comments/dao/hibernate/CommentDAO.java (revision 0) +++ lams_common/src/java/org/lamsfoundation/lams/comments/dao/hibernate/CommentDAO.java (revision 46bcb6ea758a272250071d1a571d008c6745e593) @@ -0,0 +1,164 @@ +/**************************************************************** + * 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 + * **************************************************************** + */ + +/* $Id$ */ + +package org.lamsfoundation.lams.comments.dao.hibernate; + +import java.util.List; +import java.util.SortedSet; +import java.util.TreeSet; + +import org.hibernate.Hibernate; +import org.hibernate.SQLQuery; +import org.lamsfoundation.lams.comments.Comment; +import org.lamsfoundation.lams.comments.dao.ICommentDAO; +import org.lamsfoundation.lams.comments.util.TopicComparator; +import org.springframework.orm.hibernate3.HibernateTemplate; +import org.springframework.orm.hibernate3.support.HibernateDaoSupport; + +/** + * @author Fiona Malikoff + */ +public class CommentDAO extends HibernateDaoSupport implements ICommentDAO { + + /* Standard DAO call */ + + @Override + public void saveOrUpdate(Comment comment) { + this.getHibernateTemplate().saveOrUpdate(comment); + } + + @Override + public void update(Comment comment) { + this.getHibernateTemplate().saveOrUpdate(comment); + } + + @Override + public Comment getById(Long commentId) { + return (Comment) getHibernateTemplate().get(Comment.class, commentId); + } + + /* Session Level Lookups */ + private static final String SQL_QUERY_FIND_ROOT_TOPICS = "from " + Comment.class.getName() + " cs " + + " where cs.parent is null and cs.session.externalId=:externalId and " + + " cs.session.externalIdType=:externalIdType and cs.session.externalSignature=:externalSignature"; + + @Override + public Comment getRootTopic(Long externalId, Integer externalIdType, String externalSignature) { + @SuppressWarnings("rawtypes") + List list = getSession().createQuery(SQL_QUERY_FIND_ROOT_TOPICS).setLong("externalId", externalId) + .setInteger("externalIdType", externalIdType).setString("externalSignature", externalSignature).list(); + if (list != null && list.size() > 0) + return (Comment) list.get(0); + return null; + } + + /* Thread based lookups - Returns a complex structure so that the likes information can be passed + * back with it. */ + private static final String SQL_QUERY_FIND_FIRST_THREAD_TOP = "select uid from lams_comment" + + " where root_comment_uid = :rootUid and comment_level = 1 order by uid DESC"; + + private static final String SQL_QUERY_FIND_NEXT_THREAD_TOP = "select uid from lams_comment" + + " where root_comment_uid = :rootUid and uid < :lastUid and comment_level = 1 order by uid DESC"; + +// private static final String SQL_QUERY_FIND_NEXT_THREAD_MESSAGES = "from " + Comment.class.getName() +// + " where root_comment_uid = ? and comment_level > 1 and thread_comment_uid in ?"; + + private static final String SQL_QUERY_FIND_NEXT_THREAD_MESSAGES = + "SELECT c.*, SUM(l.vote) likes_total, l2.vote user_vote FROM lams_comment c " + + " LEFT JOIN lams_comment_likes l ON c.uid = l.comment_uid " + + " LEFT JOIN lams_comment_likes l2 ON c.uid = l2.comment_uid AND l2.user_id=:userId " + + " WHERE c.thread_comment_uid IN (:threadIds) " + + " GROUP BY c.uid"; + +// private static final String SQL_QUERY_GET_COMPLETE_THREAD = "from " + Comment.class.getName() +// + " where thread_comment_uid = ?"; + + private static final String SQL_QUERY_GET_COMPLETE_THREAD = + "SELECT c.*, SUM(l.vote) likes_total, l2.vote user_vote FROM lams_comment c " + + " LEFT JOIN lams_comment_likes l ON c.uid = l.comment_uid " + + " LEFT JOIN lams_comment_likes l2 ON c.uid = l2.comment_uid AND l2.user_id=:userId " + + " WHERE c.thread_comment_uid = :threadId " + + " GROUP BY c.uid"; + + + @Override + @SuppressWarnings("unchecked") + public SortedSet getThreadByThreadId(Long threadCommentId, Integer userId) { + SQLQuery query = getSession().createSQLQuery(SQL_QUERY_GET_COMPLETE_THREAD); + query.addEntity("comment", Comment.class) + .addScalar("likes_total", Hibernate.INTEGER) + .addScalar("user_vote", Hibernate.INTEGER) + .setLong("userId", userId != null ? userId : 0) + .setLong("threadId", threadCommentId); + List results = query.list(); + return upgradeComments(results); + } + + private SortedSet upgradeComments(List rawObjects) { + SortedSet results = new TreeSet(new TopicComparator()); + for ( Object[] rawObject : rawObjects ) { + Comment comment = (Comment) rawObject[0]; + Integer likeCount = (Integer) rawObject[1]; + comment.setLikeCount(likeCount != null ? likeCount : 0); + Integer userVote = (Integer) rawObject[2]; + comment.setVote(userVote); + results.add(comment); + } + return results; + } + + @Override + @SuppressWarnings("unchecked") + public SortedSet getNextThreadByThreadId(final Long rootTopicId, final Long previousThreadMessageId, Integer numberOfThreads, Integer userId) { + HibernateTemplate template = this.getHibernateTemplate(); + + template.setMaxResults(numberOfThreads); + List threadUidList = null; + if (previousThreadMessageId == null || previousThreadMessageId == 0L) { + threadUidList = (List) getSession().createSQLQuery(SQL_QUERY_FIND_FIRST_THREAD_TOP) + .setLong("rootUid", rootTopicId) + .list(); + } else { + threadUidList = (List) getSession().createSQLQuery(SQL_QUERY_FIND_NEXT_THREAD_TOP) + .setLong("rootUid", rootTopicId) + .setLong("lastUid", previousThreadMessageId) + .list(); + } + template.setMaxResults(0); + + if (threadUidList != null && threadUidList.size() > 0) { + SQLQuery query = getSession().createSQLQuery(SQL_QUERY_FIND_NEXT_THREAD_MESSAGES); + query.addEntity("comment", Comment.class) + .addScalar("likes_total", Hibernate.INTEGER) + .addScalar("user_vote", Hibernate.INTEGER) + .setLong("userId", userId != null ? userId : 0) + .setParameterList("threadIds", threadUidList); + List results = query.list(); + return upgradeComments(results); + } + return new TreeSet(); + } + +} Index: lams_common/src/java/org/lamsfoundation/lams/comments/dao/hibernate/CommentLikeDAO.java =================================================================== diff -u --- lams_common/src/java/org/lamsfoundation/lams/comments/dao/hibernate/CommentLikeDAO.java (revision 0) +++ lams_common/src/java/org/lamsfoundation/lams/comments/dao/hibernate/CommentLikeDAO.java (revision 46bcb6ea758a272250071d1a571d008c6745e593) @@ -0,0 +1,48 @@ +/**************************************************************** + * 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 + * **************************************************************** + */ + +/* $Id$ */ + +package org.lamsfoundation.lams.comments.dao.hibernate; + +import org.apache.log4j.Logger; +import org.lamsfoundation.lams.comments.dao.ICommentLikeDAO; +import org.springframework.orm.hibernate3.support.HibernateDaoSupport; + +public class CommentLikeDAO extends HibernateDaoSupport implements ICommentLikeDAO { + private static Logger log = Logger.getLogger(CommentLikeDAO.class); + + private static String INSERT_LIKE = "INSERT IGNORE INTO lams_comment_likes(comment_uid, user_id, vote) VALUES (:comment,:user,:vote);"; + + public boolean addLike( Long commentUid, Integer userId, Integer vote) { + int status = getSession().createSQLQuery(INSERT_LIKE) + .setParameter("comment", commentUid) + .setParameter("user", userId) + .setParameter("vote", vote) + .executeUpdate(); + + log.debug("Insert returned "+status); + return status == 1; + } + +} Index: lams_common/src/java/org/lamsfoundation/lams/comments/dao/hibernate/CommentSessionDAO.java =================================================================== diff -u --- lams_common/src/java/org/lamsfoundation/lams/comments/dao/hibernate/CommentSessionDAO.java (revision 0) +++ lams_common/src/java/org/lamsfoundation/lams/comments/dao/hibernate/CommentSessionDAO.java (revision 46bcb6ea758a272250071d1a571d008c6745e593) @@ -0,0 +1,38 @@ +/**************************************************************** + * 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 + * **************************************************************** + */ + +/* $Id$ */ + +package org.lamsfoundation.lams.comments.dao.hibernate; + +import org.lamsfoundation.lams.comments.CommentSession; +import org.lamsfoundation.lams.comments.dao.ICommentSessionDAO; +import org.springframework.orm.hibernate3.support.HibernateDaoSupport; + +public class CommentSessionDAO extends HibernateDaoSupport implements ICommentSessionDAO { + + public void save( CommentSession session) { + this.getHibernateTemplate().save( session); + } + +} Index: lams_common/src/java/org/lamsfoundation/lams/comments/dto/CommentDTO.java =================================================================== diff -u --- lams_common/src/java/org/lamsfoundation/lams/comments/dto/CommentDTO.java (revision 0) +++ lams_common/src/java/org/lamsfoundation/lams/comments/dto/CommentDTO.java (revision 46bcb6ea758a272250071d1a571d008c6745e593) @@ -0,0 +1,139 @@ +package org.lamsfoundation.lams.comments.dto; +/**************************************************************** + * 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 + * **************************************************************** + */ + + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import org.lamsfoundation.lams.comments.Comment; + + +public class CommentDTO { + + private Comment comment; + private String authorname; + private boolean isAuthor; + private boolean hasAttachment; + private short level; + private int threadNum; + private boolean liked; + private boolean disliked; + + /** + * Get a CommentDTO instance from a given Comment. + * + * @param msg + * @return + */ + public static CommentDTO getCommentDTO(Comment comment){ + if(comment == null) + return null; + + CommentDTO dto = new CommentDTO(); + dto.setComment(comment); + if(comment.getCreatedBy() != null) + dto.setAuthorname(comment.getCreatedBy().getFirstName()+" "+comment.getCreatedBy().getLastName()); + + dto.liked = false; + dto.disliked = false; + + return dto; + } + + /** + * Get a list of CommentDTO according to given list of Comment. + * @param msgList + * @return + */ + public static List getCommentDTO(List commentList){ + List retSet = new ArrayList(); + if(commentList == null || commentList.isEmpty()) + return retSet; + + Iterator iter = commentList.iterator(); + while(iter.hasNext()){ + Comment msg = iter.next(); + retSet.add(getCommentDTO(msg)); + } + return retSet; + } + //-------------------------------DTO get/set method------------------------------ + public String getAuthorname() { + return authorname; + } + public void setAuthorname(String authorname) { + this.authorname = authorname; + } + public boolean getHasAttachment() { + return hasAttachment; + } + public void setHasAttachment(boolean isAttachment) { + this.hasAttachment = isAttachment; + } + public Comment getComment() { + return comment; + } + public void setComment(Comment comment) { + this.comment = comment; + } + public short getLevel() { + return level; + } + public void setLevel(short level) { + this.level = level; + } + public int getThreadNum() { + return threadNum; + } + public void setThreadNum(int threadNum) { + this.threadNum = threadNum; + } + + public boolean isAuthor() { + return isAuthor; + } + + public void setIsAuthor(boolean isAuthor) { + this.isAuthor = isAuthor; + } + + public boolean isLiked() { + return liked; + } + + public void setLiked(boolean liked) { + this.liked = liked; + } + + public boolean isDisliked() { + return disliked; + } + + public void setDisliked(boolean disliked) { + this.disliked = disliked; + } + + +} Index: lams_common/src/java/org/lamsfoundation/lams/comments/service/CommentService.java =================================================================== diff -u --- lams_common/src/java/org/lamsfoundation/lams/comments/service/CommentService.java (revision 0) +++ lams_common/src/java/org/lamsfoundation/lams/comments/service/CommentService.java (revision 46bcb6ea758a272250071d1a571d008c6745e593) @@ -0,0 +1,275 @@ +/**************************************************************** + * Copyright (C) 2008 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 + * **************************************************************** + */ + +/* $Id$ */ +package org.lamsfoundation.lams.comments.service; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.SortedSet; + +import org.apache.log4j.Logger; +import org.lamsfoundation.lams.comments.Comment; +import org.lamsfoundation.lams.comments.CommentSession; +import org.lamsfoundation.lams.comments.dao.ICommentDAO; +import org.lamsfoundation.lams.comments.dao.ICommentLikeDAO; +import org.lamsfoundation.lams.comments.dao.ICommentSessionDAO; +import org.lamsfoundation.lams.comments.dto.CommentDTO; +import org.lamsfoundation.lams.comments.util.CommentConstants; +import org.lamsfoundation.lams.usermanagement.User; +import org.lamsfoundation.lams.usermanagement.service.IUserManagementService; +import org.lamsfoundation.lams.util.MessageService; +import org.lamsfoundation.lams.util.audit.IAuditService; + +/** + * + * Comments Widget Service + * + * @author Fiona Malikoff + * + */ +public class CommentService implements ICommentService { + + private static Logger log = Logger.getLogger(CommentService.class); + + // Services + private IUserManagementService userService; + private MessageService messageService; + private IAuditService auditService; + private ICommentSessionDAO commentSessionDAO; + private ICommentDAO commentDAO; + private ICommentLikeDAO commentLikeDAO; + + @Override + public List getTopicThread(Long externalId, Integer externalType, String externalSignature, Long lastCommentSeqId, Integer pageSize, Integer userId) { + + long lastThreadMessageUid = lastCommentSeqId != null ? lastCommentSeqId.longValue() : 0L; + Integer usePagingSize = pageSize != null ? pageSize : CommentConstants.DEFAULT_PAGE_SIZE; + + // hidden root of all the threads! + Comment rootTopic = commentDAO.getRootTopic(externalId, externalType, externalSignature); + + // first time through - no root topic. + if ( rootTopic == null ) { + return new ArrayList(); + } + + SortedSet comments = commentDAO.getNextThreadByThreadId(rootTopic.getUid(), lastThreadMessageUid, usePagingSize, userId); + return getSortedCommentDTO(comments); + } + + private List getSortedCommentDTO(SortedSet comments) { + + List msgDtoList = new ArrayList(); + for ( Comment comment : comments ) { + CommentDTO dto = CommentDTO.getCommentDTO(comment); + dto.setLevel(comment.getCommentLevel()); + dto.setThreadNum(comment.getThreadComment().getUid().intValue()); + msgDtoList.add(dto); + } + return msgDtoList; + } + + public List getThread( Long threadId, Integer userId ) { + SortedSet comments = commentDAO.getThreadByThreadId(threadId, userId); + return getSortedCommentDTO(comments); + } + + // Do we need to synchronize this method? Would be nice but it is the equivalent of tool session creation + // and we don't synchonize them! + public Comment createOrGetRoot(Long externalId, Integer externalIdType, String externalSignature, User user) { + Comment rootComment = commentDAO.getRootTopic(externalId, externalIdType, externalSignature); + return ( rootComment != null ? rootComment : + createRoot(externalId, externalIdType, externalSignature, user) ); + } + + public Comment getRoot(Long externalId, Integer externalIdType, String externalSignature) { + return commentDAO.getRootTopic(externalId, externalIdType, externalSignature); + } + + private Comment createRoot(Long externalId, Integer externalIdType, String externalSignature, User user ) { + + CommentSession session = new CommentSession(); + session.setExternalId(externalId); + session.setExternalIdType(externalIdType); + session.setExternalSignature(externalSignature); + + Comment comment = new Comment(); + comment.setBody("Hidden Root Message"); + comment.setHideFlag(true); + comment.setReplyNumber(0); // no replies yet + comment.setSession(session); + comment.updateModificationData(user); + comment.setCommentLevel((short) 0); + comment.setRootComment(comment); + comment.setThreadComment(null); // this one is not part of a thread! + + commentSessionDAO.save(session); + commentDAO.saveOrUpdate(comment); + return comment; + } + + public Comment createReply(Comment parent, String replyText, User user) { + + Comment replyMessage = new Comment(); + replyMessage.setBody(replyText); + replyMessage.setHideFlag(false); + replyMessage.updateModificationData(user); + + replyMessage.setParent(parent); + replyMessage.setSession(parent.getSession()); + + Comment root = parent.getRootComment(); + replyMessage.setCommentLevel((short) (parent.getCommentLevel() + 1)); + replyMessage.setRootComment(root); + + // look back up through the parents to find the thread top - will be level 1 + if ( replyMessage.getCommentLevel() == 1 ) { + replyMessage.setThreadComment(replyMessage); + } else { + Comment threadComment = parent; + while ( threadComment.getCommentLevel() > 1 ) { + threadComment = threadComment.getParent(); + } + replyMessage.setThreadComment(threadComment); + } + + commentDAO.saveOrUpdate(replyMessage); + + // update last reply date for root message + root.setLastReplyDate(new Date()); + // update reply message number for root + root.setReplyNumber(root.getReplyNumber() + 1); + commentDAO.saveOrUpdate(root); + + return replyMessage; + } + + public boolean addLike(Long commentUid, User user, Integer likeVote) { + + return commentLikeDAO.addLike(commentUid, user.getUserId(), likeVote); + } + + public Comment hideComment(Long commentUid, User user, boolean status) { + + Comment comment = commentDAO.getById(commentUid); + comment.setHideFlag(status); + commentDAO.saveOrUpdate(comment); + return comment; + } + + + public Comment createReply(Long parentId, String replyText, User user) { + + Comment parent = commentDAO.getById(parentId); + return(createReply(parent, replyText, user)); + + } + + public Comment updateComment(Long commentUid, String newBody, User user, boolean makeAuditEntry) { + Comment comment = commentDAO.getById(commentUid); + + if ( comment != null && user != null ) { + if ( makeAuditEntry ) { + Long userId = 0L; + String loginName = "Default"; + if (comment.getCreatedBy() != null) { + userId = comment.getCreatedBy().getUserId().longValue(); + loginName = comment.getCreatedBy().getLogin(); + } + getAuditService().logChange(CommentConstants.MODULE_NAME, userId, loginName, + comment.getBody(), newBody); + } + + comment.setBody(newBody); + comment.updateModificationData(user); + commentDAO.saveOrUpdate(comment); + + return comment; + } else { + log.error("Unable to update comment as comment not found or user missing. Comment uid "+commentUid + +" new body "+newBody + +" user "+(user!=null?user.getLogin():" missing")); + return null; + } + } + + public CommentDTO getComment(Long commentUid){ + Comment comment = commentDAO.getById(commentUid); + return comment != null ? CommentDTO.getCommentDTO(comment) : null; + } + + public IUserManagementService getUserService() { + return userService; + } + + public void setUserService(IUserManagementService userService) { + this.userService = userService; + } + + + public MessageService getMessageService() { + return messageService; + } + + public void setMessageService(MessageService messageService) { + this.messageService = messageService; + } + + public IAuditService getAuditService() { + return auditService; + } + + public void setAuditService(IAuditService auditService) { + this.auditService = auditService; + } + + public ICommentSessionDAO getCommentSessionDAO() { + return commentSessionDAO; + } + + public void setCommentSessionDAO(ICommentSessionDAO commentSessionDAO) { + this.commentSessionDAO = commentSessionDAO; + } + + public ICommentDAO getCommentDAO() { + return commentDAO; + } + + public void setCommentDAO(ICommentDAO commentDAO) { + this.commentDAO = commentDAO; + } + + public ICommentLikeDAO getCommentLikeDAO() { + return commentLikeDAO; + } + + public void setCommentLikeDAO(ICommentLikeDAO commentLikeDAO) { + this.commentLikeDAO = commentLikeDAO; + } + + + + + // ------------------------------------------------------------------------- +} Index: lams_common/src/java/org/lamsfoundation/lams/comments/service/ICommentService.java =================================================================== diff -u --- lams_common/src/java/org/lamsfoundation/lams/comments/service/ICommentService.java (revision 0) +++ lams_common/src/java/org/lamsfoundation/lams/comments/service/ICommentService.java (revision 46bcb6ea758a272250071d1a571d008c6745e593) @@ -0,0 +1,72 @@ +/**************************************************************** + * Copyright (C) 2008 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 + * **************************************************************** + */ + +/* $Id$ */ +package org.lamsfoundation.lams.comments.service; + +import java.util.List; + +import org.lamsfoundation.lams.comments.Comment; +import org.lamsfoundation.lams.comments.dto.CommentDTO; +import org.lamsfoundation.lams.usermanagement.User; +import org.lamsfoundation.lams.util.MessageService; + + +public interface ICommentService { + + /** Gets the comments for a tool, based on the tool session and the tool's id. Allows for paging */ + List getTopicThread(Long externalId, Integer externalType, String externalSignature, Long lastMsgSeqId, Integer pageSize, Integer userId); + + /** Saves a comment - either creating a whole tree one if there is no parent or saving under the given parent. */ + Comment createReply(Long parentId, String replyText, User user); + Comment createReply(Comment parent, String replyText, User user); + + /** Gets the dummy root for the comment system and if one doesn't exist for this session then set it up! */ + Comment createOrGetRoot(Long externalId, Integer externalIdType, String externalSignature, User user ); + + /** Gets the dummy root. If it doesn't exist, returns null. */ + Comment getRoot(Long externalId, Integer externalIdType, String externalSignature ); + + /** + * Get one complete thread within a topic Note that the return type is DTO. + */ + List getThread( Long threadId, Integer userId ); + + /** Get a single comment. Note that the return type is DTO */ + CommentDTO getComment(Long commentUid); + + /** Update the body in a comment, and update the modified time, who by, etc, etc */ + Comment updateComment(Long commentUid, String newBody, User user, boolean makeAuditEntry); + + /** Update the like list for Comment commentUid by likeVote. Returns true if like/dislike added. */ + boolean addLike(Long commentUid, User user, Integer likeVote); + + /** Update the hidden / not hidden flag */ + Comment hideComment(Long commentUid, User user, boolean status); + /** + * Get the id of the root topic of this comment. + */ +// public Long getRootTopicId(Long commentId);// + + /* Utility calls for the Action */ + MessageService getMessageService(); +} \ No newline at end of file Index: lams_common/src/java/org/lamsfoundation/lams/comments/util/CommentConstants.java =================================================================== diff -u --- lams_common/src/java/org/lamsfoundation/lams/comments/util/CommentConstants.java (revision 0) +++ lams_common/src/java/org/lamsfoundation/lams/comments/util/CommentConstants.java (revision 46bcb6ea758a272250071d1a571d008c6745e593) @@ -0,0 +1,62 @@ +/**************************************************************** + * 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 + * **************************************************************** + */ + +/* $Id$ */ +package org.lamsfoundation.lams.comments.util; + +public class CommentConstants { + + public static final String MODULE_NAME = "Comments"; + + // type of comments - initially only 1 - tool comments! Found in ATTR_EXTERNAL_TYPE + public static final int TYPE_TOOL = 1; + + public static final String ATTR_EXTERNAL_ID = "externalID"; + public static final String ATTR_EXTERNAL_SIG = "externalSig"; + public static final String ATTR_EXTERNAL_TYPE = "externalType"; + public static final String ATTR_SESSION_MAP_ID = "sessionMapID"; + public static final String ATTR_COMMENT_THREAD = "commentThread"; + public static final String ATTR_COMMENT = "comment"; + public static final String ATTR_PARENT_COMMENT_ID = "parentUid"; + public static final String ATTR_ROOT_COMMENT_UID = "rootUid"; + public static final String ATTR_BODY = "body"; + public static final String ATTR_COMMENT_ID = "commentUid"; + public static final String ATTR_LIKE_COUNT = "likeCount"; + public static final String ATTR_THREAD_ID = "threadUid"; + public static final String ATTR_ERR_MESSAGE = "errMessage"; + public static final String ATTR_HIDE_FLAG = "hideFlag"; + public static final String ATTR_STATUS = "status"; + + // for paging long topics & inlining reply + public static final String PAGE_LAST_ID = "pageLastId"; + public static final String PAGE_SIZE = "size"; + public static final int DEFAULT_PAGE_SIZE = 4; + public static final String ATTR_NO_MORE_PAGES = "noMorePages"; + + public static final int MAX_BODY_LENGTH = 5000; + + // message keys + public static final String KEY_BODY_VALIDATION = "label.body.validation"; + + +} + Index: lams_common/src/java/org/lamsfoundation/lams/comments/util/TopicComparator.java =================================================================== diff -u --- lams_common/src/java/org/lamsfoundation/lams/comments/util/TopicComparator.java (revision 0) +++ lams_common/src/java/org/lamsfoundation/lams/comments/util/TopicComparator.java (revision 46bcb6ea758a272250071d1a571d008c6745e593) @@ -0,0 +1,97 @@ +/**************************************************************** + * 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 + * **************************************************************** + */ + +/* $Id$ */ + +package org.lamsfoundation.lams.comments.util; + +import java.util.Comparator; + +import org.apache.log4j.Logger; +import org.lamsfoundation.lams.comments.Comment; + +/** + * This class implements the java.util.Comparator interface. Based on the Topic Comparator from Forum. + */ +public class TopicComparator implements Comparator { + private static final Logger log = Logger.getLogger(TopicComparator.class); + + public int compare(Comment msg1, Comment msg2) { + + short level1,level2; + level1 = msg1.getCommentLevel(); + level2 = msg2.getCommentLevel(); + Comment parent1,parent2; + //choose the smaller level value + short lessLevel = level1>level2? level2:level1; + for(int compareLevel=0;compareLevel <= lessLevel;compareLevel++){ + //init value, loop from current message + parent1 = msg1; + parent2 = msg2; + level1 = msg1.getCommentLevel(); + level2 = msg2.getCommentLevel(); + while(level1 > compareLevel){ + //get parent until assigned level + if(parent1 == null){ + log.error("Comment "+ parent1 +" level "+ level1 +" has null parent"); + return 0; + } + parent1 = parent1.getParent(); + level1--; + } + while(level2 > compareLevel){ + //get parent until assigned level + if(parent2 == null){ + log.error("Comment "+ parent2 +" level "+ level2 +" has null parent"); + return 0; + } + parent2 = parent2.getParent(); + level2--; + } + //this comparison will handle different branch node + if(parent1 != parent2){ + // show the newest first - this will have the higher uid + return compareUids(compareLevel, parent1, parent2); + } + //this comparison will handle same branch node + //the direct parent level, their parent(or themselves) are still equal + if(compareLevel==lessLevel){ + if(msg1.getCommentLevel() != msg2.getCommentLevel()) + return msg1.getCommentLevel() - msg2.getCommentLevel(); + else{ + return compareUids(compareLevel, msg1, parent2); + } + } + + } + return msg1.getUpdated().before(msg2.getUpdated())?-1:1; + } + + // If Level = 1 then "top" so sort newest first. Otherwise sort oldest first. + private int compareUids(int compareLevel, Comment c1, Comment c2) { + if ( compareLevel <= 1 ) + return c1.getUid() < c2.getUid() ? 1 : -1; + else + return c1.getUid() > c2.getUid() ? 1 : -1; + } +} Index: lams_common/src/java/org/lamsfoundation/lams/dbupdates/patch2040051.sql =================================================================== diff -u --- lams_common/src/java/org/lamsfoundation/lams/dbupdates/patch2040051.sql (revision 0) +++ lams_common/src/java/org/lamsfoundation/lams/dbupdates/patch2040051.sql (revision 46bcb6ea758a272250071d1a571d008c6745e593) @@ -0,0 +1,65 @@ +-- Turn off autocommit, so nothing is committed if there is an error +SET AUTOCOMMIT = 0; + +-- LDEV-3631 Simple Commenting Widget +CREATE TABLE lams_comment_session ( + `uid` bigint(20) NOT NULL AUTO_INCREMENT, + `external_id` bigint(20) DEFAULT NULL, + `external_id_type` int(1) DEFAULT NULL, + `external_signature` varchar(40) DEFAULT NULL, + PRIMARY KEY (`uid`), + UNIQUE INDEX `comment_ext_sig_user` (`external_id`,`external_id_type`,`external_signature`) +); + +CREATE TABLE lams_comment ( + `uid` bigint(20) NOT NULL AUTO_INCREMENT, + `session_id` bigint(20) NOT NULL, + `body` text DEFAULT NULL, + `create_by` bigint(20) DEFAULT NULL, + `create_date` datetime DEFAULT NULL, + `update_date` datetime DEFAULT NULL, + `update_by` bigint(20) DEFAULT NULL, + `last_modified` datetime DEFAULT NULL, + `last_reply_date` datetime DEFAULT NULL, + `reply_number` int(11) DEFAULT NULL, + `hide_flag` smallint(6) DEFAULT NULL, + `parent_uid` bigint(20) DEFAULT NULL, + `root_comment_uid` bigint(20) DEFAULT NULL, + `comment_level` smallint(6) DEFAULT NULL, + `thread_comment_uid` bigint(20) DEFAULT NULL, + `likes` text DEFAULT NULL, + `dislikes` text DEFAULT NULL, + `like_count` int(11) DEFAULT 0, + PRIMARY KEY (`uid`), + KEY `FK_comment_session` (`session_id`), + KEY `FK_comment_create` (`create_by`), + KEY `FK_comment_modify` (`update_by`), + KEY `FK_comment_parent` (`parent_uid`), + KEY `FK_comment_root` (`root_comment_uid`), + KEY `FK_comment_thread` (`thread_comment_uid`), + CONSTRAINT `FK_comment_session` FOREIGN KEY (`session_id`) REFERENCES `lams_comment_session` (`uid`), + CONSTRAINT `FK_comment_create` FOREIGN KEY (`create_by`) REFERENCES `lams_user` (`user_id`), + CONSTRAINT `FK_comment_modify` FOREIGN KEY (`update_by`) REFERENCES `lams_user` (`user_id`), + CONSTRAINT `FK_comment_parent` FOREIGN KEY (`parent_uid`) REFERENCES `lams_comment` (`uid`), + CONSTRAINT `FK_comment_root` FOREIGN KEY (`root_comment_uid`) REFERENCES `lams_comment` (`uid`), + CONSTRAINT `FK_comment_thread` FOREIGN KEY (`thread_comment_uid`) REFERENCES `lams_comment` (`uid`) +); + +CREATE TABLE lams_comment_likes ( + `uid` bigint(20) NOT NULL AUTO_INCREMENT, + `comment_uid` bigint(20) NOT NULL, + `user_id` bigint(20) NOT NULL, + `vote` tinyint(1) DEFAULT NULL, + PRIMARY KEY (`uid`), + UNIQUE INDEX `comment_like_unique` (`comment_uid`, `user_id`), + KEY `FK_commentlike_comment` (`comment_uid`), + KEY `FK_commentlike_user` (`user_id`), + CONSTRAINT `FK_commentlike_comment` FOREIGN KEY (`comment_uid`) REFERENCES `lams_comment` (`uid`), + CONSTRAINT `FK_commentlike_user` FOREIGN KEY (`user_id`) REFERENCES `lams_user` (`user_id`) +); + +COMMIT; +SET AUTOCOMMIT = 1; + + +
CommentDTO
Comment
java.util.Comparator