/** * <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.restapi.repository.course; import static org.olat.restapi.security.RestSecurityHelper.isAuthorEditor; import java.io.File; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import javax.servlet.http.HttpServletRequest; import javax.ws.rs.Consumes; import javax.ws.rs.GET; import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.CacheControl; import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Request; import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.ResponseBuilder; import javax.ws.rs.core.Response.Status; import org.olat.basesecurity.BaseSecurity; import org.olat.basesecurity.BaseSecurityManager; import org.olat.basesecurity.GroupRoles; import org.olat.core.CoreSpringFactory; import org.olat.core.id.Identity; import org.olat.core.id.IdentityEnvironment; import org.olat.core.logging.OLog; import org.olat.core.logging.Tracing; import org.olat.course.CourseFactory; import org.olat.course.ICourse; import org.olat.course.assessment.AssessmentManager; import org.olat.course.nodes.AssessableCourseNode; import org.olat.course.nodes.CourseNode; import org.olat.course.nodes.IQTESTCourseNode; import org.olat.course.run.environment.CourseEnvironment; import org.olat.course.run.scoring.ScoreAccounting; import org.olat.course.run.scoring.ScoreEvaluation; import org.olat.course.run.userview.UserCourseEnvironment; import org.olat.course.run.userview.UserCourseEnvironmentImpl; import org.olat.group.BusinessGroup; import org.olat.group.BusinessGroupService; import org.olat.ims.qti.QTIResultSet; import org.olat.ims.qti.container.AssessmentContext; import org.olat.ims.qti.container.HttpItemInput; import org.olat.ims.qti.container.ItemContext; import org.olat.ims.qti.container.ItemInput; import org.olat.ims.qti.container.ItemsInput; import org.olat.ims.qti.container.SectionContext; import org.olat.ims.qti.navigator.Info; import org.olat.ims.qti.navigator.MenuItemNavigator; import org.olat.ims.qti.navigator.Navigator; import org.olat.ims.qti.process.AssessmentFactory; import org.olat.ims.qti.process.AssessmentInstance; import org.olat.modules.ModuleConfiguration; import org.olat.modules.iq.IQManager; import org.olat.repository.RepositoryEntry; import org.olat.repository.RepositoryManager; import org.olat.repository.RepositoryService; import org.olat.restapi.security.RestSecurityHelper; import org.olat.restapi.support.vo.AssessableResultsVO; /** * * Description:<br> * Retrieve and import course assessments * * <P> * Initial Date: 7 apr. 2010 <br> * @author srosse, stephane.rosse@frentix.com */ @Path("repo/courses/{courseId}/assessments") public class CourseAssessmentWebService { private static final OLog log = Tracing.createLoggerFor(CourseAssessmentWebService.class); private static final String VERSION = "1.0"; private static final CacheControl cc = new CacheControl(); static { cc.setMaxAge(-1); } /** * Retireves the version of the Course Assessment Web Service. * @response.representation.200.mediaType text/plain * @response.representation.200.doc The version of this specific Web Service * @response.representation.200.example 1.0 * @return */ @GET @Path("version") @Produces(MediaType.TEXT_PLAIN) public Response getVersion() { return Response.ok(VERSION).build(); } /** * Returns the results of the course. * @response.representation.200.qname {http://www.example.com}assessableResultsVO * @response.representation.200.mediaType application/xml, application/json * @response.representation.200.doc Array of results for the whole the course * @response.representation.200.example {@link org.olat.restapi.support.vo.Examples#SAMPLE_ASSESSABLERESULTSVOes} * @response.representation.401.doc The roles of the authenticated user are not sufficient * @response.representation.404.doc The course not found * @param courseId The course resourceable's id * @param httpRequest The HTTP request * @param request The REST request * @return */ @GET @Produces({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON}) public Response getCourseResults(@PathParam("courseId") Long courseId, @Context HttpServletRequest httpRequest, @Context Request request) { if(!RestSecurityHelper.isAuthor(httpRequest)) { return Response.serverError().status(Status.UNAUTHORIZED).build(); } ICourse course = CoursesWebService.loadCourse(courseId); if(course == null) { return Response.serverError().status(Status.NOT_FOUND).build(); } //fxdiff VCRP-1,2: access control of resources List<Identity> courseUsers = loadUsers(course); int i=0; Date lastModified = null; AssessableResultsVO[] results = new AssessableResultsVO[courseUsers.size()]; for(Identity courseUser:courseUsers) { AssessableResultsVO result = getRootResult(courseUser, course); if(lastModified == null || (result.getLastModifiedDate() != null && lastModified.before(result.getLastModifiedDate()))) { lastModified = result.getLastModifiedDate(); } results[i++] = result; } if(lastModified != null) { Response.ResponseBuilder response = request.evaluatePreconditions(lastModified); if(response != null) { return response.build(); } return Response.ok(results).lastModified(lastModified).cacheControl(cc).build(); } return Response.ok(results).build(); } /** * Returns the results of the course. * @response.representation.200.qname {http://www.example.com}assessableResultsVO * @response.representation.200.mediaType application/xml, application/json * @response.representation.200.doc The result of the course * @response.representation.200.example {@link org.olat.restapi.support.vo.Examples#SAMPLE_ASSESSABLERESULTSVO} * @response.representation.401.doc The roles of the authenticated user are not sufficient * @response.representation.404.doc The identity or the course not found * @param courseId The course resourceable's id * @param identityKey The id of the user * @param httpRequest The HTTP request * @param request The REST request * @return */ @GET @Path("users/{identityKey}") @Produces({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON}) public Response getCourseResultsOf(@PathParam("courseId") Long courseId, @PathParam("identityKey") Long identityKey, @Context HttpServletRequest httpRequest, @Context Request request) { if(!RestSecurityHelper.isAuthor(httpRequest)) { return Response.serverError().status(Status.UNAUTHORIZED).build(); } try { Identity userIdentity = BaseSecurityManager.getInstance().loadIdentityByKey(identityKey, false); if(userIdentity == null) { return Response.serverError().status(Status.NOT_FOUND).build(); } ICourse course = CoursesWebService.loadCourse(courseId); if(course == null) { return Response.serverError().status(Status.NOT_FOUND).build(); } AssessableResultsVO results = getRootResult(userIdentity, course); if(results.getLastModifiedDate() != null) { Response.ResponseBuilder response = request.evaluatePreconditions(results.getLastModifiedDate()); if (response != null) { return response.build(); } } ResponseBuilder response = Response.ok(results); if(results.getLastModifiedDate() != null) { response = response.lastModified(results.getLastModifiedDate()).cacheControl(cc); } return response.build(); } catch (Throwable e) { throw new WebApplicationException(e); } } /** * Exports results for an assessable course node for all students. * @response.representation.200.qname {http://www.example.com}assessableResultsVO * @response.representation.200.mediaType application/xml, application/json * @response.representation.200.doc Export all results of all user of the course * @response.representation.200.example {@link org.olat.restapi.support.vo.Examples#SAMPLE_ASSESSABLERESULTSVOes} * @response.representation.401.doc The roles of the authenticated user are not sufficient * @response.representation.404.doc The course not found * @param courseId The course resourceable's id * @param nodeId The id of the course building block * @param httpRequest The HTTP request * @param request The REST request * @return */ @GET @Path("{nodeId}") @Produces( { MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON }) public Response getAssessableResults(@PathParam("courseId") Long courseId, @PathParam("nodeId") Long nodeId, @Context HttpServletRequest httpRequest, @Context Request request) { if(!RestSecurityHelper.isAuthor(httpRequest)) { return Response.serverError().status(Status.UNAUTHORIZED).build(); } ICourse course = CoursesWebService.loadCourse(courseId); if(course == null) { return Response.serverError().status(Status.NOT_FOUND).build(); } else if (!isAuthorEditor(course, httpRequest)) { return Response.serverError().status(Status.UNAUTHORIZED).build(); } //fxdiff VCRP-1,2: access control of resources List<Identity> courseUsers = loadUsers(course); int i=0; Date lastModified = null; AssessableResultsVO[] results = new AssessableResultsVO[courseUsers.size()]; for(Identity courseUser:courseUsers) { AssessableResultsVO result = getNodeResult(courseUser, course, nodeId); if(lastModified == null || (result.getLastModifiedDate() != null && lastModified.before(result.getLastModifiedDate()))) { lastModified = result.getLastModifiedDate(); } results[i++] = result; } if(lastModified != null) { Response.ResponseBuilder response = request.evaluatePreconditions(lastModified); if(response != null) { return response.build(); } return Response.ok(results).lastModified(lastModified).cacheControl(cc).build(); } return Response.ok(results).build(); } /** * Imports results for an assessable course node for the authenticated student. * @response.representation.qname {http://www.example.com}assessableResultsVO * @response.representation.mediaType application/xml, application/json * @response.representation.doc A result to import * @response.representation.example {@link org.olat.restapi.support.vo.Examples#SAMPLE_ASSESSABLERESULTSVO} * @response.representation.200.doc Import successful * @response.representation.401.doc The roles of the authenticated user are not sufficient * @response.representation.404.doc The identity not found * @param courseId The resourceable id of the course * @param nodeId The id of the course building block * @param resultsVO The results * @param request The HTTP request * @return */ @POST @Path("{nodeId}") @Consumes( { MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON }) public Response postAssessableResults(@PathParam("courseId") Long courseId, @PathParam("nodeId") String nodeId, AssessableResultsVO resultsVO, @Context HttpServletRequest request) { if(!RestSecurityHelper.isAuthor(request)) { return Response.serverError().status(Status.UNAUTHORIZED).build(); } Identity identity = RestSecurityHelper.getUserRequest(request).getIdentity(); if(identity == null) { return Response.serverError().status(Status.NOT_FOUND).build(); } attachAssessableResults(courseId, nodeId, identity, resultsVO); return Response.ok().build(); } private void attachAssessableResults(Long courseResourceableId, String nodeKey, Identity requestIdentity, AssessableResultsVO resultsVO) { try { ICourse course = CourseFactory.openCourseEditSession(courseResourceableId); CourseNode node = getParentNode(course, nodeKey); if (!(node instanceof AssessableCourseNode)) { throw new IllegalArgumentException( "The supplied node key does not refer to an AssessableCourseNode"); } BaseSecurity securityManager = BaseSecurityManager.getInstance(); Identity userIdentity = securityManager.loadIdentityByKey(resultsVO.getIdentityKey()); // create an identenv with no roles, no attributes, no locale IdentityEnvironment ienv = new IdentityEnvironment(); ienv.setIdentity(userIdentity); UserCourseEnvironment userCourseEnvironment = new UserCourseEnvironmentImpl(ienv, course.getCourseEnvironment()); // Fetch all score and passed and calculate score accounting for the // entire course userCourseEnvironment.getScoreAccounting().evaluateAll(); if (node instanceof IQTESTCourseNode) { importTestItems(course, nodeKey, requestIdentity, resultsVO); } else { AssessableCourseNode assessableNode = (AssessableCourseNode) node; ScoreEvaluation scoreEval = new ScoreEvaluation(resultsVO.getScore(), Boolean.TRUE, Boolean.TRUE, new Long(nodeKey));//not directly pass this key assessableNode.updateUserScoreEvaluation(scoreEval, userCourseEnvironment, requestIdentity, true); } CourseFactory.saveCourseEditorTreeModel(course.getResourceableId()); CourseFactory.closeCourseEditSession(course.getResourceableId(), true); } catch (Throwable e) { throw new WebApplicationException(e); } } private void importTestItems(ICourse course, String nodeKey, Identity identity, AssessableResultsVO resultsVO) { try { IQManager iqManager = CoreSpringFactory.getImpl(IQManager.class); // load the course and the course node CourseNode courseNode = getParentNode(course, nodeKey); ModuleConfiguration modConfig = courseNode.getModuleConfiguration(); // check if the result set is already saved QTIResultSet set = iqManager.getLastResultSet(identity, course.getResourceableId(), courseNode.getIdent()); if (set == null) { String resourcePathInfo = course.getResourceableId() + File.separator + courseNode.getIdent(); // The use of these classes AssessmentInstance, AssessmentContext and // Navigator // allow the use of the persistence mechanism of OLAT without // duplicating the code. // The consequence is that we must loop on section and items and set the // navigator on // the right position before submitting the inputs. AssessmentInstance ai = AssessmentFactory.createAssessmentInstance(identity, "", modConfig, false, course.getResourceableId(), courseNode.getIdent(), resourcePathInfo, null); Navigator navigator = ai.getNavigator(); navigator.startAssessment(); // The type of the navigator depends on the setting of the course node boolean perItem = (navigator instanceof MenuItemNavigator); Map<String, ItemInput> datas = convertToHttpItemInput(resultsVO.getResults()); AssessmentContext ac = ai.getAssessmentContext(); int sectioncnt = ac.getSectionContextCount(); // loop on the sections for (int i = 0; i < sectioncnt; i++) { SectionContext sc = ac.getSectionContext(i); navigator.goToSection(i); ItemsInput iips = new ItemsInput(); int itemcnt = sc.getItemContextCount(); // loop on the items for (int j = 0; j < itemcnt; j++) { ItemContext it = sc.getItemContext(j); if (datas.containsKey(it.getIdent())) { if (perItem) { // save the datas on a per item base navigator.goToItem(i, j); // the navigator can give informations on its current status Info info = navigator.getInfo(); if (info.containsError()) { // some items cannot processed twice } else { iips.addItemInput(datas.get(it.getIdent())); navigator.submitItems(iips); iips = new ItemsInput(); } } else { // put for a section iips.addItemInput(datas.get(it.getIdent())); } } } if (!perItem) { // save the inputs of the section. In a section based navigation, // we must saved the inputs of the whole section at once navigator.submitItems(iips); } } navigator.submitAssessment(); // persist the QTIResultSet (o_qtiresultset and o_qtiresult) on the // database // TODO iqManager.persistResults(ai, course.getResourceableId(), // courseNode.getIdent(), identity, "127.0.0.1"); // write the reporting file on the file system // The path is <olatdata> / resreporting / <username> / Assessment / // <assessId>.xml // TODO Document docResReporting = iqManager.getResultsReporting(ai, // identity, Locale.getDefault()); // TODO FilePersister.createResultsReporting(docResReporting, identity, // ai.getFormattedType(), ai.getAssessID()); // prepare all instances needed to save the score at the course node // level CourseEnvironment cenv = course.getCourseEnvironment(); IdentityEnvironment identEnv = new IdentityEnvironment(); identEnv.setIdentity(identity); UserCourseEnvironment userCourseEnv = new UserCourseEnvironmentImpl(identEnv, cenv); // update scoring overview for the user in the current course Float score = ac.getScore(); Boolean passed = ac.isPassed(); ScoreEvaluation sceval = new ScoreEvaluation(score, passed, passed, new Long(nodeKey));//perhaps don't pass this key directly AssessableCourseNode acn = (AssessableCourseNode) courseNode; // assessment nodes are assessable boolean incrementUserAttempts = true; acn.updateUserScoreEvaluation(sceval, userCourseEnv, identity, incrementUserAttempts); } else { log.error("Result set already saved"); } } catch (Exception e) { log.error("", e); } } private Map<String, ItemInput> convertToHttpItemInput(Map<Long, String> results) { Map<String, ItemInput> datas = new HashMap<String, ItemInput>(); for (Long key : results.keySet()) { HttpItemInput iip = new HttpItemInput(results.get(key)); iip.putSingle(key.toString(), results.get(key)); //TODO somehow obtain answer from value datas.put(iip.getIdent(), iip); } return datas; } private CourseNode getParentNode(ICourse course, String parentNodeId) { if (parentNodeId == null) { return course.getRunStructure().getRootNode(); } else { return course.getEditorTreeModel().getCourseNode(parentNodeId); } } /** * Returns the results of a student at a specific assessable node * @response.representation.200.qname {http://www.example.com}assessableResultsVO * @response.representation.200.mediaType application/xml, application/json * @response.representation.200.doc The result of a user at a specific node * @response.representation.200.example {@link org.olat.restapi.support.vo.Examples#SAMPLE_ASSESSABLERESULTSVO} * @response.representation.401.doc The roles of the authenticated user are not sufficient * @response.representation.404.doc The identity or the course not found * @param courseId The course resourceable's id * @param nodeId The ident of the course building block * @param identityKey The id of the user * @param httpRequest The HTTP request * @param request The REST request * @return */ @GET @Path("{nodeId}/users/{identityKey}") @Produces({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON}) public Response getCourseNodeResultsForNode(@PathParam("courseId") Long courseId, @PathParam("nodeId") Long nodeId, @PathParam("identityKey") Long identityKey, @Context HttpServletRequest httpRequest, @Context Request request) { if(!RestSecurityHelper.isAuthor(httpRequest)) { return Response.serverError().status(Status.UNAUTHORIZED).build(); } try { Identity userIdentity = BaseSecurityManager.getInstance().loadIdentityByKey(identityKey, false); if(userIdentity == null) { return Response.serverError().status(Status.NOT_FOUND).build(); } ICourse course = CoursesWebService.loadCourse(courseId); if(course == null) { return Response.serverError().status(Status.NOT_FOUND).build(); } AssessableResultsVO results = getNodeResult(userIdentity, course, nodeId); if(results.getLastModifiedDate() != null) { Response.ResponseBuilder response = request.evaluatePreconditions(results.getLastModifiedDate()); if(response != null) { return response.build(); } return Response.ok(results).lastModified(results.getLastModifiedDate()).cacheControl(cc).build(); } return Response.ok(results).build(); } catch (Throwable e) { throw new WebApplicationException(e); } } private AssessableResultsVO getRootResult(Identity identity, ICourse course) { CourseNode rootNode = course.getRunStructure().getRootNode(); return getRootResult(identity, course, rootNode); } private AssessableResultsVO getNodeResult(Identity identity, ICourse course, Long nodeId) { CourseNode courseNode = course.getRunStructure().getNode(nodeId.toString()); return getRootResult(identity, course, courseNode); } private AssessableResultsVO getRootResult(Identity identity, ICourse course, CourseNode courseNode) { AssessableResultsVO results = new AssessableResultsVO(); results.setIdentityKey(identity.getKey()); // create an identenv with no roles, no attributes, no locale IdentityEnvironment ienv = new IdentityEnvironment(); ienv.setIdentity(identity); UserCourseEnvironment userCourseEnvironment = new UserCourseEnvironmentImpl(ienv, course.getCourseEnvironment()); // Fetch all score and passed and calculate score accounting for the entire course ScoreAccounting scoreAccounting = userCourseEnvironment.getScoreAccounting(); scoreAccounting.evaluateAll(); if(courseNode instanceof AssessableCourseNode) { AssessableCourseNode assessableRootNode = (AssessableCourseNode)courseNode; ScoreEvaluation scoreEval = scoreAccounting.evalCourseNode(assessableRootNode); results.setScore(scoreEval.getScore()); results.setPassed(scoreEval.getPassed()); results.setLastModifiedDate(getLastModificationDate(identity, course, courseNode)); } return results; } private Date getLastModificationDate(Identity assessedIdentity, ICourse course, CourseNode courseNode) { AssessmentManager am = course.getCourseEnvironment().getAssessmentManager(); return am.getScoreLastModifiedDate(courseNode, assessedIdentity); } private List<Identity> loadUsers(ICourse course) { List<Identity> identities = new ArrayList<Identity>(); List<BusinessGroup> groups = course.getCourseEnvironment().getCourseGroupManager().getAllBusinessGroups(); Set<Long> check = new HashSet<Long>(); BusinessGroupService businessGroupService = CoreSpringFactory.getImpl(BusinessGroupService.class); List<Identity> participants = businessGroupService.getMembers(groups, GroupRoles.participant.name()); for(Identity participant:participants) { if(!check.contains(participant.getKey())) { identities.add(participant); check.add(participant.getKey()); } } RepositoryService repositoryService = CoreSpringFactory.getImpl(RepositoryService.class); RepositoryEntry re = RepositoryManager.getInstance().lookupRepositoryEntry(course, false); if(re != null) { List<Identity> ids = repositoryService.getMembers(re, GroupRoles.participant.name()); for(Identity id:ids) { if(!check.contains(id.getKey())) { identities.add(id); check.add(id.getKey()); } } } return identities; } }