/**
* <a href="http://www.openolat.org">
* OpenOLAT - Online Learning and Training</a><br>
* <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 the
* <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a>
* <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>
* Initial code contributed and copyrighted by<br>
* frentix GmbH, http://www.frentix.com
* <p>
*/
package org.olat.course.assessment.manager;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.UUID;
import org.olat.basesecurity.BaseSecurity;
import org.olat.core.CoreSpringFactory;
import org.olat.core.commons.modules.bc.FolderConfig;
import org.olat.core.commons.modules.bc.vfs.OlatRootFileImpl;
import org.olat.core.commons.modules.bc.vfs.OlatRootFolderImpl;
import org.olat.core.commons.persistence.DB;
import org.olat.core.commons.persistence.DBFactory;
import org.olat.core.commons.services.taskexecutor.LongRunnable;
import org.olat.core.commons.services.taskexecutor.Sequential;
import org.olat.core.commons.services.taskexecutor.Task;
import org.olat.core.commons.services.taskexecutor.TaskAwareRunnable;
import org.olat.core.commons.services.taskexecutor.TaskExecutorManager;
import org.olat.core.gui.translator.Translator;
import org.olat.core.id.Identity;
import org.olat.core.id.IdentityEnvironment;
import org.olat.core.id.OLATResourceable;
import org.olat.core.id.Roles;
import org.olat.core.logging.OLog;
import org.olat.core.logging.Tracing;
import org.olat.core.logging.activity.OlatResourceableType;
import org.olat.core.logging.activity.ThreadLocalUserActivityLogger;
import org.olat.core.logging.activity.ThreadLocalUserActivityLoggerInstaller;
import org.olat.core.util.FileUtils;
import org.olat.core.util.Formatter;
import org.olat.core.util.SessionInfo;
import org.olat.core.util.StringHelper;
import org.olat.core.util.UserSession;
import org.olat.core.util.Util;
import org.olat.core.util.WebappHelper;
import org.olat.core.util.ZipUtil;
import org.olat.core.util.i18n.I18nManager;
import org.olat.core.util.mail.ContactList;
import org.olat.core.util.mail.MailBundle;
import org.olat.core.util.mail.MailContextImpl;
import org.olat.core.util.mail.MailManager;
import org.olat.core.util.resource.OresHelper;
import org.olat.core.util.vfs.VFSContainer;
import org.olat.core.util.vfs.VFSItem;
import org.olat.core.util.vfs.VFSLeaf;
import org.olat.core.util.vfs.VFSManager;
import org.olat.course.CourseFactory;
import org.olat.course.ICourse;
import org.olat.course.assessment.AssessmentHelper;
import org.olat.course.assessment.AssessmentLoggingAction;
import org.olat.course.assessment.AssessmentManager;
import org.olat.course.assessment.bulk.BulkAssessmentOverviewController;
import org.olat.course.assessment.model.BulkAssessmentDatas;
import org.olat.course.assessment.model.BulkAssessmentFeedback;
import org.olat.course.assessment.model.BulkAssessmentRow;
import org.olat.course.assessment.model.BulkAssessmentSettings;
import org.olat.course.nodes.AssessableCourseNode;
import org.olat.course.nodes.CourseNode;
import org.olat.course.nodes.GTACourseNode;
import org.olat.course.nodes.MSCourseNode;
import org.olat.course.nodes.ProjectBrokerCourseNode;
import org.olat.course.nodes.TACourseNode;
import org.olat.course.nodes.gta.GTAManager;
import org.olat.course.nodes.gta.TaskList;
import org.olat.course.nodes.gta.TaskProcess;
import org.olat.course.nodes.ta.ReturnboxController;
import org.olat.course.run.environment.CourseEnvironment;
import org.olat.course.run.scoring.ScoreEvaluation;
import org.olat.course.run.userview.UserCourseEnvironment;
import org.olat.course.run.userview.UserCourseEnvironmentImpl;
import org.olat.repository.RepositoryEntry;
import org.olat.user.UserManager;
import org.olat.util.logging.activity.LoggingResourceable;
/**
* The task which execute the bulk assessment<br>
*
* Initial date: 20.11.2013<br>
* @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
*
*/
public class BulkAssessmentTask implements LongRunnable, TaskAwareRunnable, Sequential {
private static final long serialVersionUID = 4614724183354689151L;
private static final OLog log = Tracing.createLoggerFor(BulkAssessmentTask.class);
private OLATResourceable courseRes;
private String courseNodeIdent;
private BulkAssessmentDatas datas;
private BulkAssessmentSettings settings;
private Long coachedIdentity;
private transient Task task;
private transient File unzipped;
public BulkAssessmentTask(OLATResourceable courseRes, AssessableCourseNode courseNode,
BulkAssessmentDatas datas, Long coachedIdentity) {
this.courseRes = OresHelper.clone(courseRes);
this.courseNodeIdent = courseNode.getIdent();
this.settings = new BulkAssessmentSettings(courseNode);
this.datas = datas;
this.coachedIdentity = coachedIdentity;
}
public String getCourseNodeIdent() {
return courseNodeIdent;
}
public BulkAssessmentSettings getSettings() {
return settings;
}
public BulkAssessmentDatas getDatas() {
return datas;
}
@Override
public void setTask(Task task) {
this.task = task;
}
/**
* Used by to task executor, without any GUI
*/
@Override
public void run() {
final List<BulkAssessmentFeedback> feedbacks = new ArrayList<>();
try {
log.audit("Start process bulk assessment");
LoggingResourceable[] infos = new LoggingResourceable[2];
if(task != null && task.getCreator() != null) {
UserSession session = new UserSession();
session.setIdentity(task.getCreator());
session.setSessionInfo(new SessionInfo(task.getCreator().getKey(), task.getCreator().getName()));
ThreadLocalUserActivityLoggerInstaller.initUserActivityLogger(session);
infos[0] = LoggingResourceable.wrap(courseRes, OlatResourceableType.course);
ThreadLocalUserActivityLogger.addLoggingResourceInfo(infos[0]);
infos[1] = LoggingResourceable.wrap(getCourseNode());
ThreadLocalUserActivityLogger.addLoggingResourceInfo(infos[1]);
}
doProcess(feedbacks);
log.audit("End process bulk assessment");
cleanup();
ThreadLocalUserActivityLogger.log(AssessmentLoggingAction.ASSESSMENT_BULK, getClass(), infos);
} catch (Exception e) {
log.error("", e);
feedbacks.add(new BulkAssessmentFeedback("", "bulk.assessment.error"));
throw e;
} finally {
cleanupUnzip();
sendFeedback(feedbacks);
}
}
public List<BulkAssessmentFeedback> process() {
List<BulkAssessmentFeedback> feedbacks = new ArrayList<>();
try {
LoggingResourceable infos = LoggingResourceable.wrap(getCourseNode());
ThreadLocalUserActivityLogger.addLoggingResourceInfo(infos);
doProcess(feedbacks);
cleanup();
} catch (Exception e) {
log.error("", e);
feedbacks.add(new BulkAssessmentFeedback("", "bulk.assessment.error"));
} finally {
cleanupUnzip();
}
return feedbacks;
}
private void cleanup() {
if(StringHelper.containsNonWhitespace(datas.getDataBackupFile())) {
OlatRootFileImpl backupFile = new OlatRootFileImpl(datas.getDataBackupFile(), null);
if(backupFile.exists()) {
File dir = backupFile.getBasefile().getParentFile();
if(dir != null && dir.exists()) {
FileUtils.deleteDirsAndFiles(dir, true, true);
}
}
}
cleanupUnzip();
}
private void cleanupUnzip() {
try {
if(unzipped != null && unzipped.exists()) {
FileUtils.deleteDirsAndFiles(unzipped, true, true);
}
} catch (Exception e) {
log.error("Cannot cleanup unzipped datas after bulk assessment", e);
}
}
private void sendFeedback(List<BulkAssessmentFeedback> feedbacks) {
if(task == null) {
log.error("Haven't a task to know creator and modifiers of the task", null);
return;
}
Identity creator = task.getCreator();
String language = creator.getUser().getPreferences().getLanguage();
Locale locale = I18nManager.getInstance().getLocaleOrDefault(language);
Translator translator = Util.createPackageTranslator(BulkAssessmentOverviewController.class, locale,
Util.createPackageTranslator(AssessmentManager.class, locale));
MailManager mailManager = CoreSpringFactory.getImpl(MailManager.class);
TaskExecutorManager taskManager = CoreSpringFactory.getImpl(TaskExecutorManager.class);
String feedbackStr = renderFeedback(feedbacks, translator);
MailBundle mail = new MailBundle();
mail.setToId(creator);
mail.setFrom(WebappHelper.getMailConfig("mailReplyTo"));
List<Identity> modifiers = taskManager.getModifiers(task);
if(modifiers.size() > 0) {
ContactList cc = new ContactList("CC");
cc.addAllIdentites(modifiers);
mail.setContactList(cc);
}
String businessPath = "";
ICourse course = CourseFactory.loadCourse(courseRes);
CourseNode node = course.getRunStructure().getNode(courseNodeIdent);
String courseTitle = course.getCourseTitle();
String nodeTitle = node.getShortTitle();
String numOfAssessedIds = Integer.toString(datas == null ? 0 : datas.getRowsSize());
String date = Formatter.getInstance(locale).formatDateAndTime(new Date());
mail.setContext(new MailContextImpl(courseRes, courseNodeIdent, businessPath));
String subject = translator.translate("confirmation.mail.subject", new String[]{ courseTitle, nodeTitle });
String body = translator.translate("confirmation.mail.body", new String[]{ courseTitle, nodeTitle, feedbackStr, numOfAssessedIds, date });
mail.setContent(subject, body);
mailManager.sendMessage(mail);
}
public static String renderFeedback(List<BulkAssessmentFeedback> feedbacks, Translator translator) {
UserManager userManager = CoreSpringFactory.getImpl(UserManager.class);
StringBuilder sb = new StringBuilder();
for(BulkAssessmentFeedback feedback:feedbacks) {
String errorKey = feedback.getErrorKey();
String msg = translator.translate(errorKey);
String assessedName;
if(feedback.getAssessedIdentity() != null) {
assessedName = userManager.getUserDisplayName(feedback.getAssessedIdentity());
} else {
assessedName = feedback.getAssessedId();
}
sb.append(assessedName).append(": ").append(msg).append("\n");
}
return sb.toString();
}
public static boolean isBulkAssessable(CourseNode courseNode) {
boolean bulkAssessability = false;
if (courseNode instanceof MSCourseNode
|| courseNode instanceof TACourseNode
|| courseNode instanceof GTACourseNode
|| courseNode instanceof ProjectBrokerCourseNode) {
// now a more fine granular check on bulk features. only show wizard for nodes that have at least one
BulkAssessmentSettings settings = new BulkAssessmentSettings((AssessableCourseNode)courseNode);
if (settings.isHasPassed() || settings.isHasScore() || settings.isHasUserComment() || settings.isHasReturnFiles()) {
bulkAssessability = true;
}
}
return bulkAssessability;
}
private AssessableCourseNode getCourseNode() {
ICourse course = CourseFactory.loadCourse(courseRes);
CourseNode node = course.getRunStructure().getNode(courseNodeIdent);
if(node instanceof AssessableCourseNode) {
return (AssessableCourseNode)node;
}
return null;
}
private void doProcess(List<BulkAssessmentFeedback> feedbacks) {
final DB dbInstance = DBFactory.getInstance();
final BaseSecurity securityManager = CoreSpringFactory.getImpl(BaseSecurity.class);
final Identity coachIdentity = securityManager.loadIdentityByKey(coachedIdentity);
final ICourse course = CourseFactory.loadCourse(courseRes);
final AssessableCourseNode courseNode = getCourseNode();
final Roles studentRoles = new Roles(false, false, false, false, false, false, false, false);
final boolean hasUserComment = courseNode.hasCommentConfigured();
final boolean hasScore = courseNode.hasScoreConfigured();
final boolean hasPassed = courseNode.hasPassedConfigured();
final boolean hasReturnFiles = (StringHelper.containsNonWhitespace(datas.getReturnFiles())
&& (courseNode instanceof TACourseNode || courseNode instanceof GTACourseNode));
if(hasReturnFiles) {
try {
OlatRootFileImpl returnFilesZipped = new OlatRootFileImpl(datas.getReturnFiles(), null);
String tmp = FolderConfig.getCanonicalTmpDir();
unzipped = new File(tmp, UUID.randomUUID().toString() + File.separatorChar);
unzipped.mkdirs();
ZipUtil.unzip(returnFilesZipped.getBasefile(), unzipped);
} catch (Exception e) {
log.error("Cannot unzip the return files during bulk assessment", e);
}
}
Float min = null;
Float max = null;
Float cut = null;
if (hasScore) {
min = courseNode.getMinScoreConfiguration();
max = courseNode.getMaxScoreConfiguration();
}
if (hasPassed) {
cut = courseNode.getCutValueConfiguration();
}
int count = 0;
List<BulkAssessmentRow> rows = datas.getRows();
for(BulkAssessmentRow row:rows) {
Long identityKey = row.getIdentityKey();
if(identityKey == null) {
feedbacks.add(new BulkAssessmentFeedback("bulk.action.no.such.user", row.getAssessedId()));
continue;//nothing to do
}
Identity identity = securityManager.loadIdentityByKey(identityKey);
IdentityEnvironment ienv = new IdentityEnvironment(identity, studentRoles);
UserCourseEnvironment uce = new UserCourseEnvironmentImpl(ienv, course.getCourseEnvironment());
//update comment, empty string will reset comment
String userComment = row.getComment();
if(hasUserComment && userComment != null){
// Update userComment in db
courseNode.updateUserUserComment(userComment, uce, coachIdentity);
//LD: why do we have to update the efficiency statement?
//EfficiencyStatementManager esm = EfficiencyStatementManager.getInstance();
//esm.updateUserEfficiencyStatement(uce);
}
//update score
Float score = row.getScore();
if(hasScore && score != null){
// score < minimum score
if ((min != null && score.floatValue() < min.floatValue()) || (score.floatValue() < AssessmentHelper.MIN_SCORE_SUPPORTED)) {
//"bulk.action.lessThanMin";
}
// score > maximum score
else if ((max != null && score.floatValue() > max.floatValue())
|| (score.floatValue() > AssessmentHelper.MAX_SCORE_SUPPORTED)) {
//"bulk.action.greaterThanMax";
} else {
// score between minimum and maximum score
ScoreEvaluation se;
if (hasPassed && cut != null){
Boolean passed = (score.floatValue() >= cut.floatValue()) ? Boolean.TRUE : Boolean.FALSE;
se = new ScoreEvaluation(score, passed);
} else {
se = new ScoreEvaluation(score, null);
}
// Update score,passed properties in db, and the user's efficiency statement
courseNode.updateUserScoreEvaluation(se, uce, coachIdentity, false);
}
}
Boolean passed = row.getPassed();
if (hasPassed && passed != null && cut == null) { // Configuration of manual assessment --> Display passed/not passed: yes, Type of display: Manual by tutor
ScoreEvaluation seOld = courseNode.getUserScoreEvaluation(uce);
Float oldScore = seOld.getScore();
ScoreEvaluation se = new ScoreEvaluation(oldScore, passed);
// Update score,passed properties in db, and the user's efficiency statement
boolean incrementAttempts = false;
courseNode.updateUserScoreEvaluation(se, uce, coachIdentity, incrementAttempts);
}
boolean identityHasReturnFile = false;
if(hasReturnFiles && row.getReturnFiles() != null && row.getReturnFiles().size() > 0) {
String assessedId = row.getAssessedId();
File assessedFolder = new File(unzipped, assessedId);
identityHasReturnFile = assessedFolder.exists();
if(identityHasReturnFile) {
processReturnFile(courseNode, row, uce, assessedFolder);
}
}
if(courseNode instanceof GTACourseNode) {
//push the state further
GTACourseNode gtaNode = (GTACourseNode)courseNode;
if((hasScore && score != null) || (hasPassed && passed != null)) {
//pushed to graded
updateTasksState(gtaNode, uce, TaskProcess.grading);
} else if(hasReturnFiles) {
//push to revised
updateTasksState(gtaNode, uce, TaskProcess.correction);
}
}
if(count++ % 5 == 0) {
dbInstance.commitAndCloseSession();
} else {
dbInstance.commit();
}
}
}
private void updateTasksState(GTACourseNode courseNode, UserCourseEnvironment uce, TaskProcess status) {
final GTAManager gtaManager = CoreSpringFactory.getImpl(GTAManager.class);
Identity identity = uce.getIdentityEnvironment().getIdentity();
RepositoryEntry entry = uce.getCourseEnvironment().getCourseGroupManager().getCourseEntry();
org.olat.course.nodes.gta.Task gtaTask;
TaskList taskList = gtaManager.getTaskList(entry, courseNode);
if(taskList == null) {
taskList = gtaManager.createIfNotExists(entry, courseNode);
gtaTask = gtaManager.createTask(null, taskList, status, null, identity, courseNode);
} else {
gtaTask = gtaManager.getTask(identity, taskList);
if(gtaTask == null) {
gtaManager.createTask(null, taskList, status, null, identity, courseNode);
}
}
gtaManager.nextStep(status, courseNode);
}
private void processReturnFile(AssessableCourseNode courseNode, BulkAssessmentRow row, UserCourseEnvironment uce, File assessedFolder) {
String assessedId = row.getAssessedId();
Identity identity = uce.getIdentityEnvironment().getIdentity();
VFSContainer returnBox = getReturnBox(uce, courseNode, identity);
if(returnBox != null) {
for(String returnFilename:row.getReturnFiles()) {
File returnFile = new File(assessedFolder, returnFilename);
VFSItem currentReturnLeaf = returnBox.resolve(returnFilename);
if(currentReturnLeaf != null) {
//remove the current file (delete make a version if it is enabled)
currentReturnLeaf.delete();
}
VFSLeaf returnLeaf = returnBox.createChildLeaf(returnFilename);
if(returnFile.exists()) {
try {
InputStream inStream = new FileInputStream(returnFile);
VFSManager.copyContent(inStream, returnLeaf);
} catch (FileNotFoundException e) {
log.error("Cannot copy return file " + returnFilename + " from " + assessedId, e);
}
}
}
}
}
/**
* Return the target folder of the assessed identity. This is a factory method which take care
* of the type of the course node.
*
* @param uce
* @param courseNode
* @param identity
* @return
*/
private VFSContainer getReturnBox(UserCourseEnvironment uce, CourseNode courseNode, Identity identity) {
VFSContainer returnContainer = null;
if(courseNode instanceof GTACourseNode) {
final GTAManager gtaManager = CoreSpringFactory.getImpl(GTAManager.class);
CourseEnvironment courseEnv = uce.getCourseEnvironment();
returnContainer = gtaManager.getCorrectionContainer(courseEnv, (GTACourseNode)courseNode, identity);
} else {
String returnPath = ReturnboxController.getReturnboxPathRelToFolderRoot(uce.getCourseEnvironment(), courseNode);
OlatRootFolderImpl rootFolder = new OlatRootFolderImpl(returnPath, null);
VFSItem assessedItem = rootFolder.resolve(identity.getName());
if(assessedItem == null) {
returnContainer = rootFolder.createChildContainer(identity.getName());
} else if(assessedItem instanceof VFSContainer) {
returnContainer = (VFSContainer)assessedItem;
}
}
return returnContainer;
}
}