/**
* OLAT - Online Learning and Training<br>
* http://www.olat.org
* <p>
* Licensed under the Apache License, Version 2.0 (the "License"); <br>
* you may not use this file except in compliance with the License.<br>
* You may obtain a copy of the License at
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* Unless required by applicable law or agreed to in writing,<br>
* software distributed under the License is distributed on an "AS IS" BASIS, <br>
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br>
* See the License for the specific language governing permissions and <br>
* limitations under the License.
* <p>
* Copyright (c) since 2004 at Multimedia- & E-Learning Services (MELS),<br>
* University of Zurich, Switzerland.
* <hr>
* <a href="http://www.openolat.org">
* OpenOLAT - Online Learning and Training</a><br>
* This file has been modified by the OpenOLAT community. Changes are licensed
* under the Apache 2.0 license as the original file.
*/
package org.olat.course.assessment.manager;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import org.olat.basesecurity.BaseSecurity;
import org.olat.basesecurity.Constants;
import org.olat.basesecurity.GroupRoles;
import org.olat.core.commons.services.notifications.NotificationHelper;
import org.olat.core.commons.services.notifications.NotificationsHandler;
import org.olat.core.commons.services.notifications.NotificationsManager;
import org.olat.core.commons.services.notifications.Publisher;
import org.olat.core.commons.services.notifications.PublisherData;
import org.olat.core.commons.services.notifications.Subscriber;
import org.olat.core.commons.services.notifications.SubscriptionContext;
import org.olat.core.commons.services.notifications.SubscriptionInfo;
import org.olat.core.commons.services.notifications.manager.NotificationsUpgradeHelper;
import org.olat.core.commons.services.notifications.model.SubscriptionListItem;
import org.olat.core.commons.services.notifications.model.TitleItem;
import org.olat.core.gui.translator.Translator;
import org.olat.core.id.Identity;
import org.olat.core.id.context.BusinessControlFactory;
import org.olat.core.logging.AssertException;
import org.olat.core.logging.OLog;
import org.olat.core.logging.Tracing;
import org.olat.core.util.Util;
import org.olat.core.util.nodes.INode;
import org.olat.course.CourseFactory;
import org.olat.course.CourseModule;
import org.olat.course.ICourse;
import org.olat.course.Structure;
import org.olat.course.assessment.AssessmentHelper;
import org.olat.course.assessment.AssessmentManager;
import org.olat.course.groupsandrights.CourseGroupManager;
import org.olat.course.groupsandrights.CourseRights;
import org.olat.course.nodes.AssessableCourseNode;
import org.olat.course.nodes.CourseNode;
import org.olat.course.nodes.CourseNodeFactory;
import org.olat.course.nodes.STCourseNode;
import org.olat.course.nodes.ScormCourseNode;
import org.olat.group.BusinessGroup;
import org.olat.group.BusinessGroupService;
import org.olat.modules.assessment.AssessmentEntry;
import org.olat.modules.assessment.manager.AssessmentEntryDAO;
import org.olat.modules.scorm.assessment.ScormAssessmentManager;
import org.olat.repository.RepositoryManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* Description:<br>
* Calculates it the user has any assessment news for the notification system.
* Currently this checks for new tests
* <P>
* Initial Date: 21-giu-2005 <br>
*
* @author Roberto Bagnoli
*/
@Service
public class AssessmentNotificationsHandler implements NotificationsHandler {
private static final OLog log = Tracing.createLoggerFor(AssessmentNotificationsHandler.class);
private static final String CSS_CLASS_USER_ICON = "o_icon_user";
@Autowired
private BaseSecurity securityManager;
@Autowired
private RepositoryManager repositoryManager;
@Autowired
private NotificationsManager notificationsManager;
@Autowired
private BusinessGroupService businessGroupService;
@Autowired
private AssessmentEntryDAO courseNodeAssessmentDao;
/**
* Returns the <code>SubscriptionContext</code> to use for assessment
* notification about specified <code>ICourse</code>.<br>
* <br>
* <b>PRE CONDITIONS</b>
* <ul>
* <li> <code>course != null</code>
* </ul>
* If <code>ident == null</code>, the subscription context is (created and)
* returned without authorization control
*
* @param ident the identity, if null, no subscription check will be made
* @param course
* @return the subscription context to use or <code>null</code> if the
* identity associated to the request is not allowed to be notified
* @see #canSubscribeForAssessmentNotification(Identity, ICourse)
*/
public SubscriptionContext getAssessmentSubscriptionContext(Identity ident, ICourse course) {
SubscriptionContext sctx = null;
if (ident == null || canSubscribeForAssessmentNotification(ident, course)) {
// Creates a new SubscriptionContext only if not found into cache
if (sctx == null) {
// a subscription context showing to the root node (the course's root
// node is started when clicking such a notification)
CourseNode cn = course.getRunStructure().getRootNode();
Long resourceableId = course.getResourceableId();
sctx = new SubscriptionContext(CourseModule.ORES_COURSE_ASSESSMENT, resourceableId, cn.getIdent());
}
}
return sctx;
}
/**
* Shortcut for
* <code>getAssessmentSubscriptionContext((Identity) null, course)</code>
*
* @param course
* @return the AssessmentSubscriptionContext
* @see #getAssessmentSubscriptionContext(Identity, ICourse)
*/
private SubscriptionContext getAssessmentSubscriptionContext(ICourse course) {
return getAssessmentSubscriptionContext((Identity) null, course);
}
/**
* Return <code>PublisherData</code> instance to use for assessment
* notification.<br>
* <br>
* <b>PRE CONDITIONS</b>
* <ul>
* <li> <code>course != null</code>
* </ul>
*
* @param course
* @param the business path
* @return the publisherdata
*/
public PublisherData getAssessmentPublisherData(ICourse course, String businessPath) {
return new PublisherData(CourseModule.ORES_COURSE_ASSESSMENT, String.valueOf(course.getCourseEnvironment().getCourseResourceableId()), businessPath);
}
/**
* Signal the <code>NotificationsManager</code> about assessment news
* available for a course.<br>
* <br>
* <b>PRE CONDITIONS</b>
* <ul>
* <li> <code>courseId != null</code>
* </ul>
*
* @param courseId the resourceable id of the course to signal news about
* @param ident the identity to ignore news for
*/
public void markPublisherNews(Identity ident, Long courseId) {
ICourse course = loadCourseFromId(courseId);
if (course == null) throw new AssertException("course with id " + courseId + " not found!");
markPublisherNews(ident, course);
}
/**
* Signal the <code>NotificationsManager</code> about assessment news
* available on a course.<br>
* <br>
* <b>PRE CONDITIONS</b>
* <ul>
* <li> <code>course != null</code>
* </ul>
*
* @param course the course to signal news about
* @param ident the identity to ignore news for
*/
private void markPublisherNews(Identity ident, ICourse course) {
SubscriptionContext subsContext = getAssessmentSubscriptionContext(course);
if (subsContext != null) {
notificationsManager.markPublisherNews(subsContext, ident, true);
}
}
/**
* Assessment notification rights check.<br>
* Tests if an <code>Identity</code> can subscribe for assessment
* notification for the specified <code>ICourse</code>.<br>
* <br>
* <b>PRE CONDITIONS</b>
* <ul>
* <li> <code>course != null</code>
* </ul>
*
* @param ident the identity to check rights for. Can be <code>null</code>
* @param course the course to check rights against
* @return if <code>ident == null</code> this method always returns false;
* otherwise subscriptions rights are met only by course
* administrators and course coaches
*/
private boolean canSubscribeForAssessmentNotification(Identity ident, ICourse course) {
if (ident == null) return false;
CourseGroupManager grpMan = course.getCourseEnvironment().getCourseGroupManager();
boolean isInstitutionalResourceManager = securityManager.isIdentityInSecurityGroup(ident, securityManager.findSecurityGroupByName(Constants.GROUP_INST_ORES_MANAGER));
return isInstitutionalResourceManager || grpMan.isIdentityCourseAdministrator(ident) || grpMan.isIdentityCourseCoach(ident) || grpMan.hasRight(ident, CourseRights.RIGHT_ASSESSMENT);
}
/**
* Utility method.<br>
* Load an instance of <code>ICourse</code> given its numeric resourceable
* id
*/
private ICourse loadCourseFromId(Long courseId) {
return CourseFactory.loadCourse(courseId);
}
/**
* Utility method.<br>
* Build (recursively) the list of all test nodes belonging to the specified
* <code>ICourse</code>.<br>
* The returned <code>List</code> is empty if course has no
* AssessableCourseNode. Structure course node are excluded from the list.<br>
* <br>
* <b>PRE CONDITIONS</b>
* <ul>
* <li> <code>course != null</code>
* </ul>
* <br>
* <b>POST CONDITIONS</b>
* <ul>
* <li> The returned list, if not empty, contains ONLY instances of type
* <code>AssessableCourseNode</code>
* </ul>
*/
private List<AssessableCourseNode> getCourseTestNodes(ICourse course) {
List<AssessableCourseNode> assessableNodes = new ArrayList<AssessableCourseNode>();
Structure courseStruct = course.getRunStructure();
CourseNode rootNode = courseStruct.getRootNode();
getCourseTestNodes(rootNode, assessableNodes);
return assessableNodes;
}
/**
* Recursive step used by <code>getCourseAssessableNodes(ICourse)</code>.<br>
* <br>
* <b>PRE CONDITIONS</b>
* <ul>
* <li> <code>course != null</code>
* <li> <code>result != null</code>
* </ul>
*
* @see #getCourseTestNodes(ICourse)
*/
private void getCourseTestNodes(INode node, List<AssessableCourseNode> result) {
if (node != null) {
if (node instanceof AssessableCourseNode && !(node instanceof STCourseNode)) {
result.add((AssessableCourseNode) node);
}
for (int i = 0; i < node.getChildCount(); i++) {
getCourseTestNodes(node.getChildAt(i), result);
}
}
}
private boolean courseStatus(ICourse course) {
return course != null
&& !course.getCourseEnvironment().getCourseGroupManager().getCourseEntry().getRepositoryEntryStatus().isUnpublished()
&& !course.getCourseEnvironment().getCourseGroupManager().getCourseEntry().getRepositoryEntryStatus().isClosed();
}
/**
* @see org.olat.core.commons.services.notifications.NotificationsHandler#createSubscriptionInfo(org.olat.core.commons.services.notifications.Subscriber,
* java.util.Locale, java.util.Date)
*/
public SubscriptionInfo createSubscriptionInfo(final Subscriber subscriber, Locale locale, Date compareDate) {
SubscriptionInfo si = null;
Publisher p = subscriber.getPublisher();
if(!NotificationsUpgradeHelper.checkCourse(p)) {
//course don't exist anymore
notificationsManager.deactivate(p);
return notificationsManager.getNoSubscriptionInfo();
}
try {
Date latestNews = p.getLatestNewsDate();
Identity identity = subscriber.getIdentity();
// do not try to create a subscription info if state is deleted - results in
// exceptions, course
// can't be loaded when already deleted
if (notificationsManager.isPublisherValid(p) && compareDate.before(latestNews)) {
Long courseId = new Long(p.getData());
final ICourse course = loadCourseFromId(courseId);
if (courseStatus(course)) {
// course admins or users with the course right to have full access to
// the assessment tool will have full access to user tests
CourseGroupManager cgm = course.getCourseEnvironment().getCourseGroupManager();
final boolean hasFullAccess = (cgm.isIdentityCourseAdministrator(identity) ? true : cgm.hasRight(identity,
CourseRights.RIGHT_ASSESSMENT));
final Set<Identity> coachedUsers = new HashSet<Identity>();
if (!hasFullAccess) {
// initialize list of users, only when user has not full access
List<BusinessGroup> coachedGroups = cgm.getOwnedBusinessGroups(identity);
List<Identity> coachedIdentites = businessGroupService.getMembers(coachedGroups, GroupRoles.participant.name());
coachedUsers.addAll(coachedIdentites);
}
List<AssessableCourseNode> testNodes = getCourseTestNodes(course);
Translator translator = Util.createPackageTranslator(AssessmentManager.class, locale);
for (AssessableCourseNode test:testNodes) {
List<AssessmentEntry> assessments = courseNodeAssessmentDao.loadAssessmentEntryBySubIdent(cgm.getCourseEntry(), test.getIdent());
for(AssessmentEntry assessment:assessments) {
Date modDate = assessment.getLastModified();
Identity assessedIdentity = assessment.getIdentity();
if (modDate.after(compareDate) && (hasFullAccess || coachedUsers.contains(assessedIdentity))) {
BigDecimal score = assessment.getScore();
if(test instanceof ScormCourseNode) {
ScormCourseNode scormTest = (ScormCourseNode)test;
//check if completed or passed
String status = ScormAssessmentManager.getInstance().getLastLessonStatus(assessedIdentity.getName(), course.getCourseEnvironment(), scormTest);
if(!"passed".equals(status) && !"completed".equals(status)) {
continue;
}
}
String desc;
String type = translator.translate("notifications.entry." + test.getType());
if(score == null) {
desc = translator.translate("notifications.entry.attempt", new String[] { test.getShortTitle(),
NotificationHelper.getFormatedName(assessedIdentity), type });
} else {
String scoreStr =AssessmentHelper.getRoundedScore(score);
desc = translator.translate("notifications.entry", new String[] { test.getShortTitle(),
NotificationHelper.getFormatedName(assessedIdentity), scoreStr, type });
}
String urlToSend = null;
String businessPath = null;
if(p.getBusinessPath() != null) {
businessPath = p.getBusinessPath() + "[Users:0][Node:" + test.getIdent() + "][Identity:" + assessedIdentity.getKey() + "]";
urlToSend = BusinessControlFactory.getInstance().getURLFromBusinessPathString(businessPath);
}
SubscriptionListItem subListItem = new SubscriptionListItem(desc, urlToSend, businessPath, modDate, CSS_CLASS_USER_ICON);
if(si == null) {
String title = translator.translate("notifications.header", new String[]{course.getCourseTitle()});
String css = CourseNodeFactory.getInstance().getCourseNodeConfigurationEvenForDisabledBB(test.getType()).getIconCSSClass();
si = new SubscriptionInfo(subscriber.getKey(), p.getType(), new TitleItem(title, css), null);
}
si.addSubscriptionListItem(subListItem);
}
}
}
}
}
if(si == null) {
si = notificationsManager.getNoSubscriptionInfo();
}
return si;
} catch (Exception e) {
log.error("Error while creating assessment notifications", e);
checkPublisher(p);
return notificationsManager.getNoSubscriptionInfo();
}
}
private void checkPublisher(Publisher p) {
try {
if(!NotificationsUpgradeHelper.checkCourse(p)) {
log.info("deactivating publisher with key; " + p.getKey(), null);
notificationsManager.deactivate(p);
}
} catch (Exception e) {
log.error("", e);
}
}
@Override
public String createTitleInfo(Subscriber subscriber, Locale locale) {
try {
Long resId = subscriber.getPublisher().getResId();
String displayName = repositoryManager.lookupDisplayNameByOLATResourceableId(resId);
Translator trans = Util.createPackageTranslator(AssessmentManager.class, locale);
return trans.translate("notifications.title", new String[]{ displayName });
} catch (Exception e) {
log.error("Error while creating assessment notifications for subscriber: " + subscriber.getKey(), e);
checkPublisher(subscriber.getPublisher());
return "-";
}
}
@Override
public String getType() {
return "AssessmentManager";
}
}