diff --git a/assessment-api/assessment-actors/src/main/scala/org/sunbird/actors/QuestionActor.scala b/assessment-api/assessment-actors/src/main/scala/org/sunbird/actors/QuestionActor.scala index 3f0ed87fc..89d32184a 100644 --- a/assessment-api/assessment-actors/src/main/scala/org/sunbird/actors/QuestionActor.scala +++ b/assessment-api/assessment-actors/src/main/scala/org/sunbird/actors/QuestionActor.scala @@ -8,7 +8,7 @@ import org.sunbird.common.{DateUtils, Platform} import org.sunbird.graph.OntologyEngineContext import org.sunbird.graph.nodes.DataNode import org.sunbird.graph.utils.NodeUtil -import org.sunbird.managers.AssessmentManager +import org.sunbird.managers.{AssessmentManager, CopyManager} import org.sunbird.utils.RequestUtil import java.util @@ -36,6 +36,7 @@ class QuestionActor @Inject()(implicit oec: OntologyEngineContext) extends BaseA case "systemUpdateQuestion" => systemUpdate(request) case "listQuestions" => listQuestions(request) case "rejectQuestion" => reject(request) + case "copyQuestion" => copy(request) case _ => ERROR(request.getOperation) } @@ -128,4 +129,9 @@ class QuestionActor @Inject()(implicit oec: OntologyEngineContext) extends BaseA AssessmentManager.updateNode(updateRequest) }) } + + def copy(request: Request): Future[Response] ={ + RequestUtil.restrictProperties(request) + CopyManager.copy(request) + } } diff --git a/assessment-api/assessment-actors/src/main/scala/org/sunbird/actors/QuestionSetActor.scala b/assessment-api/assessment-actors/src/main/scala/org/sunbird/actors/QuestionSetActor.scala index 7ce683869..b4a68317c 100644 --- a/assessment-api/assessment-actors/src/main/scala/org/sunbird/actors/QuestionSetActor.scala +++ b/assessment-api/assessment-actors/src/main/scala/org/sunbird/actors/QuestionSetActor.scala @@ -1,7 +1,6 @@ package org.sunbird.actors import java.util - import javax.inject.Inject import org.apache.commons.collections4.CollectionUtils import org.sunbird.`object`.importer.{ImportConfig, ImportManager} @@ -13,7 +12,7 @@ import org.sunbird.graph.OntologyEngineContext import org.sunbird.graph.nodes.DataNode import org.sunbird.graph.dac.model.Node import org.sunbird.managers.HierarchyManager.hierarchyPrefix -import org.sunbird.managers.{AssessmentManager, HierarchyManager, UpdateHierarchyManager} +import org.sunbird.managers.{AssessmentManager, CopyManager, HierarchyManager, UpdateHierarchyManager} import org.sunbird.utils.RequestUtil import scala.collection.JavaConverters._ @@ -40,6 +39,7 @@ class QuestionSetActor @Inject()(implicit oec: OntologyEngineContext) extends Ba case "rejectQuestionSet" => reject(request) case "importQuestionSet" => importQuestionSet(request) case "systemUpdateQuestionSet" => systemUpdate(request) + case "copyQuestionSet" => copy(request) case _ => ERROR(request.getOperation) } @@ -157,4 +157,8 @@ class QuestionSetActor @Inject()(implicit oec: OntologyEngineContext) extends Ba }).map(node => ResponseHandler.OK.put("identifier", identifier).put("status", "success")) } + def copy(request: Request): Future[Response] ={ + RequestUtil.restrictProperties(request) + CopyManager.copy(request) + } } diff --git a/assessment-api/assessment-actors/src/main/scala/org/sunbird/managers/CopyManager.scala b/assessment-api/assessment-actors/src/main/scala/org/sunbird/managers/CopyManager.scala new file mode 100644 index 000000000..dc73939e9 --- /dev/null +++ b/assessment-api/assessment-actors/src/main/scala/org/sunbird/managers/CopyManager.scala @@ -0,0 +1,292 @@ +package org.sunbird.managers + +import org.apache.commons.collections.CollectionUtils +import org.apache.commons.collections4.MapUtils +import org.apache.commons.lang.StringUtils +import org.sunbird.common.{JsonUtils, Platform} +import org.sunbird.common.dto.{Request, Response, ResponseHandler} +import org.sunbird.common.exception.{ClientException, ServerException} +import org.sunbird.graph.OntologyEngineContext +import org.sunbird.graph.common.Identifier +import org.sunbird.graph.dac.model.Node +import org.sunbird.graph.nodes.DataNode +import org.sunbird.graph.schema.DefinitionNode +import org.sunbird.graph.utils.{NodeUtil, ScalaJsonUtils} +import org.sunbird.telemetry.logger.TelemetryManager +import org.sunbird.utils.{AssessmentConstants, BranchingUtil, HierarchyConstants} + +import java.util +import java.util.concurrent.{CompletionException} +import java.util.{Optional, UUID} +import scala.collection.JavaConversions.{mapAsScalaMap} +import scala.collection.JavaConverters._ +import scala.concurrent.{ExecutionContext, Future} + +object CopyManager { + + private val originMetadataKeys: util.List[String] = Platform.getStringList("assessment.copy.origin_data", new util.ArrayList[String]()) + private val internalHierarchyProps = List("identifier", "parent", "index", "depth") + private val metadataNotTobeCopied = Platform.config.getStringList("assessment.copy.props_to_remove") + + def copy(request: Request)(implicit ec: ExecutionContext, oec: OntologyEngineContext): Future[Response] = { + validateRequest(request) + DataNode.read(request).map(node => { + validateExistingNode(request, node) + val copiedNodeFuture: Future[Node] = node.getMetadata.get(AssessmentConstants.MIME_TYPE) match { + case AssessmentConstants.QUESTIONSET_MIME_TYPE => + node.setInRelations(null) + node.setOutRelations(null) + validateShallowCopyReq(node, request) + copyQuestionSet(node, request) + case AssessmentConstants.QUESTION_MIME_TYPE => + node.setInRelations(null) + copyNode(node, request) + } + copiedNodeFuture.map(copiedNode => { + val response = ResponseHandler.OK() + response.put("node_id", new util.HashMap[String, AnyRef]() { + { + put(node.getIdentifier, copiedNode.getIdentifier) + } + }) + response.put(AssessmentConstants.VERSION_KEY, copiedNode.getMetadata.get(AssessmentConstants.VERSION_KEY)) + response + }) + }).flatMap(f => f) recoverWith { case e: CompletionException => throw e.getCause } + } + + def validateExistingNode(request: Request, node: Node) = { + val requestObjectType = request.getObjectType + val nodeObjectType = node.getObjectType + if (!StringUtils.equalsIgnoreCase(requestObjectType, nodeObjectType)) throw new ClientException(AssessmentConstants.ERR_INVALID_OBJECT_TYPE, s"Please Provide Valid ${requestObjectType} Identifier") + if (StringUtils.equalsIgnoreCase(node.getObjectType, AssessmentConstants.QUESTION) && StringUtils.equalsIgnoreCase(node.getMetadata.getOrDefault(AssessmentConstants.VISIBILITY, AssessmentConstants.VISIBILITY_PARENT).asInstanceOf[String], AssessmentConstants.VISIBILITY_PARENT)) + throw new ClientException(AssessmentConstants.ERR_INVALID_REQUEST, "Question With Visibility Parent Cannot Be Copied Individually!") + } + + def copyQuestionSet(originNode: Node, request: Request)(implicit ex: ExecutionContext, oec: OntologyEngineContext): Future[Node] = { + val copyType = request.getRequest.get(AssessmentConstants.COPY_TYPE).asInstanceOf[String] + copyNode(originNode, request).map(node => { + val req = new Request(request) + req.put(AssessmentConstants.ROOT_ID, request.get(AssessmentConstants.IDENTIFIER)) + req.put(AssessmentConstants.MODE, request.get(AssessmentConstants.MODE)) + HierarchyManager.getHierarchy(req).map(response => { + val originHierarchy = response.getResult.getOrDefault(AssessmentConstants.QUESTIONSET, new util.HashMap[String, AnyRef]()).asInstanceOf[java.util.Map[String, AnyRef]] + copyType match { + case AssessmentConstants.COPY_TYPE_SHALLOW => updateShallowHierarchy(request, node, originNode, originHierarchy) + case _ => updateHierarchy(request, node, originNode, originHierarchy, copyType) + } + }).flatMap(f => f) + }).flatMap(f => f) recoverWith { case e: CompletionException => throw e.getCause } + } + + + def copyNode(node: Node, request: Request)(implicit ec: ExecutionContext, oec: OntologyEngineContext): Future[Node] = { + val copyCreateReq: Future[Request] = getCopyRequest(node, request) + copyCreateReq.map(req => { + DataNode.create(req).map(copiedNode => { + Future(copiedNode) + }).flatMap(f => f) + }).flatMap(f => f) + } + + def updateHierarchy(request: Request, node: Node, originNode: Node, originHierarchy: util.Map[String, AnyRef], copyType: String)(implicit ec: ExecutionContext, oec: OntologyEngineContext): Future[Node] = { + prepareHierarchyRequest(originHierarchy, originNode, node, copyType, request).map(req => { + val hierarchyRequest = new Request(request) + hierarchyRequest.putAll(req) + val nodesModified: java.util.HashMap[String, AnyRef] = hierarchyRequest.getRequest.get(HierarchyConstants.NODES_MODIFIED).asInstanceOf[java.util.HashMap[String, AnyRef]] + val branchingRecord = BranchingUtil.generateBranchingRecord(nodesModified) + val originalRequest = JsonUtils.deserialize(ScalaJsonUtils.serialize(hierarchyRequest), classOf[Request]) + val BLExists = branchingRecord.exists(BLRecord => BLRecord._2.asInstanceOf[util.HashMap[String, AnyRef]].get(AssessmentConstants.CONTAINS_BL) == true) + val (updateHierarchyReq, branchingUpdateReq) = if (BLExists) (hierarchyRequest, JsonUtils.deserialize(ScalaJsonUtils.serialize(hierarchyRequest), classOf[Request])) else (originalRequest, new Request()) + UpdateHierarchyManager.updateHierarchy(updateHierarchyReq).map(response => { + if (!ResponseHandler.checkError(response)) response + else { + TelemetryManager.info(s"Update Hierarchy Failed For Copy Question Set Having Identifier: ${node.getIdentifier} | Response is: " + response) + throw new ServerException("ERR_QUESTIONSET_COPY", "Something Went Wrong, Please Try Again") + } + }).map(response => { + if (BLExists) { + BranchingUtil.hierarchyRequestModifier(branchingUpdateReq, branchingRecord, response.getResult.get(AssessmentConstants.IDENTIFIERS).asInstanceOf[util.Map[String, String]]) + UpdateHierarchyManager.updateHierarchy(branchingUpdateReq).map(response_ => { + if (!ResponseHandler.checkError(response_)) node + else { + TelemetryManager.info(s"Update Hierarchy Failed For Copy Question Set Having Identifier: ${node.getIdentifier} | Response is: " + response) + throw new ServerException("ERR_QUESTIONSET_COPY", "Something Went Wrong, Please Try Again") + } + }) + } else Future(node) + }).flatMap(f => f) + }).flatMap(f => f) + } + + def prepareHierarchyRequest(originHierarchy: util.Map[String, AnyRef], originNode: Node, node: Node, copyType: String, request: Request)(implicit ec: ExecutionContext, oec: OntologyEngineContext): Future[util.Map[String, AnyRef]] = { + val children: util.List[util.Map[String, AnyRef]] = originHierarchy.get("children").asInstanceOf[util.List[util.Map[String, AnyRef]]] + if (null != children && !children.isEmpty) { + val nodesModified = new util.HashMap[String, AnyRef]() + val hierarchy = new util.HashMap[String, AnyRef]() + val idMap = new util.HashMap[String, String]() + hierarchy.put(node.getIdentifier, new util.HashMap[String, AnyRef]() { + { + put(AssessmentConstants.CHILDREN, new util.ArrayList[String]()) + put(AssessmentConstants.ROOT, true.asInstanceOf[AnyRef]) + put(AssessmentConstants.PRIMARY_CATEGORY, node.getMetadata.get(AssessmentConstants.PRIMARY_CATEGORY)) + } + }) + populateHierarchyRequest(children, nodesModified, hierarchy, node.getIdentifier, copyType, request, idMap) + getExternalData(idMap.keySet().asScala.toList, request).map(exData => { + idMap.asScala.toMap.foreach(entry => { + nodesModified.get(entry._2).asInstanceOf[java.util.Map[String, AnyRef]].get("metadata").asInstanceOf[util.Map[String, AnyRef]].putAll(exData.getOrDefault(entry._1, new util.HashMap[String, AnyRef]()).asInstanceOf[util.Map[String, AnyRef]]) + }) + new util.HashMap[String, AnyRef]() { + { + put(AssessmentConstants.NODES_MODIFIED, nodesModified) + put(AssessmentConstants.HIERARCHY, hierarchy) + } + } + }) + + } else Future(new util.HashMap[String, AnyRef]()) + } + + def getExternalData(identifiers: List[String], request: Request)(implicit ec: ExecutionContext, oec: OntologyEngineContext): Future[util.Map[String, AnyRef]] = { + val req = new Request(request) + req.getContext().putAll(Map("objectType" -> "Question", "schemaName" -> "question").asJava) + req.put("identifiers", identifiers) + val result = new util.HashMap[String, AnyRef]() + val externalProps = DefinitionNode.getExternalProps(req.getContext.getOrDefault("graph_id", "domain").asInstanceOf[String], req.getContext.getOrDefault("version", "1.0").asInstanceOf[String], req.getContext.getOrDefault("schemaName", "question").asInstanceOf[String]) + val externalPropsResponse = oec.graphService.readExternalProps(req, externalProps) + externalPropsResponse.map(response => { + identifiers.map(id => { + val externalData = Optional.ofNullable(response.get(id).asInstanceOf[util.Map[String, AnyRef]]).orElse(new util.HashMap[String, AnyRef]()) + result.put(id, externalData) + }) + result + }) + } + + def populateHierarchyRequest(children: util.List[util.Map[String, AnyRef]], nodesModified: util.HashMap[String, AnyRef], hierarchy: util.HashMap[String, AnyRef], parentId: String, copyType: String, request: Request, idMap: java.util.Map[String, String])(implicit ec: ExecutionContext, oec: OntologyEngineContext): Unit = { + if (null != children && !children.isEmpty) { + children.asScala.toList.foreach(child => { + val id = if ("Parent".equalsIgnoreCase(child.get(AssessmentConstants.VISIBILITY).asInstanceOf[String])) { + val identifier = UUID.randomUUID().toString + nodesModified.put(identifier, new util.HashMap[String, AnyRef]() { + { + put(AssessmentConstants.METADATA, cleanUpCopiedData(new util.HashMap[String, AnyRef]() { + { + putAll(child) + put(AssessmentConstants.COPY_OF, child.getOrDefault(AssessmentConstants.IDENTIFIER,"")) + put(AssessmentConstants.CHILDREN, new util.ArrayList()) + internalHierarchyProps.map(key => remove(key)) + } + }, copyType)) + put(AssessmentConstants.ROOT, false.asInstanceOf[AnyRef]) + put(AssessmentConstants.OBJECT_TYPE, child.getOrDefault(AssessmentConstants.OBJECT_TYPE, "")) + put("isNew", true.asInstanceOf[AnyRef]) + put("setDefaultValue", false.asInstanceOf[AnyRef]) + } + }) + if (StringUtils.equalsIgnoreCase(AssessmentConstants.QUESTION_MIME_TYPE, child.getOrDefault("mimeType", "").asInstanceOf[String])) + idMap.put(child.getOrDefault("identifier", "").asInstanceOf[String], identifier) + identifier + } else + child.get(AssessmentConstants.IDENTIFIER).asInstanceOf[String] + if (StringUtils.equalsIgnoreCase(child.getOrDefault(AssessmentConstants.MIME_TYPE, "").asInstanceOf[String], AssessmentConstants.QUESTIONSET_MIME_TYPE)) + hierarchy.put(id, new util.HashMap[String, AnyRef]() { + { + put(AssessmentConstants.CHILDREN, new util.ArrayList[String]()) + put(AssessmentConstants.ROOT, false.asInstanceOf[AnyRef]) + put(AssessmentConstants.PRIMARY_CATEGORY, child.get(AssessmentConstants.PRIMARY_CATEGORY)) + } + }) + hierarchy.get(parentId).asInstanceOf[util.Map[String, AnyRef]].get(AssessmentConstants.CHILDREN).asInstanceOf[util.List[String]].add(id) + populateHierarchyRequest(child.get(AssessmentConstants.CHILDREN).asInstanceOf[util.List[util.Map[String, AnyRef]]], nodesModified, hierarchy, id, copyType, request, idMap) + }) + } + } + + def updateShallowHierarchy(request: Request, node: Node, originNode: Node, originHierarchy: util.Map[String, AnyRef])(implicit ec: ExecutionContext, oec: OntologyEngineContext): Future[Node] = { + val childrenHierarchy = originHierarchy.get("children").asInstanceOf[util.List[util.Map[String, AnyRef]]] + val req = new Request(request) + req.getContext.put(AssessmentConstants.SCHEMA_NAME, request.getContext.getOrDefault(AssessmentConstants.SCHEMA_NAME, AssessmentConstants.QUESTIONSET_SCHEMA_NAME)) + req.getContext.put(AssessmentConstants.VERSION, request.getContext.getOrDefault(AssessmentConstants.VERSION, AssessmentConstants.SCHEMA_VERSION)) + req.getContext.put(AssessmentConstants.IDENTIFIER, node.getIdentifier) + req.put(AssessmentConstants.HIERARCHY, ScalaJsonUtils.serialize(new java.util.HashMap[String, AnyRef]() { + { + put(AssessmentConstants.IDENTIFIER, node.getIdentifier) + put(AssessmentConstants.CHILDREN, childrenHierarchy) + } + })) + DataNode.update(req).map(node => node) + } + + def getCopyRequest(node: Node, request: Request)(implicit ec: ExecutionContext, oec: OntologyEngineContext): Future[Request] = { + val metadata: util.Map[String, AnyRef] = NodeUtil.serialize(node, new util.ArrayList(), node.getObjectType.toLowerCase.replace("image", ""), request.getContext.getOrDefault(AssessmentConstants.VERSION, "").asInstanceOf[String]) + val requestMap = request.getRequest + requestMap.remove(AssessmentConstants.MODE) + requestMap.remove(AssessmentConstants.COPY_SCHEME).asInstanceOf[String] + val copyType = requestMap.remove(AssessmentConstants.COPY_TYPE).asInstanceOf[String] + val originData: java.util.Map[String, AnyRef] = getOriginData(metadata, copyType) + cleanUpCopiedData(metadata, copyType) + metadata.putAll(requestMap) + metadata.put(AssessmentConstants.STATUS, "Draft") + metadata.put(AssessmentConstants.ORIGIN, node.getIdentifier) + metadata.put(AssessmentConstants.IDENTIFIER, Identifier.getIdentifier(request.getContext.get("graph_id").asInstanceOf[String], Identifier.getUniqueIdFromTimestamp)) + if (MapUtils.isNotEmpty(originData)) + metadata.put(AssessmentConstants.ORIGIN_DATA, originData) + request.getContext().put(AssessmentConstants.SCHEMA_NAME, node.getObjectType.toLowerCase.replace("image", "")) + val req = new Request(request) + req.setRequest(metadata) + val graphId = request.getContext.getOrDefault("graph_id", "").asInstanceOf[String] + val version = request.getContext.getOrDefault("version", "").asInstanceOf[String] + val externalProps = if (StringUtils.equalsIgnoreCase(AssessmentConstants.QUESTIONSET_MIME_TYPE, node.getMetadata.getOrDefault("mimeType", "").asInstanceOf[String])) { + DefinitionNode.getExternalProps(graphId, version, AssessmentConstants.QUESTIONSET_SCHEMA_NAME).diff(List("hierarchy")) + } else { + DefinitionNode.getExternalProps(graphId, version, AssessmentConstants.QUESTION_SCHEMA_NAME) + } + val readReq = new Request() + readReq.setContext(request.getContext) + readReq.put("identifier", node.getIdentifier) + readReq.put("fields", externalProps.asJava) + DataNode.read(readReq).map(node => { + val metadata: util.Map[String, AnyRef] = NodeUtil.serialize(node, externalProps.asJava, node.getObjectType.toLowerCase.replace + ("image", ""), request.getContext.get("version").asInstanceOf[String]) + externalProps.foreach(prop => { + val propValue = metadata.get(prop) + if (metadata.containsKey(prop) && propValue != null) { + req.put(prop, propValue) + } + }) + Future(req) + }).flatMap(f=>f) + } + + def getOriginData(metadata: util.Map[String, AnyRef], copyType: String): java.util.Map[String, AnyRef] = (Map(AssessmentConstants.COPY_TYPE -> copyType) ++ originMetadataKeys.asScala.filter(key => metadata.containsKey(key)).map(key => key -> metadata.get(key)).toMap).asJava + + def validateRequest(request: Request)(implicit ec: ExecutionContext, oec: OntologyEngineContext): Unit = { + val keysNotPresent = AssessmentConstants.REQUIRED_KEYS.filter(key => emptyCheckFilter(request.getRequest.getOrDefault(key, ""))) + if (keysNotPresent.nonEmpty) + throw new ClientException(AssessmentConstants.ERR_INVALID_REQUEST, "Please provide valid value for " + keysNotPresent) + } + + def validateShallowCopyReq(node: Node, request: Request) = { + val copyType: String = request.getRequest.get("copyType").asInstanceOf[String] + if (StringUtils.equalsIgnoreCase("shallow", copyType) && !StringUtils.equalsIgnoreCase("Live", node.getMetadata.get("status").asInstanceOf[String])) + throw new ClientException(AssessmentConstants.ERR_INVALID_REQUEST, "QuestionSet With Status " + node.getMetadata.get(AssessmentConstants.STATUS).asInstanceOf[String].toLowerCase + " Cannot Be Partially (Shallow) Copied.") + if(StringUtils.equalsIgnoreCase("shallow", copyType) && StringUtils.equalsIgnoreCase(request.get(AssessmentConstants.MODE).asInstanceOf[String], "edit")) + request.getRequest.remove(AssessmentConstants.MODE) + } + + def emptyCheckFilter(key: AnyRef): Boolean = key match { + case k: String => k.asInstanceOf[String].isEmpty + case k: util.Map[String, AnyRef] => MapUtils.isEmpty(k.asInstanceOf[util.Map[String, AnyRef]]) + case k: util.List[String] => CollectionUtils.isEmpty(k.asInstanceOf[util.List[String]]) + case _ => true + } + + def cleanUpCopiedData(metadata: util.Map[String, AnyRef], copyType: String): util.Map[String, AnyRef] = { + if (StringUtils.equalsIgnoreCase(AssessmentConstants.COPY_TYPE_SHALLOW, copyType)) { + metadata.keySet().removeAll(metadataNotTobeCopied.asScala.toList.filter(str => !str.contains("dial")).asJava) + } else metadata.keySet().removeAll(metadataNotTobeCopied) + metadata + } +} \ No newline at end of file diff --git a/assessment-api/assessment-actors/src/main/scala/org/sunbird/utils/AssessmentContants.scala b/assessment-api/assessment-actors/src/main/scala/org/sunbird/utils/AssessmentContants.scala new file mode 100644 index 000000000..16e47ccde --- /dev/null +++ b/assessment-api/assessment-actors/src/main/scala/org/sunbird/utils/AssessmentContants.scala @@ -0,0 +1,48 @@ +package org.sunbird.utils + +object AssessmentConstants { + val REQUIRED_KEYS: List[String] = List("createdBy", "createdFor") + val ERR_INVALID_REQUEST: String = "ERR_INVALID_REQUEST" + val ERR_INVALID_OBJECT_TYPE: String = "ERR_INVALID_OBJECT_TYPE" + val COPY_SCHEME: String = "copyScheme" + val MIME_TYPE: String = "mimeType" + val QUESTIONSET_MIME_TYPE: String = "application/vnd.sunbird.questionset" + val STATUS: String = "status" + val COPY_TYPE: String = "copyType" + val SCHEMA_NAME: String = "schemaName" + val VERSION: String = "version" + val ROOT_ID: String = "rootId" + val MODE: String = "mode" + val COPY_TYPE_SHALLOW: String = "shallow" + val QUESTIONSET_SCHEMA_NAME: String = "questionset" + val SCHEMA_VERSION: String = "1.0" + val IDENTIFIER: String = "identifier" + val QUESTION: String = "question" + val HIERARCHY: String = "hierarchy" + val CHILDREN: String = "children" + val ORIGIN: String = "origin" + val ORIGIN_DATA: String = "originData" + val CONTENT_TYPE: String = "contentType" + val ROOT: String = "root" + val NODES_MODIFIED: String = "nodesModified" + val VISIBILITY: String = "visibility" + val METADATA: String = "metadata" + val VERSION_KEY: String = "versionKey" + val PRIMARY_CATEGORY : String = "primaryCategory" + val QUESTIONSET : String = "questionSet" + val OBJECT_TYPE : String = "objectType" + val COPY_TYPE_DEEP: String = "deep" + val QUESTION_MIME_TYPE: String = "application/vnd.sunbird.question" + val QUESTION_SCHEMA_NAME: String = "question" + val VISIBILITY_PARENT: String = "Parent" + val VISIBILITY_DEFAULT: String = "Default" + val BRANCHING_LOGIC: String = "branchingLogic" + val COPY_OF: String = "copyOf" + val CONTAINS_BL: String = "containsBL" + val IDENTIFIERS: String = "identifiers" + val IS_NEW: String = "isNew" + val TARGET: String = "target" + val PRE_CONDITION: String = "preCondition" + val SOURCE: String = "source" + val PRE_CONDITION_VAR : String = "var" +} diff --git a/assessment-api/assessment-actors/src/main/scala/org/sunbird/utils/BranchingUtil.scala b/assessment-api/assessment-actors/src/main/scala/org/sunbird/utils/BranchingUtil.scala new file mode 100644 index 000000000..fa9098816 --- /dev/null +++ b/assessment-api/assessment-actors/src/main/scala/org/sunbird/utils/BranchingUtil.scala @@ -0,0 +1,121 @@ +package org.sunbird.utils + +import org.apache.commons.lang.StringUtils +import org.sunbird.common.dto.Request + +import java.util +import scala.collection.JavaConverters._ +import scala.collection.JavaConversions.{asScalaBuffer} + +object BranchingUtil { + + def generateBranchingRecord(nodesModified: util.HashMap[String, AnyRef]): util.HashMap[String, AnyRef] = { + val idSet = nodesModified.keySet().asScala.toList + val branchingRecord = new util.HashMap[String, AnyRef]() + idSet.map(id => { + val nodeMetaData = nodesModified.getOrDefault(id, new util.HashMap()).asInstanceOf[util.Map[String, AnyRef]].getOrDefault(AssessmentConstants.METADATA, new util.HashMap()).asInstanceOf[util.Map[String, AnyRef]] + val containsBL = nodeMetaData.containsKey(AssessmentConstants.BRANCHING_LOGIC) + branchingRecord.put(id, new util.HashMap[String, AnyRef]() { + { + if (containsBL) { + put(AssessmentConstants.BRANCHING_LOGIC, nodeMetaData.get(AssessmentConstants.BRANCHING_LOGIC)) + nodeMetaData.remove(AssessmentConstants.BRANCHING_LOGIC) + } + put(AssessmentConstants.CONTAINS_BL, containsBL.asInstanceOf[AnyRef]) + put(AssessmentConstants.COPY_OF, nodeMetaData.get(AssessmentConstants.COPY_OF).asInstanceOf[String]) + } + }) + nodeMetaData.remove(AssessmentConstants.COPY_OF) + }) + branchingRecord + } + + def hierarchyRequestModifier(request: Request, branchingRecord: util.HashMap[String, AnyRef], identifiers: util.Map[String, String]) = { + val nodesModified: java.util.HashMap[String, AnyRef] = request.getRequest.get(HierarchyConstants.NODES_MODIFIED).asInstanceOf[java.util.HashMap[String, AnyRef]] + val hierarchy: java.util.HashMap[String, AnyRef] = request.getRequest.get(HierarchyConstants.HIERARCHY).asInstanceOf[java.util.HashMap[String, AnyRef]] + val oldToNewIdMap = getIdentifierMapping(branchingRecord, identifiers) + branchingRecord.keySet().asScala.toList.map(id => { + val nodeInfo = branchingRecord.get(id).asInstanceOf[util.HashMap[String, AnyRef]] + val node = nodesModified.get(id).asInstanceOf[util.HashMap[String, AnyRef]] + val nodeMetaData = node.get(AssessmentConstants.METADATA).asInstanceOf[util.HashMap[String, AnyRef]] + val newId = identifiers.get(id) + if (nodeInfo.get(AssessmentConstants.CONTAINS_BL).asInstanceOf[Boolean]) { + val branchingLogic = nodeInfo.get(AssessmentConstants.BRANCHING_LOGIC).asInstanceOf[util.HashMap[String, AnyRef]] + branchingLogicModifier(branchingLogic, oldToNewIdMap) + nodeMetaData.put(AssessmentConstants.BRANCHING_LOGIC, branchingLogic) + } + node.put(AssessmentConstants.IS_NEW, false.asInstanceOf[AnyRef]) + nodesModified.remove(id) + nodesModified.put(newId, node) + }) + hierarchy.keySet().asScala.toList.map(id => { + val nodeHierarchy = hierarchy.get(id).asInstanceOf[util.HashMap[String, AnyRef]] + val children = nodeHierarchy.get(AssessmentConstants.CHILDREN).asInstanceOf[util.ArrayList[String]] + val newChildrenList = new util.ArrayList[String] + children.map(identifier => { + if (identifiers.containsKey(identifier)) newChildrenList.add(identifiers.get(identifier)) else newChildrenList.add(identifier) + }) + nodeHierarchy.put(AssessmentConstants.CHILDREN, newChildrenList) + if (identifiers.containsKey(id)) { + hierarchy.remove(id) + hierarchy.put(identifiers.get(id), nodeHierarchy) + } + }) + request + } + + def branchingLogicModifier(branchingLogic: util.HashMap[String, AnyRef], oldToNewIdMap: util.Map[String, String]): Unit = { + branchingLogic.keySet().asScala.toList.map(identifier => { + val nodeBL = branchingLogic.get(identifier).asInstanceOf[util.HashMap[String, AnyRef]] + nodeBL.keySet().asScala.toList.map(key => { + if (StringUtils.equalsIgnoreCase(key, AssessmentConstants.TARGET)) branchingLogicArrayHandler(nodeBL, AssessmentConstants.TARGET, oldToNewIdMap) + else if (StringUtils.equalsIgnoreCase(key, AssessmentConstants.PRE_CONDITION)) preConditionHandler(nodeBL, oldToNewIdMap) + else if (StringUtils.equalsIgnoreCase(key, AssessmentConstants.SOURCE)) branchingLogicArrayHandler(nodeBL, AssessmentConstants.SOURCE, oldToNewIdMap) + }) + if (oldToNewIdMap.containsKey(identifier)) { + branchingLogic.put(oldToNewIdMap.get(identifier), nodeBL) + branchingLogic.remove(identifier) + } + }) + } + + def getIdentifierMapping(branchingRecord: util.HashMap[String, AnyRef], identifiers: util.Map[String, String]): util.Map[String, String] = { + val oldToNewIdMap = new util.HashMap[String, String]() + branchingRecord.keySet().asScala.toList.map(id => { + val nodeInfo = branchingRecord.get(id).asInstanceOf[util.HashMap[String, AnyRef]] + val newId = identifiers.get(id) + val oldId = nodeInfo.get(AssessmentConstants.COPY_OF).asInstanceOf[String] + oldToNewIdMap.put(oldId, newId) + }) + oldToNewIdMap + } + + def branchingLogicArrayHandler(nodeBL: util.HashMap[String, AnyRef], name: String, oldToNewIdMap: util.Map[String, String]) = { + val branchingLogicArray = nodeBL.getOrDefault(name, new util.ArrayList[String]).asInstanceOf[util.ArrayList[String]] + val newBranchingLogicArray = new util.ArrayList[String]() + branchingLogicArray.map(id => { + if (oldToNewIdMap.containsKey(id)) { + newBranchingLogicArray.add(oldToNewIdMap.get(id)) + } else newBranchingLogicArray.add(id) + }) + nodeBL.put(name, newBranchingLogicArray) + } + + def preConditionHandler(nodeBL: util.HashMap[String, AnyRef], oldToNewIdMap: util.Map[String, String]): Unit = { + val preCondition = nodeBL.get(AssessmentConstants.PRE_CONDITION).asInstanceOf[util.HashMap[String, AnyRef]] + preCondition.keySet().asScala.toList.map(key => { + val conjunctionArray = preCondition.get(key).asInstanceOf[util.ArrayList[String]] + val condition = conjunctionArray.get(0).asInstanceOf[util.HashMap[String, AnyRef]] + condition.keySet().asScala.toList.map(logicOp => { + val conditionArray = condition.get(logicOp).asInstanceOf[util.ArrayList[String]] + val sourceQuestionRecord = conditionArray.get(0).asInstanceOf[util.HashMap[String, AnyRef]] + val preConditionVar = sourceQuestionRecord.get(AssessmentConstants.PRE_CONDITION_VAR).asInstanceOf[String] + val stringArray = preConditionVar.split("\\.") + if (oldToNewIdMap.containsKey(stringArray(0))) { + val newString = oldToNewIdMap.get(stringArray(0)) + "." + stringArray.drop(1).mkString(".") + sourceQuestionRecord.put(AssessmentConstants.PRE_CONDITION_VAR, newString) + } + }) + }) + } +} diff --git a/assessment-api/assessment-actors/src/test/resources/application.conf b/assessment-api/assessment-actors/src/test/resources/application.conf index 24d3a74f2..b9ad82663 100644 --- a/assessment-api/assessment-actors/src/test/resources/application.conf +++ b/assessment-api/assessment-actors/src/test/resources/application.conf @@ -412,5 +412,15 @@ import { } } +assessment.copy.origin_data=["name", "author", "license", "organisation"] +assessment.copy.props_to_remove=["downloadUrl", "artifactUrl", "variants", + "createdOn", "collections", "children", "lastUpdatedOn", "SYS_INTERNAL_LAST_UPDATED_ON", + "versionKey", "s3Key", "status", "pkgVersion", "toc_url", "mimeTypesCount", + "contentTypesCount", "leafNodesCount", "childNodes", "prevState", "lastPublishedOn", + "flagReasons", "compatibilityLevel", "size", "publishChecklist", "publishComment", + "LastPublishedBy", "rejectReasons", "rejectComment", "gradeLevel", "subject", + "medium", "board", "topic", "purpose", "subtopic", "contentCredits", + "owner", "collaborators", "creators", "contributors", "badgeAssertions", "dialcodes", + "concepts", "keywords", "reservedDialcodes", "dialcodeRequired", "leafNodes", "sYS_INTERNAL_LAST_UPDATED_ON", "prevStatus", "lastPublishedBy", "streamingUrl"] diff --git a/assessment-api/assessment-actors/src/test/scala/org/sunbird/actors/CopySpec.scala b/assessment-api/assessment-actors/src/test/scala/org/sunbird/actors/CopySpec.scala new file mode 100644 index 000000000..772a23d23 --- /dev/null +++ b/assessment-api/assessment-actors/src/test/scala/org/sunbird/actors/CopySpec.scala @@ -0,0 +1,380 @@ +package org.sunbird.actors + +import org.mortbay.util.StringUtil +import org.sunbird.common.dto.{Request, Response, ResponseParams} +import org.sunbird.graph.dac.model.Node +import org.sunbird.utils.AssessmentConstants +import org.sunbird.common.exception.ResponseCode + +import java.util +import scala.collection.JavaConversions.mapAsJavaMap +import scala.collection.JavaConverters.asJavaIterableConverter +import scala.collection.mutable + +object CopySpec { + + private def getQuestionSetRequest(): Request = { + val request = new Request() + request.setContext(new java.util.HashMap[String, AnyRef]() { + { + put("graph_id", "domain") + put("version", "1.0") + put("objectType", "QuestionSet") + put("schemaName", "questionset") + } + }) + request.setObjectType("QuestionSet") + request + } + + def getQuestionSetCopyRequest(): Request = { + val request = getQuestionSetRequest() + request.putAll(new util.HashMap[String, AnyRef]() { + { + put("createdBy", "Shikshalokam") + put("createdFor", new util.ArrayList[String]() { + { + add("Shikshalokam") + } + }) + put("name", "NewRootNode") + } + }) + request + } + + def getInvalidQuestionSetCopyRequest(): Request = { + val request = getQuestionSetRequest() + request.putAll(new util.HashMap[String, AnyRef]() { + { + put("name", "NewRootNode") + } + }) + request + } + + def getInvalidQuestionCopyRequest(): Request = { + val request = getQuestionRequest() + request.putAll(new util.HashMap[String, AnyRef]() { + { + put("name", "NewQuestion") + } + }) + request + } + + private def getQuestionRequest(): Request = { + val request = new Request() + request.setContext(new java.util.HashMap[String, AnyRef]() { + { + put("graph_id", "domain") + put("version", "1.0") + put("objectType", "Question") + put("schemaName", "question") + } + }) + request.setObjectType("Question") + request + } + + def getQuestionCopyRequest(): Request = { + val request = getQuestionRequest() + request.putAll(new util.HashMap[String, AnyRef]() { + { + put("createdBy", "Shikshalokam") + put("createdFor", new util.ArrayList[String]() { + { + add("Shikshalokam") + } + }) + put("name", "NewQuestion") + } + }) + request + } + + private def getNode(objectType: String, identifier: String, primaryCategory: String, visibility: String, name: String, id: Long, + status: String): Node = { + val node = new Node("domain", "DATA_NODE", objectType) + node.setGraphId("domain") + node.setIdentifier(identifier) + node.setId(id) + node.setNodeType("DATA_NODE") + node.setObjectType(objectType) + node.setMetadata(new util.HashMap[String, AnyRef]() { + { + put("code", "xyz") + put("mimeType", { + if (StringUtil.endsWithIgnoreCase(objectType, AssessmentConstants.QUESTIONSET_SCHEMA_NAME)) { + AssessmentConstants.QUESTIONSET_MIME_TYPE + } else { + AssessmentConstants.QUESTION_MIME_TYPE + } + }) + put("createdOn", "2022-03-16T14:35:11.040+0530") + put("objectType", objectType) + put("primaryCategory", primaryCategory) + put("contentDisposition", "inline") + put("contentEncoding", "gzip") + put("lastUpdatedOn", "2022-03-16T14:38:51.287+0530") + put("showSolutions", "No") + put("allowAnonymousAccess", "Yes") + put("identifier", identifier) + put("lastStatusChangedOn", "2022-03-16T14:35:11.040+0530") + put("visibility", visibility) + put("showTimer", "No") + put("version", 1.asInstanceOf[Number]) + put("showFeedback", "No") + put("versionKey", "1234") + put("license", "CC BY 4.0") + put("compatibilityLevel", 5.asInstanceOf[Number]) + put("name", name) + put("status", status) + } + }) + node + } + + def getExistingRootNode(): Node = { + val node = getNode("QuestionSet", "do_1234", "Observation", AssessmentConstants.VISIBILITY_DEFAULT, "ExistingRootNode", 1234, + "Live") + node.getMetadata.put("childNodes", Array("do_5678")) + node + } + + def getNewRootNode(): Node = { + val node = getNode("QuestionSet", "do_9876", "Observation", AssessmentConstants.VISIBILITY_DEFAULT, "NewRootNode", 0, "Draft") + node.getMetadata.put("origin", "do_1234") + node.getMetadata.put("originData", "{\"name\":\"ExistingRootNode\",\"copyType\":\"deep\"}") + node.getMetadata.put("createdFor", Array("ShikshaLokam")) + node.getMetadata.put("createdBy", "ShikshaLokam") + node.setExternalData(new util.HashMap[String, AnyRef]() { + { + put("instructions", "This is the instruction.") + put("outcomeDeclaration", "This is outcomeDeclaration.") + } + }) + node + } + + def getExistingQuestionNode(): Node = { + val node = getNode("Question", "do_1234", "Slider", AssessmentConstants.VISIBILITY_DEFAULT, "ExistingQuestionNode", 1234, + "Live") + node + } + + def getQuestionNode(): Node = { + val node = getNode("Question", "do_5678", "Slider", AssessmentConstants.VISIBILITY_PARENT, "Question1", 0, "Draft") + node.setExternalData(new util.HashMap[String, AnyRef]() { + { + put("answer", "This is Answer.") + put("body", "This is Body.") + } + }) + node + } + + def getNewQuestionNode(): Node = { + val node = getNode("Question", "do_5678", "Slider", AssessmentConstants.VISIBILITY_DEFAULT, "NewQuestion", 0, "Draft") + node.setExternalData(new util.HashMap[String, AnyRef]() { + { + put("answer", "This is Answer.") + put("body", "This is Body.") + } + }) + node.getMetadata.put("origin", "do_1234") + node.getMetadata.put("originData", "{\\\"name\\\":\\\"Q2\\\",\\\"copyType\\\":\\\"deep\\\",\\\"license\\\":\\\"CC BY 4.0\\\"}") + node.getMetadata.put("createdFor", Array("ShikshaLokam")) + node.getMetadata.put("createdBy", "ShikshaLokam") + node + } + + def getSuccessfulResponse(): Response = { + val response = new Response + response.setVer("3.0") + val responseParams = new ResponseParams + responseParams.setStatus("successful") + response.setParams(responseParams) + response.setResponseCode(ResponseCode.OK) + response + } + + def getExternalPropsRequest(): Request = { + val request = getQuestionSetRequest() + request.putAll(new util.HashMap[String, AnyRef]() { + { + put("instructions", "This is the instruction.") + put("outcomeDeclaration", "This is outcomeDeclaration.") + } + }) + request + } + + def getExternalPropsResponseWithData(): Response = { + val response = getSuccessfulResponse() + response.put("instructions", "This is the instruction for this QuestionSet") + response.put("outcomeDeclaration", "This is the outcomeDeclaration for this QuestionSet") + response.put("hierarchy", "{\"code\":\"ExistingRootNode\",\"allowSkip\":\"Yes\",\"containsUserData\":\"No\"," + + "\"channel\":\"{{all}}\",\"language\":[\"English\"],\"showHints\":\"No\",\"mimeType\":\"application/vnd" + "" + ".sunbird" + "" + + ".questionset\",\"createdOn\":\"2022-03-16T14:35:11.040+0530\",\"objectType\":\"QuestionSet\"," + + "\"primaryCategory\":\"Observation\",\"contentDisposition\":\"inline\",\"contentEncoding\":\"gzip\"," + + "\"lastUpdatedOn\":\"2022-03-16T14:38:51.287+0530\",\"generateDIALCodes\":\"No\",\"showSolutions\":\"No\"," + + "\"allowAnonymousAccess\":\"Yes\",\"identifier\":\"do_1234\"," + "\"lastStatusChangedOn\":\"2022-03-16T14:35:11.040+0530\"," + + "\"requiresSubmit\":\"No\",\"visibility\":\"Default\"," + "" + "" + "\"IL_SYS_NODE_TYPE\":\"DATA_NODE\",\"showTimer\":\"No\"," + + "\"childNodes\":[\"do_113495678820704256110\"]," + "\"setType\":\"materialised\",\"version\":1," + "\"showFeedback\":\"No\"," + + "\"versionKey\":\"1647421731287\"," + "\"license\":\"CC BY 4.0\",\"depth\":0," + "\"compatibilityLevel\":5," + + "\"IL_FUNC_OBJECT_TYPE\":\"QuestionSet\"," + "\"allowBranching\":\"No\"," + "\"navigationMode\":\"non-linear\"," + + "\"name\":\"CopyQuestionSet\",\"shuffle\":true," + "\"IL_UNIQUE_ID\":\"do_11349567701798912019\",\"status\":\"Live\"," + + "\"children\":[{\"parent\":\"do_11349567701798912019\",\"code\":\"Q1\",\"channel\":\"{{channel_id}}\"," + + "\"language\":[\"English\"],\"mimeType\":\"application/vnd.sunbird.question\"," + + "\"createdOn\":\"2022-03-16T14:38:51.043+0530\",\"objectType\":\"Question\",\"primaryCategory\":\"Slider\"," + + "\"contentDisposition\":\"inline\",\"lastUpdatedOn\":\"2022-03-16T14:38:51.042+0530\"," + "\"contentEncoding\":\"gzip\"," + + "\"showSolutions\":\"No\",\"allowAnonymousAccess\":\"Yes\"," + "\"identifier\":\"do_113495678820704256110\"," + + "\"lastStatusChangedOn\":\"2022-03-16T14:38:51.043+0530\"," + "\"visibility\":\"Parent\",\"showTimer\":\"No\",\"index\":1," + + "\"languageCode\":[\"en\"],\"version\":1," + "\"versionKey\":\"1647421731066\",\"showFeedback\":\"No\",\"license\":\"CC BY " + + "4.0\",\"depth\":1," + "\"compatibilityLevel\":4,\"name\":\"Q1\",\"status\":\"Live\"}]}") + response.put("body", "This is Body") + response.put("answer", "This is Answer") + response + } + + def getReadPropsResponseForQuestion(): Response = { + val response = getSuccessfulResponse() + response.put("answer", "This is Answer 2") + response.put("body", "This is Body 2") + response + } + + def getUpsertNode(): Node = { + val node = getNewRootNode() + node.setExternalData(new util.HashMap[String, AnyRef]() { + { + put("hierarchy", "{\\\"identifier\\\":\\\"do_9876\\\"," + "\\\"children\\\":[{\\\"parent\\\":\\\"do_9876\\\"," + + "\\\"code\\\":\\\"b65f36d1-a243-4043-9df7-da14a2dd83b9\\\",\\\"channel\\\":\\\"{{channel_id}}\\\"," + + "\\\"language\\\":[\\\"English\\\"],\\\"mimeType\\\":\\\"application/vnd.sunbird.question\\\"," + + "\\\"createdOn\\\":\\\"2022-03-23T15:45:28.620+0530\\\",\\\"objectType\\\":\\\"Question\\\"," + + "\\\"primaryCategory\\\":\\\"Slider\\\",\\\"contentDisposition\\\":\\\"inline\\\"," + + "\\\"lastUpdatedOn\\\":\\\"2022-03-23T15:45:28.616+0530\\\",\\\"contentEncoding\\\":\\\"gzip\\\"," + + "\\\"showSolutions\\\":\\\"No\\\",\\\"allowAnonymousAccess\\\":\\\"Yes\\\"," + + "\\\"identifier\\\":\\\"do_11350066609045504013\\\",\\\"lastStatusChangedOn\\\":\\\"2022-03-23T15:45:28.621+0530\\\"," + + "\\\"visibility\\\":\\\"Parent\\\",\\\"showTimer\\\":\\\"No\\\",\\\"index\\\":1,\\\"languageCode\\\":[\\\"en\\\"]," + + "\\\"version\\\":1,\\\"versionKey\\\":\\\"1648030746815\\\",\\\"showFeedback\\\":\\\"No\\\",\\\"license\\\":\\\"CC BY " + + "4.0\\\",\\\"depth\\\":1,\\\"compatibilityLevel\\\":4,\\\"name\\\":\\\"Q1\\\",\\\"status\\\":\\\"Draft\\\"}]}") + } + }) + node + } + + private def generateStaticBranchingLogic(): util.HashMap[String, AnyRef] = { + new util.HashMap[String, AnyRef]() { + { + put("do_11351041198373273619", new util.HashMap[String, AnyRef]() { + put("target", new util.ArrayList[String]() { + { + add("do_113510411984044032111") + } + }) + put("preCondition", new util.HashMap[String, AnyRef]()) + put("source", new util.ArrayList[String]()) + }) + put("do_113510411984044032111", new util.HashMap[String, AnyRef]() { + put("target", new util.ArrayList[String]()) + put("preCondition", new util.HashMap[String, AnyRef]() { + { + put("and", new util.ArrayList[util.HashMap[String, AnyRef]]() { + add(new util.HashMap[String, AnyRef]() { + put("eq", new util.ArrayList[AnyRef]() { + { + add(new util.HashMap[String, String]() { + put("var", "do_11351041198373273619" + ".response1.value") + put("type", "responseDeclaration") + }) + add("0") + } + }) + }) + }) + } + }) + put("source", new util.ArrayList[String]() { + { + add("do_11351041198373273619") + } + }) + }) + } + } + } + + def generateNodesModified(identifier: String, withBranchingLogic: Boolean): util.HashMap[String, AnyRef] = { + val nodesModified = new util.HashMap[String, AnyRef]() + nodesModified.put(identifier, new util.HashMap[String, AnyRef]() { + { + put("setDefaultValue", false.asInstanceOf[AnyRef]) + put("metadata", new util.HashMap[String, AnyRef]() { + { + putAll((getNode("QuestionSet", "do_5678", "Observation", AssessmentConstants.VISIBILITY_PARENT, "Observation", 0, + "Draft").getMetadata)) + put("copyOf", "do_113510411984478208113") + if (withBranchingLogic) put("branchingLogic", generateStaticBranchingLogic) + } + }) + put("root", false.asInstanceOf[AnyRef]) + put("isNew", (!withBranchingLogic).asInstanceOf[AnyRef]) + put("objectType", "QuestionSet") + } + }) + nodesModified + } + + def generateBranchingRecord(): util.HashMap[String, AnyRef] = { + val nodeBLRecord = new util.HashMap[String, AnyRef]() + nodeBLRecord.put("afa2bef1-b5db-45d9-b0d7-aeea757906c3", new util.HashMap[String, AnyRef]() { + { + put("containsBL", true.asInstanceOf[AnyRef]) + put("branchingLogic", generateStaticBranchingLogic()) + put("copyOf", "do_113510411984478208113") + } + }) + nodeBLRecord + } + + def generateIdentifiers(): util.Map[String, String] = { + val idMap: mutable.Map[String, String] = mutable.Map() + idMap += ("afa2bef1-b5db-45d9-b0d7-aeea757906c3" -> "do_11351201604857856013") + mapAsJavaMap(idMap) + } + + def generateUpdateRequest(withBranchingLogic: Boolean, identifier: String): Request = { + val request = getQuestionSetRequest() + request.put(AssessmentConstants.NODES_MODIFIED, generateNodesModified(identifier, withBranchingLogic)) + request.put(AssessmentConstants.HIERARCHY, new util.HashMap[String, AnyRef]() { + { + put("do_11351201402236108811", new util.HashMap[String, AnyRef]() { + { + put(AssessmentConstants.CHILDREN, new util.ArrayList[String]() { + { + add(identifier) + } + }) + put(AssessmentConstants.PRIMARY_CATEGORY, "Observation") + put(AssessmentConstants.ROOT, true.asInstanceOf[AnyRef]) + } + }) + put(identifier, new util.HashMap[String, AnyRef]() { + { + put(AssessmentConstants.CHILDREN, new util.ArrayList[String]() { + { + add("do_11351041198373273619") + add("do_113510411984044032111") + } + }) + put(AssessmentConstants.PRIMARY_CATEGORY, "Observation") + put(AssessmentConstants.ROOT, false.asInstanceOf[AnyRef]) + } + }) + } + }) + request + } +} diff --git a/assessment-api/assessment-actors/src/test/scala/org/sunbird/actors/QuestionActorTest.scala b/assessment-api/assessment-actors/src/test/scala/org/sunbird/actors/QuestionActorTest.scala index 67c2f62af..349bc916a 100644 --- a/assessment-api/assessment-actors/src/test/scala/org/sunbird/actors/QuestionActorTest.scala +++ b/assessment-api/assessment-actors/src/test/scala/org/sunbird/actors/QuestionActorTest.scala @@ -315,6 +315,45 @@ class QuestionActorTest extends BaseSpec with MockFactory { assert("successful".equals(response.getParams.getStatus)) } + it should "return success response for 'copyQuestion'" in { + implicit val oec: OntologyEngineContext = mock[OntologyEngineContext] + val graphDB = mock[GraphService] + (oec.graphService _).expects().returns(graphDB).anyNumberOfTimes() + val nodes: util.List[Node] = getCategoryNode() + (graphDB.getNodeByUniqueIds(_: String, _: SearchCriteria)).expects(*, *).returns(Future(nodes)).anyNumberOfTimes() + (graphDB.getNodeByUniqueId(_: String, _: String, _: Boolean, _: Request)).expects("domain", "do_1234", false, *).returns(Future(CopySpec.getExistingQuestionNode())).anyNumberOfTimes() + (graphDB.readExternalProps(_: Request, _: List[String])).expects(*, List("objectMetadata")).returns(Future(CopySpec.getSuccessfulResponse())).anyNumberOfTimes() + (graphDB.readExternalProps(_: Request, _: List[String])).expects(*, *).returns(Future(CopySpec.getReadPropsResponseForQuestion())).anyNumberOfTimes() + (graphDB.addNode(_: String, _: Node)).expects(*, *).returns(Future(CopySpec.getNewQuestionNode())) + (graphDB.saveExternalProps(_: Request)).expects(*).returns(Future(CopySpec.getSuccessfulResponse())).anyNumberOfTimes + val request = CopySpec.getQuestionCopyRequest() + request.putAll(mapAsJavaMap(Map("identifier" -> "do_1234", "mode" -> "", "copyType"-> "deep"))) + request.setOperation("copyQuestion") + val response = callActor(request, Props(new QuestionActor())) + assert("successful".equals(response.getParams.getStatus)) + } + + it should "return error response for 'copyQuestion' when createdFor & createdBy is missing" in { + implicit val oec: OntologyEngineContext = mock[OntologyEngineContext] + val request = CopySpec.getInvalidQuestionSetCopyRequest() + request.putAll(mapAsJavaMap(Map("identifier" -> "do_1234", "mode" -> "", "copyType"-> "deep"))) + request.setOperation("copyQuestion") + val response = callActor(request, Props(new QuestionActor())) + assert("failed".equals(response.getParams.getStatus)) + } + + it should "return error response for 'copyQuestion' when visibility is Parent" in { + implicit val oec: OntologyEngineContext = mock[OntologyEngineContext] + val graphDB = mock[GraphService] + (oec.graphService _).expects().returns(graphDB).anyNumberOfTimes() + val request = CopySpec.getQuestionCopyRequest() + (graphDB.getNodeByUniqueId(_: String, _: String, _: Boolean, _: Request)).expects("domain", "do_1234", false, *).returns(Future(CopySpec.getQuestionNode())).anyNumberOfTimes() + request.putAll(mapAsJavaMap(Map("identifier" -> "do_1234", "mode" -> "", "copyType"-> "deep"))) + request.setOperation("copyQuestion") + val response = callActor(request, Props(new QuestionActor())) + assert("failed".equals(response.getParams.getStatus)) + } + private def getQuestionRequest(): Request = { val request = new Request() request.setContext(new java.util.HashMap[String, AnyRef]() { diff --git a/assessment-api/assessment-actors/src/test/scala/org/sunbird/actors/QuestionSetActorTest.scala b/assessment-api/assessment-actors/src/test/scala/org/sunbird/actors/QuestionSetActorTest.scala index 3f94322b8..25591cd35 100644 --- a/assessment-api/assessment-actors/src/test/scala/org/sunbird/actors/QuestionSetActorTest.scala +++ b/assessment-api/assessment-actors/src/test/scala/org/sunbird/actors/QuestionSetActorTest.scala @@ -11,7 +11,8 @@ import org.sunbird.graph.nodes.DataNode.getRelationMap import org.sunbird.graph.utils.ScalaJsonUtils import org.sunbird.graph.{GraphService, OntologyEngineContext} import org.sunbird.kafka.client.KafkaClient -import org.sunbird.utils.JavaJsonUtils +import org.sunbird.managers.CopyManager +import org.sunbird.utils.{AssessmentConstants, BranchingUtil, JavaJsonUtils} import java.util import scala.collection.JavaConversions._ @@ -539,6 +540,74 @@ class QuestionSetActorTest extends BaseSpec with MockFactory { assert("successful".equals(response.getParams.getStatus)) } + it should "return success response for 'copyQuestionSet' (Deep)" in { + implicit val oec: OntologyEngineContext = mock[OntologyEngineContext] + val graphDB = mock[GraphService] + (oec.graphService _).expects().returns(graphDB).anyNumberOfTimes() + val nodes: util.List[Node] = getCategoryNode() + (graphDB.getNodeByUniqueIds(_: String, _: SearchCriteria)).expects(*, *).returns(Future(nodes)).anyNumberOfTimes() + (graphDB.getNodeByUniqueId(_: String, _: String, _: Boolean, _: Request)).expects("domain", "do_1234", false, *).returns(Future(CopySpec.getExistingRootNode())).anyNumberOfTimes() + (graphDB.getNodeByUniqueId(_: String, _: String, _: Boolean, _: Request)).expects("domain", "do_9876", false, *).returns(Future(CopySpec.getNewRootNode())).anyNumberOfTimes() + (graphDB.getNodeByUniqueId(_: String, _: String, _: Boolean, _: Request)).expects("domain", "do_9876.img", false, *).returns(Future(CopySpec.getNewRootNode())).anyNumberOfTimes() + (graphDB.getNodeByUniqueId(_: String, _: String, _: Boolean, _: Request)).expects("domain", *, false, *).returns(Future(CopySpec.getQuestionNode())).anyNumberOfTimes() + (graphDB.readExternalProps(_: Request, _: List[String])).expects(*, List("objectMetadata")).returns(Future(CopySpec.getSuccessfulResponse())).anyNumberOfTimes() + (graphDB.readExternalProps(_: Request, _: List[String])).expects(*, *).returns(Future(CopySpec.getExternalPropsResponseWithData())).anyNumberOfTimes() + (graphDB.updateExternalProps(_: Request)).expects(*).returns(Future(CopySpec.getSuccessfulResponse())).anyNumberOfTimes + (graphDB.saveExternalProps(_: Request)).expects(*).returns(Future(CopySpec.getSuccessfulResponse())).anyNumberOfTimes + (graphDB.upsertNode(_: String, _: Node, _: Request)).expects(*, *, *).returns(Future(CopySpec.getUpsertNode())).anyNumberOfTimes() + inSequence { + (graphDB.addNode(_: String, _: Node)).expects(*, *).returns(Future(CopySpec.getNewRootNode())) + (graphDB.addNode(_: String, _: Node)).expects(*, *).returns(Future(CopySpec.getQuestionNode())) + } + val request = CopySpec.getQuestionSetCopyRequest() + request.putAll(mapAsJavaMap(Map("identifier" -> "do_1234", "mode" -> "", "copyType"-> "deep"))) + request.setOperation("copyQuestionSet") + val response = callActor(request, Props(new QuestionSetActor())) + assert("successful".equals(response.getParams.getStatus)) + } + + it should "return success response for 'copyQuestionSet' (Shallow)" in { + implicit val oec: OntologyEngineContext = mock[OntologyEngineContext] + val graphDB = mock[GraphService] + (oec.graphService _).expects().returns(graphDB).anyNumberOfTimes() + val nodes: util.List[Node] = getCategoryNode() + (graphDB.getNodeByUniqueIds(_: String, _: SearchCriteria)).expects(*, *).returns(Future(nodes)).anyNumberOfTimes() + (graphDB.getNodeByUniqueId(_: String, _: String, _: Boolean, _: Request)).expects("domain", "do_1234", false, *).returns(Future(CopySpec.getExistingRootNode())).anyNumberOfTimes() + (graphDB.getNodeByUniqueId(_: String, _: String, _: Boolean, _: Request)).expects("domain", "do_5678", false, *).returns(Future(CopySpec.getNewRootNode())).anyNumberOfTimes() + (graphDB.readExternalProps(_: Request, _: List[String])).expects(*, List("objectMetadata")).returns(Future(CopySpec.getSuccessfulResponse())).anyNumberOfTimes() + (graphDB.readExternalProps(_: Request, _: List[String])).expects(*, *).returns(Future(CopySpec.getExternalPropsResponseWithData())).anyNumberOfTimes() + (graphDB.addNode(_: String, _: Node)).expects(*, *).returns(Future(CopySpec.getQuestionNode())) + (graphDB.saveExternalProps(_: Request)).expects(*).returns(Future(CopySpec.getSuccessfulResponse())).anyNumberOfTimes + (graphDB.upsertNode(_: String, _: Node, _: Request)).expects(*, *, *).returns(Future(CopySpec.getUpsertNode())).anyNumberOfTimes() + (graphDB.updateExternalProps(_: Request)).expects(*).returns(Future(CopySpec.getSuccessfulResponse())).anyNumberOfTimes + val request = CopySpec.getQuestionSetCopyRequest() + request.putAll(mapAsJavaMap(Map("identifier" -> "do_1234", "mode" -> "", "copyType"-> "shallow"))) + request.setOperation("copyQuestionSet") + val response = callActor(request, Props(new QuestionSetActor())) + assert("successful".equals(response.getParams.getStatus)) + } + + it should "return error response for 'copyQuestionSet' when createdFor & createdBy is missing" in { + implicit val oec: OntologyEngineContext = mock[OntologyEngineContext] + val request = CopySpec.getInvalidQuestionCopyRequest() + request.putAll(mapAsJavaMap(Map("identifier" -> "do_1234", "mode" -> "", "copyType"-> "deep"))) + request.setOperation("copyQuestionSet") + val response = callActor(request, Props(new QuestionSetActor())) + assert("failed".equals(response.getParams.getStatus)) + } + + it should "return expected result for 'generateBranchingRecord'" in { + val result = BranchingUtil.generateBranchingRecord(CopySpec.generateNodesModified("afa2bef1-b5db-45d9-b0d7-aeea757906c3", true)) + assert(result == CopySpec.generateBranchingRecord) + } + + it should "return expected result for 'hierarchyRequestModifier'" in { + val result = BranchingUtil.hierarchyRequestModifier(CopySpec.generateUpdateRequest(false, "afa2bef1-b5db-45d9-b0d7-aeea757906c3"), CopySpec.generateBranchingRecord(), CopySpec.generateIdentifiers()) + val expectedResult = CopySpec.generateUpdateRequest(true, "do_11351201604857856013") + assert(result.getRequest.get(AssessmentConstants.NODES_MODIFIED) == expectedResult.getRequest.get(AssessmentConstants.NODES_MODIFIED)) + assert(result.getRequest.get(AssessmentConstants.HIERARCHY) == expectedResult.getRequest.get(AssessmentConstants.HIERARCHY)) + } + private def getQuestionSetRequest(): Request = { val request = new Request() request.setContext(new java.util.HashMap[String, AnyRef]() { diff --git a/assessment-api/assessment-service/app/controllers/v4/QuestionController.scala b/assessment-api/assessment-service/app/controllers/v4/QuestionController.scala index 0af9f3676..d929bb272 100644 --- a/assessment-api/assessment-service/app/controllers/v4/QuestionController.scala +++ b/assessment-api/assessment-service/app/controllers/v4/QuestionController.scala @@ -2,6 +2,8 @@ package controllers.v4 import akka.actor.{ActorRef, ActorSystem} import controllers.BaseController +import org.sunbird.utils.AssessmentConstants + import javax.inject.{Inject, Named} import play.api.mvc.ControllerComponents import utils.{ActorNames, ApiId, QuestionOperations} @@ -130,4 +132,15 @@ class QuestionController @Inject()(@Named(ActorNames.QUESTION_ACTOR) questionAct questionRequest.getContext.put("identifier", identifier) getResult(ApiId.REJECT_QUESTION, questionActor, questionRequest) } + + def copy(identifier: String, mode: Option[String]) = Action.async { implicit request => + val headers = commonHeaders() + val body = requestBody() + val question = body.getOrDefault("question", new java.util.HashMap()).asInstanceOf[java.util.Map[String, Object]]; + question.putAll(headers) + question.putAll(Map("identifier" -> identifier, "mode" -> mode.getOrElse(""), "copyType" -> AssessmentConstants.COPY_TYPE_DEEP).asJava) + val questionRequest = getRequest(question, headers, QuestionOperations.copyQuestion.toString) + setRequestContext(questionRequest, version, objectType, schemaName) + getResult(ApiId.COPY_QUESTION, questionActor, questionRequest) + } } diff --git a/assessment-api/assessment-service/app/controllers/v4/QuestionSetController.scala b/assessment-api/assessment-service/app/controllers/v4/QuestionSetController.scala index a82f320ae..f2fa9cdce 100644 --- a/assessment-api/assessment-service/app/controllers/v4/QuestionSetController.scala +++ b/assessment-api/assessment-service/app/controllers/v4/QuestionSetController.scala @@ -158,4 +158,15 @@ class QuestionSetController @Inject()(@Named(ActorNames.QUESTION_SET_ACTOR) ques questionSetRequest.getContext.put("identifier", identifier); getResult(ApiId.SYSTEM_UPDATE_QUESTION_SET, questionSetActor, questionSetRequest) } + + def copy(identifier: String, mode: Option[String], copyType: String) = Action.async { implicit request => + val headers = commonHeaders() + val body = requestBody() + val questionSet = body.getOrDefault("questionset", new java.util.HashMap()).asInstanceOf[java.util.Map[String, Object]]; + questionSet.putAll(headers) + questionSet.putAll(Map("identifier" -> identifier, "mode" -> mode.getOrElse(""), "copyType" -> copyType).asJava) + val questionSetRequest = getRequest(questionSet, headers, QuestionSetOperations.copyQuestionSet.toString) + setRequestContext(questionSetRequest, version, objectType, schemaName) + getResult(ApiId.COPY_QUESTION_SET, questionSetActor, questionSetRequest) + } } diff --git a/assessment-api/assessment-service/app/utils/ApiId.scala b/assessment-api/assessment-service/app/utils/ApiId.scala index d45e57597..062340338 100644 --- a/assessment-api/assessment-service/app/utils/ApiId.scala +++ b/assessment-api/assessment-service/app/utils/ApiId.scala @@ -24,6 +24,7 @@ object ApiId { val SYSTEM_UPDATE_QUESTION = "api.question.system.update" val LIST_QUESTIONS = "api.questions.list" val REJECT_QUESTION = "api.question.reject" + val COPY_QUESTION = "api.question.copy" //QuestionSet APIs val CREATE_QUESTION_SET = "api.questionset.create" @@ -40,5 +41,5 @@ object ApiId { val REJECT_QUESTION_SET = "api.questionset.reject" val IMPORT_QUESTION_SET = "api.questionset.import" val SYSTEM_UPDATE_QUESTION_SET = "api.questionset.system.update" - + val COPY_QUESTION_SET = "api.questionset.copy" } diff --git a/assessment-api/assessment-service/app/utils/QuestionOperations.scala b/assessment-api/assessment-service/app/utils/QuestionOperations.scala index 57e9e3815..8d71d88e1 100644 --- a/assessment-api/assessment-service/app/utils/QuestionOperations.scala +++ b/assessment-api/assessment-service/app/utils/QuestionOperations.scala @@ -1,5 +1,5 @@ package utils object QuestionOperations extends Enumeration { - val createQuestion, readQuestion, readPrivateQuestion, updateQuestion, reviewQuestion, publishQuestion, retireQuestion, importQuestion, systemUpdateQuestion, listQuestions, rejectQuestion = Value + val createQuestion, readQuestion, readPrivateQuestion, updateQuestion, reviewQuestion, publishQuestion, retireQuestion, importQuestion, systemUpdateQuestion, listQuestions, rejectQuestion, copyQuestion = Value } diff --git a/assessment-api/assessment-service/app/utils/QuestionSetOperations.scala b/assessment-api/assessment-service/app/utils/QuestionSetOperations.scala index afcd22e2b..43f0266b1 100644 --- a/assessment-api/assessment-service/app/utils/QuestionSetOperations.scala +++ b/assessment-api/assessment-service/app/utils/QuestionSetOperations.scala @@ -1,7 +1,7 @@ package utils object QuestionSetOperations extends Enumeration { - val createQuestionSet, readQuestionSet, readPrivateQuestionSet, updateQuestionSet, reviewQuestionSet, publishQuestionSet, - retireQuestionSet, addQuestion, removeQuestion, updateHierarchyQuestion, readHierarchyQuestion, - rejectQuestionSet, importQuestionSet, systemUpdateQuestionSet = Value + val createQuestionSet, readQuestionSet, readPrivateQuestionSet, updateQuestionSet, reviewQuestionSet, publishQuestionSet, + retireQuestionSet, addQuestion, removeQuestion, updateHierarchyQuestion, readHierarchyQuestion, + rejectQuestionSet, importQuestionSet, systemUpdateQuestionSet, copyQuestionSet = Value } diff --git a/assessment-api/assessment-service/conf/application.conf b/assessment-api/assessment-service/conf/application.conf index 82fe873b5..5e82dfa27 100644 --- a/assessment-api/assessment-service/conf/application.conf +++ b/assessment-api/assessment-service/conf/application.conf @@ -420,4 +420,14 @@ import { } } -root_node_visibility=["Default","Private"] \ No newline at end of file +root_node_visibility=["Default","Private"] +assessment.copy.origin_data=["name", "author", "license", "organisation"] +assessment.copy.props_to_remove=["downloadUrl", "artifactUrl", "variants", + "createdOn", "collections", "children", "lastUpdatedOn", "SYS_INTERNAL_LAST_UPDATED_ON", + "versionKey", "s3Key", "status", "pkgVersion", "toc_url", "mimeTypesCount", + "contentTypesCount", "leafNodesCount", "childNodes", "prevState", "lastPublishedOn", + "flagReasons", "compatibilityLevel", "size", "publishChecklist", "publishComment", + "LastPublishedBy", "rejectReasons", "rejectComment", "gradeLevel", "subject", + "medium", "board", "topic", "purpose", "subtopic", "contentCredits", + "owner", "collaborators", "creators", "contributors", "badgeAssertions", "dialcodes", + "concepts", "keywords", "reservedDialcodes", "dialcodeRequired", "leafNodes", "sYS_INTERNAL_LAST_UPDATED_ON", "prevStatus", "lastPublishedBy", "streamingUrl"] \ No newline at end of file diff --git a/assessment-api/assessment-service/conf/routes b/assessment-api/assessment-service/conf/routes index e33c9c1da..b76bb70a9 100644 --- a/assessment-api/assessment-service/conf/routes +++ b/assessment-api/assessment-service/conf/routes @@ -19,10 +19,11 @@ PATCH /question/v4/update/:identifier controllers.v4.QuestionControl POST /question/v4/review/:identifier controllers.v4.QuestionController.review(identifier:String) POST /question/v4/publish/:identifier controllers.v4.QuestionController.publish(identifier:String) DELETE /question/v4/retire/:identifier controllers.v4.QuestionController.retire(identifier:String) -POST /question/v4/import controllers.v4.QuestionController.importQuestion() +POST /question/v4/import controllers.v4.QuestionController.importQuestion() PATCH /question/v4/system/update/:identifier controllers.v4.QuestionController.systemUpdate(identifier:String) POST /question/v4/list controllers.v4.QuestionController.list(fields:Option[String]) POST /question/v4/reject/:identifier controllers.v4.QuestionController.reject(identifier:String) +POST /question/v4/copy/:identifier controllers.v4.QuestionController.copy(identifier:String, mode:Option[String]) # QuestionSet API's POST /questionset/v4/create controllers.v4.QuestionSetController.create @@ -37,5 +38,6 @@ DELETE /questionset/v4/remove controllers.v4.QuestionSetC PATCH /questionset/v4/hierarchy/update controllers.v4.QuestionSetController.updateHierarchy GET /questionset/v4/hierarchy/:identifier controllers.v4.QuestionSetController.getHierarchy(identifier:String, mode:Option[String]) POST /questionset/v4/reject/:identifier controllers.v4.QuestionSetController.reject(identifier:String) -POST /questionset/v4/import controllers.v4.QuestionSetController.importQuestionSet() -PATCH /questionset/v4/system/update/:identifier controllers.v4.QuestionSetController.systemUpdate(identifier:String) \ No newline at end of file +POST /questionset/v4/import controllers.v4.QuestionSetController.importQuestionSet() +PATCH /questionset/v4/system/update/:identifier controllers.v4.QuestionSetController.systemUpdate(identifier:String) +POST /questionset/v4/copy/:identifier controllers.v4.QuestionSetController.copy(identifier:String, mode:Option[String], type:String?="deep") \ No newline at end of file