From 604c6b82ae21fa00f9a904f872a57a74efdd4dc2 Mon Sep 17 00:00:00 2001 From: Sahil-tarento <140611066+Sahil-tarento@users.noreply.github.com> Date: Wed, 5 Jun 2024 10:56:24 +0530 Subject: [PATCH] 4.8.15 dev v1 (#584) * KB-4705 | DEV| Assessment | BE | Consumption Logic for the QuestionWeightage Assessment Type (#564) * Added string handling in admin patch API * Assessment V5 Changes Assessment V5 Changes * KB-4705 | DEV| Assessment | BE | Consumption Logic for the QuestionWeightage Assessment Type 1. Removed the hardcoded attributes and added from assessment hierarchy. * KB-4705 | DEV| Assessment | BE | Consumption Logic for the QuestionWeightage Assessment Type 1. Added proper comments and logs. 2. Refractored the code. 3. Added logic for optionweightage. --------- Co-authored-by: karthik-tarento * Retry Attempts Enabled for Assessment V5 * KB-4705 | DEV| Assessment | BE | Consumption Logic for the QuestionWeightage Assessment Type (#565) 1. Optional weightage score calculation enhancements. * Adding the insight API implementation for MDO channel (#571) * SaveStateMethod * Maintaining the order of the fields. * KB-4705 | DEV| Assessment | BE | Consumption Logic for the QuestionWeightage Assessment Type (#580) 1. Enhanchements for questionScheme. * KB-5130 | DEV| Assessment | BE | Enhancement in Consumption Logic for the QuestionWeightage Assessment Type. (#581) 1. Added logic for new attributes added. 2. Added questionLevel param in Question read API. * KB-5130 | DEV| Assessment | BE | Enhancement in Consumption Logic for the QuestionWeightage Assessment Type. (#583) 1. Score calculation fix. --------- Co-authored-by: tarentomaheshvakkund <139739142+tarentomaheshvakkund@users.noreply.github.com> Co-authored-by: karthik-tarento Co-authored-by: saipradeep_ravipati --- .../controller/AssessmentController.java | 53 + .../repo/AssessmentRepositoryImpl.java | 1 + .../service/AssessmentServiceV5.java | 21 + .../service/AssessmentServiceV5Impl.java | 1018 +++++++++++++++++ .../service/AssessmentUtilServiceV2.java | 12 + .../service/AssessmentUtilServiceV2Impl.java | 308 ++++- .../common/util/CbExtServerProperties.java | 11 + .../org/sunbird/common/util/Constants.java | 20 + .../service/InsightsServiceImpl.java | 27 +- src/main/resources/application.properties | 8 +- 10 files changed, 1472 insertions(+), 7 deletions(-) create mode 100644 src/main/java/org/sunbird/assessment/service/AssessmentServiceV5.java create mode 100644 src/main/java/org/sunbird/assessment/service/AssessmentServiceV5Impl.java diff --git a/src/main/java/org/sunbird/assessment/controller/AssessmentController.java b/src/main/java/org/sunbird/assessment/controller/AssessmentController.java index 70d693e1b..843bda5a4 100644 --- a/src/main/java/org/sunbird/assessment/controller/AssessmentController.java +++ b/src/main/java/org/sunbird/assessment/controller/AssessmentController.java @@ -13,6 +13,7 @@ import org.sunbird.assessment.service.AssessmentService; import org.sunbird.assessment.service.AssessmentServiceV2; import org.sunbird.assessment.service.AssessmentServiceV4; +import org.sunbird.assessment.service.AssessmentServiceV5; import org.sunbird.common.model.SBApiResponse; import org.sunbird.common.util.Constants; @@ -28,6 +29,9 @@ public class AssessmentController { @Autowired AssessmentServiceV4 assessmentServiceV4; + @Autowired + AssessmentServiceV5 assessmentServiceV5; + /** * validates, submits and inserts assessments and quizzes into the db * @@ -216,4 +220,53 @@ public ResponseEntity readWheebox(@RequestHeader("x-authenticated-user-token" SBApiResponse response = assessmentServiceV4.readWheebox(authUserToken); return new ResponseEntity<>(response, response.getResponseCode()); } + + @GetMapping("/v5/quml/assessment/read/{assessmentIdentifier}") + public ResponseEntity readAssessmentV5( + @PathVariable("assessmentIdentifier") String assessmentIdentifier, + @RequestHeader(Constants.X_AUTH_TOKEN) String token,@RequestParam(name = "editMode" ,required = false) String editMode) { + boolean edit = !StringUtils.isEmpty(editMode) && Boolean.parseBoolean(editMode); + SBApiResponse readResponse = assessmentServiceV5.readAssessment(assessmentIdentifier, token,edit); + return new ResponseEntity<>(readResponse, readResponse.getResponseCode()); + } + + @PostMapping("/v5/quml/question/list") + public ResponseEntity readQuestionListV5(@Valid @RequestBody Map requestBody, + @RequestHeader("x-authenticated-user-token") String authUserToken,@RequestParam(name = "editMode" ,required = false) String editMode) { + boolean edit = !StringUtils.isEmpty(editMode) && Boolean.parseBoolean(editMode); + SBApiResponse response = assessmentServiceV5.readQuestionList(requestBody, authUserToken,edit); + return new ResponseEntity<>(response, response.getResponseCode()); + } + + @PostMapping("/v5/user/assessment/submit") + public ResponseEntity submitUserAssessmentV5(@Valid @RequestBody Map requestBody, + @RequestHeader("x-authenticated-user-token") String authUserToken,@RequestParam(name = "editMode" ,required = false) String editMode) { + boolean edit = !StringUtils.isEmpty(editMode) && Boolean.parseBoolean(editMode); + SBApiResponse submitResponse = assessmentServiceV5.submitAssessmentAsync(requestBody, authUserToken,edit); + return new ResponseEntity<>(submitResponse, submitResponse.getResponseCode()); + } + + @GetMapping("/v5/quml/assessment/retake/{assessmentIdentifier}") + public ResponseEntity retakeAssessmentV5( + @PathVariable("assessmentIdentifier") String assessmentIdentifier, + @RequestHeader(Constants.X_AUTH_TOKEN) String token,@RequestParam(name = "editMode" ,required = false) String editMode) { + Boolean edit = !StringUtils.isEmpty(editMode) && Boolean.parseBoolean(editMode); + SBApiResponse readResponse = assessmentServiceV5.retakeAssessment(assessmentIdentifier, token,edit); + return new ResponseEntity<>(readResponse, readResponse.getResponseCode()); + } + + @PostMapping("/v5/quml/assessment/result") + public ResponseEntity readAssessmentResultV5(@Valid @RequestBody Map requestBody, + @RequestHeader("x-authenticated-user-token") String authUserToken) { + SBApiResponse response = assessmentServiceV5.readAssessmentResultV5(requestBody, authUserToken); + return new ResponseEntity<>(response, response.getResponseCode()); + } + + @PostMapping("/v5/user/assessment/save") + public ResponseEntity saveUserAssessmentV5(@Valid @RequestBody Map requestBody, + @RequestHeader("x-authenticated-user-token") String authUserToken,@RequestParam(name = "editMode" ,required = false) String editMode) { + boolean edit = !StringUtils.isEmpty(editMode) && Boolean.parseBoolean(editMode); + SBApiResponse submitResponse = assessmentServiceV5.submitAssessmentAsync(requestBody, authUserToken,edit); + return new ResponseEntity<>(submitResponse, submitResponse.getResponseCode()); + } } diff --git a/src/main/java/org/sunbird/assessment/repo/AssessmentRepositoryImpl.java b/src/main/java/org/sunbird/assessment/repo/AssessmentRepositoryImpl.java index 2bd64e584..b486e7ce9 100644 --- a/src/main/java/org/sunbird/assessment/repo/AssessmentRepositoryImpl.java +++ b/src/main/java/org/sunbird/assessment/repo/AssessmentRepositoryImpl.java @@ -182,4 +182,5 @@ public Boolean updateUserAssesmentDataToDB(String userId, String assessmentIdent fieldsToBeUpdated, compositeKeys); return true; } + } \ No newline at end of file diff --git a/src/main/java/org/sunbird/assessment/service/AssessmentServiceV5.java b/src/main/java/org/sunbird/assessment/service/AssessmentServiceV5.java new file mode 100644 index 000000000..50eddd5d3 --- /dev/null +++ b/src/main/java/org/sunbird/assessment/service/AssessmentServiceV5.java @@ -0,0 +1,21 @@ +package org.sunbird.assessment.service; + +import org.sunbird.common.model.SBApiResponse; + +import java.util.Map; + +public interface AssessmentServiceV5 { + + public SBApiResponse readAssessment(String assessmentIdentifier, String token,boolean editMode); + + public SBApiResponse readQuestionList(Map requestBody, String authUserToken,boolean editMode); + + public SBApiResponse retakeAssessment(String assessmentIdentifier, String token,Boolean editMode); + + public SBApiResponse readAssessmentResultV5(Map request, String userAuthToken); + + public SBApiResponse submitAssessmentAsync(Map data, String userAuthToken,boolean editMode); + + public SBApiResponse saveAssessmentAsync(Map data, String userAuthToken,boolean editMode); + +} diff --git a/src/main/java/org/sunbird/assessment/service/AssessmentServiceV5Impl.java b/src/main/java/org/sunbird/assessment/service/AssessmentServiceV5Impl.java new file mode 100644 index 000000000..fbe8e461f --- /dev/null +++ b/src/main/java/org/sunbird/assessment/service/AssessmentServiceV5Impl.java @@ -0,0 +1,1018 @@ +package org.sunbird.assessment.service; + +import com.beust.jcommander.internal.Lists; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.collections.MapUtils; +import org.apache.commons.lang.StringUtils; +import org.mortbay.util.ajax.JSON; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; +import org.springframework.util.ObjectUtils; +import org.sunbird.assessment.repo.AssessmentRepository; +import org.sunbird.common.model.SBApiResponse; +import org.sunbird.common.service.OutboundRequestHandlerServiceImpl; +import org.sunbird.common.util.AccessTokenValidator; +import org.sunbird.common.util.CbExtServerProperties; +import org.sunbird.common.util.Constants; +import org.sunbird.common.util.ProjectUtil; +import org.sunbird.core.producer.Producer; + +import java.io.IOException; +import java.sql.Timestamp; +import java.util.*; + +import static java.util.stream.Collectors.toList; + +@Service +@SuppressWarnings("unchecked") +public class AssessmentServiceV5Impl implements AssessmentServiceV5 { + + private final Logger logger = LoggerFactory.getLogger(AssessmentServiceV5Impl.class); + @Autowired + CbExtServerProperties serverProperties; + + @Autowired + Producer kafkaProducer; + + @Autowired + OutboundRequestHandlerServiceImpl outboundRequestHandlerService; + + @Autowired + AssessmentUtilServiceV2 assessUtilServ; + + @Autowired + ObjectMapper mapper; + + @Autowired + AssessmentRepository assessmentRepository; + + @Autowired + AccessTokenValidator accessTokenValidator; + + @Override + public SBApiResponse retakeAssessment(String assessmentIdentifier, String token,Boolean editMode) { + logger.info("AssessmentServicev5Impl::retakeAssessment... Started"); + SBApiResponse response = ProjectUtil.createDefaultResponse(Constants.API_RETAKE_ASSESSMENT_GET); + String errMsg = ""; + int retakeAttemptsAllowed = 0; + int retakeAttemptsConsumed = 0; + try { + String userId = accessTokenValidator.fetchUserIdFromAccessToken(token); + if (StringUtils.isBlank(userId)) { + updateErrorDetails(response, Constants.USER_ID_DOESNT_EXIST, HttpStatus.INTERNAL_SERVER_ERROR); + return response; + } + + Map assessmentAllDetail = assessUtilServ + .readAssessmentHierarchyFromCache(assessmentIdentifier,editMode,token); + if (MapUtils.isEmpty(assessmentAllDetail)) { + updateErrorDetails(response, Constants.ASSESSMENT_HIERARCHY_READ_FAILED, + HttpStatus.INTERNAL_SERVER_ERROR); + return response; + } + if (assessmentAllDetail.get(Constants.MAX_ASSESSMENT_RETAKE_ATTEMPTS) != null) { + retakeAttemptsAllowed = (int) assessmentAllDetail.get(Constants.MAX_ASSESSMENT_RETAKE_ATTEMPTS); + } + + // if (serverProperties.isAssessmentRetakeCountVerificationEnabled()) { + retakeAttemptsConsumed = calculateAssessmentRetakeCount(userId, assessmentIdentifier); + //} + } catch (Exception e) { + errMsg = String.format("Error while calculating retake assessment. Exception: %s", e.getMessage()); + logger.error(errMsg, e); + } + if (StringUtils.isNotBlank(errMsg)) { + updateErrorDetails(response, errMsg, HttpStatus.INTERNAL_SERVER_ERROR); + } else { + response.getResult().put(Constants.TOTAL_RETAKE_ATTEMPTS_ALLOWED, retakeAttemptsAllowed); + response.getResult().put(Constants.RETAKE_ATTEMPTS_CONSUMED, retakeAttemptsConsumed); + } + logger.info("AssessmentServicev5Impl::retakeAssessment... Completed"); + return response; + } + + @Override + public SBApiResponse readAssessment(String assessmentIdentifier, String token,boolean editMode) { + logger.info("AssessmentServicev5Impl::readAssessment... Started"); + SBApiResponse response = ProjectUtil.createDefaultResponse(Constants.API_READ_ASSESSMENT); + String errMsg = ""; + try { + String userId = accessTokenValidator.fetchUserIdFromAccessToken(token); + if (StringUtils.isBlank(userId)) { + updateErrorDetails(response, Constants.USER_ID_DOESNT_EXIST, HttpStatus.INTERNAL_SERVER_ERROR); + return response; + } + logger.info(String.format("ReadAssessment... UserId: %s, AssessmentIdentifier: %s", userId, assessmentIdentifier)); + + Map assessmentAllDetail = null ; + + // Step-1 : Read assessment using assessment Id from the Assessment Service + if(editMode) { + assessmentAllDetail = assessUtilServ.fetchHierarchyFromAssessServc(assessmentIdentifier,token); + } + else { + assessmentAllDetail = assessUtilServ + .readAssessmentHierarchyFromCache(assessmentIdentifier,editMode,token); + } + + if (MapUtils.isEmpty(assessmentAllDetail)) { + updateErrorDetails(response, Constants.ASSESSMENT_HIERARCHY_READ_FAILED, + HttpStatus.INTERNAL_SERVER_ERROR); + return response; + } + + //Step-2 : If Practice Assessment return without saving + if (Constants.PRACTICE_QUESTION_SET + .equalsIgnoreCase((String) assessmentAllDetail.get(Constants.PRIMARY_CATEGORY))||editMode) { + response.getResult().put(Constants.QUESTION_SET, readAssessmentLevelData(assessmentAllDetail)); + return response; + } + + // Step-3 : If read user submitted assessment + List> existingDataList = assessUtilServ.readUserSubmittedAssessmentRecords( + userId, assessmentIdentifier); + Timestamp assessmentStartTime = new Timestamp(new Date().getTime()); + + if (existingDataList.isEmpty()) { + logger.info("Assessment read first time for user."); + // Add Null check for expectedDuration.throw bad questionSet Assessment Exam + if(null == assessmentAllDetail.get(Constants.EXPECTED_DURATION)){ + errMsg = Constants.ASSESSMENT_INVALID; } + else { + int expectedDuration = (Integer) assessmentAllDetail.get(Constants.EXPECTED_DURATION); + Timestamp assessmentEndTime = calculateAssessmentSubmitTime(expectedDuration, + assessmentStartTime, 0); + Map assessmentData = readAssessmentLevelData(assessmentAllDetail); + assessmentData.put(Constants.START_TIME, assessmentStartTime.getTime()); + assessmentData.put(Constants.END_TIME, assessmentEndTime.getTime()); + response.getResult().put(Constants.QUESTION_SET, assessmentData); + Boolean isAssessmentUpdatedToDB = assessmentRepository.addUserAssesmentDataToDB(userId, + assessmentIdentifier, assessmentStartTime, assessmentEndTime, + (Map) (response.getResult().get(Constants.QUESTION_SET)), + Constants.NOT_SUBMITTED); + if (Boolean.FALSE.equals(isAssessmentUpdatedToDB)) { + errMsg = Constants.ASSESSMENT_DATA_START_TIME_NOT_UPDATED; + } + } + } else { + logger.info("Assessment read... user has details... "); + Date existingAssessmentEndTime = (Date) (existingDataList.get(0) + .get(Constants.END_TIME)); + Timestamp existingAssessmentEndTimeTimestamp = new Timestamp( + existingAssessmentEndTime.getTime()); + if (assessmentStartTime.compareTo(existingAssessmentEndTimeTimestamp) < 0 + && Constants.NOT_SUBMITTED.equalsIgnoreCase((String) existingDataList.get(0).get(Constants.STATUS))) { + String questionSetFromAssessmentString = (String) existingDataList.get(0) + .get(Constants.ASSESSMENT_READ_RESPONSE_KEY); + Map questionSetFromAssessment = new Gson().fromJson( + questionSetFromAssessmentString, new TypeToken>() { + }.getType()); + questionSetFromAssessment.put(Constants.START_TIME, assessmentStartTime.getTime()); + questionSetFromAssessment.put(Constants.END_TIME, + existingAssessmentEndTimeTimestamp.getTime()); + response.getResult().put(Constants.QUESTION_SET, questionSetFromAssessment); + } else if ((assessmentStartTime.compareTo(existingAssessmentEndTime) < 0 + && ((String) existingDataList.get(0).get(Constants.STATUS)) + .equalsIgnoreCase(Constants.SUBMITTED)) + || assessmentStartTime.compareTo(existingAssessmentEndTime) > 0) { + logger.info( + "Incase the assessment is submitted before the end time, or the endtime has exceeded, read assessment freshly "); + if (assessmentAllDetail.get(Constants.MAX_ASSESSMENT_RETAKE_ATTEMPTS) != null) { + int retakeAttemptsAllowed = (int) assessmentAllDetail.get(Constants.MAX_ASSESSMENT_RETAKE_ATTEMPTS); + int retakeAttemptsConsumed = calculateAssessmentRetakeCount(userId, assessmentIdentifier); + if(retakeAttemptsConsumed >= retakeAttemptsAllowed) { + errMsg = Constants.ASSESSMENT_RETRY_ATTEMPTS_CROSSED; + updateErrorDetails(response, errMsg, HttpStatus.INTERNAL_SERVER_ERROR); + return response; + } + } + Map assessmentData = readAssessmentLevelData(assessmentAllDetail); + int expectedDuration = (Integer) assessmentAllDetail.get(Constants.EXPECTED_DURATION); + assessmentStartTime = new Timestamp(new Date().getTime()); + Timestamp assessmentEndTime = calculateAssessmentSubmitTime(expectedDuration, + assessmentStartTime, 0); + assessmentData.put(Constants.START_TIME, assessmentStartTime.getTime()); + assessmentData.put(Constants.END_TIME, assessmentEndTime.getTime()); + response.getResult().put(Constants.QUESTION_SET, assessmentData); + Boolean isAssessmentUpdatedToDB = assessmentRepository.addUserAssesmentDataToDB(userId, + assessmentIdentifier, assessmentStartTime, assessmentEndTime, + assessmentData, Constants.NOT_SUBMITTED); + if (Boolean.FALSE.equals(isAssessmentUpdatedToDB)) { + errMsg = Constants.ASSESSMENT_DATA_START_TIME_NOT_UPDATED; + } + } + } + } catch (Exception e) { + errMsg = String.format("Error while reading assessment. Exception: %s", e.getMessage()); + logger.error(errMsg, e); + } + if (StringUtils.isNotBlank(errMsg)) { + updateErrorDetails(response, errMsg, HttpStatus.INTERNAL_SERVER_ERROR); + } + return response; + } + + @Override + public SBApiResponse readQuestionList(Map requestBody, String authUserToken,boolean editMode) { + SBApiResponse response = ProjectUtil.createDefaultResponse(Constants.API_QUESTIONS_LIST); + String errMsg; + Map result = new HashMap<>(); + try { + List identifierList = new ArrayList<>(); + List questionList = new ArrayList<>(); + result = validateQuestionListAPI(requestBody, authUserToken, identifierList,editMode); + errMsg = result.get(Constants.ERROR_MESSAGE); + if (StringUtils.isNotBlank(errMsg)) { + updateErrorDetails(response, errMsg, HttpStatus.BAD_REQUEST); + return response; + } + + String assessmentIdFromRequest = (String) requestBody.get(Constants.ASSESSMENT_ID_KEY); + Map questionsMap = assessUtilServ.readQListfromCache(identifierList,assessmentIdFromRequest,editMode,authUserToken); + for (String questionId : identifierList) { + questionList.add(assessUtilServ.filterQuestionMapDetail((Map) questionsMap.get(questionId), + result.get(Constants.PRIMARY_CATEGORY))); + } + if (errMsg.isEmpty() && identifierList.size() == questionList.size()) { + response.getResult().put(Constants.QUESTIONS, questionList); + } + } catch (Exception e) { + errMsg = String.format("Failed to fetch the question list. Exception: %s", e.getMessage()); + logger.error(errMsg, e); + } + if (StringUtils.isNotBlank(errMsg)) { + updateErrorDetails(response, errMsg, HttpStatus.BAD_REQUEST); + } + return response; + } + + public SBApiResponse readAssessmentResultV5(Map request, String userAuthToken) { + SBApiResponse response = ProjectUtil.createDefaultResponse(Constants.API_READ_ASSESSMENT_RESULT); + try { + String userId = accessTokenValidator.fetchUserIdFromAccessToken(userAuthToken); + if (StringUtils.isBlank(userId)) { + updateErrorDetails(response, Constants.USER_ID_DOESNT_EXIST, HttpStatus.INTERNAL_SERVER_ERROR); + return response; + } + + String errMsg = validateAssessmentReadResult(request); + if (StringUtils.isNotBlank(errMsg)) { + updateErrorDetails(response, errMsg, HttpStatus.BAD_REQUEST); + return response; + } + + Map requestBody = (Map) request.get(Constants.REQUEST); + String assessmentIdentifier = (String) requestBody.get(Constants.ASSESSMENT_ID_KEY); + + List> existingDataList = assessUtilServ.readUserSubmittedAssessmentRecords( + userId, assessmentIdentifier); + + if (existingDataList.isEmpty()) { + updateErrorDetails(response, Constants.USER_ASSESSMENT_DATA_NOT_PRESENT, HttpStatus.BAD_REQUEST); + return response; + } + + String statusOfLatestObject = (String) existingDataList.get(0).get(Constants.STATUS); + if (!Constants.SUBMITTED.equalsIgnoreCase(statusOfLatestObject)) { + response.getResult().put(Constants.STATUS_IS_IN_PROGRESS, true); + return response; + } + + String latestResponse = (String) existingDataList.get(0).get(Constants.SUBMIT_ASSESSMENT_RESPONSE_KEY); + if (StringUtils.isNotBlank(latestResponse)) { + response.putAll(mapper.readValue(latestResponse, new TypeReference>() { + })); + } + } catch (Exception e) { + String errMsg = String.format("Failed to process Assessment read response. Excption: %s", e.getMessage()); + updateErrorDetails(response, errMsg, HttpStatus.INTERNAL_SERVER_ERROR); + } + return response; + } + + public SBApiResponse submitAssessmentAsync(Map submitRequest, String userAuthToken,boolean editMode) { + logger.info("AssessmentServicev5Impl::submitAssessmentAsync.. started"); + SBApiResponse outgoingResponse = ProjectUtil.createDefaultResponse(Constants.API_SUBMIT_ASSESSMENT); + long assessmentCompletionTime= Calendar.getInstance().getTime().getTime(); + try { + // Step-1 fetch userid + String userId = accessTokenValidator.fetchUserIdFromAccessToken(userAuthToken); + if (ObjectUtils.isEmpty(userId)) { + updateErrorDetails(outgoingResponse, Constants.USER_ID_DOESNT_EXIST, HttpStatus.BAD_REQUEST); + return outgoingResponse; + } + String assessmentIdFromRequest = (String) submitRequest.get(Constants.IDENTIFIER); + String errMsg; + List> sectionListFromSubmitRequest = new ArrayList<>(); + List> hierarchySectionList = new ArrayList<>(); + Map assessmentHierarchy = new HashMap<>(); + Map existingAssessmentData = new HashMap<>(); + //Confirm whether the submitted request sections and questions match. + errMsg = validateSubmitAssessmentRequest(submitRequest, userId, hierarchySectionList, + sectionListFromSubmitRequest, assessmentHierarchy, existingAssessmentData,userAuthToken,editMode); + if (StringUtils.isNotBlank(errMsg)) { + updateErrorDetails(outgoingResponse, errMsg, HttpStatus.BAD_REQUEST); + return outgoingResponse; + } + Date assessmentStart = (Date) existingAssessmentData.get(Constants.START_TIME); + long assessmentStartTime = assessmentStart.getTime(); + int maxAssessmentRetakeAttempts = (Integer) assessmentHierarchy.get(Constants.MAX_ASSESSMENT_RETAKE_ATTEMPTS); + int retakeAttemptsConsumed = calculateAssessmentRetakeCount(userId, assessmentIdFromRequest); + String assessmentPrimaryCategory = (String) assessmentHierarchy.get(Constants.PRIMARY_CATEGORY); + String assessmentType=((String) assessmentHierarchy.get(Constants.ASSESSMENT_TYPE)).toLowerCase(); + String scoreCutOffType ; + if(assessmentType.equalsIgnoreCase(Constants.QUESTION_WEIGHTAGE)){ + scoreCutOffType= Constants.SECTION_LEVEL_SCORE_CUTOFF; + }else { + scoreCutOffType= Constants.ASSESSMENT_LEVEL_SCORE_CUTOFF; + } + List> sectionLevelsResults = new ArrayList<>(); + for (Map hierarchySection : hierarchySectionList) { + String hierarchySectionId = (String) hierarchySection.get(Constants.IDENTIFIER); + String userSectionId = ""; + Map userSectionData = new HashMap<>(); + for (Map sectionFromSubmitRequest : sectionListFromSubmitRequest) { + userSectionId = (String) sectionFromSubmitRequest.get(Constants.IDENTIFIER); + if (userSectionId.equalsIgnoreCase(hierarchySectionId)) { + userSectionData = sectionFromSubmitRequest; + break; + } + } + + hierarchySection.put(Constants.SCORE_CUTOFF_TYPE, scoreCutOffType); + List> questionsListFromSubmitRequest = new ArrayList<>(); + if (userSectionData.containsKey(Constants.CHILDREN) + && !ObjectUtils.isEmpty(userSectionData.get(Constants.CHILDREN))) { + questionsListFromSubmitRequest = (List>) userSectionData + .get(Constants.CHILDREN); + } + List desiredKeys = Lists.newArrayList(Constants.IDENTIFIER); + List questionsList = questionsListFromSubmitRequest.stream() + .flatMap(x -> desiredKeys.stream().filter(x::containsKey).map(x::get)).collect(toList()); + List questionsListFromAssessmentHierarchy = questionsList.stream() + .map(object -> Objects.toString(object, null)).collect(toList()); + Map result = new HashMap<>(); + Map questionSetDetailsMap = getParamDetailsForQTypes(assessmentHierarchy,hierarchySectionId); + switch (scoreCutOffType) { + case Constants.ASSESSMENT_LEVEL_SCORE_CUTOFF: { + result.putAll(createResponseMapWithProperStructure(hierarchySection, + assessUtilServ.validateQumlAssessmentV2(questionSetDetailsMap,questionsListFromAssessmentHierarchy, + questionsListFromSubmitRequest,assessUtilServ.readQListfromCache(questionsListFromAssessmentHierarchy,assessmentIdFromRequest,editMode,userAuthToken)))); + Map finalRes= calculateAssessmentFinalResults(result); + outgoingResponse.getResult().putAll(finalRes); + outgoingResponse.getResult().put(Constants.PRIMARY_CATEGORY, assessmentPrimaryCategory); + if (!Constants.PRACTICE_QUESTION_SET.equalsIgnoreCase(assessmentPrimaryCategory) && !editMode) { + String questionSetFromAssessmentString = (String) existingAssessmentData + .get(Constants.ASSESSMENT_READ_RESPONSE_KEY); + Map questionSetFromAssessment = null; + if (StringUtils.isNotBlank(questionSetFromAssessmentString)) { + questionSetFromAssessment = mapper.readValue(questionSetFromAssessmentString, + new TypeReference>() { + }); + } + writeDataToDatabaseAndTriggerKafkaEvent(submitRequest, userId, questionSetFromAssessment, finalRes, + (String) assessmentHierarchy.get(Constants.PRIMARY_CATEGORY)); + } + return outgoingResponse; + } + case Constants.SECTION_LEVEL_SCORE_CUTOFF: { + result.putAll(createResponseMapWithProperStructure(hierarchySection, + assessUtilServ.validateQumlAssessmentV2(questionSetDetailsMap,questionsListFromAssessmentHierarchy, + questionsListFromSubmitRequest,assessUtilServ.readQListfromCache(questionsListFromAssessmentHierarchy,assessmentIdFromRequest,editMode,userAuthToken)))); + sectionLevelsResults.add(result); + } + break; + default: + break; + } + } + if (Constants.SECTION_LEVEL_SCORE_CUTOFF.equalsIgnoreCase(scoreCutOffType)) { + Map result = calculateSectionFinalResults(sectionLevelsResults,assessmentStartTime,assessmentCompletionTime,maxAssessmentRetakeAttempts,retakeAttemptsConsumed); + outgoingResponse.getResult().putAll(result); + outgoingResponse.getParams().setStatus(Constants.SUCCESS); + outgoingResponse.setResponseCode(HttpStatus.OK); + outgoingResponse.getResult().put(Constants.PRIMARY_CATEGORY, assessmentPrimaryCategory); + if (!Constants.PRACTICE_QUESTION_SET.equalsIgnoreCase(assessmentPrimaryCategory) && !editMode) { + String questionSetFromAssessmentString = (String) existingAssessmentData + .get(Constants.ASSESSMENT_READ_RESPONSE_KEY); + Map questionSetFromAssessment = null; + if (StringUtils.isNotBlank(questionSetFromAssessmentString)) { + questionSetFromAssessment = mapper.readValue(questionSetFromAssessmentString, + new TypeReference>() { + }); + } + writeDataToDatabaseAndTriggerKafkaEvent(submitRequest, userId, questionSetFromAssessment, result, + (String) assessmentHierarchy.get(Constants.PRIMARY_CATEGORY)); + } + return outgoingResponse; + } + + } catch (Exception e) { + String errMsg = String.format("Failed to process assessment submit request. Exception: ", e.getMessage()); + logger.error(errMsg, e); + updateErrorDetails(outgoingResponse, errMsg, HttpStatus.INTERNAL_SERVER_ERROR); + } + return outgoingResponse; + } + + private void updateErrorDetails(SBApiResponse response, String errMsg, HttpStatus responseCode) { + response.getParams().setStatus(Constants.FAILED); + response.getParams().setErrmsg(errMsg); + response.setResponseCode(responseCode); + } + + private int calculateAssessmentRetakeCount(String userId, String assessmentId) { + List> userAssessmentDataList = assessUtilServ.readUserSubmittedAssessmentRecords(userId, + assessmentId); + return (int) userAssessmentDataList.stream() + .filter(userData -> userData.containsKey(Constants.SUBMIT_ASSESSMENT_RESPONSE_KEY) + && null != userData.get(Constants.SUBMIT_ASSESSMENT_RESPONSE_KEY)) + .count(); + } + + private Timestamp calculateAssessmentSubmitTime(int expectedDuration, Timestamp assessmentStartTime, + int bufferTime) { + Calendar cal = Calendar.getInstance(); + cal.setTimeInMillis(assessmentStartTime.getTime()); + if (bufferTime > 0) { + cal.add(Calendar.SECOND, + expectedDuration + Integer.parseInt(serverProperties.getUserAssessmentSubmissionDuration())); + } else { + cal.add(Calendar.SECOND, expectedDuration); + } + return new Timestamp(cal.getTime().getTime()); + } + + private Map readAssessmentLevelData(Map assessmentAllDetail) { + List assessmentParams = serverProperties.getAssessmentLevelParams(); + Map assessmentFilteredDetail = new HashMap<>(); + for (String assessmentParam : assessmentParams) { + if ((assessmentAllDetail.containsKey(assessmentParam))) { + assessmentFilteredDetail.put(assessmentParam, assessmentAllDetail.get(assessmentParam)); + } + } + readSectionLevelParams(assessmentAllDetail, assessmentFilteredDetail); + return assessmentFilteredDetail; + } + + private void readSectionLevelParams(Map assessmentAllDetail, + Map assessmentFilteredDetail) { + List> sectionResponse = new ArrayList<>(); + List sectionIdList = new ArrayList<>(); + List sectionParams = serverProperties.getAssessmentSectionParams(); + List> sections = (List>) assessmentAllDetail.get(Constants.CHILDREN); + for (Map section : sections) { + sectionIdList.add((String) section.get(Constants.IDENTIFIER)); + Map newSection = new HashMap<>(); + for (String sectionParam : sectionParams) { + if (section.containsKey(sectionParam)) { + newSection.put(sectionParam, section.get(sectionParam)); + } + } + List> questions = (List>) section.get(Constants.CHILDREN); + int maxQuestions = (int) section.getOrDefault(Constants.MAX_QUESTIONS, questions.size()); + List childNodeList = questions.stream() + .map(question -> (String) question.get(Constants.IDENTIFIER)) + .limit(maxQuestions) + .collect(toList()); + Collections.shuffle(childNodeList); + newSection.put(Constants.CHILD_NODES, childNodeList); + sectionResponse.add(newSection); + } + assessmentFilteredDetail.put(Constants.CHILDREN, sectionResponse); + assessmentFilteredDetail.put(Constants.CHILD_NODES, sectionIdList); + } + + private Map validateQuestionListAPI(Map requestBody, String authUserToken, + List identifierList,boolean editMode) throws IOException { + Map result = new HashMap<>(); + String userId = accessTokenValidator.fetchUserIdFromAccessToken(authUserToken); + if (StringUtils.isBlank(userId)) { + result.put(Constants.ERROR_MESSAGE, Constants.USER_ID_DOESNT_EXIST); + return result; + } + String assessmentIdFromRequest = (String) requestBody.get(Constants.ASSESSMENT_ID_KEY); + if (StringUtils.isBlank(assessmentIdFromRequest)) { + result.put(Constants.ERROR_MESSAGE, Constants.ASSESSMENT_ID_KEY_IS_NOT_PRESENT_IS_EMPTY); + return result; + } + identifierList.addAll(getQuestionIdList(requestBody)); + if (identifierList.isEmpty()) { + result.put(Constants.ERROR_MESSAGE, Constants.IDENTIFIER_LIST_IS_EMPTY); + return result; + } + + Map assessmentAllDetail = assessUtilServ + .readAssessmentHierarchyFromCache(assessmentIdFromRequest,editMode,authUserToken); + + if (MapUtils.isEmpty(assessmentAllDetail)) { + result.put(Constants.ERROR_MESSAGE, Constants.ASSESSMENT_HIERARCHY_READ_FAILED); + return result; + } + String primaryCategory = (String) assessmentAllDetail.get(Constants.PRIMARY_CATEGORY); + if (Constants.PRACTICE_QUESTION_SET + .equalsIgnoreCase(primaryCategory)||editMode) { + result.put(Constants.PRIMARY_CATEGORY, primaryCategory); + result.put(Constants.ERROR_MESSAGE, StringUtils.EMPTY); + return result; + } + + Map userAssessmentAllDetail = new HashMap(); + + List> existingDataList = assessUtilServ.readUserSubmittedAssessmentRecords( + userId, assessmentIdFromRequest); + String questionSetFromAssessmentString = (!existingDataList.isEmpty()) + ? (String) existingDataList.get(0).get(Constants.ASSESSMENT_READ_RESPONSE_KEY) + : ""; + if (StringUtils.isNotBlank(questionSetFromAssessmentString)) { + userAssessmentAllDetail.putAll(mapper.readValue(questionSetFromAssessmentString, + new TypeReference>() { + })); + } else { + result.put(Constants.ERROR_MESSAGE, Constants.USER_ASSESSMENT_DATA_NOT_PRESENT); + return result; + } + + if (!MapUtils.isEmpty(userAssessmentAllDetail)) { + result.put(Constants.PRIMARY_CATEGORY, (String) userAssessmentAllDetail.get(Constants.PRIMARY_CATEGORY)); + List questionsFromAssessment = new ArrayList<>(); + List> sections = (List>) userAssessmentAllDetail + .get(Constants.CHILDREN); + for (Map section : sections) { + // Out of the list of questions received in the payload, checking if the request + // has only those ids which are a part of the user's latest assessment + // Fetching all the remaining questions details from the Redis + questionsFromAssessment.addAll((List) section.get(Constants.CHILD_NODES)); + } + if (validateQuestionListRequest(identifierList, questionsFromAssessment)) { + result.put(Constants.ERROR_MESSAGE, StringUtils.EMPTY); + } else { + result.put(Constants.ERROR_MESSAGE, Constants.THE_QUESTIONS_IDS_PROVIDED_DONT_MATCH); + } + return result; + } else { + result.put(Constants.ERROR_MESSAGE, Constants.ASSESSMENT_ID_INVALID); + return result; + } + } + + private List getQuestionIdList(Map questionListRequest) { + try { + if (questionListRequest.containsKey(Constants.REQUEST)) { + Map request = (Map) questionListRequest.get(Constants.REQUEST); + if ((!ObjectUtils.isEmpty(request)) && request.containsKey(Constants.SEARCH)) { + Map searchObj = (Map) request.get(Constants.SEARCH); + if (!ObjectUtils.isEmpty(searchObj) && searchObj.containsKey(Constants.IDENTIFIER) + && !CollectionUtils.isEmpty((List) searchObj.get(Constants.IDENTIFIER))) { + return (List) searchObj.get(Constants.IDENTIFIER); + } + } + } + } catch (Exception e) { + logger.error(String.format("Failed to process the questionList request body. %s", e.getMessage())); + } + return Collections.emptyList(); + } + + private Boolean validateQuestionListRequest(List identifierList, List questionsFromAssessment) { + return (new HashSet<>(questionsFromAssessment).containsAll(identifierList)) ? Boolean.TRUE : Boolean.FALSE; + } + + private String validateSubmitAssessmentRequest(Map submitRequest, String userId, + List> hierarchySectionList, List> sectionListFromSubmitRequest, + Map assessmentHierarchy, Map existingAssessmentData,String token,boolean editMode) throws Exception { + submitRequest.put(Constants.USER_ID, userId); + if (StringUtils.isEmpty((String) submitRequest.get(Constants.IDENTIFIER))) { + return Constants.INVALID_ASSESSMENT_ID; + } + String assessmentIdFromRequest = (String) submitRequest.get(Constants.IDENTIFIER); + assessmentHierarchy.putAll(assessUtilServ.readAssessmentHierarchyFromCache(assessmentIdFromRequest,editMode,token)); + if (MapUtils.isEmpty(assessmentHierarchy)) { + return Constants.READ_ASSESSMENT_FAILED; + } + + hierarchySectionList.addAll((List>) assessmentHierarchy.get(Constants.CHILDREN)); + sectionListFromSubmitRequest.addAll((List>) submitRequest.get(Constants.CHILDREN)); + if (((String) (assessmentHierarchy.get(Constants.PRIMARY_CATEGORY))) + .equalsIgnoreCase(Constants.PRACTICE_QUESTION_SET) || editMode) + return ""; + + List> existingDataList = assessUtilServ.readUserSubmittedAssessmentRecords( + userId, (String) submitRequest.get(Constants.IDENTIFIER)); + if (existingDataList.isEmpty()) { + return Constants.USER_ASSESSMENT_DATA_NOT_PRESENT; + } else { + existingAssessmentData.putAll(existingDataList.get(0)); + } + + //if (Constants.SUBMITTED.equalsIgnoreCase((String) existingAssessmentData.get(Constants.STATUS))) { + // return Constants.ASSESSMENT_ALREADY_SUBMITTED; + //} + + Date assessmentStartTime = (Date) existingAssessmentData.get(Constants.START_TIME); + if (assessmentStartTime == null) { + return Constants.READ_ASSESSMENT_START_TIME_FAILED; + } + int expectedDuration = (Integer) assessmentHierarchy.get(Constants.EXPECTED_DURATION); + Timestamp later = calculateAssessmentSubmitTime(expectedDuration, + new Timestamp(assessmentStartTime.getTime()), + Integer.parseInt(serverProperties.getUserAssessmentSubmissionDuration())); + Timestamp submissionTime = new Timestamp(new Date().getTime()); + int time = submissionTime.compareTo(later); + if (time <= 0) { + List desiredKeys = Lists.newArrayList(Constants.IDENTIFIER); + List hierarchySectionIds = hierarchySectionList.stream() + .flatMap(x -> desiredKeys.stream().filter(x::containsKey).map(x::get)).collect(toList()); + List submitSectionIds = sectionListFromSubmitRequest.stream() + .flatMap(x -> desiredKeys.stream().filter(x::containsKey).map(x::get)).collect(toList()); + if (!new HashSet<>(hierarchySectionIds).containsAll(submitSectionIds)) { + return Constants.WRONG_SECTION_DETAILS; + } else { + String areQuestionIdsSame = validateIfQuestionIdsAreSame(submitRequest, + sectionListFromSubmitRequest, desiredKeys, userId, existingAssessmentData); + if (!areQuestionIdsSame.isEmpty()) + return areQuestionIdsSame; + } + } else { + return Constants.ASSESSMENT_SUBMIT_EXPIRED; + } + + return ""; + } + + private String validateIfQuestionIdsAreSame(Map submitRequest, + List> sectionListFromSubmitRequest, List desiredKeys, String userId, + Map existingAssessmentData) throws Exception { + String questionSetFromAssessmentString = (String) existingAssessmentData + .get(Constants.ASSESSMENT_READ_RESPONSE_KEY); + if (StringUtils.isNotBlank(questionSetFromAssessmentString)) { + Map questionSetFromAssessment = mapper.readValue(questionSetFromAssessmentString, + new TypeReference>() { + }); + if (questionSetFromAssessment != null && questionSetFromAssessment.get(Constants.CHILDREN) != null) { + List> sections = (List>) questionSetFromAssessment + .get(Constants.CHILDREN); + List desiredKey = Lists.newArrayList(Constants.CHILD_NODES); + List questionList = sections.stream() + .flatMap(x -> desiredKey.stream().filter(x::containsKey).map(x::get)).collect(toList()); + List questionIdsFromAssessmentHierarchy = new ArrayList<>(); + List> questionsListFromSubmitRequest = new ArrayList<>(); + for (Object question : questionList) { + questionIdsFromAssessmentHierarchy.addAll((List) question); + } + for (Map userSectionData : sectionListFromSubmitRequest) { + if (userSectionData.containsKey(Constants.CHILDREN) + && !ObjectUtils.isEmpty(userSectionData.get(Constants.CHILDREN))) { + questionsListFromSubmitRequest + .addAll((List>) userSectionData.get(Constants.CHILDREN)); + } + } + List userQuestionIdsFromSubmitRequest = questionsListFromSubmitRequest.stream() + .flatMap(x -> desiredKeys.stream().filter(x::containsKey).map(x::get)) + .collect(toList()); + if (!new HashSet<>(questionIdsFromAssessmentHierarchy).containsAll(userQuestionIdsFromSubmitRequest)) { + return Constants.ASSESSMENT_SUBMIT_INVALID_QUESTION; + } + } + } else { + return Constants.ASSESSMENT_SUBMIT_QUESTION_READ_FAILED; + } + return ""; + } + + public Map createResponseMapWithProperStructure(Map hierarchySection, + Map resultMap) { + Map sectionLevelResult = new HashMap<>(); + sectionLevelResult.put(Constants.IDENTIFIER, hierarchySection.get(Constants.IDENTIFIER)); + sectionLevelResult.put(Constants.OBJECT_TYPE, hierarchySection.get(Constants.OBJECT_TYPE)); + sectionLevelResult.put(Constants.PRIMARY_CATEGORY, hierarchySection.get(Constants.PRIMARY_CATEGORY)); + sectionLevelResult.put(Constants.PASS_PERCENTAGE, hierarchySection.get(Constants.MINIMUM_PASS_PERCENTAGE)); + Double result; + if (!ObjectUtils.isEmpty(resultMap)) { + result = (Double) resultMap.get(Constants.RESULT); + sectionLevelResult.put(Constants.RESULT, result); + sectionLevelResult.put(Constants.TOTAL, resultMap.get(Constants.TOTAL)); + sectionLevelResult.put(Constants.BLANK, resultMap.get(Constants.BLANK)); + sectionLevelResult.put(Constants.CORRECT, resultMap.get(Constants.CORRECT)); + sectionLevelResult.put(Constants.INCORRECT, resultMap.get(Constants.INCORRECT)); + sectionLevelResult.put(Constants.CHILDREN,resultMap.get(Constants.CHILDREN)); + sectionLevelResult.put(Constants.SECTION_RESULT,resultMap.get(Constants.SECTION_RESULT)); + sectionLevelResult.put(Constants.TOTAL_MARKS,resultMap.get(Constants.TOTAL_MARKS)); + sectionLevelResult.put(Constants.SECTION_MARKS,resultMap.get(Constants.SECTION_MARKS)); + + + } else { + result = 0.0; + sectionLevelResult.put(Constants.RESULT, result); + List childNodes = (List) hierarchySection.get(Constants.CHILDREN); + sectionLevelResult.put(Constants.TOTAL, childNodes.size()); + sectionLevelResult.put(Constants.BLANK, childNodes.size()); + sectionLevelResult.put(Constants.CORRECT, 0); + sectionLevelResult.put(Constants.INCORRECT, 0); + } + sectionLevelResult.put(Constants.PASS, + result >= ((Integer) hierarchySection.get(Constants.MINIMUM_PASS_PERCENTAGE))); + sectionLevelResult.put(Constants.OVERALL_RESULT, result); + return sectionLevelResult; + } + + private Map calculateAssessmentFinalResults(Map assessmentLevelResult) { + Map res = new HashMap<>(); + try { + res.put(Constants.CHILDREN, Collections.singletonList(assessmentLevelResult)); + Double result = (Double) assessmentLevelResult.get(Constants.RESULT); + res.put(Constants.OVERALL_RESULT, result); + res.put(Constants.TOTAL, assessmentLevelResult.get(Constants.TOTAL)); + res.put(Constants.BLANK, assessmentLevelResult.get(Constants.BLANK)); + res.put(Constants.CORRECT, assessmentLevelResult.get(Constants.CORRECT)); + res.put(Constants.PASS_PERCENTAGE, assessmentLevelResult.get(Constants.PASS_PERCENTAGE)); + res.put(Constants.INCORRECT, assessmentLevelResult.get(Constants.INCORRECT)); + Integer minimumPassPercentage = (Integer) assessmentLevelResult.get(Constants.PASS_PERCENTAGE); + res.put(Constants.PASS, result >= minimumPassPercentage); + } catch (Exception e) { + logger.error("Failed to calculate Assessment final results. Exception: ", e); + } + return res; + } + + private void writeDataToDatabaseAndTriggerKafkaEvent(Map submitRequest, String userId, + Map questionSetFromAssessment, Map result, String primaryCategory) { + try { + if (questionSetFromAssessment.get(Constants.START_TIME) != null) { + Long existingAssessmentStartTime = (Long) questionSetFromAssessment.get(Constants.START_TIME); + Timestamp startTime = new Timestamp(existingAssessmentStartTime); + Boolean isAssessmentUpdatedToDB = assessmentRepository.updateUserAssesmentDataToDB(userId, + (String) submitRequest.get(Constants.IDENTIFIER), submitRequest, result, Constants.SUBMITTED, + startTime); + if (Boolean.TRUE.equals(isAssessmentUpdatedToDB)) { + Map kafkaResult = new HashMap<>(); + kafkaResult.put(Constants.CONTENT_ID_KEY, submitRequest.get(Constants.IDENTIFIER)); + kafkaResult.put(Constants.COURSE_ID, + submitRequest.get(Constants.COURSE_ID) != null ? submitRequest.get(Constants.COURSE_ID) + : ""); + kafkaResult.put(Constants.BATCH_ID, + submitRequest.get(Constants.BATCH_ID) != null ? submitRequest.get(Constants.BATCH_ID) : ""); + kafkaResult.put(Constants.USER_ID, submitRequest.get(Constants.USER_ID)); + kafkaResult.put(Constants.ASSESSMENT_ID_KEY, submitRequest.get(Constants.IDENTIFIER)); + kafkaResult.put(Constants.PRIMARY_CATEGORY, primaryCategory); + kafkaResult.put(Constants.TOTAL_SCORE, result.get(Constants.OVERALL_RESULT)); + if ((primaryCategory.equalsIgnoreCase("Competency Assessment") + && submitRequest.containsKey("competencies_v3") + && submitRequest.get("competencies_v3") != null)) { + Object[] obj = (Object[]) JSON.parse((String) submitRequest.get("competencies_v3")); + if (obj != null) { + Object map = obj[0]; + ObjectMapper m = new ObjectMapper(); + Map props = m.convertValue(map, Map.class); + kafkaResult.put(Constants.COMPETENCY, props.isEmpty() ? "" : props); + System.out.println(obj); + + } + System.out.println(obj); + } + kafkaProducer.push(serverProperties.getAssessmentSubmitTopic(), kafkaResult); + } + } + } catch (Exception e) { + logger.error("Failed to write data for assessment submit response. Exception: ", e); + } + } + + private Map calculateSectionFinalResults(List> sectionLevelResults,long assessmentStartTime,long assessmentCompletionTime,int maxAssessmentRetakeAttempts,int retakeAttemptsConsumed) { + Map res = new HashMap<>(); + Double result; + Integer correct = 0; + Integer blank = 0; + Integer inCorrect = 0; + Integer total = 0; + Integer totalSectionMarks = 0; + Integer totalMarks = 0; + int pass = 0; + Double totalResult = 0.0; + try { + for (Map sectionChildren : sectionLevelResults) { + res.put(Constants.CHILDREN, sectionLevelResults); + result = (Double) sectionChildren.get(Constants.RESULT); + totalResult += result; + total += (Integer) sectionChildren.get(Constants.TOTAL); + blank += (Integer) sectionChildren.get(Constants.BLANK); + correct += (Integer) sectionChildren.get(Constants.CORRECT); + inCorrect += (Integer) sectionChildren.get(Constants.INCORRECT); + Integer minimumPassPercentage = (Integer) sectionChildren.get(Constants.PASS_PERCENTAGE); + if (result >= minimumPassPercentage) { + pass++; + } + totalSectionMarks += (Integer) sectionChildren.get(Constants.SECTION_MARKS); + totalMarks += (Integer) sectionChildren.get(Constants.TOTAL_MARKS); + } + res.put(Constants.OVERALL_RESULT, totalResult / sectionLevelResults.size()); + res.put(Constants.TOTAL, total); + res.put(Constants.BLANK, blank); + res.put(Constants.CORRECT, correct); + res.put(Constants.INCORRECT, inCorrect); + res.put(Constants.PASS, (pass == sectionLevelResults.size())); + res.put(Constants.TIME_TAKEN_FOR_ASSESSMENT,assessmentCompletionTime-assessmentStartTime); + res.put(Constants.MAX_ASSESSMENT_RETAKE_ATTEMPTS,maxAssessmentRetakeAttempts); + res.put(Constants.RETAKE_ATTEMPT_CONSUMED,retakeAttemptsConsumed); + double totalPercentage = ((double) totalSectionMarks / (double)totalMarks) * 100; + res.put(Constants.TOTAL_PERCENTAGE, totalPercentage); + } catch (Exception e) { + logger.error("Failed to calculate assessment score. Exception: ", e); + } + return res; + } + + private String validateAssessmentReadResult(Map request) { + String errMsg = ""; + if (MapUtils.isEmpty(request) || !request.containsKey(Constants.REQUEST)) { + return Constants.INVALID_REQUEST; + } + + Map requestBody = (Map) request.get(Constants.REQUEST); + if (MapUtils.isEmpty(requestBody)) { + return Constants.INVALID_REQUEST; + } + List missingAttribs = new ArrayList(); + if (!requestBody.containsKey(Constants.ASSESSMENT_ID_KEY) + || StringUtils.isBlank((String) requestBody.get(Constants.ASSESSMENT_ID_KEY))) { + missingAttribs.add(Constants.ASSESSMENT_ID_KEY); + } + + if (!requestBody.containsKey(Constants.BATCH_ID) + || StringUtils.isBlank((String) requestBody.get(Constants.BATCH_ID))) { + missingAttribs.add(Constants.BATCH_ID); + } + + if (!requestBody.containsKey(Constants.COURSE_ID) + || StringUtils.isBlank((String) requestBody.get(Constants.COURSE_ID))) { + missingAttribs.add(Constants.COURSE_ID); + } + + if (!missingAttribs.isEmpty()) { + errMsg = "One or more mandatory fields are missing in Request. Mandatory fields are : " + + missingAttribs.toString(); + } + + return errMsg; + } + + + /** + * Generates a map containing marks for each question. + * The input is a map where each key is a section name, and the value is another map. + * This inner map has proficiency keys, and each proficiency key maps to a map containing various attributes including "marksForQuestion". + * The output map's keys are of the format "sectionKey|proficiencyKey" and values are the corresponding marks for that question. + * + * @param qSectionSchemeMap a map representing sections and their respective proficiency maps + * @return a map where each key is a combination of section and proficiency, and each value is the marks for that question + */ + public Map generateMarkMap(Map> qSectionSchemeMap) { + Map markMap = new HashMap<>(); + logger.info("Starting to generate mark map from qSectionSchemeMap"); + qSectionSchemeMap.keySet().forEach(sectionKey -> { + Map proficiencyMap = qSectionSchemeMap.get(sectionKey); + proficiencyMap.forEach((key, value) -> { + Map values = (Map) value; + markMap.put(sectionKey + "|" + key, values.get("marksForQuestion")); + }); + }); + logger.info("Completed generating mark map"); + return markMap; + } + + + /** + * Retrieves the parameter details for question types based on the given assessment hierarchy. + * + * @param assessmentHierarchy a map containing the assessment hierarchy details. + * @return a map containing the parameter details for the question types. + * @throws IOException if there is an error processing the question section schema. + */ + private Map getParamDetailsForQTypes(Map assessmentHierarchy,String hierarchySectionId) throws IOException { + logger.info("Starting getParamDetailsForQTypes with assessmentHierarchy: {}", assessmentHierarchy); + Map questionSetDetailsMap = new HashMap<>(); + String assessmentType = (String) assessmentHierarchy.get(Constants.ASSESSMENT_TYPE); + questionSetDetailsMap.put(Constants.ASSESSMENT_TYPE, assessmentType); + questionSetDetailsMap.put(Constants.MINIMUM_PASS_PERCENTAGE, assessmentHierarchy.get(Constants.MINIMUM_PASS_PERCENTAGE)); + questionSetDetailsMap.put(Constants.TOTAL_MARKS, assessmentHierarchy.get(Constants.TOTAL_MARKS)); + if (assessmentType.equalsIgnoreCase(Constants.QUESTION_WEIGHTAGE)) { + String questionSectionSchema= (String) assessmentHierarchy.get(Constants.QUESTION_SECTION_SCHEME); + questionSetDetailsMap.put(Constants.QUESTION_SECTION_SCHEME, generateMarkMap(mapper.readValue(questionSectionSchema, + new TypeReference>() { + }))); + questionSetDetailsMap.put(Constants.NEGATIVE_MARKING_PERCENTAGE, assessmentHierarchy.get(Constants.NEGATIVE_MARKING_PERCENTAGE)); + questionSetDetailsMap.put("hierarchySectionId",hierarchySectionId); + } + logger.info("Completed getParamDetailsForQTypes with result: {}", questionSetDetailsMap); + return questionSetDetailsMap; + } + + public SBApiResponse saveAssessmentAsync(Map submitRequest, String token,boolean editMode) { + logger.info("AssessmentServicev5Impl::saveAssessmentAsync... Started"); + SBApiResponse response = ProjectUtil.createDefaultResponse(Constants.API_READ_ASSESSMENT); + String assessmentIdentifier = (String) submitRequest.get(Constants.IDENTIFIER); + String errMsg = ""; + try { + String userId = accessTokenValidator.fetchUserIdFromAccessToken(token); + if (StringUtils.isBlank(userId)) { + updateErrorDetails(response, Constants.USER_ID_DOESNT_EXIST, HttpStatus.INTERNAL_SERVER_ERROR); + return response; + } + logger.info(String.format("saveAssessmentAsync... UserId: %s, AssessmentIdentifier: %s", userId, assessmentIdentifier)); + Map assessmentAllDetail = null ; + // Step-1 : Read assessment using assessment Id from the Assessment Service + if(editMode) { + assessmentAllDetail = assessUtilServ.fetchHierarchyFromAssessServc(assessmentIdentifier,token); + } + else { + assessmentAllDetail = assessUtilServ + .readAssessmentHierarchyFromCache(assessmentIdentifier,editMode,token); + } + if (MapUtils.isEmpty(assessmentAllDetail)) { + updateErrorDetails(response, Constants.ASSESSMENT_HIERARCHY_READ_FAILED, + HttpStatus.INTERNAL_SERVER_ERROR); + return response; + } + //Step-2 : If Practice Assessment return without saving + if (Constants.PRACTICE_QUESTION_SET + .equalsIgnoreCase((String) assessmentAllDetail.get(Constants.PRIMARY_CATEGORY))||editMode) { + response.getResult().put(Constants.QUESTION_SET, readAssessmentLevelData(assessmentAllDetail)); + return response; + } + // Step-3 : If read user submitted assessment + List> existingDataList = assessUtilServ.readUserSubmittedAssessmentRecords( + userId, assessmentIdentifier); + + //Confirm whether the submitted request sections and questions match. + if (existingDataList.isEmpty()) { + updateErrorDetails(response, Constants.ASSESSMENT_HIERARCHY_READ_FAILED, + HttpStatus.INTERNAL_SERVER_ERROR); + return response; + } + else { + logger.info("Assessment read... user has details... "); + Date existingAssessmentStartTime = (Date) (existingDataList.get(0) + .get(Constants.START_TIME_KEY)); + Date existingAssessmentEndTime = (Date) (existingDataList.get(0) + .get(Constants.END_TIME)); + Timestamp existingAssessmentEndTimeTimestamp = new Timestamp( + existingAssessmentEndTime.getTime()); + Timestamp existingAssessmentStarTimeTimestamp = new Timestamp( + existingAssessmentStartTime.getTime()); + List> sectionListFromSubmitRequest = new ArrayList<>(); + List> hierarchySectionList = new ArrayList<>(); + Map assessmentHierarchy = new HashMap<>(); + Map existingAssessmentData = new HashMap<>(); + errMsg = validateSubmitAssessmentRequest(submitRequest, userId, hierarchySectionList, + sectionListFromSubmitRequest, assessmentHierarchy, existingAssessmentData,token,editMode); + if (StringUtils.isNotBlank(errMsg)) { + updateErrorDetails(response, Constants.ASSESSMENT_HIERARCHY_READ_FAILED, + HttpStatus.INTERNAL_SERVER_ERROR); + return response; + } + if (existingAssessmentStarTimeTimestamp.compareTo(existingAssessmentEndTimeTimestamp) < 0 + && Constants.NOT_SUBMITTED.equalsIgnoreCase((String) existingDataList.get(0).get(Constants.STATUS))) { + String questionSetFromAssessmentString = (String) existingDataList.get(0) + .get(Constants.ASSESSMENT_READ_RESPONSE_KEY); + Map questionSetFromAssessment = new Gson().fromJson( + questionSetFromAssessmentString, new TypeToken>() { + }.getType()); + questionSetFromAssessment.put(Constants.START_TIME, existingAssessmentStarTimeTimestamp.getTime()); + questionSetFromAssessment.put(Constants.END_TIME, + existingAssessmentStarTimeTimestamp.getTime()); + response.getResult().put(Constants.QUESTION_SET, questionSetFromAssessment); + Boolean isAssessmentUpdatedToDB = assessmentRepository.updateUserAssesmentDataToDB(userId, + (String) submitRequest.get(Constants.IDENTIFIER), submitRequest, null, null, + null); + if (Boolean.FALSE.equals(isAssessmentUpdatedToDB)) { + errMsg = Constants.ASSESSMENT_DATA_START_TIME_NOT_UPDATED; + response.getResult().put("ASSESSMENT_UPDATE", false); + + } + else { + response.getResult().put("ASSESSMENT_UPDATE", true); + } + + } else { + updateErrorDetails(response, Constants.ASSESSMENT_HIERARCHY_READ_FAILED, + HttpStatus.INTERNAL_SERVER_ERROR); + return response; + } + } + } catch (Exception e) { + errMsg = String.format("Error while reading assessment. Exception: %s", e.getMessage()); + logger.error(errMsg, e); + } + if (StringUtils.isNotBlank(errMsg)) { + updateErrorDetails(response, errMsg, HttpStatus.INTERNAL_SERVER_ERROR); + } + return response; + } +} diff --git a/src/main/java/org/sunbird/assessment/service/AssessmentUtilServiceV2.java b/src/main/java/org/sunbird/assessment/service/AssessmentUtilServiceV2.java index c1df199d7..3de59dd36 100644 --- a/src/main/java/org/sunbird/assessment/service/AssessmentUtilServiceV2.java +++ b/src/main/java/org/sunbird/assessment/service/AssessmentUtilServiceV2.java @@ -25,4 +25,16 @@ public Map validateQumlAssessment(List originalQuestionL public Map fetchHierarchyFromAssessServc(String qSetId,String token); public Map fetchWheebox(String userId); + + /** + * Validates a Quml assessment by comparing the original list of questions with the user's provided list of questions. + * + * @param questionSetDetailsMap a map containing details about the question set. + * @param originalQuestionList a list of original question identifiers. + * @param userQuestionList a list of maps where each map represents a user's question with its details. + * @param questionMap a map containing additional question-related information. + * @return a map with validation results and resultMap. + */ + public Map validateQumlAssessmentV2(Map questionSetDetailsMap, List originalQuestionList, + List> userQuestionList, Map questionMap); } diff --git a/src/main/java/org/sunbird/assessment/service/AssessmentUtilServiceV2Impl.java b/src/main/java/org/sunbird/assessment/service/AssessmentUtilServiceV2Impl.java index 09578af8b..685136682 100644 --- a/src/main/java/org/sunbird/assessment/service/AssessmentUtilServiceV2Impl.java +++ b/src/main/java/org/sunbird/assessment/service/AssessmentUtilServiceV2Impl.java @@ -24,8 +24,6 @@ import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; -import static org.sunbird.common.util.Constants.INSIGHTS_LEARNING_HOURS_REDIS_KEY; - @Service public class AssessmentUtilServiceV2Impl implements AssessmentUtilServiceV2 { @@ -453,4 +451,310 @@ public Map fetchWheebox(String userId) { } return new HashMap<>(); } + + + /** + * + * @param questionSetDetailsMap a map containing details about the question set. + * @param originalQuestionList a list of original question identifiers. + * @param userQuestionList a list of maps where each map represents a user's question with its details. + * @param questionMap a map containing additional question-related information. + * @return a map with validation results and resultMap. + */ + public Map validateQumlAssessmentV2(Map questionSetDetailsMap,List originalQuestionList, + List> userQuestionList,Map questionMap) { + try { + Integer correct = 0; + Integer blank = 0; + Integer inCorrect = 0; + Integer sectionMarks =0; + Map questionSetSectionScheme = new HashMap<>(); + String assessmentType= (String)questionSetDetailsMap.get(Constants.ASSESSMENT_TYPE); + String negativeWeightAgeEnabled; + int negativeMarksValue = 0; + int minimumPassPercentage = (int) questionSetDetailsMap.get(Constants.MINIMUM_PASS_PERCENTAGE); + Integer totalMarks= (Integer) questionSetDetailsMap.get(Constants.TOTAL_MARKS); + String sectionName = (String)questionSetDetailsMap.get("hierarchySectionId"); + Map resultMap = new HashMap<>(); + Map answers = getQumlAnswers(originalQuestionList,questionMap); + Map optionWeightages = new HashMap<>(); + if (assessmentType.equalsIgnoreCase(Constants.OPTION_WEIGHTAGE)) { + optionWeightages = getOptionWeightages(originalQuestionList, questionMap); + } else if (assessmentType.equalsIgnoreCase(Constants.QUESTION_WEIGHTAGE)) { + questionSetSectionScheme = (Map) questionSetDetailsMap.get(Constants.QUESTION_SECTION_SCHEME); + negativeWeightAgeEnabled = (String) questionSetDetailsMap.get(Constants.NEGATIVE_MARKING_PERCENTAGE); + negativeMarksValue = Integer.parseInt(negativeWeightAgeEnabled.replace("%", "")); + } + for (Map question : userQuestionList) { + Map proficiencyMap = getProficiencyMap(questionMap, question); + List marked = new ArrayList<>(); + List> options = new ArrayList<>(); + options = handleqTypeQuestion(question, options, marked, assessmentType, optionWeightages, sectionMarks); + if (CollectionUtils.isEmpty(marked)){ + blank++; + question.put(Constants.RESULT,Constants.BLANK); + } + else { + List answer = (List) answers.get(question.get(Constants.IDENTIFIER)); + sortAnswers(answer); + sortAnswers(marked); + if(assessmentType.equalsIgnoreCase(Constants.QUESTION_WEIGHTAGE)) { + if (answer.equals(marked)) { + question.put(Constants.RESULT, Constants.CORRECT); + correct++; + sectionMarks = handleCorrectAnswer(sectionMarks, questionSetSectionScheme, sectionName, proficiencyMap); + } else { + question.put(Constants.RESULT, Constants.INCORRECT); + inCorrect++; + sectionMarks = handleIncorrectAnswer(negativeMarksValue, sectionMarks, questionSetSectionScheme, sectionName, proficiencyMap); + } + } + sectionMarks = calculateScoreForOptionWeightage(question, assessmentType, optionWeightages, options, sectionMarks, marked); + } + } + blank = handleBlankAnswers(userQuestionList, answers, blank); + updateResultMap(userQuestionList, correct, blank, inCorrect, resultMap, sectionMarks, totalMarks); + computeSectionResults(sectionMarks, totalMarks, minimumPassPercentage, resultMap); + return resultMap; + } catch (Exception ex) { + logger.error("Error when verifying assessment. Error : ", ex); + } + return new HashMap<>(); + } + + private static Integer calculateScoreForOptionWeightage(Map question, String assessmentType, Map optionWeightages, List> options, Integer sectionMarks, List marked) { + if (assessmentType.equalsIgnoreCase(Constants.OPTION_WEIGHTAGE)) { + Map optionWeightageMap = (Map) optionWeightages.get(question.get(Constants.IDENTIFIER)); + for (Map.Entry optionWeightAgeFromOptions : optionWeightageMap.entrySet()) { + String submittedQuestionSetIndex = marked.get(0); + if (submittedQuestionSetIndex.equals(optionWeightAgeFromOptions.getKey())) { + sectionMarks = sectionMarks + Integer.parseInt((String) optionWeightAgeFromOptions.getValue()); + } + } + } + return sectionMarks; + } + + private List> handleqTypeQuestion(Map question, List> options, List marked, String assessmentType, Map optionWeightages, Integer sectionMarks) { + if (question.containsKey(Constants.QUESTION_TYPE)) { + String questionType = ((String) question.get(Constants.QUESTION_TYPE)).toLowerCase(); + Map editorStateObj = (Map) question.get(Constants.EDITOR_STATE); + options = (List>) editorStateObj + .get(Constants.OPTIONS); + getMarkedIndexForEachQuestion(questionType, options, marked, assessmentType, optionWeightages, sectionMarks); + } + return options; + } + + + /** + * Retrieves option weightages for a list of questions corresponding to their options. + * + * @param questions the list of questionIDs/doIds. + * @param questionMap the map containing questions/Question Level details. + * @return a map containing Identifier mapped to their option and option weightages. + * @throws Exception if there is an error processing the questions. + */ + private Map getOptionWeightages(List questions, Map questionMap) throws Exception { + logger.info("Retrieving option weightages for questions based on the options..."); + Map ret = new HashMap<>(); + for (String questionId : questions) { + Map optionWeightage = new HashMap<>(); + Map question = (Map) questionMap.get(questionId); + if (question.containsKey(Constants.QUESTION_TYPE)) { + String questionType = ((String) question.get(Constants.QUESTION_TYPE)).toLowerCase(); + Map editorStateObj = (Map) question.get(Constants.EDITOR_STATE); + List> options = (List>) editorStateObj.get(Constants.OPTIONS); + switch (questionType) { + case Constants.MCQ_SCA: + case Constants.MCQ_MCA: + for (Map option : options) { + Map valueObj = (Map) option.get(Constants.VALUE); + optionWeightage.put(valueObj.get(Constants.VALUE).toString(), valueObj.get(Constants.OPTION_WEIGHT).toString()); + } + break; + default: + break; + } + } + ret.put(question.get(Constants.IDENTIFIER).toString(), optionWeightage); + } + logger.info("Option weightages retrieved successfully."); + return ret; + } + + /** + * Gets index for each question based on the question type. + * + * + * @param questionType the type of question. + * @param options the list of options. + * @param marked the list to store marked indices. + * @param assessmentType the type of assessment. + * @param optionWeightages the map of option weightages. + * @param sectionMarks the current section marks. + */ + private void getMarkedIndexForEachQuestion(String questionType, List> options, List marked, String assessmentType, Map optionWeightages, Integer sectionMarks) { + logger.info("Getting marks or index for each question..."); + switch (questionType) { + case Constants.MTF: + for (Map option : options) { + marked.add(option.get(Constants.INDEX).toString() + "-" + + option.get(Constants.SELECTED_ANSWER).toString().toLowerCase()); + } + break; + case Constants.FTB: + for (Map option : options) { + marked.add((String) option.get(Constants.SELECTED_ANSWER)); + } + break; + case Constants.MCQ_SCA: + case Constants.MCQ_MCA: + if (assessmentType.equalsIgnoreCase(Constants.QUESTION_WEIGHTAGE)) { + getMarkedIndexForQuestionWeightAge(options, marked); + } else if (assessmentType.equalsIgnoreCase(Constants.OPTION_WEIGHTAGE)) { + getMarkedIndexForOptionWeightAge(options,marked); + } + break; + default: + break; + } + logger.info("Marks or index retrieved successfully."); + } + + /** + * Gets index for each question based on the question type. + * + * @param options the list of options. + */ + private void getMarkedIndexForOptionWeightAge(List> options, List marked) { + logger.info("Processing marks for option weightage..."); + for (Map option : options) { + String submittedQuestionSetIndex = (String) option.get(Constants.INDEX); + marked.add(submittedQuestionSetIndex); + } + logger.info("Marks for option weightage processed successfully."); + } + + /** + * Retrieves the index of marked options from the provided options list if it is a correct answer. + * + * @param options the list of options. + * @param marked the list to store marked indices for correct answer. + */ + private void getMarkedIndexForQuestionWeightAge(List> options, List marked) { + for (Map option : options) { + if ((boolean) option.get(Constants.SELECTED_ANSWER)) { + marked.add((String) option.get(Constants.INDEX)); + } + } + } + + /** + * Handles the correct answer scenario by updating the section marks. + * + * @param sectionMarks the current section marks. + * @param questionSetSectionScheme the question set section scheme. + * @param sectionName the name of the section. + * @param proficiencyMap the proficiency map containing question levels. + * @return the updated section marks. + */ + private Integer handleCorrectAnswer(Integer sectionMarks, Map questionSetSectionScheme, String sectionName, Map proficiencyMap) { + logger.info("Handling correct answer scenario..."); + sectionMarks = sectionMarks + (Integer) questionSetSectionScheme.get(sectionName + "|" + proficiencyMap.get(Constants.QUESTION_LEVEL)); + logger.info("Correct answer scenario handled successfully."); + return sectionMarks; + } + + /** + * Handles the incorrect answer scenario by updating the section marks . + * and applying negative marking if applicable. + * + * @param negativeMarksValue the value of negative marks for incorrect answers. + * @param sectionMarks the current section marks. + * @param questionSetSectionScheme the question set section scheme. + * @param sectionName the name of the section. + * @param proficiencyMap the proficiency map containing question levels. + * @return the updated section marks. + */ + private Integer handleIncorrectAnswer(int negativeMarksValue,Integer sectionMarks, Map questionSetSectionScheme, String sectionName, Map proficiencyMap) { + logger.info("Handling incorrect answer scenario..."); + if (negativeMarksValue > 0) { + sectionMarks = sectionMarks - (Integer) questionSetSectionScheme.get(sectionName + "|" + proficiencyMap.get(Constants.QUESTION_LEVEL)); + } + logger.info("Incorrect answer scenario handled successfully."); + return sectionMarks; + } + + /** + * Handles blank answers by counting skipped questions. + * + * @param userQuestionList the list of user questions. + * @param answers the map containing answers. + * @param blank the current count of blank answers. + * @return the updated count of blank answers. + */ + private Integer handleBlankAnswers(List> userQuestionList, Map answers, Integer blank) { + logger.info("Handling blank answers..."); + // Increment the blank counter for skipped question objects + if (answers.size() > userQuestionList.size()) { + blank += answers.size() - userQuestionList.size(); + } + logger.info("Blank answers handled successfully."); + return blank; + } + + /** + * Updates the result map with assessment data. + * + * @param userQuestionList the list of user questions. + * @param correct the count of correct answers. + * @param blank the count of blank answers. + * @param inCorrect the count of incorrect answers. + * @param resultMap the map to store assessment results. + * @param sectionMarks the section marks obtained. + * @param totalMarks the total marks for the assessment. + */ + private void updateResultMap(List> userQuestionList, Integer correct, Integer blank, Integer inCorrect, Map resultMap, Integer sectionMarks, Integer totalMarks) { + logger.info("Updating result map..."); + int total; + total = correct + blank + inCorrect; + resultMap.put(Constants.RESULT, total == 0 ? 0 : ((correct * 100d) / total)); + resultMap.put(Constants.INCORRECT, inCorrect); + resultMap.put(Constants.BLANK, blank); + resultMap.put(Constants.CORRECT, correct); + resultMap.put(Constants.TOTAL, total); + resultMap.put(Constants.CHILDREN, userQuestionList); + resultMap.put(Constants.SECTION_MARKS, sectionMarks); + resultMap.put(Constants.TOTAL_MARKS, totalMarks); + logger.info("Result map updated successfully."); + } + + + /** + * Computes the result of a section based on section marks, total marks, and minimum pass value. + * + * @param sectionMarks the marks obtained in the section. + * @param totalMarks the total marks available for the section. + * @param minimumPassValue the minimum percentage required to pass the section. + * @param resultMap the map to store the section result. + */ + private void computeSectionResults(Integer sectionMarks, Integer totalMarks, int minimumPassValue, Map resultMap) { + logger.info("Computing section results..."); + if (sectionMarks > 0 && ((sectionMarks / totalMarks) * 100 >= minimumPassValue)) { + resultMap.put(Constants.SECTION_RESULT, Constants.PASS); + } else { + resultMap.put(Constants.SECTION_RESULT, Constants.FAIL); + } + logger.info("Section results computed successfully."); + } + + private Map getProficiencyMap(Map questionMap, Map question) { + return (Map) questionMap.get(question.get(Constants.IDENTIFIER)); + } + + private void sortAnswers(List answer) { + if (answer.size() > 1) + Collections.sort(answer); + } } \ No newline at end of file diff --git a/src/main/java/org/sunbird/common/util/CbExtServerProperties.java b/src/main/java/org/sunbird/common/util/CbExtServerProperties.java index 34ab70f6e..614dd9b87 100644 --- a/src/main/java/org/sunbird/common/util/CbExtServerProperties.java +++ b/src/main/java/org/sunbird/common/util/CbExtServerProperties.java @@ -759,6 +759,9 @@ public void setRedisWheeboxKey(String redisWheeboxKey) { @Value("${user.bulk.upload.category.value}") private String bulkUploadCategoryValue; + @Value("#{${insights.mapping.key}}") + private Map insightsMappingKey; + public boolean qListFromCacheEnabled() { return qListFromCacheEnabled; } @@ -2685,4 +2688,12 @@ public List getBulkUploadCategoryValue() { public void setBulkUploadCategoryValue(String bulkUploadCategoryValue) { this.bulkUploadCategoryValue = bulkUploadCategoryValue; } + + public Map getInsightsMappingKey() { + return insightsMappingKey; + } + + public void setInsightsMappingKey(Map insightsMappingKey) { + this.insightsMappingKey = insightsMappingKey; + } } \ No newline at end of file diff --git a/src/main/java/org/sunbird/common/util/Constants.java b/src/main/java/org/sunbird/common/util/Constants.java index 3a62e9792..96e678d88 100644 --- a/src/main/java/org/sunbird/common/util/Constants.java +++ b/src/main/java/org/sunbird/common/util/Constants.java @@ -601,6 +601,7 @@ public class Constants { public static final String USER_ID_DOESNT_EXIST = "User Id doesn't exist! Please supply a valid auth token"; public static final String ASSESSMENT_DATA_START_TIME_NOT_UPDATED = "Assessment Data & Start Time not updated in the DB."; public static final String FAILED_TO_GET_QUESTION_DETAILS = "Failed to get Question List data from the Question List Api."; + public static final String ASSESSMENT_RETRY_ATTEMPTS_CROSSED = "Maximum retry attempts for assessment reached."; public static final String ASSESSMENT_HIERARCHY_READ_FAILED = "Assessment hierarchy read failed, failed to process request"; public static final String ASSESSMENT_ID_KEY_IS_NOT_PRESENT_IS_EMPTY = "Assessment Id Key is not present/is empty"; @@ -1025,6 +1026,17 @@ public class Constants { public static final String PARENTS = "parents"; public static final String SUB_SECTORS = "subsectors"; public static final String COURSE_LINK = "courseLink"; + public static final String NEGATIVE_MARKING_PERCENTAGE = "negativeMarkingPercentage"; + public static final String ASSESSMENT_TYPE = "assessmentType"; + public static final String TOTAL_MARKS = "totalMarks"; + public static final String QUESTION_SECTION_SCHEME = "questionSectionScheme"; + public static final String OPTION_WEIGHTAGE = "optionalWeightage"; + public static final String QUESTION_WEIGHTAGE = "questionWeightage"; + public static final String QUESTION_LEVEL = "questionLevel"; + public static final String TOTAL_SECTION_MARKS = "totalSectionMarks"; + public static final String SECTION_RESULT = "sectionResult"; + public static final String SECTION_MARKS= "sectionMarks"; + public static final String FAIL = "fail"; public static final String API_CALENDAR_EVENT_BULK_UPLOAD = "api.calendar.event.bulk.upload"; public static final String TABLE_CALENDAR_EVENT_BULK_UPLOAD = "calendar_event_bulk_upload"; public static final String EVENT = "event"; @@ -1056,6 +1068,7 @@ public class Constants { public static final String DOMICILE_MEDIUM = "domicileMedium"; public static final String PINCODE = "pinCode"; public static final String EMPLOYEE_CODE = "employeeCode"; + public static final String OPTION_WEIGHT = "optionWeight"; public static final String PROFILE_STATUS = "profileStatus"; public static final String NOT_VERIFIED = "NOT-VERIFIED"; public static final String PROFILE_STATUS_UPDATED_ON = "profileStatusUpdatedOn"; @@ -1063,6 +1076,13 @@ public class Constants { public static final String PROFILE_GROUP_STATUS = "profileGroupStatus"; public static final String PROFILE_DESIGNATION_STATUS = "profileDesignationStatus"; public static final String NOT_MY_USER = "NOT-MY-USER"; + public static final String REQUEST_TYPE = "requestType"; + public static final String INSIGHT_FIELD_KEY = ".insights.fields"; + public static final String INSIGHT_REDIS_KEY_MAPPING = ".insights.redis.key.mapping"; + public static final String INSIGHT_PROPERTY_FIELDS = ".insights.property.fields"; + public static final String TIME_TAKEN_FOR_ASSESSMENT= "timeTakenForAssessment"; + public static final String RETAKE_ATTEMPT_CONSUMED = "retakeAttemptsConsumed"; + public static final String TOTAL_PERCENTAGE = "totalPercentage"; private Constants() { throw new IllegalStateException("Utility class"); diff --git a/src/main/java/org/sunbird/insights/controller/service/InsightsServiceImpl.java b/src/main/java/org/sunbird/insights/controller/service/InsightsServiceImpl.java index 3dec4c867..be11dd4f3 100644 --- a/src/main/java/org/sunbird/insights/controller/service/InsightsServiceImpl.java +++ b/src/main/java/org/sunbird/insights/controller/service/InsightsServiceImpl.java @@ -1,5 +1,7 @@ package org.sunbird.insights.controller.service; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.collections.MapUtils; import org.apache.commons.lang3.StringUtils; @@ -14,6 +16,8 @@ import org.sunbird.common.util.CbExtServerProperties; import org.sunbird.common.util.Constants; import org.sunbird.common.util.ProjectUtil; +import org.sunbird.common.util.PropertiesCache; + import java.math.BigDecimal; import java.math.RoundingMode; @@ -39,6 +43,8 @@ public class InsightsServiceImpl implements InsightsService { @Autowired CbExtServerProperties extServerProperties; + ObjectMapper mapper = new ObjectMapper(); + public SBApiResponse insights(Map requestBody,String userId) throws Exception { String [] labelsCertificates = {extServerProperties.getInsightsLabelCertificatesAcross(),extServerProperties.getInsightsLabelCertificatesYourDepartment()} ; String [] labelsLearningHours = {extServerProperties.getInsightsLabelLearningHoursYourDepartment(),extServerProperties.getInsightsLabelLearningHoursAcross()} ; @@ -174,9 +180,24 @@ public SBApiResponse readInsightsForOrganisation(Map requestBody response.setResponseCode(HttpStatus.BAD_REQUEST); return response; } + Map insightKeyMapping = serverProperties.getInsightsMappingKey(); + String valueForInsightKey = insightKeyMapping.getOrDefault(filter.get(Constants.REQUEST_TYPE), Constants.ORGANISATION); Map responseMap = new HashMap<>(); - Map organisationInsideFields = serverProperties.getOrganisationInsightFields(); - Map redisKeyForInsight = serverProperties.getOrganisationInsightRedisKeyMapping(); + String organisationInsideFieldsProperty = PropertiesCache.getInstance().getProperty(valueForInsightKey + Constants.INSIGHT_FIELD_KEY); + String redisKeyForInsightsProperty = PropertiesCache.getInstance().getProperty(valueForInsightKey + Constants.INSIGHT_REDIS_KEY_MAPPING); + String cssPropertiesForInsightsProperty = PropertiesCache.getInstance().getProperty(valueForInsightKey + Constants.INSIGHT_PROPERTY_FIELDS); + if (StringUtils.isBlank(organisationInsideFieldsProperty) || StringUtils.isBlank(redisKeyForInsightsProperty)) { + response.getParams().setStatus(Constants.FAILED); + response.put(MESSAGE, "Not able to find value for requestType."); + response.setResponseCode(HttpStatus.BAD_REQUEST); + return response; + } + Map organisationInsideFields = mapper.readValue(organisationInsideFieldsProperty, new TypeReference>() { + }); + Map redisKeyForInsight = mapper.readValue(redisKeyForInsightsProperty, new TypeReference>() { + }); + Map cssPropertiesForInsight = mapper.readValue(cssPropertiesForInsightsProperty, new TypeReference>() { + }); List> organisationDataMapList = new ArrayList<>(); for (String organisationId: organizations) { Map organisationMap = new HashMap<>(); @@ -184,7 +205,7 @@ public SBApiResponse readInsightsForOrganisation(Map requestBody for (Map.Entry insightFields: organisationInsideFields.entrySet()) { Map nudgesData = new HashMap<>(); nudgesData.put(Constants.ICON, insightFields.getValue()); - populateNudgeForMicroSite(insightFields.getKey(), organisationId, serverProperties.getOrganisationInsightPropertyFields(), + populateNudgeForMicroSite(insightFields.getKey(), organisationId, cssPropertiesForInsight, redisKeyForInsight.get(insightFields.getKey()), nudgesData); nudgesDataList.add(nudgesData); } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index d763d924f..b524020a2 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -192,7 +192,7 @@ assessment.question.list.path=question/v4/list #assessment.question.list.path=question/v/list assessment.read.assessmentLevel.params=name,identifier,primaryCategory,versionKey,mimeType,code,version,objectType,status,expectedDuration,totalQuestions,maxQuestions,description assessment.read.sectionLevel.params=parent,name,identifier,description,trackable,primaryCategory,versionKey,mimeType,code,version,objectType,status,index,maxQuestions,scoreCutoffType,minimumPassPercentage,additionalInstructions -assessment.read.questionLevel.params=parent,name,identifier,primaryCategory,body,versionKey,mimeType,code,objectType,status,qType,index,showSolutions,allowAnonymousAccess,visibility,version,showFeedback,license +assessment.read.questionLevel.params=parent,name,identifier,primaryCategory,body,versionKey,mimeType,code,objectType,status,qType,index,showSolutions,allowAnonymousAccess,visibility,version,showFeedback,license,questionLevel assessment.read.min.question.params=parent,name,identifier,primaryCategory,versionKey,mimeType,objectType,qType #Course Reminder Notification @@ -422,4 +422,8 @@ organisation.insights.redis.key.mapping={"Average Course Rating":"dashboard_cour bulk.upload.allowed.roles.creation=CBP_ADMIN,CONTENT_CREATOR,CONTENT_REVIEWER,FRAC_ADMIN,FRAC_COMPETENCY_MEMBER,FRAC_COMPETENCY_REVIEWER,FRAC_REVIEWER_L1,FRAC_REVIEWER_L2,IFU_MEMBER,MDO_ADMIN,MDO_LEADER,PUBLIC,WAT_MEMBER,PROGRAM_COORDINATOR,MDO_DASHBOARD_USER user.bulk.upload.gender.value=Male,Female,Others -user.bulk.upload.category.value=General,OBC,SC,ST \ No newline at end of file +user.bulk.upload.category.value=General,OBC,SC,ST +insights.mapping.key={"PROVIDER_INSIGHT":"organisation","MDO_INSIGHT":"mdo"} +mdo.insights.fields={"Total Users":"https://portal.karmayogi.nic.in/content-store/orgStore/01376822290813747263/1716545372950_Network.svg","Certificates":"https://portal.karmayogi.nic.in/content-store/orgStore/01376822290813747263/1715594032620_badges.svg","Enrolments":"https://portal.karmayogi.nic.in/content-store/orgStore/01376822290813747263/1716545463334_knowledge-resources.svg","Content Published":"https://portal.karmayogi.nic.in/content-store/orgStore/01376822290813747263/1716545105444_Program.svg","24hr Login":"https://portal.karmayogi.nic.in/content-store/orgStore/01376822290813747263/1716545326988_percent.svg"} +mdo.insights.property.fields={"valueColor": "#FFFFFF","labelColor": "#FFFFFF","linebreak":"false","background": "#1B4CA1","iconColor": "#FFFFFF"} +mdo.insights.redis.key.mapping={"Total Users":"dashboard_user_count_by_user_org","Certificates":"dashboard_certificates_generated_count_by_user_org","Enrolments":"dashboard_enrolment_count_by_user_org","Content Published":"dashboard_live_course_count_by_course_org","24hr Login":"dashboard_login_percent_last_24_hrs_by_user_org"} \ No newline at end of file