package org.sakaiproject.lessonbuildertool.tool.producers; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.text.SimpleDateFormat; import java.text.DateFormat; import java.util.Calendar; import java.net.URLEncoder; import org.sakaiproject.lessonbuildertool.SimplePageComment; import org.sakaiproject.lessonbuildertool.SimplePageItem; import org.sakaiproject.lessonbuildertool.SimplePage; import org.sakaiproject.lessonbuildertool.SimplePageLogEntry; import org.sakaiproject.lessonbuildertool.SimpleStudentPage; import org.sakaiproject.lessonbuildertool.model.SimplePageToolDao; import org.sakaiproject.lessonbuildertool.tool.beans.SimplePageBean; import org.sakaiproject.lessonbuildertool.tool.view.CommentsViewParameters; import org.sakaiproject.lessonbuildertool.tool.view.GeneralViewParameters; import org.sakaiproject.site.api.Site; import org.sakaiproject.site.cover.SiteService; import org.sakaiproject.site.api.Group; import org.sakaiproject.user.api.User; import org.sakaiproject.user.cover.UserDirectoryService; import org.sakaiproject.time.cover.TimeService; import org.sakaiproject.util.ResourceLoader; import uk.org.ponder.messageutil.MessageLocator; import uk.org.ponder.localeutil.LocaleGetter; import uk.org.ponder.rsf.components.UIBranchContainer; import uk.org.ponder.rsf.components.UIContainer; import uk.org.ponder.rsf.components.UIInternalLink; import uk.org.ponder.rsf.components.UILink; import uk.org.ponder.rsf.components.UIOutput; import uk.org.ponder.rsf.components.UIVerbatim; import uk.org.ponder.rsf.components.decorators.UIFreeAttributeDecorator; import uk.org.ponder.rsf.components.decorators.UIStyleDecorator; import uk.org.ponder.rsf.flow.jsfnav.NavigationCase; import uk.org.ponder.rsf.flow.jsfnav.NavigationCaseReporter; import uk.org.ponder.rsf.view.ComponentChecker; import uk.org.ponder.rsf.view.ViewComponentProducer; import uk.org.ponder.rsf.viewstate.SimpleViewParameters; import uk.org.ponder.rsf.viewstate.ViewParameters; import uk.org.ponder.rsf.viewstate.ViewParamsReporter; public class CommentsProducer implements ViewComponentProducer, ViewParamsReporter, NavigationCaseReporter { public static final String VIEW_ID = "Comments"; private SimplePageBean simplePageBean; private MessageLocator messageLocator; private LocaleGetter localeGetter; private SimplePageToolDao simplePageToolDao; private HashMap<String, String> anonymousLookup = new HashMap<String, String>(); private HashMap<Long, String> itemToPageowner = null; private String currentUserId; private String owner = null; private boolean filter; private boolean canEditPage = false; Locale M_locale = null; DateFormat df = null; DateFormat dfTime = null; DateFormat dfDate = null; public String getViewID() { return VIEW_ID; } public void fillComponents(UIContainer tofill, ViewParameters viewparams, ComponentChecker checker) { CommentsViewParameters params = (CommentsViewParameters) viewparams; filter = params.filter; // set up locale String langLoc[] = localeGetter.get().toString().split("_"); if (langLoc.length >= 2) { if ("en".equals(langLoc[0]) && "ZA".equals(langLoc[1])) { M_locale = new Locale("en", "GB"); } else { M_locale = new Locale(langLoc[0], langLoc[1]); } } else { M_locale = new Locale(langLoc[0]); } df = DateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.DEFAULT, new ResourceLoader().getLocale()); df.setTimeZone(TimeService.getLocalTimeZone()); dfTime = DateFormat.getTimeInstance(DateFormat.SHORT, M_locale); dfTime.setTimeZone(TimeService.getLocalTimeZone()); dfDate = DateFormat.getDateInstance(DateFormat.MEDIUM, M_locale); dfDate.setTimeZone(TimeService.getLocalTimeZone()); // errors redirect back to ShowPage. But if this is embedded in the page, ShowPage // will call us again. This is very hard for the user to recover from. So trap // all possible errors. It may result in an incomplete page or something invalid, // but better that than an infinite recursion. try { SimplePage currentPage = simplePageToolDao.getPage(params.pageId); simplePageBean.setCurrentSiteId(params.siteId); simplePageBean.setCurrentPage(currentPage); simplePageBean.setCurrentPageId(params.pageId); UIOutput.make(tofill, "mainlist").decorate(new UIFreeAttributeDecorator("aria-label", messageLocator.getMessage("simplepage.comments-section"))); SimplePageItem commentsItem = simplePageToolDao.findItem(params.itemId); if(commentsItem != null && commentsItem.getSakaiId() != null && !commentsItem.getSakaiId().equals("")) { SimpleStudentPage studentPage = simplePageToolDao.findStudentPage(Long.valueOf(commentsItem.getSakaiId())); if(studentPage != null) { owner = studentPage.getOwner(); } } if(params.deleteComment != null) { simplePageBean.deleteComment(params.deleteComment); } List<SimplePageComment> comments; if(!filter && !params.studentContentItem) { comments = (List<SimplePageComment>) simplePageToolDao.findComments(params.itemId); }else if(filter && !params.studentContentItem) { comments = (List<SimplePageComment>) simplePageToolDao.findCommentsOnItemByAuthor(params.itemId, params.author); }else if(filter && params.studentContentItem) { List<SimpleStudentPage> studentPages = simplePageToolDao.findStudentPages(params.itemId); Site site = SiteService.getSite(currentPage.getSiteId()); itemToPageowner = new HashMap<Long, String>(); List<Long> commentsItemIds = new ArrayList<Long>(); for(SimpleStudentPage p : studentPages) { // If the page is deleted, don't show the comments if(!p.isDeleted()) { commentsItemIds.add(p.getCommentsSection()); String pageOwner = p.getOwner(); String pageGroup = p.getGroup(); if (pageGroup != null) pageOwner = pageGroup; try { String o = null; if (pageGroup != null) o = site.getGroup(pageGroup).getTitle(); else o = UserDirectoryService.getUser(pageOwner).getDisplayName(); if (o != null) pageOwner = o; } catch (Exception ignore) {}; itemToPageowner.put(p.getCommentsSection(), pageOwner); } } comments = simplePageToolDao.findCommentsOnItemsByAuthor(commentsItemIds, params.author); }else { List<SimpleStudentPage> studentPages = simplePageToolDao.findStudentPages(params.itemId); List<Long> commentsItemIds = new ArrayList<Long>(); for(SimpleStudentPage p : studentPages) { commentsItemIds.add(p.getCommentsSection()); } comments = simplePageToolDao.findCommentsOnItems(commentsItemIds); } // Make sure everything is chronological Collections.sort(comments, new Comparator<SimplePageComment>() { public int compare(SimplePageComment c1, SimplePageComment c2) { if (itemToPageowner != null) { String o1 = itemToPageowner.get(c1.getItemId()); String o2 = itemToPageowner.get(c2.getItemId()); if (o1 != o2) { if (o1 == null) return 1; return o1.compareTo(o2); } } return c1.getTimePosted().compareTo(c2.getTimePosted()); } }); currentUserId = UserDirectoryService.getCurrentUser().getId(); boolean anonymous = simplePageBean.findItem(params.itemId).isAnonymous(); if(anonymous) { int i = 1; for(SimplePageComment comment : comments) { if(!anonymousLookup.containsKey(comment.getAuthor())) { anonymousLookup.put(comment.getAuthor(), messageLocator.getMessage("simplepage.anonymous") + " " + i); i++; } } } boolean highlighted = false; // We don't want page owners to edit or grade comments on their page // at the moment. Perhaps add option? canEditPage = simplePageBean.getEditPrivs() == 0; boolean showGradingMessage = canEditPage && commentsItem.getGradebookId() != null && !params.filter; Date lastViewed = null; // remains null if never viewed before SimplePageLogEntry log = simplePageBean.getLogEntry(params.itemId); if (log != null) lastViewed = log.getLastViewed(); int newItems = 0; // Remove any "phantom" comments. So that the anonymous order stays the same, // comments are deleted by removing all content. Also check to see if any comments // have been graded yet. Finally, if we're filtering, it takes out comments not by // this author. for(int i = comments.size()-1; i >= 0; i--) { if(comments.get(i).getComment() == null || comments.get(i).getComment().equals("")) { comments.remove(i); }else if(params.filter && !comments.get(i).getAuthor().equals(params.author)) { comments.remove(i); }else{ if(showGradingMessage && comments.get(i).getPoints() != null) showGradingMessage = false; if (lastViewed == null) newItems ++; // all items are new if never viewed else if (comments.get(i).getTimePosted().after(lastViewed)) newItems ++; } } // update date only if we actually are going to see all the comments, and it's // not a weird view (i.e. filter is on) // The situation with items <= 5 is actually dubious. The user has them on the // screen, but there's no way to know whether he's actually seen them. Some users // are going to be surprised either way we do it. if (params.showAllComments || params.showNewComments || (!params.filter && newItems <= 5)) { if (log != null) { simplePageBean.update(log); } else { log = simplePageToolDao.makeLogEntry(currentUserId, params.itemId, null); simplePageBean.saveItem(log); } } // Make sure we don't show the grading message if there's nothing to grade. if(comments.size() == 0) { showGradingMessage = false; } boolean editable = false; if(comments.size() <= 5 || params.showAllComments || params.filter) { for(int i = 0; i < comments.size(); i++) { // We don't want them editing on the grading screen, which is why we also check filter. boolean canEdit = simplePageBean.canModifyComment(comments.get(i), canEditPage) && !params.filter; printComment(comments.get(i), tofill, (params.postedComment == comments.get(i).getId()), anonymous, canEdit, params, commentsItem, currentPage); if(!highlighted) { highlighted = (params.postedComment == comments.get(i).getId()); } if(!editable) editable = canEdit; } }else { UIBranchContainer container = UIBranchContainer.make(tofill, "commentList:"); UIOutput.make(container, "commentDiv"); // UIBranchContainer container = UIBranchContainer.make(tofill, "commentDiv:"); CommentsViewParameters eParams = new CommentsViewParameters(VIEW_ID); eParams.placementId = params.placementId; eParams.itemId = params.itemId; eParams.showAllComments=true; eParams.showNewComments=false; eParams.pageId = params.pageId; eParams.siteId = params.siteId; UIInternalLink.make(container, "to-load", eParams); UIOutput.make(container, "load-more-link", messageLocator.getMessage("simplepage.see_all_comments").replace("{}", Integer.toString(comments.size()))); if (!params.showNewComments && newItems > 5) { container = UIBranchContainer.make(tofill, "commentList:"); UIOutput.make(container, "commentDiv"); // UIBranchContainer container = UIBranchContainer.make(tofill, "commentDiv:"); eParams = new CommentsViewParameters(VIEW_ID); eParams.placementId = params.placementId; eParams.itemId = params.itemId; eParams.showAllComments=false; eParams.showNewComments=true; eParams.pageId = params.pageId; eParams.siteId = params.siteId; UIInternalLink.make(container, "to-load", eParams); UIOutput.make(container, "load-more-link", messageLocator.getMessage("simplepage.see_new_comments").replace("{}", Integer.toString(newItems))); } int start = comments.size()-5; if (params.showNewComments) start = 0; // Show 5 most recent comments for(int i = start; i < comments.size(); i++) { if (!params.showNewComments || lastViewed == null || comments.get(i).getTimePosted().after(lastViewed)) { boolean canEdit = simplePageBean.canModifyComment(comments.get(i), canEditPage); printComment(comments.get(i), tofill, (params.postedComment == comments.get(i).getId()), anonymous, canEdit, params, commentsItem, currentPage); if(!highlighted) { highlighted = (params.postedComment == comments.get(i).getId()); } if(!editable) editable = canEdit; } } } if(highlighted) { // We have something to highlight UIOutput.make(tofill, "highlightScript"); } if(showGradingMessage) { UIOutput.make(tofill, "gradingAlert"); } if(anonymous && canEditPage && comments.size() > 0 && lastViewed == null) { // Tells the admin that they can see the names, but everyone else can't UIOutput.make(tofill, "anonymousAlert"); }else if(editable && simplePageBean.getEditPrivs() != 0) { // Warns user that they only have 30 mins to edit. UIOutput.make(tofill, "editAlert"); } } catch (Exception e) { e.printStackTrace(); System.out.println("comments error " + e); }; } private Long lastTitle = -1L; public void printComment(SimplePageComment comment, UIContainer tofill, boolean highlight, boolean anonymous, boolean showModifiers, CommentsViewParameters params, SimplePageItem commentsItem, SimplePage currentPage) { if (canEditPage && itemToPageowner != null && comment.getItemId() != lastTitle) { UIBranchContainer commentContainer = UIBranchContainer.make(tofill, "commentList:"); UIOutput.make(commentContainer, "commentTitle", messageLocator.getMessage("simplepage.comments-grading").replace("{}", itemToPageowner.get(comment.getItemId()))); lastTitle = comment.getItemId(); } // print title if this is a comment on a different page. Normally this // shold only happen for subpages of student pages String pageTitle = null; if (currentPage.getPageId() != comment.getPageId()) { SimplePage commentPage = simplePageBean.getPage(comment.getPageId()); pageTitle = commentPage.getTitle(); } UIBranchContainer commentContainer = UIBranchContainer.make(tofill, "commentList:"); UIOutput.make(commentContainer, "commentDiv"); if(highlight) commentContainer.decorate(new UIStyleDecorator("highlight-comment")); if(!filter && params.author != null && params.author.equals(comment.getAuthor())) { commentContainer.decorate(new UIStyleDecorator("backgroundHighlight")); } String author; if(!anonymous) { try { User user = UserDirectoryService.getUser(comment.getAuthor()); author = user.getDisplayName(); }catch(Exception ex) { author = messageLocator.getMessage("simplepage.comment-unknown-user"); ex.printStackTrace(); } }else { author = anonymousLookup.get(comment.getAuthor()); if(comment.getAuthor().equals(owner)) { author = messageLocator.getMessage("simplepage.comment-author-owner"); } if(author == null) author = "Anonymous User"; // Shouldn't ever occur if(simplePageBean.getEditPrivs() == 0) { try { User user = UserDirectoryService.getUser(comment.getAuthor()); author += " (" + user.getDisplayName() + ")"; }catch(Exception ex) { author += " (" + messageLocator.getMessage("simplepage.comment-unknown-user") + ")"; } }else if(comment.getAuthor().equals(currentUserId)) { author += " (" + messageLocator.getMessage("simplepage.comment-you") + ")"; } } UIOutput authorOutput = UIOutput.make(commentContainer, "userId", author); if(comment.getAuthor().equals(currentUserId)) { authorOutput.decorate(new UIStyleDecorator("specialCommenter")); authorOutput.decorate(new UIStyleDecorator("personalComment")); }else if(comment.getAuthor().equals(owner)) { authorOutput.decorate(new UIStyleDecorator("specialCommenter")); authorOutput.decorate(new UIStyleDecorator("ownerComment")); } if (pageTitle != null) UIOutput.make(commentContainer, "pageTitle", pageTitle); String timeDifference = getTimeDifference(comment.getTimePosted().getTime()); UIOutput.make(commentContainer, "timePosted", timeDifference); if(showModifiers) { UIOutput.make(commentContainer, "deleteSpan"); CommentsViewParameters eParams = (CommentsViewParameters) params.copy(); eParams.placementId = params.placementId; eParams.deleteComment = comment.getUUID(); eParams.pageId = params.pageId; eParams.siteId = params.siteId; UIInternalLink.make(commentContainer, "deleteCommentURL", eParams); UIOutput.make(commentContainer, "deleteComment"). decorate(new UIFreeAttributeDecorator("title", messageLocator.getMessage("simplepage.comment-delete").replace("{}", author))); UIOutput.make(commentContainer, "editComment"). decorate(new UIFreeAttributeDecorator("onclick", "edit($(this), " + comment.getId() + ");")). decorate(new UIFreeAttributeDecorator("title", messageLocator.getMessage("simplepage.comment-edit").replace("{}", author))); if(!filter && simplePageBean.getEditPrivs() == 0 && commentsItem.getGradebookId() != null) { UIOutput.make(commentContainer, "gradingSpan"); UIOutput.make(commentContainer, "commentsUUID", comment.getUUID()); UIOutput.make(commentContainer, "commentPoints", (comment.getPoints() == null? "" : String.valueOf(comment.getPoints()))); UIOutput.make(commentContainer, "pointsBox").decorate( new UIFreeAttributeDecorator("title", messageLocator.getMessage("simplepage.grade-for-student").replace("{}", author))); UIOutput.make(commentContainer, "maxpoints", " / " + commentsItem.getGradebookPoints()); UIOutput.make(commentContainer, "authorUUID", comment.getAuthor()); } } if(filter && simplePageBean.getEditPrivs() == 0) { UIOutput.make(commentContainer, "contextSpan"); // because this is called via /faces, the full Sakai context is not set up. // in particular, UIInternalLink will generate the wrong thing. Thus we // make up a full URL ourselves. String pars = "/portal/tool/" + URLEncoder.encode(params.placementId) + "/ShowPage?path=none" + "&author=" + URLEncoder.encode(comment.getAuthor()); // Need to provide the item ID if(!params.studentContentItem && params.pageItemId != -1L) { pars += "&itemId=" + URLEncoder.encode(Long.toString(params.pageItemId)); } UILink contextLink = UILink.make(commentContainer, "contextLink", messageLocator.getMessage("simplepage.show-context"), pars); if (itemToPageowner == null) contextLink.decorate( new UIFreeAttributeDecorator("title", messageLocator.getMessage("simplepage.context-link-title-1"). replace("{}", author))); else contextLink.decorate( new UIFreeAttributeDecorator("title", messageLocator.getMessage("simplepage.context-link-title-2"). replace("{1}", author).replace("{2}", itemToPageowner.get(comment.getItemId())))); } String dateString = df.format(comment.getTimePosted()); if (!filter) UIOutput.make(commentContainer, "replyTo"). decorate(new UIFreeAttributeDecorator("onclick", "replyToComment($(this),'" + messageLocator.getMessage("simplepage.in-reply-to").replace("{1}", author).replace("{2}", dateString) + "')")). decorate(new UIFreeAttributeDecorator("title", messageLocator.getMessage("simplepage.comment-reply").replace("{}",author))); if(!comment.getHtml()) { UIOutput.make(commentContainer, "comment", comment.getComment()); }else { UIVerbatim.make(commentContainer, "comment", comment.getComment()); } } public String getTimeDifference(long timeMillis) { long difference = Math.round((System.currentTimeMillis() - timeMillis) / 1000); // In seconds // These constants are calculated to take rounding into effect, and try to give a fairly // accurate representation of the time difference using words. String descrip = ""; if(difference < 45) { descrip = messageLocator.getMessage("simplepage.seconds"); }else if(difference < 90) { descrip = messageLocator.getMessage("simplepage.one_min"); }else if(difference < 3570) { // 2 mins --> 59 mins int minutes = Math.max(2, Math.round(difference / 60)); descrip = messageLocator.getMessage("simplepage.x_min").replace("{}", String.valueOf(minutes)); }else if(difference < 7170) { descrip = messageLocator.getMessage("simplepage.one_hour"); }else if(difference < 84600) { // 2 hours --> 23 hours int hours = Math.max(2, Math.round(difference / 3600)); descrip = messageLocator.getMessage("simplepage.x_hour").replace("{}", String.valueOf(hours)); }else if(difference < 129600) { descrip = messageLocator.getMessage("simplepage.one_day"); }else if(difference < 2548800) { // 2 days --> 29 days int days = Math.max(2, Math.round(difference / 86400)); descrip = messageLocator.getMessage("simplepage.x_day").replace("{}", String.valueOf(days)); }else if(difference < 3888000) { descrip = messageLocator.getMessage("simplepage.one_month"); }else if(difference < 29808000) { // 2 months --> 11 months int months = Math.max(2, Math.round(difference / 2592000)); descrip = messageLocator.getMessage("simplepage.x_month").replace("{}", String.valueOf(months)); }else if(difference < 47304000) { descrip = messageLocator.getMessage("simplepage.one_year"); }else { // 2+ years int years = Math.max(2, Math.round(difference / 31536000)); descrip = messageLocator.getMessage("simplepage.x_year").replace("{}", String.valueOf(years)); } Date d = new Date(timeMillis); Date now = new Date(); Calendar cpost = Calendar.getInstance(TimeService.getLocalTimeZone(), M_locale); Calendar cnow = Calendar.getInstance(TimeService.getLocalTimeZone(), M_locale); cpost.setTime(d); cnow.setTime(now); if(cpost.get(Calendar.MONTH) == cnow.get(Calendar.MONTH) && cpost.get(Calendar.DATE) == cnow.get(Calendar.DATE) && cpost.get(Calendar.YEAR) == cnow.get(Calendar.YEAR)) { return dfTime.format(d) + " (" + descrip + ")"; }else if(d.getYear() == now.getYear()) { return dfDate.format(d) + " (" + descrip + ")"; }else { return dfDate.format(d) + " (" + descrip + ")"; } } public void setSimplePageBean(SimplePageBean bean) { simplePageBean = bean; } public void setMessageLocator(MessageLocator locator) { messageLocator = locator; } public void setLocaleGetter(LocaleGetter getter) { localeGetter = getter; } public void setSimplePageToolDao(SimplePageToolDao simplePageToolDao) { this.simplePageToolDao = simplePageToolDao; } public ViewParameters getViewParameters() { return new CommentsViewParameters(); } public List reportNavigationCases() { List<NavigationCase> togo = new ArrayList<NavigationCase>(); togo.add(new NavigationCase(null, new SimpleViewParameters(ShowPageProducer.VIEW_ID))); togo.add(new NavigationCase("success", new SimpleViewParameters(ShowPageProducer.VIEW_ID))); GeneralViewParameters commentsParameters = new GeneralViewParameters(ShowPageProducer.VIEW_ID); commentsParameters.postedComment = true; togo.add(new NavigationCase("added-comment", commentsParameters)); return togo; } }