Index: lams_build/lib/lams/lams.jar =================================================================== diff -u -ra4fd7ba340a2beac7436660039f13b9c8708f172 -r58de3acfcde3052a939146df7d0172599e56633a Binary files differ Index: lams_common/src/java/org/lamsfoundation/lams/rating/dao/IRatingDAO.java =================================================================== diff -u -r5dbbc7b9946ede2b5de406e9adb39928e1dda083 -r58de3acfcde3052a939146df7d0172599e56633a --- lams_common/src/java/org/lamsfoundation/lams/rating/dao/IRatingDAO.java (.../IRatingDAO.java) (revision 5dbbc7b9946ede2b5de406e9adb39928e1dda083) +++ lams_common/src/java/org/lamsfoundation/lams/rating/dao/IRatingDAO.java (.../IRatingDAO.java) (revision 58de3acfcde3052a939146df7d0172599e56633a) @@ -130,5 +130,9 @@ * See Peer Review for example usage. */ String getRatingSelectJoinSQL(Integer ratingStyle, boolean getByUser); - + + /** + * Get all the raw ratings for a combination of criteria and item ids. Used by Peer Review to do SPA analysis. + */ + List getRatingsByCriteriasAndItems(Collection ratingCriteriaIds, Collection itemIds); } Index: lams_common/src/java/org/lamsfoundation/lams/rating/dao/hibernate/RatingDAO.java =================================================================== diff -u -r5dbbc7b9946ede2b5de406e9adb39928e1dda083 -r58de3acfcde3052a939146df7d0172599e56633a --- lams_common/src/java/org/lamsfoundation/lams/rating/dao/hibernate/RatingDAO.java (.../RatingDAO.java) (revision 5dbbc7b9946ede2b5de406e9adb39928e1dda083) +++ lams_common/src/java/org/lamsfoundation/lams/rating/dao/hibernate/RatingDAO.java (.../RatingDAO.java) (revision 58de3acfcde3052a939146df7d0172599e56633a) @@ -25,16 +25,13 @@ package org.lamsfoundation.lams.rating.dao.hibernate; -import java.text.NumberFormat; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.List; -import java.util.Locale; import java.util.Map; import java.util.Set; -import org.hibernate.Query; import org.lamsfoundation.lams.dao.hibernate.LAMSBaseDAO; import org.lamsfoundation.lams.rating.dao.IRatingDAO; import org.lamsfoundation.lams.rating.dto.ItemRatingCriteriaDTO; @@ -59,6 +56,9 @@ private static final String FIND_RATINGS_BY_USER_CRITERIA = "FROM " + Rating.class.getName() + " AS r where r.ratingCriteria.ratingCriteriaId=? AND r.learner.userId=?"; + private static final String FIND_RATINGS_BY_ITEM_CRITERIA = "FROM " + Rating.class.getName() + + " AS r where r.ratingCriteria.ratingCriteriaId IN (:ratingCriteriaIds) AND r.itemId IN (:itemIds)"; + private static final String FIND_RATING_AVERAGE_BY_ITEM = "SELECT AVG(r.rating), COUNT(*) FROM " + Rating.class.getName() + " AS r where r.ratingCriteria.ratingCriteriaId=? AND r.toolSessionId=? AND r.itemId=?"; @@ -363,5 +363,17 @@ else return getByUser ? TOOL_SELECT_LEFT_JOIN_BY_USER_STANDARD : TOOL_SELECT_LEFT_JOIN_FOR_USER_STANDARD; } + + /** + * Get all the raw ratings for a combination of criteria and item ids. Used by Peer Review to do SPA analysis. + */ + @SuppressWarnings("unchecked") + public List getRatingsByCriteriasAndItems(Collection ratingCriteriaIds, Collection itemIds) { + return getSession().createQuery(FIND_RATINGS_BY_ITEM_CRITERIA) + .setParameterList("ratingCriteriaIds", ratingCriteriaIds) + .setParameterList("itemIds", itemIds) + .list(); + } + } Index: lams_common/src/java/org/lamsfoundation/lams/rating/service/IRatingService.java =================================================================== diff -u -r5dbbc7b9946ede2b5de406e9adb39928e1dda083 -r58de3acfcde3052a939146df7d0172599e56633a --- lams_common/src/java/org/lamsfoundation/lams/rating/service/IRatingService.java (.../IRatingService.java) (revision 5dbbc7b9946ede2b5de406e9adb39928e1dda083) +++ lams_common/src/java/org/lamsfoundation/lams/rating/service/IRatingService.java (.../IRatingService.java) (revision 58de3acfcde3052a939146df7d0172599e56633a) @@ -200,5 +200,9 @@ Map countUsersRatedEachItemByCriteria(final Long criteriaId, final Long toolSessionId, final Collection itemIds, Integer excludeUserId); + /** + * Get all the raw ratings for a combination of criteria and item ids. Used by Peer Review to do SPA analysis. + */ + List getRatingsByCriteriasAndItems(Collection ratingCriteriaIds, Collection itemIds); } Index: lams_common/src/java/org/lamsfoundation/lams/rating/service/RatingService.java =================================================================== diff -u -r8e090b3ddf269cdffececa4bc55a9333da5b0858 -r58de3acfcde3052a939146df7d0172599e56633a --- lams_common/src/java/org/lamsfoundation/lams/rating/service/RatingService.java (.../RatingService.java) (revision 8e090b3ddf269cdffececa4bc55a9333da5b0858) +++ lams_common/src/java/org/lamsfoundation/lams/rating/service/RatingService.java (.../RatingService.java) (revision 58de3acfcde3052a939146df7d0172599e56633a) @@ -51,7 +51,6 @@ import org.lamsfoundation.lams.rating.dto.RatingCommentDTO; import org.lamsfoundation.lams.rating.dto.StyledCriteriaRatingDTO; import org.lamsfoundation.lams.rating.dto.StyledRatingDTO; -import org.lamsfoundation.lams.rating.model.AuthoredItemRatingCriteria; import org.lamsfoundation.lams.rating.model.LearnerItemRatingCriteria; import org.lamsfoundation.lams.rating.model.Rating; import org.lamsfoundation.lams.rating.model.RatingComment; @@ -770,6 +769,15 @@ public String getRatingSelectJoinSQL(Integer ratingStyle, boolean getByUser) { return ratingDAO.getRatingSelectJoinSQL(ratingStyle, getByUser); } + + /** + * Get all the raw ratings for a combination of criteria and item ids. Used by Peer Review to do SPA analysis. + */ + @Override + public List getRatingsByCriteriasAndItems(Collection ratingCriteriaIds, Collection itemIds) { + return ratingDAO.getRatingsByCriteriasAndItems(ratingCriteriaIds, itemIds); + } + /* ********** Used by Spring to "inject" the linked objects ************* */ public void setRatingDAO(IRatingDAO ratingDAO) { Index: lams_common/src/java/org/lamsfoundation/lams/util/NumberUtil.java =================================================================== diff -u -r51fb2a37254f24bb2a805d4ffd54482c779f43fa -r58de3acfcde3052a939146df7d0172599e56633a --- lams_common/src/java/org/lamsfoundation/lams/util/NumberUtil.java (.../NumberUtil.java) (revision 51fb2a37254f24bb2a805d4ffd54482c779f43fa) +++ lams_common/src/java/org/lamsfoundation/lams/util/NumberUtil.java (.../NumberUtil.java) (revision 58de3acfcde3052a939146df7d0172599e56633a) @@ -41,8 +41,31 @@ } /** + * Format a given float or double to the I18N format specified by the locale, with a fixed number of decimal places. + * If numFractionDigits is 2: 4.051 -> 4.05, 4 -> 4.00 + * + * @param mark + * @param locale + * @param numFractionDigits + * @return + */ + public static String formatLocalisedNumberForceDecimalPlaces(Number number, Locale locale, int numFractionDigits) { + NumberFormat format = null; + if (locale == null) { + format = NumberFormat.getInstance(NumberUtil.getServerLocale()); + } else { + format = NumberFormat.getInstance(locale); + } + format.setMinimumFractionDigits(numFractionDigits); + return format.format(number); + } + + + /** * Format a given float or double to the I18N format specified by the locale. If no locale supplied, uses LAMS * server's default locale. + * + * Will set it to numFractionDigits decimal places if needed. If numFractionDigits is 2: 4.051 -> 4.05, 4 stays as 4. * * @param mark * @param locale Index: lams_tool_preview/conf/language/lams/ApplicationResources.properties =================================================================== diff -u -re7ae6912ebf080177211942a9d5539cef1c7e920 -r58de3acfcde3052a939146df7d0172599e56633a --- lams_tool_preview/conf/language/lams/ApplicationResources.properties (.../ApplicationResources.properties) (revision e7ae6912ebf080177211942a9d5539cef1c7e920) +++ lams_tool_preview/conf/language/lams/ApplicationResources.properties (.../ApplicationResources.properties) (revision 58de3acfcde3052a939146df7d0172599e56633a) @@ -144,12 +144,6 @@ label.monitoring.view =View {0} label.close =Close event.sent.results.subject =LAMS: {0} results were released -event.sent.results.body =

{0}

Your results are:

{1}

This message was send automatically, please do not reply to it.

-event.sent.results.criteria.comment =

{0}:

    {1}

-event.sent.results.criteria.star =

{0} (Star): {1}

    {2}
-event.sent.results.criteria.hedge =

{0} (Mark Hedging): {1} out of {2}

-event.sent.results.criteria.rank =

{0} (Rank): {1} out of {2}

-event.sent.results.criteria.rankAll =

{0} (Rank All): {1}

event.sent.results.no.results =No results msg.results.sent ={0} Email(s) Sent button.email.results =Email Results @@ -173,5 +167,30 @@ label.user.hidden =Remove user from rating label.hide.user.confirmation =Are you sure you want to stop rating this user? It will lead to removal of all ratings and comments left by him/her. - +label.file.downloaded=Spreadsheet downloaded +email.SPA=SPA +email.SPA.factor=SPA Factor +email.SAPA=SAPA +email.SAPA.factor=SAPA Factor +email.self.rating=Self rating +email.peers.rating=Peers\' rating +email.explanation.SPA.less.one=SPA < 1.0 +email.explanation.SPA.one=SPA = 1.0 +email.explanation.SPA.greater.one=SPA > 1.0 +email.explanation.SAPA.less.one=SAPA < 1.0 +email.explanation.SAPA.one=SAPA = 1.0 +email.explanation.SAPA.greater.one=SAPA > 1.0 +email.explanation.1=Your performance is below expectation and your self-assessment is too low. +email.explanation.2=Your performance met expectation but your self-assessment is too low. +email.explanation.3=Your performance exceeds expectation but your self-assessment is too low. +email.explanation.4=Your performance is below expectation and your self-assessment is about right. +email.explanation.5=Your performance met expectation and your self-assessment is about right. +email.explanation.6=Your performance exceeds expectation and your self-assessment is about right. +email.explanation.7=Your performance is below expectation and your self-assessment is too high. +email.explanation.8=Your performance met expectation but your self-assessment is too high. +email.explanation.9=Your performance exceeds expectation but your self-assessment is too high. +email.send.automatically=This message was sent automatically, please do not reply to it. +email.label.feedback=Feedback from group members for {0}: +email.label.self.rating=Self rating +email.label.peers.ratng=Peers #======= End labels: Exported 166 labels for en AU ===== Index: lams_tool_preview/src/java/org/lamsfoundation/lams/tool/peerreview/service/PeerreviewServiceImpl.java =================================================================== diff -u -r64ee69765400783a3e284e7856aa91cdd01f4831 -r58de3acfcde3052a939146df7d0172599e56633a --- lams_tool_preview/src/java/org/lamsfoundation/lams/tool/peerreview/service/PeerreviewServiceImpl.java (.../PeerreviewServiceImpl.java) (revision 64ee69765400783a3e284e7856aa91cdd01f4831) +++ lams_tool_preview/src/java/org/lamsfoundation/lams/tool/peerreview/service/PeerreviewServiceImpl.java (.../PeerreviewServiceImpl.java) (revision 58de3acfcde3052a939146df7d0172599e56633a) @@ -56,7 +56,6 @@ import org.lamsfoundation.lams.notebook.service.ICoreNotebookService; import org.lamsfoundation.lams.rating.dto.ItemRatingDTO; import org.lamsfoundation.lams.rating.dto.StyledCriteriaRatingDTO; -import org.lamsfoundation.lams.rating.dto.StyledRatingDTO; import org.lamsfoundation.lams.rating.model.LearnerItemRatingCriteria; import org.lamsfoundation.lams.rating.model.RatingCriteria; import org.lamsfoundation.lams.rating.service.IRatingService; @@ -79,6 +78,7 @@ import org.lamsfoundation.lams.tool.peerreview.model.Peerreview; import org.lamsfoundation.lams.tool.peerreview.model.PeerreviewSession; import org.lamsfoundation.lams.tool.peerreview.model.PeerreviewUser; +import org.lamsfoundation.lams.tool.peerreview.util.EmailAnalysisBuilder; import org.lamsfoundation.lams.tool.peerreview.util.PeerreviewToolContentHandler; import org.lamsfoundation.lams.tool.peerreview.util.SpreadsheetBuilder; import org.lamsfoundation.lams.tool.service.ILamsToolService; @@ -88,7 +88,6 @@ import org.lamsfoundation.lams.util.ExcelCell; import org.lamsfoundation.lams.util.JsonUtil; import org.lamsfoundation.lams.util.MessageService; -import org.springframework.util.StringUtils; /** * @author Andrey Balan @@ -459,108 +458,36 @@ return peerreviewUserDao.getPagedUsers(toolSessionId, page, size, sorting, searchString); } - @Override - public int emailReportToSessionUsers(Long toolContentId, Long sessionId) { - List users = peerreviewUserDao.getBySessionID(sessionId); - Peerreview peerreview = getPeerreviewByContentId(toolContentId); - String subject = getResultsEmailSubject(peerreview); - List criterias = getRatingCriterias(toolContentId); - int numEmailsSent = 0; - for (PeerreviewUser user : users) { - if (emailReport(toolContentId, sessionId, user, peerreview, criterias, subject) != 1) { - log.error("Unable to email to all users in session " + sessionId + ". Have processed " + numEmailsSent - + " so far."); - return -1; - } - numEmailsSent++; - } - return numEmailsSent; + private String getResultsEmailSubject(Peerreview peerreview) { + return getLocalisedMessage("event.sent.results.subject", new Object[] { peerreview.getTitle() }); } + @Override public int emailReportToUser(Long toolContentId, Long sessionId, Long userId) { - - PeerreviewUser user = peerreviewUserDao.getUserByUserIDAndSessionID(userId, sessionId); + PeerreviewSession session = peerreviewSessionDao.getSessionBySessionId(sessionId); Peerreview peerreview = getPeerreviewByContentId(toolContentId); - return emailReport(toolContentId, sessionId, user, peerreview, getRatingCriterias(toolContentId), - getResultsEmailSubject(peerreview)); - } - private String getResultsEmailSubject(Peerreview peerreview) { - return getLocalisedMessage("event.sent.results.subject", new Object[] { peerreview.getTitle() }); - } - - private int emailReport(Long toolContentId, Long sessionId, PeerreviewUser user, Peerreview peerreview, - List ratingCriterias, String subject) { - - int userId = user.getUserId().intValue(); - String name = StringEscapeUtils.escapeCsv(user.getFirstName() + " " + user.getLastName()); - - StringBuilder notificationMessage = new StringBuilder(); - - for (RatingCriteria criteria : ratingCriterias) { - int sorting = (criteria.isStarStyleRating() || criteria.isHedgeStyleRating()) - ? PeerreviewConstants.SORT_BY_AVERAGE_RESULT_DESC : PeerreviewConstants.SORT_BY_AVERAGE_RESULT_ASC; - StyledCriteriaRatingDTO dto = getUsersRatingsCommentsByCriteriaIdDTO(toolContentId, sessionId, criteria, - user.getUserId(), false, sorting, null, true, false); - generateRatingEntryForEmail(notificationMessage, criteria, dto); - } - - eventNotificationService.sendMessage(null, userId, IEventNotificationService.DELIVERY_METHOD_MAIL, subject, - getLocalisedMessage("event.sent.results.body", new Object[] { name, notificationMessage.toString() }), - true); - + String email = new EmailAnalysisBuilder(peerreview, session, ratingService, peerreviewSessionDao, + peerreviewUserDao, this, messageService).generateHTMLEMailForLearner(userId); + eventNotificationService.sendMessage(null, userId.intValue(), IEventNotificationService.DELIVERY_METHOD_MAIL, + getResultsEmailSubject(peerreview), email, true); return 1; - } - private void generateRatingEntryForEmail(StringBuilder notificationMessage, RatingCriteria criteria, - StyledCriteriaRatingDTO dto) { - String escapedTitle = StringEscapeUtils.escapeHtml(dto.getRatingCriteria().getTitle()); - if (dto.getRatingDtos().size() >= 1) { - if (criteria.isCommentRating()) { - StringBuilder comments = new StringBuilder(); - for (StyledRatingDTO ratingDto : dto.getRatingDtos()) { - if (ratingDto.getComment() != null) { - String escaped = StringEscapeUtils.escapeHtml(ratingDto.getComment()); - comments.append("
  • ").append(escaped).append("
  • "); - } - } - notificationMessage.append(getLocalisedMessage("event.sent.results.criteria.comment", - new Object[] { escapedTitle, StringUtils.replace(comments.toString(), "<BR>", "
    ") })); - } else { - String avgRating = dto.getRatingDtos().get(0).getAverageRating().length() > 0 - ? dto.getRatingDtos().get(0).getAverageRating() : "0"; - StringBuilder comments = null; - if (criteria.isStarStyleRating()) { - if (criteria.isCommentsEnabled()) { - comments = new StringBuilder(); - for (StyledRatingDTO ratingDto : dto.getRatingDtos()) { - if (ratingDto.getComment() != null) { - String escaped = StringEscapeUtils.escapeHtml(ratingDto.getComment()); - comments.append("
  • ").append(escaped).append("
  • "); - } - } - } - notificationMessage.append(getLocalisedMessage("event.sent.results.criteria.star", - new Object[] { escapedTitle, avgRating, comments != null ? comments.toString() : "" })); - } else if (criteria.isRankingStyleRating()) { - if (criteria.getMaxRating() > 0) { - notificationMessage.append(getLocalisedMessage("event.sent.results.criteria.rank", - new Object[] { escapedTitle, avgRating, criteria.getMaxRating() })); - } else { - notificationMessage.append(getLocalisedMessage("event.sent.results.criteria.rankAll", - new Object[] { escapedTitle, avgRating })); - } - } else { // hedge style rating - notificationMessage.append(getLocalisedMessage("event.sent.results.criteria.hedge", - new Object[] { escapedTitle, avgRating, criteria.getMaxRating() })); - } - } - } else { - notificationMessage.append(escapedTitle).append(getLocalisedMessage("event.sent.results.no.results", null)); + @Override + public int emailReportToSessionUsers(Long toolContentId, Long sessionId) { + PeerreviewSession session = peerreviewSessionDao.getSessionBySessionId(sessionId); + Peerreview peerreview = getPeerreviewByContentId(toolContentId); + Map emails = new EmailAnalysisBuilder(peerreview, session, ratingService, + peerreviewSessionDao, peerreviewUserDao, this, messageService).generateHTMLEmailsForSession(); + for ( Map.Entry entry : emails.entrySet() ) { + eventNotificationService.sendMessage(null, entry.getKey().intValue(), + IEventNotificationService.DELIVERY_METHOD_MAIL, + getResultsEmailSubject(peerreview), + entry.getValue(), true); } - notificationMessage.append("\n"); + return emails.size(); } @Override Index: lams_tool_preview/src/java/org/lamsfoundation/lams/tool/peerreview/util/EmailAnalysisBuilder.java =================================================================== diff -u --- lams_tool_preview/src/java/org/lamsfoundation/lams/tool/peerreview/util/EmailAnalysisBuilder.java (revision 0) +++ lams_tool_preview/src/java/org/lamsfoundation/lams/tool/peerreview/util/EmailAnalysisBuilder.java (revision 58de3acfcde3052a939146df7d0172599e56633a) @@ -0,0 +1,573 @@ +package org.lamsfoundation.lams.tool.peerreview.util; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.TreeMap; + +import org.apache.commons.lang.StringEscapeUtils; +import org.apache.commons.lang.StringUtils; +import org.apache.log4j.Logger; +import org.lamsfoundation.lams.rating.dto.StyledCriteriaRatingDTO; +import org.lamsfoundation.lams.rating.dto.StyledRatingDTO; +import org.lamsfoundation.lams.rating.model.Rating; +import org.lamsfoundation.lams.rating.model.RatingCriteria; +import org.lamsfoundation.lams.rating.service.IRatingService; +import org.lamsfoundation.lams.tool.peerreview.PeerreviewConstants; +import org.lamsfoundation.lams.tool.peerreview.dao.PeerreviewSessionDAO; +import org.lamsfoundation.lams.tool.peerreview.dao.PeerreviewUserDAO; +import org.lamsfoundation.lams.tool.peerreview.model.Peerreview; +import org.lamsfoundation.lams.tool.peerreview.model.PeerreviewSession; +import org.lamsfoundation.lams.tool.peerreview.model.PeerreviewUser; +import org.lamsfoundation.lams.tool.peerreview.service.IPeerreviewService; +import org.lamsfoundation.lams.tool.peerreview.util.EmailAnalysisBuilder.LearnerData.SingleCriteriaData; +import org.lamsfoundation.lams.util.MessageService; +import org.lamsfoundation.lams.util.NumberUtil; + +/** + * Creates the Self and Peer Assessment email, comparing the learner's self assessment against their peers' assessment + * of their contribution. + */ +public class EmailAnalysisBuilder { + + private static Logger log = Logger.getLogger(EmailAnalysisBuilder.class.getName()); + + private Peerreview peerreview; + private PeerreviewSession session; + private IRatingService ratingService; + private List criterias; + private PeerreviewUserDAO peerreviewUserDao; + private IPeerreviewService service; + private MessageService messageService; + + // same across the whole lesson + private String htmlCriteriaTableStart; + private String htmlCriteriaTableEnd; + private String htmlOverallResultsTable; + // htmlORTHighlightedCellReplacementText used to highlight the correct cell in the final table. This allows the table to be generated once for the session and then + // updated for each user + private String htmlORTHighlightedCellReplacementText = "%%%%REPLACESTYLEWITHHIGHLIGHT%%%%"; + + // Criterias to report on in the top table. Does not include comments. Always use this for the criteria table so that the order is the same and the data matches the heading! + private List criteriaForCriteriaTable = new ArrayList(); + private Map learnerDataMap = new TreeMap(); // lams user id, learner data + private Map groupAveragePerCriteria = new HashMap(); // key activity id, session wide average of the learner's averages (peerRatingIncSelf) + private Double averageOfAverages; // Average of individualCriteriaAverage across all activities + private int countNonCommentCriteria = 0; + + // cache some labels as they are used repeatedly + private String selfRatingString; + private String peerRatingString; + + // HTML Styles + private static final String width100pc = "width:100%;"; + private static final String tableBorderedStyle = "border-collapse:collapse;border-width:1px;border-style:solid;border-color:#777;"; + private static final String centeredStyle = "text-align:center;"; + private static final String headerBackgroundStyle = "background:#B9CDE5;"; + private static final String highlightBackgroundStyle = "background:#D7E4BD;"; + private static final String borderBelow = "border-bottom-width:1px;border-bottom-style:solid;border-bottom-color:#777;"; + private static final String bold = "font-weight:bold"; + private static final String zebraEvenRow = "border-width:1px;border-style:solid;border-color:#777;background:#FCD5B5;"; + private static final String zebraOddRow = "border-width:1px;border-style:solid;border-color:#777;background:#B9CDE5;"; + + public EmailAnalysisBuilder(Peerreview peerreview, PeerreviewSession session, IRatingService ratingService, + PeerreviewSessionDAO peerreviewSessionDao, PeerreviewUserDAO peerreviewUserDao, IPeerreviewService service, + MessageService messageService) { + this.peerreview = peerreview; + this.session = session; + this.ratingService = ratingService; + this.peerreviewUserDao = peerreviewUserDao; + this.service = service; + this.messageService = messageService; + + criterias = ratingService.getCriteriasByToolContentId(peerreview.getContentId()); + preGenerateBoilerplate(); + selfRatingString = getLocalisedMessage("email.self.rating"); + peerRatingString = getLocalisedMessage("email.peers.rating"); + + } + + public Map generateHTMLEmailsForSession() { + + HashMap emailMap = new HashMap(); + generateTeamData(); + for (Map.Entry entry : learnerDataMap.entrySet()) { + emailMap.put(entry.getKey(), buildEmail(entry.getKey())); + } + return emailMap; + + } + + public String generateHTMLEMailForLearner(Long userId) { + generateTeamData(); + return buildEmail(userId); + } + + private String buildEmail(Long userId) { + + processComments(userId); + + StringBuilder htmlText; // new for each user + htmlText = new StringBuilder(); + htmlText.append("\n"); + + LearnerData learnerData = learnerDataMap.get(userId); + generateCriteriaTable(htmlText, learnerData); + htmlText.append("

     

    \n"); + generateFeedbackComments(htmlText, learnerData); + htmlText.append("

     

    \n"); + generateSelfPeerTable(htmlText, learnerData); + htmlText.append("

     

    \n"); + generateSAPASPAExplanation(htmlText, learnerData.getSPAFactor(), learnerData.getSAPAFactor()); + + htmlText.append("

    ").append(getLocalisedMessage("email.send.automatically")).append("

    \n"); + htmlText.append("\n"); + return htmlText.toString(); + } + + public class LearnerData { + + public class SingleCriteriaData { + Double selfRating; + Double peerRatingExcSelf; + Double peerRatingIncSelf; + + SingleCriteriaData(Double selfRating, Double peerRatingExcSelf, Double peerRatingIncSelf) { + this.selfRating = selfRating; + this.peerRatingExcSelf = peerRatingExcSelf; + this.peerRatingIncSelf = peerRatingIncSelf; + } + + } + + public String name; + public Map criteriaDataMap = new HashMap(); // Criteria id as key + public Double individualCriteriaAverage; // average of peerRatingIncSelf across all activities + public List commentDTOs = new ArrayList(); + private Double spa = null; + private Double sapa = null; + + LearnerData(String name) { + this.name = name; + } + + protected void addCriteraDataRow(Long criteraId, Double selfRating, + Double peerRatingExcSelf, Double peerRatingIncSelf) { + criteriaDataMap.put(criteraId, + new SingleCriteriaData(selfRating, peerRatingExcSelf, peerRatingIncSelf)); + } + + // must not be called until data analysis calculations are done - needs averageOfAverages + protected Double getSPAFactor() { + if (spa == null) { + spa = averageOfAverages > 0 ? individualCriteriaAverage / averageOfAverages : 0D; + } + return spa; + } + + protected Double getSAPAFactor() { + if (sapa == null) { + double sumSelfRatings = 0d; + double sumPeerRatings = 0d; + for (SingleCriteriaData criteriaData : criteriaDataMap.values()) { + sumSelfRatings += criteriaData.selfRating; + sumPeerRatings += criteriaData.peerRatingExcSelf; + } + sapa = sumPeerRatings > 0 ? Math.sqrt(sumSelfRatings / sumPeerRatings) : 0d; + } + return sapa; + } + + protected Double getCriteriaComparison(Long ratingCriteriaId) { + Double groupAverage = groupAveragePerCriteria.get(ratingCriteriaId); + SingleCriteriaData data = criteriaDataMap.get(ratingCriteriaId); + if (data != null && groupAverage != null && groupAverage > 0) { + return data.peerRatingIncSelf / groupAverage; + } else { + return 0d; + } + } + } + + private String roundTo2Places(Double d) { + if (d == null || Double.isNaN(d)) + return "0"; + + BigDecimal bd = new BigDecimal(d); + bd = bd.setScale(2, RoundingMode.HALF_UP); + + return NumberUtil.formatLocalisedNumberForceDecimalPlaces(bd.doubleValue(), (Locale) null, 2); + } + + /* Only needs to be called once for the session - works out all the basic data for the whole team */ + private void generateTeamData() { + + List users = peerreviewUserDao.getBySessionID(session.getSessionId()); + for (PeerreviewUser user : users) { + learnerDataMap.put(user.getUserId(), + new LearnerData(StringEscapeUtils.escapeCsv(user.getFirstName() + " " + user.getLastName()))); + } + + Map criteriaMarkSumMap = new HashMap(); + Map criteriaMarkCountMap = new HashMap(); + double averageOfAveragesSum = 0d; + int averageOfAveragesCount = 0; + + // Process all the criterias and build up the summary data for each rated user. Store in temporary map. + HashMap> tally = processRawRatingData(); + + // Now calculate the averages / self values and store ready for output + for ( Map.Entry> itemEntry : tally.entrySet()) { + Long itemId = itemEntry.getKey(); + HashMap itemMap = itemEntry.getValue(); + Double userMarkSum = 0D; + LearnerData learnerData = learnerDataMap.get(itemId); + + for ( Map.Entry criteriaEntry : itemMap.entrySet() ) { + + Long criteriaId = criteriaEntry.getKey(); + SummingData sd = criteriaEntry.getValue(); + + double peerRatingIncSelf = sd.numRatingsIncSelf > 0 ? sd.peerRatingIncSelf / sd.numRatingsIncSelf : 0; + double peerRatingExcSelf = sd.numRatingsExcSelf > 0 ? sd.peerRatingExcSelf / sd.numRatingsExcSelf : 0; + double selfRating = sd.selfRating; + learnerData.addCriteraDataRow(criteriaId, selfRating, peerRatingExcSelf, peerRatingIncSelf); + + Double criteriaMarkSum = criteriaMarkSumMap.get(criteriaId); + if (criteriaMarkSum == null) { + criteriaMarkSumMap.put(criteriaId, peerRatingIncSelf); + } else { + criteriaMarkSumMap.put(criteriaId, criteriaMarkSum + peerRatingIncSelf); + } + + Integer criteriaMarkCount = criteriaMarkCountMap.get(criteriaId); + if (criteriaMarkCount == null) { + criteriaMarkCountMap.put(criteriaId, 1); + } else { + criteriaMarkCountMap.put(criteriaId, criteriaMarkCount + 1); + } + userMarkSum += peerRatingIncSelf; + } + + double individualCriteriaAverage = countNonCommentCriteria > 0 ? userMarkSum / countNonCommentCriteria : 0D; + learnerData.individualCriteriaAverage = individualCriteriaAverage; + averageOfAveragesSum += individualCriteriaAverage; + averageOfAveragesCount++; + + } + + // calculate the group averages for each Criteria, then the average of all the group averages + for (Map.Entry entry : criteriaMarkSumMap.entrySet()) { + Integer markCount = criteriaMarkCountMap.get(entry.getKey()); + Double groupAvg = entry.getValue() / markCount; + groupAveragePerCriteria.put(entry.getKey(), groupAvg); + } + averageOfAverages = averageOfAveragesCount > 0 ? averageOfAveragesSum / averageOfAveragesCount : 0D; + + } + + class SummingData { + float selfRating = 0f; + float peerRatingExcSelf = 0f; + int numRatingsExcSelf = 0; + float peerRatingIncSelf = 0f; + int numRatingsIncSelf = 0; + } + + /** + * tally: hashmap> + * tallying done in Java as the code to do three calcs in one SQL is complex (and hence risks errors) + * and hopefully we are only dealing with small numbers of users in each session. The code that gets the existing data + * does a lot of processing that is not needed for the SPA/SAPA calculations so getting the raw data + * avoids that processing. + */ + private HashMap> processRawRatingData() { + Collection criteriaIds = new ArrayList(); + for ( RatingCriteria criteria : criteriaForCriteriaTable ) { + criteriaIds.add(criteria.getRatingCriteriaId()); + } + List rawRatingsForSession = ratingService.getRatingsByCriteriasAndItems(criteriaIds, learnerDataMap.keySet()); + HashMap> tally = new HashMap>(); + for ( Object obj : rawRatingsForSession ) { + Rating rating = (Rating) obj; + SummingData sd = null; + Long itemId = rating.getItemId(); + HashMap itemMap = tally.get(itemId); + if ( itemMap == null ) { + itemMap = new HashMap(); + sd = new SummingData(); + itemMap.put(rating.getRatingCriteria().getRatingCriteriaId(), sd); + tally.put(itemId, itemMap); + } else { + sd = itemMap.get(rating.getRatingCriteria().getRatingCriteriaId()); + if ( sd == null ) { + sd = new SummingData(); + itemMap.put(rating.getRatingCriteria().getRatingCriteriaId(), sd); + } + } + sd.peerRatingIncSelf += rating.getRating(); + sd.numRatingsIncSelf++; + if ( rating.getItemId().longValue() == rating.getLearner().getUserId().longValue() ) { + sd.selfRating = rating.getRating(); + } else { + sd.peerRatingExcSelf += rating.getRating(); + sd.numRatingsExcSelf++; + } + } + + return tally; + } + + private void processComments(Long userId) { + LearnerData learnerData = learnerDataMap.get(userId); + for (RatingCriteria criteria : criterias) { + int sorting = (criteria.isStarStyleRating() || criteria.isHedgeStyleRating()) + ? PeerreviewConstants.SORT_BY_AVERAGE_RESULT_DESC + : PeerreviewConstants.SORT_BY_AVERAGE_RESULT_ASC; + + StyledCriteriaRatingDTO dto = service.getUsersRatingsCommentsByCriteriaIdDTO(peerreview.getContentId(), + session.getSessionId(), criteria, userId.longValue(), false, sorting, null, true, false); + + if (criteria.isCommentRating() || criteria.isCommentsEnabled()) { + learnerData.commentDTOs.add(dto); + } + } + } + + private void generateCriteriaTable(StringBuilder htmlText, LearnerData learnerData) { + htmlText.append(htmlCriteriaTableStart).append("") + .append(learnerData.name).append(""); + for (RatingCriteria criteria : criteriaForCriteriaTable) { + String criteriaComparison = roundTo2Places( + learnerData.getCriteriaComparison(criteria.getRatingCriteriaId())); + htmlText.append("") + .append(criteriaComparison).append(""); + } + htmlText.append("\n"); + htmlText.append(htmlCriteriaTableEnd); + + } + + private void generateSelfPeerTable(StringBuilder htmlText, LearnerData learnerData) { + boolean spaDone = false; + boolean evenRow = false; + String spaFactor = roundTo2Places(learnerData.getSPAFactor()); + String sapaFactor = roundTo2Places(learnerData.getSAPAFactor()); + htmlText.append(""); + for (RatingCriteria criteria : criteriaForCriteriaTable) { + SingleCriteriaData criteriaData = learnerData.criteriaDataMap.get(criteria.getRatingCriteriaId()); + if (criteriaData != null) { + String selfRating = roundTo2Places(criteriaData.selfRating); + String peerRatingExcSelf = roundTo2Places(criteriaData.peerRatingExcSelf); + htmlText.append(""); + if (!spaDone) { + htmlText.append(""); + } + htmlText.append("\n"); + if (!spaDone) { + htmlText.append(""); + spaDone = true; + } + htmlText.append("\n"); + evenRow = !evenRow; + } else { + log.warn("Unable to report on criteria " + criteria + + " as there is no data in the processed data tables. Peer review session " + + session.getSessionId()); + } + } + htmlText.append("
    ").append(criteria.getTitle()) + .append("").append(selfRatingString) + .append("") + .append(selfRating).append(" ").append(getLocalisedMessage("email.SPA.factor")) + .append("").append(spaFactor) + .append("
    ").append(peerRatingString) + .append("") + .append(peerRatingExcSelf).append(" ").append(getLocalisedMessage("email.SAPA.factor")) + .append("").append(sapaFactor) + .append("
    \n"); + } + + private void generateFeedbackComments(StringBuilder htmlText, LearnerData learnerData) { + if (learnerData.commentDTOs.size() > 0) { + htmlText.append("
    ") + .append(getLocalisedMessage("email.label.feedback", new String[] { learnerData.name })) + .append("
    \n"); + htmlText.append(""); + boolean showCommentTitle = learnerData.commentDTOs.size() > 1; + for (StyledCriteriaRatingDTO dto : learnerData.commentDTOs) { + generateCommentEntry(htmlText, dto, showCommentTitle); + } + htmlText.append("
    \n"); + } + } + + private void generateCommentEntry(StringBuilder htmlText, StyledCriteriaRatingDTO dto, boolean showCommentTitle) { + + int rowCount = 1; + String escapedTitle = StringEscapeUtils.escapeHtml(dto.getRatingCriteria().getTitle()); + if (dto.getRatingCriteria().isCommentRating() || dto.getRatingCriteria().isCommentsEnabled()) { + if (dto.getRatingDtos().size() < 1) { + htmlText.append("") + .append(escapedTitle).append("") + .append(getLocalisedMessage("event.sent.results.no.results")).append(""); + } else { + for (StyledRatingDTO ratingDto : dto.getRatingDtos()) { + if (ratingDto.getComment() != null) { + String escapedComment = StringEscapeUtils.escapeHtml(ratingDto.getComment()); + escapedComment = StringUtils.replace(escapedComment, "<BR>", "
    "); + htmlText.append(""); + if (showCommentTitle) { + htmlText.append(""); + if (rowCount == 1) { + htmlText.append("").append(escapedTitle) + .append(""); + } + htmlText.append(""); + } + htmlText.append("").append(escapedComment) + .append("\n"); + rowCount++; + } + } + } + } + } + + private void generateSAPASPAExplanation(StringBuilder htmlText, double rawSPA, double rawSAPA) { + + // The SPA/SAPA comparisons are based on values rounded to 1 decimal place. So a value of 0.98 or 1.02 is considered 1, + // while 1.06 is considered > 1. + double spa = new BigDecimal(rawSPA).setScale(1, RoundingMode.HALF_UP).doubleValue(); + double sapa = new BigDecimal(rawSAPA).setScale(1, RoundingMode.HALF_UP).doubleValue(); + + int cellToHighlight; + if (sapa < 1.0) { + if (spa < 1.0) + cellToHighlight = 1; + else if (spa > 1.0) + cellToHighlight = 3; + else + cellToHighlight = 2; + } else if (sapa > 1.0) { + if (spa < 1.0) + cellToHighlight = 7; + else if (spa > 1.0) + cellToHighlight = 9; + else + cellToHighlight = 8; + } else { + if (spa < 1.0) + cellToHighlight = 4; + else if (spa > 1.0) + cellToHighlight = 6; + else + cellToHighlight = 5; + } + + String customisedtableTextToModify = htmlOverallResultsTable.replace(htmlORTHighlightedCellReplacementText+cellToHighlight, highlightBackgroundStyle); + for ( int i=1; i<10; i++ ) { + if ( i != cellToHighlight ) + customisedtableTextToModify = customisedtableTextToModify.replaceAll(htmlORTHighlightedCellReplacementText+i, ""); + } + + htmlText.append(customisedtableTextToModify); + } + + // only needs to be run once for a session + private void preGenerateBoilerplate() { + + int numCol = 1; + for (RatingCriteria criteria : criterias) { + if (!criteria.isCommentRating()) { + numCol++; + } + } + int colWidth = 100 / numCol; + + StringBuilder tempBuffer = new StringBuilder("").append(""); + + for (RatingCriteria criteria : criterias) { + if (!criteria.isCommentRating()) { + tempBuffer.append(""); + countNonCommentCriteria++; + criteriaForCriteriaTable.add(criteria); + } + } + tempBuffer.append("\n"); + htmlCriteriaTableStart = tempBuffer.toString(); + + htmlCriteriaTableEnd = "
    ").append(getLocalisedMessage("label.learner")).append("").append(criteria.getTitle()).append("
    \n"; + + // SPA / SAPA comparison results table + tempBuffer = new StringBuilder(); + + tempBuffer.append("\n"); + tempBuffer.append(""); + + tempBuffer.append("\n"); + addExplanationCell(tempBuffer, 1, "email.explanation.1"); + addExplanationCell(tempBuffer, 2, "email.explanation.2"); + addExplanationCell(tempBuffer, 3, "email.explanation.3"); + + tempBuffer.append("\n"); + addExplanationCell(tempBuffer, 4, "email.explanation.4"); + addExplanationCell(tempBuffer, 5, "email.explanation.5"); + addExplanationCell(tempBuffer, 6, "email.explanation.6"); + + tempBuffer.append("\n"); + addExplanationCell(tempBuffer, 7, "email.explanation.7"); + addExplanationCell(tempBuffer, 8, "email.explanation.8"); + addExplanationCell(tempBuffer, 9, "email.explanation.9"); + + tempBuffer.append("\n
     ") + .append(getLocalisedMessage("email.explanation.SPA.less.one")).append("") + .append(getLocalisedMessage("email.explanation.SPA.one")).append("") + .append(getLocalisedMessage("email.explanation.SPA.greater.one")).append("
    ") + .append(getLocalisedMessage("email.explanation.SAPA.less.one")).append("
    ") + .append(getLocalisedMessage("email.explanation.SAPA.one")).append("
    ") + .append(getLocalisedMessage("email.explanation.SAPA.greater.one")).append("
    \n"); + htmlOverallResultsTable = tempBuffer.toString(); + } + + private void addExplanationCell(StringBuilder htmlText, int cellNumber, String messageKey) { + htmlText.append("") + .append(cellNumber).append(". ").append(getLocalisedMessage(messageKey)).append(""); + } + + private String getLocalisedMessage(String key, Object[] args) { + return messageService.getMessage(key, args); + } + + private String getLocalisedMessage(String key) { + return messageService.getMessage(key); + } + +}