From fdf163591666f9054405834e5bcb93f663c51789 Mon Sep 17 00:00:00 2001 From: Kartheek Palla Date: Fri, 8 Sep 2023 16:55:55 +0530 Subject: [PATCH] Issue KN-000 merge: Merge Release-5.5.0 into master (#746) * Issue #KN-427 feat: CSP migration * Issue #KN-427 feat: CSP migration * Issue #KN-439 feat: Added the variable for relative_path_prefix * Issue #KN-439 feat: Added the variable for relative_path_prefix * Issue #KN-439 test: Fixed the failed test * Issue #KN-439 test: Fixed the failed test * Issue #KN-439 fix: Fixed the cassandra issue * Issue #KN-439 feat: CSP migration * Issue #KN-439 feat: CSP migration * Issue #KN-439 feat: CSP migration * Issue #KN-439 feat: CSP migration * Issue #KN-439 feat: CSP migration * Issue #KN-439 feat: CSP migration * Issue #KN-439 feat: CSP migration * Issue #KN-439 feat: CSP migration * Issue #KN-439 feat: CSP migration * Issue #KN-439 feat: CSP migration * Issue #KN-439 feat: CSP migration * Issue #KN-439 feat: CSP migration * Issue #KN-439 feat: CSP migration * Issue #KN-439 feat: CSP migration * Issue #KN-439 feat: CSP migration * Issue #KN-439 feat: CSP migration * Issue #KN-439 feat: CSP migration * Issue #KN-439 feat: CSP migration * Issue #KN-439 feat: CSP migration * Issue #KN-439 feat: CSP migration * Issue #KN-439 feat: CSP migration * Issue #KN-439 feat: CSP migration * Issue #KN-439 feat: CSP migration * Issue #KN-439 feat: CSP migration * Issue #KN-439 feat: CSP migration * Issue #KN-439 feat: CSP migration * Issue #KN-439 feat: CSP migration * Issue #KN-439 feat: Cassandra Data migration * Issue #KN-439 feat: Cassandra Data migration * Issue #KN-439 feat: Cassandra Data migration * Issue #KN-439 feat: Cassandra Data migration * Issue #KN-439 feat: Cassandra Data migration * Issue #KN-439 feat: Cassandra Data migration * Issue #KN-439 feat: Cassandra Data migration * Issue #KN-439 feat: Cassandra Data migration * Issue #KN-439 feat: Cassandra Data migration * Issue #KN-439 feat: Cassandra Data migration * Issue #KN-439 feat: Cassandra Data migration * Issue #KN-439 fix: Collection Publishing exception handling * Issue #KN-439 fix: Collection Publishing exception handling * Issue #KN-439 fix: Collection Publishing exception handling * Issue #KN-439 fix: Collection Publishing exception handling * Issue #KN-439 fix: Collection Publishing exception handling * Issue #KN-439 fix: Collection Publishing exception handling * Issue #KN-439 fix: Collection Publishing exception handling * Issue #KN-439 fix: Collection Publishing exception handling * Issue #KN-439 fix: Collection Publishing exception handling * Issue #KN-439 fix: Collection Publishing exception handling * Issue #KN-439 fix: Collection Publishing exception handling * Issue #KN-439 fix: Collection Publishing exception handling * Issue #CO-191 fix: Content Publish failure * Issue #CO-191 fix: Content Publish failure * Issue #000 feat: S3 presto dependency added * Issue #KN-427 fix: Live node republish * Issue #KN-427 fix: Live node republish * Issue #KN-427 fix: Live node republish * Issue #KN-427 fix: Live node republish * Issue #KN-427 fix: Live node republish * Issue #KN-747 fix: Skipping contents with FW validation error during streamingUrl update * Issue #KN-747 fix: Skipping contents with FW validation error during streamingUrl update * Issue #KN-747 fix: Skipping contents with FW validation error during streamingUrl update * csp migratioon * Issue #CO-227 fix: previewUrl and streamingUrl not showing CNAME * Issue #CO-227 fix: previewUrl and streamingUrl not showing CNAME * Issue #CO-227 fix: previewUrl and streamingUrl not showing CNAME * Issue #CO-227 fix: previewUrl and streamingUrl not showing CNAME * csp migration * Update pom.xml * Update CSPNeo4jMigrator.scala * Issue #KN-747 fix: Triggered the processing in open() for video-stream-generator * Issue #KN-747 fix: Fixed replacement issue. * Update GoogleDriveUtil.scala * Update MigrationObjectUpdater.scala * google drive handling * google drive handle * google drive handle * Update CSPNeo4jMigrator.scala * Issue #KN-427 fix: Publisher Updates * Issue #KN-427 fix: Publisher Updates * Update MigrationObjectUpdater.scala * Issue #KN-427 fix: Publisher Updates * Update csp-migrator.conf * Issue #KN-427 fix: Handling external urls during csp migration * Issue #KN-427 fix: Handling external urls during csp migration * Issue #KN-427 fix: Handling external urls during csp migration * Issue #KN-427 fix: Handling external urls during csp migration * Issue #KN-427 fix: Handling external urls during csp migration * Issue #KN-427 fix: Handling external urls during csp migration * Issue #KN-427 fix: Handling external urls during csp migration * Issue #KN-802 feat: Sync QR Image to DIAL code ES index * Issue #KN-802 feat: Sync QR Image to DIAL code ES index * Issue #KN-802 feat: Sync QR Image to DIAL code ES index * Issue #DU-302: Re-migration failing because of Relative path while republishing * Issue #DU-302: Re-migration failing because of Relative path while republishing * Issue #DU-302: Re-migration failing because of Relative path while republishing * Issue #DU-302: Re-migration failing because of Relative path while republishing * Issue #DU-302: Re-migration failing because of Relative path while republishing * Issue #DU-302 fix: Fixed read_base_path to relative_path_prefix * Issue #DU-302 fix: LiveNodePublisherStreamTask parallelism increased for subtasks * Issue #KN-802 feat: Sync QR Image to DIAL code ES index * Issue #KN-802 feat: Sync QR Image to DIAL code ES index * Issue #DU-302 debug: Debug Content Auto Creator issue * Issue #KN-802 feat: Sync QR Image to DIAL code ES index * Issue #KN-802 feat: Sync QR Image to DIAL code ES index * Issue #KN-802 feat: Sync QR Image to DIAL code ES index * Issue #KN-802 feat: Sync QR Image to DIAL code ES index * Issue #KN-802 feat: Sync QR Image to DIAL code ES index * Issue #KN-802 feat: Sync QR Image to DIAL code ES index * Issue #KN-802 feat: Sync QR Image to DIAL code ES index * Issue #KN-802 feat: Sync QR Image to DIAL code ES index * Issue #KN-802 feat: Sync QR Image to DIAL code ES index * Issue #KN-802 feat: Sync QR Image to DIAL code ES index * Issue #KN-802 feat: Sync QR Image to DIAL code ES index * Issue #KN-802 feat: Sync QR Image to DIAL code ES index * Issue #KN-802 feat: Sync QR Image to DIAL code ES index * Issue #KN-802 feat: Sync QR Image to DIAL code ES index * Issue #KN-802 feat: Sync QR Image to DIAL code ES index * Issue #KN-802 feat: Sync QR Image to DIAL code ES index * Issue #KN-802 feat: Sync QR Image to DIAL code ES index * Issue #KN-802 feat: Sync QR Image to DIAL code ES index * Issue #KN-742 fix: Infinite time wait for invalid GDrive URL * Issue #KN-742 fix: Infinite time wait for invalid GDrive URL * Issue #KN-742 fix: Cassandra null hierarchy record inserts * Issue #KN-742 fix: Cassandra null hierarchy record inserts * Issue #KN-856 test: Logging to debug the issue * Issue #KN-742 fix: Shallow copy publishing fix * Issue #KN-742 fix: Shallow copy publishing fix * Issue #KN-742 fix: Shallow copy publishing fix * Issue #KN-742 fix: Shallow copy publishing fix * Issue #KN-859 feat: remove unused code from knowlg repo * Issue #KN-742 fix: ExternalUrl file issue handling * Issue #KN-742 fix: ExternalUrl file issue handling * Issue #KN-742 fix: Set the root objectType if not available for child. * Issue #KN-742 fix: Set the root objectType if not available for child. * Issue #KN-742 fix: Set the root objectType if not available for child. * setup value place holder for unit test * removed logger info Signed-off-by: Deepak Devadathan * triggering circleci build Signed-off-by: Deepak Devadathan * triggerring circle ci Signed-off-by: Deepak Devadathan * Issue #KN-889 fix: QR ImageURL not getting generated * Issue #KN-889 fix: QR ImageURL not getting generated * updated circleci build Signed-off-by: Deepak Devadathan * Issue #ED-1227 fix: updated CircleCi build * Issue #ED-1227 fix: CircleCi build issue fix * Issue #ED-1227 fix: CircleCi build issue fix * Issue #KN-920 merge: Syntax issue fix * Issue #KN-920 fix: CSP changes load cloud storage SDK from config * Issue #KN-920 fix: Jenkins Build changes * Issue #KN-921 fix: QR image issue fix * Issue #KN-921 fix: CircleCI unit test issue fox * Issue #KN-921 fix: CircleCI unit test issue fox * Issue #KN-921 fix: config issue fix (#736) --------- Signed-off-by: Deepak Devadathan Co-authored-by: Jayaprakash8887 Co-authored-by: Anil Gupta Co-authored-by: anilgupta Co-authored-by: vinukumar-vs Co-authored-by: Amit Priyadarshi Co-authored-by: Mahesh Kumar Gangula Co-authored-by: Mahesh Kumar Gangula Co-authored-by: Kenneth Heung Co-authored-by: Deepak Devadathan Co-authored-by: Aiman Sharief --- .circleci/config.yml | 8 +- .gitignore | 3 +- activity-aggregate-updater/README.md | 66 -- .../resources/activity-aggregate-updater.conf | 54 -- .../job/aggregate/common/DeDupHelper.scala | 12 - .../sunbird/job/aggregate/domain/Models.scala | 52 -- .../ActivityAggregatesFunction.scala | 449 ------------ .../CollectionProgressCompleteFunction.scala | 141 ---- .../CollectionProgressUpdateFunction.scala | 96 --- .../ContentConsumptionDeDupFunction.scala | 74 -- .../task/ActivityAggregateUpdaterConfig.scala | 135 ---- .../ActivityAggregateUpdaterStreamTask.scala | 86 --- .../src/test/resources/logback-test.xml | 18 - .../src/test/resources/test.conf | 53 -- .../src/test/resources/test.cql | 79 --- .../sunbird/job/fixture/EventFixture.scala | 194 ------ ...ActivityAggregateUpdaterTaskTestSpec.scala | 220 ------ .../spec/BaseActivityAggregateTestSpec.scala | 59 -- .../src/main/resources/asset-enrichment.conf | 14 +- .../functions/ImageEnrichmentFunction.scala | 2 +- .../functions/VideoEnrichmentFunction.scala | 2 +- .../spec/AssetEnrichmentTaskTestSpec.scala | 2 +- .../sunbird/job/fixture/EventFixture.scala | 2 +- .../src/main/resources/auto-creator-v2.conf | 12 + .../functions/AutoCreatorFunction.scala | 4 +- auto-creator-v2/src/test/resources/test.conf | 7 +- .../spec/helper/AutoCreatorSpec.scala | 2 +- .../spec/helper/ObjectUpdaterSpec.scala | 2 +- .../pom.xml | 34 +- .../resources/cassandra-data-migration.conf | 18 + .../src/main/resources/log4j.properties | 2 +- .../sunbird/job/migration/domain/Event.scala | 34 + .../CassandraDataMigrationFunction.scala | 45 ++ .../helpers/CassandraDataMigrator.scala | 78 +++ .../task/CassandraDataMigrationConfig.scala | 40 ++ .../CassandraDataMigrationStreamTask.scala | 54 ++ .../src/test/resources/test.conf | 26 + .../src/test/resources/test.cql | 31 + .../job/migration/fixture/EventFixture.scala | 26 + .../helpers/CassandraDataMigratorSpec.scala | 58 ++ .../CassandraDataMigrationTaskTestSpec.scala | 73 ++ .../main/resources/content-auto-creator.conf | 13 +- .../ContentAutoCreatorFunction.scala | 2 +- .../helpers/ContentAutoCreator.scala | 3 +- .../task/ContentAutoCreatorConfig.scala | 1 - .../spec/helper/ContentAutoCreatorSpec.scala | 22 +- .../8e57723e-4541-11eb-b378-0242ac130002.png | Bin 808 -> 0 bytes .../certificate-processor/pom.xml | 165 ----- .../src/main/resources/Verdana.ttf | Bin 129364 -> 0 bytes .../incredible/CertificateConfig.scala | 8 - .../incredible/CertificateFactory.scala | 98 --- .../incredible/CertificateGenerator.scala | 60 -- .../org/sunbird/incredible/HttpUtil.scala | 19 - .../org/sunbird/incredible/JsonKeys.scala | 123 ---- .../incredible/ScalaModuleJsonUtils.scala | 32 - .../org/sunbird/incredible/UrlManager.scala | 50 -- .../org/sunbird/incredible/pojos/Gender.scala | 5 - .../InvalidDateFormatException.scala | 6 - .../sunbird/incredible/pojos/ob/Models.scala | 232 ------- .../pojos/valuator/ExpiryDateValuator.scala | 68 -- .../pojos/valuator/IEvaluator.scala | 8 - .../pojos/valuator/IssuedDateValuator.scala | 48 -- .../incredible/processor/CertModel.scala | 8 - .../qrcode/AccessCodeGenerator.scala | 59 -- .../qrcode/QRCodeGenerationModel.scala | 6 - .../qrcode/QRCodeImageGenerator.scala | 230 ------- .../qrcode/QRCodeImageGeneratorParams.scala | 5 - .../processor/signature/Exceptions.scala | 25 - .../processor/signature/SignatureHelper.scala | 66 -- .../processor/store/StorageService.scala | 51 -- .../processor/views/SvgGenerator.scala | 72 -- .../processor/views/VarResolver.scala | 99 --- .../org/sunbird/incredible/BaseTestSpec.scala | 14 - .../incredible/CertificateGeneratorTest.scala | 47 -- .../sunbird/incredible/SvgGeneratorTest.scala | 38 - .../qrcode/AccessCodeGeneratorTest.scala | 15 - .../qrcode/QRCodeImageGeneratorTest.scala | 16 - .../valuator/ExpiryDateValuatorTest.scala | 60 -- .../valuator/IssuedDateValuatorTest.scala | 48 -- .../collection-cert-pre-processor/pom.xml | 216 ------ .../collection-cert-pre-processor.conf | 38 - .../job/collectioncert/domain/Event.scala | 29 - .../job/collectioncert/domain/Models.scala | 26 - .../CollectionCertPreProcessorFn.scala | 98 --- .../functions/IssueCertificateHelper.scala | 236 ------- .../CollectionCertPreProcessorConfig.scala | 84 --- .../task/CollectionCertPreProcessorTask.scala | 58 -- .../src/test/resources/test.conf | 45 -- .../src/test/resources/test.cql | 85 --- .../collectioncert/fixture/EventFixture.scala | 15 - .../CollectionCertPreProcessFnTestSpec.scala | 127 ---- .../CollectionCertPreProcessorTaskSpec.scala | 120 ---- .../collection-certificate-generator/pom.xml | 216 ------ .../collection-certificate-generator.conf | 35 - .../src/main/resources/log4j.properties | 11 - .../sunbird/job/certgen/domain/Event.scala | 61 -- .../sunbird/job/certgen/domain/Models.scala | 66 -- .../certgen/exceptions/ErrorMessages.scala | 16 - .../exceptions/ValidationException.scala | 8 - .../job/certgen/functions/CertMapper.scala | 95 --- .../job/certgen/functions/CertValidator.scala | 196 ------ .../CertificateGeneratorFunction.scala | 361 ---------- .../functions/CreateUserFeedFunction.scala | 49 -- .../certgen/functions/NotifierFunction.scala | 154 ----- .../task/CertificateGeneratorConfig.scala | 159 ----- .../task/CertificateGeneratorStreamTask.scala | 85 --- .../src/test/resources/logback-test.xml | 16 - .../src/test/resources/test.conf | 40 -- .../src/test/resources/test.cql | 53 -- .../job/certgen/fixture/EventFixture.scala | 24 - .../job/certgen/spec/CertValidatorTest.scala | 70 -- ...ificateGeneratorFunctionTaskTestSpec.scala | 127 ---- .../CertificateGeneratorFunctionTest.scala | 240 ------- .../spec/CreateUserFeedFunctionTest.scala | 52 -- .../certgen/spec/NotifierFunctionTest.scala | 73 -- credential-generator/pom.xml | 47 -- .../pom.xml | 64 +- .../src/main/resources/csp-migrator.conf | 98 +++ .../src/main/resources/log4j.properties | 2 +- .../job/cspmigrator/domain/Event.scala | 37 + .../CSPCassandraMigratorFunction.scala | 110 +++ .../functions/CSPNeo4jMigratorFunction.scala | 124 ++++ .../helpers/CSPCassandraMigrator.scala | 55 ++ .../helpers/CSPNeo4jMigrator.scala | 60 ++ .../cspmigrator/helpers/GoogleDriveUtil.scala | 84 +++ .../helpers/MigrationObjectReader.scala | 122 ++++ .../helpers/MigrationObjectUpdater.scala | 283 ++++++++ .../cspmigrator/helpers/URLExtractor.scala | 34 + .../cspmigrator/task/CSPMigratorConfig.scala | 91 +++ .../task/CSPMigratorStreamTask.scala | 65 ++ .../src/test/resources/base-test.conf | 54 ++ csp-migrator/src/test/resources/test.conf | 87 +++ csp-migrator/src/test/resources/test.cql | 21 + .../CSPMigratorSpec.scala | 288 ++++++++ .../URLExtractorSpec.scala | 30 + .../helper/DialcodeContextUpdaterSpec.scala | 2 +- ...DialcodeContextUpdaterStreamTaskSpec.scala | 2 +- enrolment-reconciliation/README.md | 66 -- .../resources/enrolment-reconciliation.conf | 43 -- .../job/recounciliation/domain/Event.scala | 28 - .../job/recounciliation/domain/Models.scala | 51 -- .../functions/EnrolmentReconciliationFn.scala | 321 --------- .../functions/ProgressCompleteFunction.scala | 119 ---- .../functions/ProgressUpdateFunction.scala | 85 --- .../task/EnrolmentReconciliationConfig.scala | 118 ---- .../EnrolmentReconciliationStreamTask.scala | 68 -- .../src/test/resources/test.conf | 54 -- .../src/test/resources/test.cql | 79 --- .../sunbird/job/fixture/EventFixture.scala | 194 ------ .../spec/BaseActivityAggregateTestSpec.scala | 59 -- ...nrolmentReconciliationStreamTaskSpec.scala | 121 ---- jobs-core/pom.xml | 6 +- .../org/sunbird/job/BaseProcessFunction.scala | 12 +- .../org/sunbird/job/util/CSPMetaUtil.scala | 131 ++++ .../org/sunbird/job/util/CassandraUtil.scala | 13 +- .../sunbird/job/util/CloudStorageUtil.scala | 29 +- .../org/sunbird/job/util/Neo4JUtil.scala | 31 +- .../spec/BaseProcessFunctionTestSpec.scala | 2 +- .../scala/org/sunbird/spec/BaseSpec.scala | 4 +- .../org/sunbird/spec/FileUtilsSpec.scala | 2 +- .../scala/org/sunbird/spec/HTTPUtilSpec.scala | 4 +- jobs-distribution/Dockerfile | 3 + jobs-distribution/pom.xml | 82 ++- kubernets/pipelines/build/Jenkinsfile | 5 +- .../README.md | 9 +- .../pom.xml | 26 +- .../live-video-stream-generator.conf | 94 +++ .../src/main/resources/log4j.properties | 2 +- .../job/livevideostream/domain/Event.scala | 37 + .../exception/MediaServiceException.scala | 3 + .../functions/LiveVideoStreamGenerator.scala | 91 +++ .../helpers/AwsRequestBody.scala | 6 + .../livevideostream/helpers/AwsResult.scala | 54 ++ .../helpers/AwsSignUtils.scala | 133 ++++ .../helpers/AzureRequestBody.scala | 8 + .../livevideostream/helpers/AzureResult.scala | 56 ++ .../livevideostream/helpers/CaseClasses.scala | 24 + .../livevideostream/helpers/Response.scala | 65 ++ .../job/livevideostream/helpers/Result.scala | 12 + .../service/AwsMediaService.scala | 81 +++ .../service/AzureMediaService.scala | 149 ++++ .../service/IMediaService.scala | 20 + .../service/LiveVideoStreamService.scala | 208 ++++++ .../service/impl/AwsMediaServiceImpl.scala | 47 ++ .../service/impl/AzureMediaServiceImpl.scala | 56 ++ .../service/impl/MediaServiceFactory.scala | 17 + .../task/LiveVideoStreamGeneratorConfig.scala | 60 ++ .../LiveVideoStreamGeneratorStreamTask.scala | 33 +- .../src/test/resources/job_request.cql | 2 + .../src/test/resources/logback-test.xml | 0 .../src/test/resources/test.conf | 74 ++ .../src/test/resources/test.cql | 31 + .../sunbird/job/fixture/EventFixture.scala | 14 + ...LiveVideoStreamGeneratorTaskTestSpec.scala | 129 ++++ .../LiveVideoStreamServiceTestSpec.scala | 96 +++ .../job/mvcindexer/functions/MVCIndexer.scala | 2 +- .../MVCProcessorIndexerTaskTestSpec.scala | 2 +- .../MVCProcessorIndexerServiceTestSpec.scala | 2 +- pom.xml | 31 +- .../resources/post-publish-processor.conf | 13 +- .../functions/DIALCodeLinkFunction.scala | 4 +- .../functions/PostPublishEventRouter.scala | 4 +- .../postpublish/helpers/DialHelperTest.scala | 2 +- .../PostPublishProcessorTaskTestSpec.scala | 2 +- .../src/main/resources/content-publish.conf | 16 +- .../function/CollectionPublishFunction.scala | 27 +- .../function/ContentPublishFunction.scala | 8 +- .../publish/helpers/CollectionPublisher.scala | 39 +- .../publish/helpers/ContentPublisher.scala | 11 +- .../content/task/ContentPublishConfig.scala | 2 + .../task/ContentPublishStreamTask.scala | 1 + .../spec/CollectionPublisherSpec.scala | 18 +- .../helpers/spec/ContentPublisherSpec.scala | 6 +- .../spec/ContentPublishStreamTaskSpec.scala | 4 +- publish-pipeline/live-node-publisher/pom.xml | 207 ++++++ .../main/resources/live-node-publisher.conf | 176 +++++ .../src/main/resources/log4j.properties | 2 +- .../LiveCollectionPublishFunction.scala | 218 ++++++ .../function/LiveContentPublishFunction.scala | 152 ++++ .../function/LivePublishEventRouter.scala | 55 ++ .../publish/domain/Event.scala | 40 ++ .../publish/helpers/ECMLExtractor.scala | 8 + .../helpers/ExtractableMimeTypeHelper.scala | 254 +++++++ .../helpers/LiveCollectionPublisher.scala | 649 ++++++++++++++++++ .../helpers/LiveContentPublisher.scala | 230 +++++++ .../publish/helpers/LiveObjectReader.scala | 39 ++ .../publish/helpers/LiveObjectUpdater.scala | 147 ++++ .../helpers/SyncMessagesGenerator.scala | 167 +++++ .../publish/processor/BaseProcessor.scala | 9 + .../publish/processor/EcrfObject.scala | 19 + .../publish/processor/IProcessor.scala | 16 + .../publish/processor/JsonParser.scala | 218 ++++++ .../MissingAssetValidatorProcessor.scala | 45 ++ .../processor/XMLLoaderWithCData.scala | 36 + .../publish/processor/XmlParser.scala | 232 +++++++ .../task/LiveNodePublisherConfig.scala | 105 +++ .../task/LiveNodePublisherStreamTask.scala | 62 ++ .../src/test/resources/logback-test.xml | 0 .../src/test/resources/test.conf | 140 ++++ .../src/test/resources/test.cql | 58 ++ .../livenodepublisher/domain/EventSpec.scala | 21 + .../fixture/EventFixture.scala | 9 + .../spec/ExtractableMimeTypeHelperSpec.scala | 165 +++++ .../spec/LiveCollectionPublisherSpec.scala | 226 ++++++ .../spec/LiveContentPublisherSpec.scala | 243 +++++++ .../spec/LiveObjectReaderTestSpec.scala | 53 ++ .../helpers/spec/LiveObjectUpdaterSpec.scala | 80 +++ .../LiveNodePublisherStreamTaskSpec.scala | 108 +++ publish-pipeline/pom.xml | 3 +- publish-pipeline/publish-core/pom.xml | 3 +- .../job/publish/helpers/ObjectBundle.scala | 12 +- .../job/publish/helpers/ObjectReader.scala | 7 +- .../job/publish/helpers/ObjectUpdater.scala | 25 +- .../publish-core/src/test/resources/test.conf | 2 +- .../job/publish/spec/EcarGeneratorSpec.scala | 4 +- .../publish/spec/ObjectEnrichmentSpec.scala | 2 +- .../publish/spec/ObjectReaderTestSpec.scala | 6 +- .../job/publish/spec/ObjectUpdaterSpec.scala | 4 +- .../publish/spec/ThumbnailGeneratorSpec.scala | 6 +- .../main/resources/questionset-publish.conf | 14 +- .../function/QuestionPublishFunction.scala | 8 +- .../function/QuestionSetPublishFunction.scala | 10 +- .../publish/helpers/QuestionPublisher.scala | 6 +- .../helpers/QuestionSetPublisher.scala | 8 +- .../publish/util/QuestionPublishUtil.scala | 4 +- .../helpers/spec/QuestionPublisherSpec.scala | 2 +- .../spec/QuestionSetPublisherSpec.scala | 2 +- .../util/spec/QuestionPublishUtilSpec.scala | 2 +- .../QuestionSetPublishStreamTaskSpec.scala | 2 +- .../resources/qrcode-image-generator.conf | 12 + .../QRCodeImageGeneratorFunction.scala | 32 +- .../QRCodeIndexImageUrlFunction.scala | 57 ++ .../task/QRCodeImageGeneratorConfig.scala | 19 + .../task/QRCodeImageGeneratorTask.scala | 8 +- .../util/QRCodeImageGeneratorUtil.scala | 57 +- .../src/test/resources/test.conf | 6 +- .../src/test/resources/test.cql | 10 +- .../QRCodeImageGeneratorTaskTestSpec.scala | 23 +- .../util/QRCodeImageGeneratorUtilSpec.scala | 55 ++ .../resources/relation-cache-updater.conf | 26 - .../job/relationcache/domain/Event.scala | 27 - .../functions/RelationCacheUpdater.scala | 197 ------ .../task/RelationCacheUpdaterConfig.scala | 45 -- .../src/test/resources/logback-test.xml | 16 - .../src/test/resources/test.conf | 27 - .../src/test/resources/test.cql | 13 - .../sunbird/job/fixture/EventFixture.scala | 13 - .../RelationCacheUpdaterTaskTestSpec.scala | 154 ----- .../src/main/resources/search-indexer.conf | 8 +- .../CompositeSearchIndexerFunction.scala | 10 +- .../task/SearchIndexerConfig.scala | 2 + video-stream-generator/pom.xml | 43 ++ .../MediaServiceHelper.java | 107 +++ .../resources/video-stream-generator.conf | 22 +- .../videostream/helpers/JsonUtilility.scala | 25 + .../videostream/helpers/OCIRequestBody.scala | 14 + .../job/videostream/helpers/OCIResult.scala | 52 ++ .../service/VideoStreamService.scala | 27 +- .../service/impl/MediaServiceFactory.scala | 1 + .../service/impl/OCIMediaServiceImpl.scala | 118 ++++ .../task/VideoStreamGeneratorConfig.scala | 2 +- .../VideoStreamGeneratorTaskTestSpec.scala | 2 +- .../service/VideoStreamServiceTestSpec.scala | 2 +- 303 files changed, 9160 insertions(+), 9287 deletions(-) delete mode 100644 activity-aggregate-updater/README.md delete mode 100644 activity-aggregate-updater/src/main/resources/activity-aggregate-updater.conf delete mode 100644 activity-aggregate-updater/src/main/scala/org/sunbird/job/aggregate/common/DeDupHelper.scala delete mode 100644 activity-aggregate-updater/src/main/scala/org/sunbird/job/aggregate/domain/Models.scala delete mode 100644 activity-aggregate-updater/src/main/scala/org/sunbird/job/aggregate/functions/ActivityAggregatesFunction.scala delete mode 100644 activity-aggregate-updater/src/main/scala/org/sunbird/job/aggregate/functions/CollectionProgressCompleteFunction.scala delete mode 100644 activity-aggregate-updater/src/main/scala/org/sunbird/job/aggregate/functions/CollectionProgressUpdateFunction.scala delete mode 100644 activity-aggregate-updater/src/main/scala/org/sunbird/job/aggregate/functions/ContentConsumptionDeDupFunction.scala delete mode 100644 activity-aggregate-updater/src/main/scala/org/sunbird/job/aggregate/task/ActivityAggregateUpdaterConfig.scala delete mode 100644 activity-aggregate-updater/src/main/scala/org/sunbird/job/aggregate/task/ActivityAggregateUpdaterStreamTask.scala delete mode 100644 activity-aggregate-updater/src/test/resources/logback-test.xml delete mode 100644 activity-aggregate-updater/src/test/resources/test.conf delete mode 100644 activity-aggregate-updater/src/test/resources/test.cql delete mode 100644 activity-aggregate-updater/src/test/scala/org/sunbird/job/fixture/EventFixture.scala delete mode 100644 activity-aggregate-updater/src/test/scala/org/sunbird/job/spec/ActivityAggregateUpdaterTaskTestSpec.scala delete mode 100644 activity-aggregate-updater/src/test/scala/org/sunbird/job/spec/BaseActivityAggregateTestSpec.scala rename {relation-cache-updater => cassandra-data-migration}/pom.xml (92%) create mode 100644 cassandra-data-migration/src/main/resources/cassandra-data-migration.conf rename {credential-generator/collection-cert-pre-processor => cassandra-data-migration}/src/main/resources/log4j.properties (91%) create mode 100644 cassandra-data-migration/src/main/scala/org/sunbird/job/migration/domain/Event.scala create mode 100644 cassandra-data-migration/src/main/scala/org/sunbird/job/migration/functions/CassandraDataMigrationFunction.scala create mode 100644 cassandra-data-migration/src/main/scala/org/sunbird/job/migration/helpers/CassandraDataMigrator.scala create mode 100644 cassandra-data-migration/src/main/scala/org/sunbird/job/task/CassandraDataMigrationConfig.scala create mode 100644 cassandra-data-migration/src/main/scala/org/sunbird/job/task/CassandraDataMigrationStreamTask.scala create mode 100644 cassandra-data-migration/src/test/resources/test.conf create mode 100644 cassandra-data-migration/src/test/resources/test.cql create mode 100644 cassandra-data-migration/src/test/scala/org/sunbird/job/migration/fixture/EventFixture.scala create mode 100644 cassandra-data-migration/src/test/scala/org/sunbird/job/migration/helpers/CassandraDataMigratorSpec.scala create mode 100644 cassandra-data-migration/src/test/scala/org/sunbird/job/migration/task/CassandraDataMigrationTaskTestSpec.scala delete mode 100644 credential-generator/certificate-processor/certificates/8e57723e-4541-11eb-b378-0242ac130002.png delete mode 100644 credential-generator/certificate-processor/pom.xml delete mode 100755 credential-generator/certificate-processor/src/main/resources/Verdana.ttf delete mode 100755 credential-generator/certificate-processor/src/main/scala/org/sunbird/incredible/CertificateConfig.scala delete mode 100755 credential-generator/certificate-processor/src/main/scala/org/sunbird/incredible/CertificateFactory.scala delete mode 100755 credential-generator/certificate-processor/src/main/scala/org/sunbird/incredible/CertificateGenerator.scala delete mode 100755 credential-generator/certificate-processor/src/main/scala/org/sunbird/incredible/HttpUtil.scala delete mode 100755 credential-generator/certificate-processor/src/main/scala/org/sunbird/incredible/JsonKeys.scala delete mode 100755 credential-generator/certificate-processor/src/main/scala/org/sunbird/incredible/ScalaModuleJsonUtils.scala delete mode 100755 credential-generator/certificate-processor/src/main/scala/org/sunbird/incredible/UrlManager.scala delete mode 100755 credential-generator/certificate-processor/src/main/scala/org/sunbird/incredible/pojos/Gender.scala delete mode 100755 credential-generator/certificate-processor/src/main/scala/org/sunbird/incredible/pojos/exceptions/InvalidDateFormatException.scala delete mode 100755 credential-generator/certificate-processor/src/main/scala/org/sunbird/incredible/pojos/ob/Models.scala delete mode 100755 credential-generator/certificate-processor/src/main/scala/org/sunbird/incredible/pojos/valuator/ExpiryDateValuator.scala delete mode 100755 credential-generator/certificate-processor/src/main/scala/org/sunbird/incredible/pojos/valuator/IEvaluator.scala delete mode 100755 credential-generator/certificate-processor/src/main/scala/org/sunbird/incredible/pojos/valuator/IssuedDateValuator.scala delete mode 100755 credential-generator/certificate-processor/src/main/scala/org/sunbird/incredible/processor/CertModel.scala delete mode 100755 credential-generator/certificate-processor/src/main/scala/org/sunbird/incredible/processor/qrcode/AccessCodeGenerator.scala delete mode 100755 credential-generator/certificate-processor/src/main/scala/org/sunbird/incredible/processor/qrcode/QRCodeGenerationModel.scala delete mode 100755 credential-generator/certificate-processor/src/main/scala/org/sunbird/incredible/processor/qrcode/QRCodeImageGenerator.scala delete mode 100755 credential-generator/certificate-processor/src/main/scala/org/sunbird/incredible/processor/qrcode/QRCodeImageGeneratorParams.scala delete mode 100755 credential-generator/certificate-processor/src/main/scala/org/sunbird/incredible/processor/signature/Exceptions.scala delete mode 100755 credential-generator/certificate-processor/src/main/scala/org/sunbird/incredible/processor/signature/SignatureHelper.scala delete mode 100755 credential-generator/certificate-processor/src/main/scala/org/sunbird/incredible/processor/store/StorageService.scala delete mode 100755 credential-generator/certificate-processor/src/main/scala/org/sunbird/incredible/processor/views/SvgGenerator.scala delete mode 100755 credential-generator/certificate-processor/src/main/scala/org/sunbird/incredible/processor/views/VarResolver.scala delete mode 100755 credential-generator/certificate-processor/src/test/scala/org/sunbird/incredible/BaseTestSpec.scala delete mode 100755 credential-generator/certificate-processor/src/test/scala/org/sunbird/incredible/CertificateGeneratorTest.scala delete mode 100755 credential-generator/certificate-processor/src/test/scala/org/sunbird/incredible/SvgGeneratorTest.scala delete mode 100755 credential-generator/certificate-processor/src/test/scala/org/sunbird/incredible/processor/qrcode/AccessCodeGeneratorTest.scala delete mode 100755 credential-generator/certificate-processor/src/test/scala/org/sunbird/incredible/processor/qrcode/QRCodeImageGeneratorTest.scala delete mode 100755 credential-generator/certificate-processor/src/test/scala/org/sunbird/incredible/valuator/ExpiryDateValuatorTest.scala delete mode 100755 credential-generator/certificate-processor/src/test/scala/org/sunbird/incredible/valuator/IssuedDateValuatorTest.scala delete mode 100644 credential-generator/collection-cert-pre-processor/pom.xml delete mode 100644 credential-generator/collection-cert-pre-processor/src/main/resources/collection-cert-pre-processor.conf delete mode 100644 credential-generator/collection-cert-pre-processor/src/main/scala/org/sunbird/job/collectioncert/domain/Event.scala delete mode 100644 credential-generator/collection-cert-pre-processor/src/main/scala/org/sunbird/job/collectioncert/domain/Models.scala delete mode 100644 credential-generator/collection-cert-pre-processor/src/main/scala/org/sunbird/job/collectioncert/functions/CollectionCertPreProcessorFn.scala delete mode 100644 credential-generator/collection-cert-pre-processor/src/main/scala/org/sunbird/job/collectioncert/functions/IssueCertificateHelper.scala delete mode 100644 credential-generator/collection-cert-pre-processor/src/main/scala/org/sunbird/job/collectioncert/task/CollectionCertPreProcessorConfig.scala delete mode 100644 credential-generator/collection-cert-pre-processor/src/main/scala/org/sunbird/job/collectioncert/task/CollectionCertPreProcessorTask.scala delete mode 100644 credential-generator/collection-cert-pre-processor/src/test/resources/test.conf delete mode 100644 credential-generator/collection-cert-pre-processor/src/test/resources/test.cql delete mode 100644 credential-generator/collection-cert-pre-processor/src/test/scala/org/sunbird/job/collectioncert/fixture/EventFixture.scala delete mode 100644 credential-generator/collection-cert-pre-processor/src/test/scala/org/sunbird/job/collectioncert/function/spec/CollectionCertPreProcessFnTestSpec.scala delete mode 100644 credential-generator/collection-cert-pre-processor/src/test/scala/org/sunbird/job/collectioncert/function/spec/CollectionCertPreProcessorTaskSpec.scala delete mode 100755 credential-generator/collection-certificate-generator/pom.xml delete mode 100755 credential-generator/collection-certificate-generator/src/main/resources/collection-certificate-generator.conf delete mode 100755 credential-generator/collection-certificate-generator/src/main/resources/log4j.properties delete mode 100755 credential-generator/collection-certificate-generator/src/main/scala/org/sunbird/job/certgen/domain/Event.scala delete mode 100755 credential-generator/collection-certificate-generator/src/main/scala/org/sunbird/job/certgen/domain/Models.scala delete mode 100755 credential-generator/collection-certificate-generator/src/main/scala/org/sunbird/job/certgen/exceptions/ErrorMessages.scala delete mode 100755 credential-generator/collection-certificate-generator/src/main/scala/org/sunbird/job/certgen/exceptions/ValidationException.scala delete mode 100755 credential-generator/collection-certificate-generator/src/main/scala/org/sunbird/job/certgen/functions/CertMapper.scala delete mode 100755 credential-generator/collection-certificate-generator/src/main/scala/org/sunbird/job/certgen/functions/CertValidator.scala delete mode 100755 credential-generator/collection-certificate-generator/src/main/scala/org/sunbird/job/certgen/functions/CertificateGeneratorFunction.scala delete mode 100755 credential-generator/collection-certificate-generator/src/main/scala/org/sunbird/job/certgen/functions/CreateUserFeedFunction.scala delete mode 100755 credential-generator/collection-certificate-generator/src/main/scala/org/sunbird/job/certgen/functions/NotifierFunction.scala delete mode 100755 credential-generator/collection-certificate-generator/src/main/scala/org/sunbird/job/certgen/task/CertificateGeneratorConfig.scala delete mode 100755 credential-generator/collection-certificate-generator/src/main/scala/org/sunbird/job/certgen/task/CertificateGeneratorStreamTask.scala delete mode 100755 credential-generator/collection-certificate-generator/src/test/resources/logback-test.xml delete mode 100755 credential-generator/collection-certificate-generator/src/test/resources/test.conf delete mode 100755 credential-generator/collection-certificate-generator/src/test/resources/test.cql delete mode 100644 credential-generator/collection-certificate-generator/src/test/scala/org/sunbird/job/certgen/fixture/EventFixture.scala delete mode 100644 credential-generator/collection-certificate-generator/src/test/scala/org/sunbird/job/certgen/spec/CertValidatorTest.scala delete mode 100755 credential-generator/collection-certificate-generator/src/test/scala/org/sunbird/job/certgen/spec/CertificateGeneratorFunctionTaskTestSpec.scala delete mode 100644 credential-generator/collection-certificate-generator/src/test/scala/org/sunbird/job/certgen/spec/CertificateGeneratorFunctionTest.scala delete mode 100755 credential-generator/collection-certificate-generator/src/test/scala/org/sunbird/job/certgen/spec/CreateUserFeedFunctionTest.scala delete mode 100755 credential-generator/collection-certificate-generator/src/test/scala/org/sunbird/job/certgen/spec/NotifierFunctionTest.scala delete mode 100644 credential-generator/pom.xml rename {activity-aggregate-updater => csp-migrator}/pom.xml (85%) create mode 100644 csp-migrator/src/main/resources/csp-migrator.conf rename {relation-cache-updater => csp-migrator}/src/main/resources/log4j.properties (91%) create mode 100644 csp-migrator/src/main/scala/org/sunbird/job/cspmigrator/domain/Event.scala create mode 100644 csp-migrator/src/main/scala/org/sunbird/job/cspmigrator/functions/CSPCassandraMigratorFunction.scala create mode 100644 csp-migrator/src/main/scala/org/sunbird/job/cspmigrator/functions/CSPNeo4jMigratorFunction.scala create mode 100644 csp-migrator/src/main/scala/org/sunbird/job/cspmigrator/helpers/CSPCassandraMigrator.scala create mode 100644 csp-migrator/src/main/scala/org/sunbird/job/cspmigrator/helpers/CSPNeo4jMigrator.scala create mode 100644 csp-migrator/src/main/scala/org/sunbird/job/cspmigrator/helpers/GoogleDriveUtil.scala create mode 100644 csp-migrator/src/main/scala/org/sunbird/job/cspmigrator/helpers/MigrationObjectReader.scala create mode 100644 csp-migrator/src/main/scala/org/sunbird/job/cspmigrator/helpers/MigrationObjectUpdater.scala create mode 100644 csp-migrator/src/main/scala/org/sunbird/job/cspmigrator/helpers/URLExtractor.scala create mode 100644 csp-migrator/src/main/scala/org/sunbird/job/cspmigrator/task/CSPMigratorConfig.scala create mode 100644 csp-migrator/src/main/scala/org/sunbird/job/cspmigrator/task/CSPMigratorStreamTask.scala create mode 100644 csp-migrator/src/test/resources/base-test.conf create mode 100644 csp-migrator/src/test/resources/test.conf create mode 100644 csp-migrator/src/test/resources/test.cql create mode 100644 csp-migrator/src/test/scala/org.sunbird.job.cspmigrator.spec/CSPMigratorSpec.scala create mode 100644 csp-migrator/src/test/scala/org.sunbird.job.cspmigrator.spec/URLExtractorSpec.scala delete mode 100644 enrolment-reconciliation/README.md delete mode 100644 enrolment-reconciliation/src/main/resources/enrolment-reconciliation.conf delete mode 100644 enrolment-reconciliation/src/main/scala/org/sunbird/job/recounciliation/domain/Event.scala delete mode 100644 enrolment-reconciliation/src/main/scala/org/sunbird/job/recounciliation/domain/Models.scala delete mode 100644 enrolment-reconciliation/src/main/scala/org/sunbird/job/recounciliation/functions/EnrolmentReconciliationFn.scala delete mode 100644 enrolment-reconciliation/src/main/scala/org/sunbird/job/recounciliation/functions/ProgressCompleteFunction.scala delete mode 100644 enrolment-reconciliation/src/main/scala/org/sunbird/job/recounciliation/functions/ProgressUpdateFunction.scala delete mode 100644 enrolment-reconciliation/src/main/scala/org/sunbird/job/recounciliation/task/EnrolmentReconciliationConfig.scala delete mode 100644 enrolment-reconciliation/src/main/scala/org/sunbird/job/recounciliation/task/EnrolmentReconciliationStreamTask.scala delete mode 100644 enrolment-reconciliation/src/test/resources/test.conf delete mode 100644 enrolment-reconciliation/src/test/resources/test.cql delete mode 100644 enrolment-reconciliation/src/test/scala/org/sunbird/job/fixture/EventFixture.scala delete mode 100644 enrolment-reconciliation/src/test/scala/org/sunbird/job/spec/BaseActivityAggregateTestSpec.scala delete mode 100644 enrolment-reconciliation/src/test/scala/org/sunbird/job/spec/EnrolmentReconciliationStreamTaskSpec.scala create mode 100644 jobs-core/src/main/scala/org/sunbird/job/util/CSPMetaUtil.scala rename {relation-cache-updater => live-video-stream-generator}/README.md (84%) rename {enrolment-reconciliation => live-video-stream-generator}/pom.xml (90%) create mode 100644 live-video-stream-generator/src/main/resources/live-video-stream-generator.conf rename {activity-aggregate-updater => live-video-stream-generator}/src/main/resources/log4j.properties (90%) create mode 100644 live-video-stream-generator/src/main/scala/org/sunbird/job/livevideostream/domain/Event.scala create mode 100644 live-video-stream-generator/src/main/scala/org/sunbird/job/livevideostream/exception/MediaServiceException.scala create mode 100644 live-video-stream-generator/src/main/scala/org/sunbird/job/livevideostream/functions/LiveVideoStreamGenerator.scala create mode 100644 live-video-stream-generator/src/main/scala/org/sunbird/job/livevideostream/helpers/AwsRequestBody.scala create mode 100644 live-video-stream-generator/src/main/scala/org/sunbird/job/livevideostream/helpers/AwsResult.scala create mode 100644 live-video-stream-generator/src/main/scala/org/sunbird/job/livevideostream/helpers/AwsSignUtils.scala create mode 100644 live-video-stream-generator/src/main/scala/org/sunbird/job/livevideostream/helpers/AzureRequestBody.scala create mode 100644 live-video-stream-generator/src/main/scala/org/sunbird/job/livevideostream/helpers/AzureResult.scala create mode 100644 live-video-stream-generator/src/main/scala/org/sunbird/job/livevideostream/helpers/CaseClasses.scala create mode 100644 live-video-stream-generator/src/main/scala/org/sunbird/job/livevideostream/helpers/Response.scala create mode 100644 live-video-stream-generator/src/main/scala/org/sunbird/job/livevideostream/helpers/Result.scala create mode 100644 live-video-stream-generator/src/main/scala/org/sunbird/job/livevideostream/service/AwsMediaService.scala create mode 100644 live-video-stream-generator/src/main/scala/org/sunbird/job/livevideostream/service/AzureMediaService.scala create mode 100644 live-video-stream-generator/src/main/scala/org/sunbird/job/livevideostream/service/IMediaService.scala create mode 100644 live-video-stream-generator/src/main/scala/org/sunbird/job/livevideostream/service/LiveVideoStreamService.scala create mode 100644 live-video-stream-generator/src/main/scala/org/sunbird/job/livevideostream/service/impl/AwsMediaServiceImpl.scala create mode 100644 live-video-stream-generator/src/main/scala/org/sunbird/job/livevideostream/service/impl/AzureMediaServiceImpl.scala create mode 100644 live-video-stream-generator/src/main/scala/org/sunbird/job/livevideostream/service/impl/MediaServiceFactory.scala create mode 100644 live-video-stream-generator/src/main/scala/org/sunbird/job/livevideostream/task/LiveVideoStreamGeneratorConfig.scala rename relation-cache-updater/src/main/scala/org/sunbird/job/relationcache/task/RelationCacheUpdaterStreamTask.scala => live-video-stream-generator/src/main/scala/org/sunbird/job/livevideostream/task/LiveVideoStreamGeneratorStreamTask.scala (55%) create mode 100644 live-video-stream-generator/src/test/resources/job_request.cql rename {credential-generator/collection-cert-pre-processor => live-video-stream-generator}/src/test/resources/logback-test.xml (100%) create mode 100644 live-video-stream-generator/src/test/resources/test.conf create mode 100644 live-video-stream-generator/src/test/resources/test.cql create mode 100644 live-video-stream-generator/src/test/scala/org/sunbird/job/fixture/EventFixture.scala create mode 100644 live-video-stream-generator/src/test/scala/org/sunbird/job/spec/LiveVideoStreamGeneratorTaskTestSpec.scala create mode 100644 live-video-stream-generator/src/test/scala/org/sunbird/job/spec/service/LiveVideoStreamServiceTestSpec.scala create mode 100644 publish-pipeline/live-node-publisher/pom.xml create mode 100644 publish-pipeline/live-node-publisher/src/main/resources/live-node-publisher.conf rename {enrolment-reconciliation => publish-pipeline/live-node-publisher}/src/main/resources/log4j.properties (91%) create mode 100644 publish-pipeline/live-node-publisher/src/main/scala/org/sunbird/job/livenodepublisher/function/LiveCollectionPublishFunction.scala create mode 100644 publish-pipeline/live-node-publisher/src/main/scala/org/sunbird/job/livenodepublisher/function/LiveContentPublishFunction.scala create mode 100644 publish-pipeline/live-node-publisher/src/main/scala/org/sunbird/job/livenodepublisher/function/LivePublishEventRouter.scala create mode 100644 publish-pipeline/live-node-publisher/src/main/scala/org/sunbird/job/livenodepublisher/publish/domain/Event.scala create mode 100644 publish-pipeline/live-node-publisher/src/main/scala/org/sunbird/job/livenodepublisher/publish/helpers/ECMLExtractor.scala create mode 100644 publish-pipeline/live-node-publisher/src/main/scala/org/sunbird/job/livenodepublisher/publish/helpers/ExtractableMimeTypeHelper.scala create mode 100644 publish-pipeline/live-node-publisher/src/main/scala/org/sunbird/job/livenodepublisher/publish/helpers/LiveCollectionPublisher.scala create mode 100644 publish-pipeline/live-node-publisher/src/main/scala/org/sunbird/job/livenodepublisher/publish/helpers/LiveContentPublisher.scala create mode 100644 publish-pipeline/live-node-publisher/src/main/scala/org/sunbird/job/livenodepublisher/publish/helpers/LiveObjectReader.scala create mode 100644 publish-pipeline/live-node-publisher/src/main/scala/org/sunbird/job/livenodepublisher/publish/helpers/LiveObjectUpdater.scala create mode 100644 publish-pipeline/live-node-publisher/src/main/scala/org/sunbird/job/livenodepublisher/publish/helpers/SyncMessagesGenerator.scala create mode 100644 publish-pipeline/live-node-publisher/src/main/scala/org/sunbird/job/livenodepublisher/publish/processor/BaseProcessor.scala create mode 100644 publish-pipeline/live-node-publisher/src/main/scala/org/sunbird/job/livenodepublisher/publish/processor/EcrfObject.scala create mode 100644 publish-pipeline/live-node-publisher/src/main/scala/org/sunbird/job/livenodepublisher/publish/processor/IProcessor.scala create mode 100644 publish-pipeline/live-node-publisher/src/main/scala/org/sunbird/job/livenodepublisher/publish/processor/JsonParser.scala create mode 100644 publish-pipeline/live-node-publisher/src/main/scala/org/sunbird/job/livenodepublisher/publish/processor/MissingAssetValidatorProcessor.scala create mode 100644 publish-pipeline/live-node-publisher/src/main/scala/org/sunbird/job/livenodepublisher/publish/processor/XMLLoaderWithCData.scala create mode 100644 publish-pipeline/live-node-publisher/src/main/scala/org/sunbird/job/livenodepublisher/publish/processor/XmlParser.scala create mode 100644 publish-pipeline/live-node-publisher/src/main/scala/org/sunbird/job/livenodepublisher/task/LiveNodePublisherConfig.scala create mode 100644 publish-pipeline/live-node-publisher/src/main/scala/org/sunbird/job/livenodepublisher/task/LiveNodePublisherStreamTask.scala rename {enrolment-reconciliation => publish-pipeline/live-node-publisher}/src/test/resources/logback-test.xml (100%) create mode 100644 publish-pipeline/live-node-publisher/src/test/resources/test.conf create mode 100644 publish-pipeline/live-node-publisher/src/test/resources/test.cql create mode 100644 publish-pipeline/live-node-publisher/src/test/scala/org/sunbird/job/livenodepublisher/domain/EventSpec.scala create mode 100644 publish-pipeline/live-node-publisher/src/test/scala/org/sunbird/job/livenodepublisher/fixture/EventFixture.scala create mode 100644 publish-pipeline/live-node-publisher/src/test/scala/org/sunbird/job/livenodepublisher/publish/helpers/spec/ExtractableMimeTypeHelperSpec.scala create mode 100644 publish-pipeline/live-node-publisher/src/test/scala/org/sunbird/job/livenodepublisher/publish/helpers/spec/LiveCollectionPublisherSpec.scala create mode 100644 publish-pipeline/live-node-publisher/src/test/scala/org/sunbird/job/livenodepublisher/publish/helpers/spec/LiveContentPublisherSpec.scala create mode 100644 publish-pipeline/live-node-publisher/src/test/scala/org/sunbird/job/livenodepublisher/publish/helpers/spec/LiveObjectReaderTestSpec.scala create mode 100644 publish-pipeline/live-node-publisher/src/test/scala/org/sunbird/job/livenodepublisher/publish/helpers/spec/LiveObjectUpdaterSpec.scala create mode 100644 publish-pipeline/live-node-publisher/src/test/scala/org/sunbird/job/livenodepublisher/spec/LiveNodePublisherStreamTaskSpec.scala create mode 100644 qrcode-image-generator/src/main/scala/org/sunbird/job/qrimagegenerator/functions/QRCodeIndexImageUrlFunction.scala create mode 100644 qrcode-image-generator/src/test/scala/org/sunbird/job/util/QRCodeImageGeneratorUtilSpec.scala delete mode 100644 relation-cache-updater/src/main/resources/relation-cache-updater.conf delete mode 100644 relation-cache-updater/src/main/scala/org/sunbird/job/relationcache/domain/Event.scala delete mode 100644 relation-cache-updater/src/main/scala/org/sunbird/job/relationcache/functions/RelationCacheUpdater.scala delete mode 100644 relation-cache-updater/src/main/scala/org/sunbird/job/relationcache/task/RelationCacheUpdaterConfig.scala delete mode 100644 relation-cache-updater/src/test/resources/logback-test.xml delete mode 100644 relation-cache-updater/src/test/resources/test.conf delete mode 100644 relation-cache-updater/src/test/resources/test.cql delete mode 100644 relation-cache-updater/src/test/scala/org/sunbird/job/fixture/EventFixture.scala delete mode 100644 relation-cache-updater/src/test/scala/org/sunbird/job/spec/RelationCacheUpdaterTaskTestSpec.scala create mode 100644 video-stream-generator/src/main/java/org.sunbird.job.videostream.helpers/MediaServiceHelper.java create mode 100644 video-stream-generator/src/main/scala/org/sunbird/job/videostream/helpers/JsonUtilility.scala create mode 100644 video-stream-generator/src/main/scala/org/sunbird/job/videostream/helpers/OCIRequestBody.scala create mode 100644 video-stream-generator/src/main/scala/org/sunbird/job/videostream/helpers/OCIResult.scala create mode 100644 video-stream-generator/src/main/scala/org/sunbird/job/videostream/service/impl/OCIMediaServiceImpl.scala diff --git a/.circleci/config.yml b/.circleci/config.yml index dc8b5a8c0..378d1a698 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -3,7 +3,7 @@ version: 2.0 jobs: unit-tests: docker: - - image: circleci/openjdk:stretch + - image: circleci/openjdk:14-jdk-buster-node-browsers-legacy resource_class: medium working_directory: ~/kp steps: @@ -15,12 +15,12 @@ jobs: - run: name: Installation of imagemagick command: | - sudo apt-get update || sudo apt-get update + sudo apt-get update --allow-releaseinfo-change || sudo apt-get update --allow-releaseinfo-change sudo apt-get install -y imagemagick - run: name: Execute coverage report command: | - mvn clean scoverage:report + mvn clean scoverage:report -DCLOUD_STORE_GROUP_ID=$CLOUD_STORE_GROUP_ID -DCLOUD_STORE_ARTIFACT_ID=$CLOUD_STORE_ARTIFACT_ID -DCLOUD_STORE_VERSION=$CLOUD_STORE_VERSION - run: name: Save test results command: | @@ -42,4 +42,4 @@ workflows: version: 2 build-and-test: jobs: - - unit-tests \ No newline at end of file + - unit-tests diff --git a/.gitignore b/.gitignore index c99a55f7a..39ffae611 100644 --- a/.gitignore +++ b/.gitignore @@ -15,4 +15,5 @@ kubernets/config .classpath .factorypath bin/ -.settings/ \ No newline at end of file +.settings/ +.vscode diff --git a/activity-aggregate-updater/README.md b/activity-aggregate-updater/README.md deleted file mode 100644 index cf46b9fd3..000000000 --- a/activity-aggregate-updater/README.md +++ /dev/null @@ -1,66 +0,0 @@ -# Activity Aggregate Updater - -Activity Aggregate Updater job is used to compute the progress for unit level and course level for each batch and user and updates to database. - -## Getting Started - -These instructions will get you a copy of the project up and running on your local machine for development and testing purposes. See deployment for notes on how to deploy the project on a yarn or kubernetes. -Design wiki link: https://project-sunbird.atlassian.net/wiki/spaces/SBDES/pages/1493041222/Courses+Infra+-+Design -### Prerequisites - -1. Download flink-1.13.6-scala_2.12 from [apache-flink-downloads](https://www.apache.org/dyn/closer.lua/flink/flink-1.13.6/flink-1.13.6-bin-scala_2.12.tgz). -2. Download [hadoop dependencies](https://repo.maven.apache.org/maven2/org/apache/flink/flink-shaded-hadoop-2-uber/2.8.3-10.0/flink-shaded-hadoop-2-uber-2.8.3-10.0.jar) (only for running on Yarn). Copy the hadoop dependency jar under lib folder of the flink download. -3. export HADOOP_CLASSPATH=`/hadoop classpath` either in .bashrc or current execution shell. -4. Docker installed. -5. A running yarn cluster or a kubernetes cluster. - -### Build - -mvn clean install - -## Deployment - -### Yarn - -Flink requires memory to be allocated for both job-manager and task manager. -yjm parameter assigns job-manager memory and -ytm assigns task-manager memory. - -``` -./bin/flink run -m yarn-cluster -p 2 -yjm 1024m -ytm 1024m /activity-aggregate-updater/target/activity-aggregate-updater-0.0.1.jar -``` - -### Kubernetes - -``` -# Create a single node cluster -k3d create --server-arg --no-deploy --server-arg traefik --name flink-cluster --image rancher/k3s:v1.0.0 -# Export the single node cluster into KUBECONFIG in the current shell or in ~/.bashrc. -export KUBECONFIG="$(k3d get-kubeconfig --name='flink-cluster')" - -# Only for Mac OSX -# /usr/local/bin/kubectl -> /Applications/Docker.app/Contents/Resources/bin/kubectl -rm /usr/local/bin/kubectl -brew link --overwrite kubernetes-cli - -# Create a configmap using the flink-configuration-configmap.yaml -kubectl create -f knowledge-platform-job/kubernetes/flink-configuration-configmap.yaml - -# Create pods for jobmanager-service, job-manager and task-manager using the yaml files -kubectl create -f knowledge-platform-job/kubernetes/jobmanager-service.yaml -kubectl create -f knowledge-platform-job/kubernetes/jobmanager-deployment.yaml -kubectl create -f knowledge-platform-job/kubernetes/taskmanager-deployment.yaml - -# Create a port-forwarding for accessing the job-manager UI on localhost:8081 -kubectl port-forward deployment/flink-jobmanager 8081:8081 - -# Submit the job to the Kubernetes single node cluster flink-cluster -./bin/flink run -m localhost:8081 /activity-aggregate-updater/target/activity-aggregate-updater-0.0.1.jar - -# Commands to delete the pods created in the cluster -kubectl delete deployment/flink-jobmanager -kubectl delete deployment/flink-taskmanager -kubectl delete service/flink-jobmanager -kubectl delete configmaps/flink-config - -# Command to stop the single-node cluster -k3d stop --name="flink-cluster" -``` diff --git a/activity-aggregate-updater/src/main/resources/activity-aggregate-updater.conf b/activity-aggregate-updater/src/main/resources/activity-aggregate-updater.conf deleted file mode 100644 index 9f8d79d13..000000000 --- a/activity-aggregate-updater/src/main/resources/activity-aggregate-updater.conf +++ /dev/null @@ -1,54 +0,0 @@ -include "base-config.conf" - -kafka { - input.topic = "sunbirddev.coursebatch.job.request" - output.audit.topic = "sunbirddev.telemetry.raw" - output.failed.topic = "sunbirddev.activity.agg.failed" - output.certissue.topic = "sunbirddev.issue.certificate.request" - groupId = "sunbirddev-activity-aggregate-updater-group" -} - -task { - window.shards = 1 - consumer.parallelism = 1 - dedup.parallelism = 1 - activity.agg.parallelism = 1 - enrolment.complete.parallelism = 1 -} - -lms-cassandra { - keyspace = "sunbird_courses" - consumption.table = "user_content_consumption" - user_activity_agg.table = "user_activity_agg" - user_enrolments.table = "user_enrolments" -} - -redis { - database { - relationCache.id = 10 - } -} - -dedup-redis { - host = 11.2.4.22 - port = 6379 - database.index = 3 - database.expiry = 604800 -} - -threshold.batch.read.interval = 60 // In sec -threshold.batch.read.size = 1000 -threshold.batch.write.size = 10 - -activity { - module.aggs.enabled = true - input.dedup.enabled = true - filter.processed.enrolments = true - collection.status.cache.expiry = 3600 -} - -service { - search { - basePath = "http://11.2.6.6/search" - } -} \ No newline at end of file diff --git a/activity-aggregate-updater/src/main/scala/org/sunbird/job/aggregate/common/DeDupHelper.scala b/activity-aggregate-updater/src/main/scala/org/sunbird/job/aggregate/common/DeDupHelper.scala deleted file mode 100644 index 3bce467e3..000000000 --- a/activity-aggregate-updater/src/main/scala/org/sunbird/job/aggregate/common/DeDupHelper.scala +++ /dev/null @@ -1,12 +0,0 @@ -package org.sunbird.job.aggregate.common - -import java.security.MessageDigest - -object DeDupHelper { - - def getMessageId(collectionId: String, batchId: String, userId: String, contentId: String, status: Int): String = { - val key = Array(collectionId, batchId, userId, contentId, status).mkString("|") - MessageDigest.getInstance("MD5").digest(key.getBytes).map("%02X".format(_)).mkString; - } - -} diff --git a/activity-aggregate-updater/src/main/scala/org/sunbird/job/aggregate/domain/Models.scala b/activity-aggregate-updater/src/main/scala/org/sunbird/job/aggregate/domain/Models.scala deleted file mode 100644 index fab648368..000000000 --- a/activity-aggregate-updater/src/main/scala/org/sunbird/job/aggregate/domain/Models.scala +++ /dev/null @@ -1,52 +0,0 @@ -package org.sunbird.job.aggregate.domain - -import java.util -import java.util.{Date, UUID} - -import scala.collection.JavaConverters._ - - -case class ActorObject(id: String, `type`: String = "User") - -case class EventContext(channel: String = "in.sunbird", - env: String = "Course", - sid: String = UUID.randomUUID().toString, - did: String = UUID.randomUUID().toString, - pdata: util.Map[String, String] = Map("ver" -> "3.0", "id" -> "org.sunbird.learning.platform", "pid" -> "course-progress-updater").asJava, - cdata: Array[util.Map[String, String]]) - - -case class EventData(props: Array[String], `type`: String) - -case class EventObject(id: String, `type`: String, rollup: util.Map[String, String]) - -case class TelemetryEvent(actor: ActorObject, - eid: String = "AUDIT", - edata: EventData, - ver: String = "3.0", - syncts: Long = System.currentTimeMillis(), - ets: Long = System.currentTimeMillis(), - context: EventContext = EventContext( - cdata = Array[util.Map[String, String]]() - ), - mid: String = s"LP.AUDIT.${UUID.randomUUID().toString}", - `object`: EventObject, - tags: util.List[AnyRef] = new util.ArrayList[AnyRef]() - ) - -case class ContentStatus(contentId: String, status: Int = 0, completedCount: Int = 0, viewCount: Int = 1, fromInput: Boolean = true, eventsFor: List[String] = List()) - -case class UserContentConsumption(userId: String, batchId: String, courseId: String, contents: Map[String, ContentStatus]) - -case class UserActivityAgg(activity_type: String, - user_id: String, - activity_id: String, - context_id: String, - aggregates: Map[String, Double], - agg_last_updated: Map[String, Long] - ) - -case class CollectionProgress(userId: String, batchId: String, courseId: String, progress: Int, completedOn: Date, contentStatus: Map[String, Int], inputContents: List[String], completed: Boolean = false) - -case class UserEnrolmentAgg(activityAgg: UserActivityAgg, collectionProgress: Option[CollectionProgress] = None) - diff --git a/activity-aggregate-updater/src/main/scala/org/sunbird/job/aggregate/functions/ActivityAggregatesFunction.scala b/activity-aggregate-updater/src/main/scala/org/sunbird/job/aggregate/functions/ActivityAggregatesFunction.scala deleted file mode 100644 index 70cc690dc..000000000 --- a/activity-aggregate-updater/src/main/scala/org/sunbird/job/aggregate/functions/ActivityAggregatesFunction.scala +++ /dev/null @@ -1,449 +0,0 @@ -package org.sunbird.job.aggregate.functions - -import java.lang.reflect.Type -import java.util.concurrent.TimeUnit - -import com.datastax.driver.core.Row -import com.datastax.driver.core.querybuilder.{QueryBuilder, Select, Update} -import com.google.gson.Gson -import com.google.gson.reflect.TypeToken -import com.twitter.storehaus.cache.TTLCache -import com.twitter.util.Duration -import org.apache.commons.collections.CollectionUtils -import org.apache.commons.lang3.StringUtils -import org.apache.flink.api.common.typeinfo.TypeInformation -import org.apache.flink.configuration.Configuration -import org.apache.flink.streaming.api.scala.function.ProcessWindowFunction -import org.apache.flink.streaming.api.windowing.windows.GlobalWindow -import org.slf4j.LoggerFactory -import org.sunbird.job.cache.{DataCache, RedisConnect} -import org.sunbird.job.aggregate.domain.{UserContentConsumption, _} -import org.sunbird.job.aggregate.task.ActivityAggregateUpdaterConfig -import org.sunbird.job.util.{CassandraUtil, HttpUtil} -import org.sunbird.job.{Metrics, WindowBaseProcessFunction} - -import scala.collection.JavaConverters._ - -class ActivityAggregatesFunction(config: ActivityAggregateUpdaterConfig, httpUtil: HttpUtil, @transient var cassandraUtil: CassandraUtil = null) - (implicit val stringTypeInfo: TypeInformation[String]) - extends WindowBaseProcessFunction[Map[String, AnyRef], String, Int](config) { - - val mapType: Type = new TypeToken[Map[String, AnyRef]]() {}.getType - private[this] val logger = LoggerFactory.getLogger(classOf[ActivityAggregatesFunction]) - private var cache: DataCache = _ - private var collectionStatusCache: TTLCache[String, String] = _ - lazy private val gson = new Gson() - - override def metricsList(): List[String] = { - List(config.failedEventCount, config.dbUpdateCount, config.dbReadCount, config.cacheHitCount, config.cacheMissCount, config.processedEnrolmentCount, config.retiredCCEventsCount) - } - - override def open(parameters: Configuration): Unit = { - super.open(parameters) - cassandraUtil = new CassandraUtil(config.dbHost, config.dbPort) - cache = new DataCache(config, new RedisConnect(config), config.nodeStore, List()) - cache.init() - collectionStatusCache = TTLCache[String, String](Duration.apply(config.statusCacheExpirySec, TimeUnit.SECONDS)) - } - - override def close(): Unit = { - cassandraUtil.close() - cache.close() - super.close() - } - - override def process(key: Int, - context: ProcessWindowFunction[Map[String, AnyRef], String, Int, GlobalWindow]#Context, - events: Iterable[Map[String, AnyRef]], - metrics: Metrics): Unit = { - - logger.debug("Input Events Size: " + events.toList.size) - val inputUserConsumptionList: List[UserContentConsumption] = events - .groupBy(key => (key.get(config.courseId), key.get(config.batchId), key.get(config.userId))) - .values.map(value => { - metrics.incCounter(config.processedEnrolmentCount) - val batchId = value.head(config.batchId).toString - val userId = value.head(config.userId).toString - val courseId = value.head(config.courseId).toString - val userConsumedContents = value.head(config.contents).asInstanceOf[List[Map[String, AnyRef]]] - val enrichedContents = getContentStatusFromEvent(userConsumedContents) - UserContentConsumption(userId = userId, batchId = batchId, courseId = courseId, enrichedContents) - }).toList - - // Fetch the content status from the table in batch format - val dbUserConsumption: Map[String, UserContentConsumption] = getContentStatusFromDB(events.toList, metrics) - - // Final User's ContentConsumption after merging with DB data. - // Here we have final viewcount, completedcount and identified the content which should generate AUDIT events for start and complete. - val finalUserConsumptionList = inputUserConsumptionList.map(inputData => { - val dbData = dbUserConsumption.getOrElse(getUCKey(inputData), UserContentConsumption(inputData.userId, inputData.batchId, inputData.courseId, Map())) - finalUserConsumption(inputData, dbData)(metrics) - }) - - // user_content_consumption update with viewcount and completedcout. - val userConsumptionQueries = finalUserConsumptionList.flatMap(userConsumption => getContentConsumptionQueries(userConsumption)) - updateDB(config.thresholdBatchWriteSize, userConsumptionQueries)(metrics) - - - val courseAggregations = finalUserConsumptionList.flatMap(userConsumption => { - - // Course Level Agg using the merged data of ContentConsumption per user, course and batch. - val optCourseAgg = courseActivityAgg(userConsumption, context)(metrics) - val courseAggs = if (optCourseAgg.nonEmpty) List(optCourseAgg.get) else List() - - // Identify the children of the course (only collections) for which aggregates computation required. - // Computation of aggregates using leafNodes (of the specific collection) and user completed contents. - // Here computing only "completedCount" aggregate. - if (config.moduleAggEnabled) { - val courseChildrenAggs = courseChildrenActivityAgg(userConsumption)(metrics) - courseAggs ++ courseChildrenAggs - } else courseAggs - }) - - // Saving all queries for course and it's children (only collection) aggregates. - val aggQueries = courseAggregations.map(agg => getUserAggQuery(agg.activityAgg)) - updateDB(config.thresholdBatchWriteSize, aggQueries)(metrics) - - // Saving enrolment completion data. - val collectionProgressList = courseAggregations.filter(agg => agg.collectionProgress.nonEmpty).map(agg => agg.collectionProgress.get) - - val collectionProgressUpdateList = collectionProgressList.filter(progress => !progress.completed) - context.output(config.collectionUpdateOutputTag, collectionProgressUpdateList) - - val collectionProgressCompleteList = collectionProgressList.filter(progress => progress.completed) - context.output(config.collectionCompleteOutputTag, collectionProgressCompleteList) - - // Content AUDIT Event generation and pushing to output tag. - finalUserConsumptionList.flatMap(userConsumption => contentAuditEvents(userConsumption)).foreach(event => context.output(config.auditEventOutputTag, gson.toJson(event))) - - } - - /** - * Course Level Agg using the merged data of ContentConsumption per user, course and batch. - */ - def courseActivityAgg(userConsumption: UserContentConsumption, context: ProcessWindowFunction[Map[String, AnyRef], String, Int, GlobalWindow]#Context)(implicit metrics: Metrics): Option[UserEnrolmentAgg] = { - val courseId = userConsumption.courseId - val userId = userConsumption.userId - val contextId = "cb:" + userConsumption.batchId - val key = s"$courseId:$courseId:${config.leafNodes}" - val leafNodes = readFromCache(key, metrics).distinct - if (leafNodes.isEmpty) { - logger.error(s"leaf nodes are not available for: $key") - context.output(config.failedEventOutputTag, gson.toJson(userConsumption)) - val status = getCollectionStatus(courseId) - if (StringUtils.equals("Retired", status)) { - metrics.incCounter(config.retiredCCEventsCount) - println(s"contents consumed from a retired collection: $courseId") - logger.warn(s"contents consumed from a retired collection: $courseId") - None - } else { - metrics.incCounter(config.failedEventCount) - val message = s"leaf nodes are not available for a published collection: $courseId" - logger.error(message) - throw new Exception(message) - } - } else { - val completedCount = leafNodes.intersect(userConsumption.contents.filter(cc => cc._2.status == 2).map(cc => cc._2.contentId).toList.distinct).size - val contentStatus = userConsumption.contents.map(cc => (cc._2.contentId, cc._2.status)).toMap - val inputContents = userConsumption.contents.filter(cc => cc._2.fromInput).keys.toList - val collectionProgress = if (completedCount >= leafNodes.size) { - Option(CollectionProgress(userId, userConsumption.batchId, courseId, completedCount, new java.util.Date(), contentStatus, inputContents, true)) - } else { - Option(CollectionProgress(userId, userConsumption.batchId, courseId, completedCount, null, contentStatus, inputContents)) - } - Option(UserEnrolmentAgg(UserActivityAgg("Course", userId, courseId, contextId, Map("completedCount" -> completedCount.toDouble), Map("completedCount" -> System.currentTimeMillis())), collectionProgress)) - } - } - - /** - * Identified the children of the course (only collections) for which aggregates computation required. - * Computation of aggregates using leafNodes (of the specific collection) and user completed contents. - * Here computing only "completedCount" aggregate. - */ - def courseChildrenActivityAgg(userConsumption: UserContentConsumption)(implicit metrics: Metrics): List[UserEnrolmentAgg] = { - val courseId = userConsumption.courseId - val userId = userConsumption.userId - val contextId = "cb:" + userConsumption.batchId - - // These are the child collections which require computation of aggregates - for this user. - val ancestors = userConsumption.contents.mapValues(content => { - val contentId = content.contentId - readFromCache(key = s"$courseId:$contentId:${config.ancestors}", metrics) - }).values.flatten.filter(a => !StringUtils.equals(a, courseId)).toList.distinct - - // LeafNodes of the identified child collections - for this user. - val collectionsWithLeafNodes = ancestors.map(unitId => { - (unitId, readFromCache(key = s"$courseId:$unitId:${config.leafNodes}", metrics).distinct) - }).toMap - - // Content completed - By this user. - val userCompletedContents = userConsumption.contents.filter(cc => cc._2.status == 2).map(cc => cc._2.contentId).toList.distinct - - // Child Collection UserAggregate list - for this user. - collectionsWithLeafNodes.map(e => { - val collectionId = e._1 - val leafNodes = e._2 - val completedCount = leafNodes.intersect(userCompletedContents).size - /* TODO - List - TODO 1. Generalise activityType from "Course" to "Collection". - TODO 2.Identify how to generate start and end event for CourseUnit. - */ - val activityAgg = UserActivityAgg("Course", userId, collectionId, contextId, Map("completedCount" -> completedCount), Map("completedCount" -> System.currentTimeMillis())) - UserEnrolmentAgg(activityAgg, None) - }).toList - } - - /** - * Generation of a "String" key for UserContentConsumption. - */ - def getUCKey(userConsumption: UserContentConsumption): String = { - userConsumption.userId + ":" + userConsumption.courseId + ":" + userConsumption.batchId - } - - /** - * Merging the Input and DB ContentStatus data of a User, Course and Batch (Enrolment) - * This is the critical part of the code. - */ - def finalUserConsumption(inputData: UserContentConsumption, dbData: UserContentConsumption)(implicit metrics: Metrics): UserContentConsumption = { - val dbContents = dbData.contents - val processedContents = inputData.contents.map { - case (contentId, inputCC) => { - // ContentStatus from DB. - val dbCC: ContentStatus = dbContents.getOrElse(contentId, ContentStatus(contentId, 0, 0, 0)) - val finalStatus = List(inputCC.status, dbCC.status).max // Final status is max of DB and Input ContentStatus. - val views = sumFunc(List(inputCC, dbCC), (x: ContentStatus) => { x.viewCount }) // View Count is sum of DB and Input ContentStatus. - val completion = sumFunc(List(inputCC, dbCC), (x: ContentStatus) => { x.completedCount }) // Completed Count is sum of DB and Input ContentStatus. - val eventsFor: List[String] = getEventActions(dbCC, inputCC) - // Merged ContentStatus. - (contentId, ContentStatus(contentId, finalStatus, completion, views, inputCC.fromInput, eventsFor)) - } - } - - val existingContents = processedContents.keys.toList - val remainingContents = dbData.contents.filterKeys(key => !existingContents.contains(key)) - val finalContentsMap = processedContents ++ remainingContents - UserContentConsumption(inputData.userId, inputData.batchId, inputData.courseId, finalContentsMap) - } - - /** - * This will identify whether this is the start or complete of the Content by User. - * - * @return List - Actions - "start" and "complete". - */ - def getEventActions(dbCC: ContentStatus, inputCC: ContentStatus): List[String] = { - val startAction = if (dbCC.viewCount == 0) List("start") else List() - val completeAction = if (dbCC.completedCount == 0 && inputCC.completedCount > 0) List(config.complete) else List() - startAction ::: completeAction - } - - /** - * Generic method to read data from DB (Cassandra). - * - * @return - */ - def readFromDB(columns: Map[String, AnyRef], keySpace: String, table: String, metrics: Metrics): List[Row] = { - val selectWhere: Select.Where = QueryBuilder.select().all() - .from(keySpace, table). - where() - columns.map(col => { - col._2 match { - case value: List[Any] => - selectWhere.and(QueryBuilder.in(col._1, value.asJava)) - case _ => - selectWhere.and(QueryBuilder.eq(col._1, col._2)) - } - }) - metrics.incCounter(config.dbReadCount) - cassandraUtil.find(selectWhere.toString).asScala.toList - - } - - /** - * Method to update the specific table in a batch format. - */ - def updateDB(batchSize: Int, queriesList: List[Update.Where])(implicit metrics: Metrics): Unit = { - val groupedQueries = queriesList.grouped(batchSize).toList - groupedQueries.foreach(queries => { - val cqlBatch = QueryBuilder.batch() - queries.map(query => cqlBatch.add(query)) - val result = cassandraUtil.upsert(cqlBatch.toString) - if (result) { - metrics.incCounter(config.dbUpdateCount) - } else { - val msg = "Database update has failed: " + cqlBatch.toString - logger.error(msg) - throw new Exception(msg) - } - }) - } - - def readFromCache(key: String, metrics: Metrics): List[String] = { - metrics.incCounter(config.cacheHitCount) - val list = cache.getKeyMembers(key) - if (CollectionUtils.isEmpty(list)) { - metrics.incCounter(config.cacheMissCount) - logger.info("Redis cache (smembers) not available for key: " + key) - } - list.asScala.toList - } - - def getUserAggQuery(progress: UserActivityAgg): - Update.Where = { - QueryBuilder.update(config.dbKeyspace, config.dbUserActivityAggTable) - .`with`(QueryBuilder.putAll(config.aggregates, progress.aggregates.asJava)) - .and(QueryBuilder.putAll(config.aggLastUpdated, progress.agg_last_updated.asJava)) - .where(QueryBuilder.eq(config.activityId, progress.activity_id)) - .and(QueryBuilder.eq(config.activityType, progress.activity_type)) - .and(QueryBuilder.eq(config.contextId, progress.context_id)) - .and(QueryBuilder.eq(config.activityUser, progress.user_id)) - } - - /** - * Creates the cql query for content consumption table - */ - def getContentConsumptionQueries(userContentConsumption: UserContentConsumption): List[Update.Where] = { - userContentConsumption.contents.mapValues(content => { - QueryBuilder.update(config.dbKeyspace, config.dbUserContentConsumptionTable) - .`with`(QueryBuilder.set(config.viewcount, content.viewCount)) - .and(QueryBuilder.set(config.completedcount, content.completedCount)) - .where(QueryBuilder.eq(config.batchId.toLowerCase(), userContentConsumption.batchId)) - .and(QueryBuilder.eq(config.courseId.toLowerCase(), userContentConsumption.courseId)) - .and(QueryBuilder.eq(config.userId.toLowerCase(), userContentConsumption.userId)) - .and(QueryBuilder.eq(config.contentId.toLowerCase(), content.contentId)) - }).values.toList - } - - /** - * Method to get the content status object in map format ex: (do_5874308329084 -> 2, do_59485345435 -> 3) - * It always takes the highest precedence progress values for the contents ex: (do_5874308329084 -> 2, do_5874308329084 -> 1, do_59485345435 -> 3) => (do_5874308329084 -> 2, do_59485345435 -> 3) - * - * Ex: Map("C1"->2, "C2" ->1) - * - */ - def getContentStatusFromEvent(contents: List[Map[String, AnyRef]]): Map[String, ContentStatus] = { - val enrichedContents = contents.map(content => { - (content.getOrElse(config.contentId, "").asInstanceOf[String], content.getOrElse(config.status, 0).asInstanceOf[Number]) - }).filter(t => StringUtils.isNotBlank(t._1) && (t._2.intValue() > 0)) - .map(x => { - val completedCount = if (x._2.intValue() == 2) 1 else 0 - ContentStatus(x._1, x._2.intValue(), completedCount) - }).groupBy(f => f.contentId) - - enrichedContents.map(content => { - val consumedList = content._2 - val finalStatus = consumedList.map(x => x.status).max - val views = sumFunc(consumedList, (x: ContentStatus) => { - x.viewCount - }) - val completion = sumFunc(consumedList, (x: ContentStatus) => { - x.completedCount - }) - (content._1, ContentStatus(content._1, finalStatus, completion, views)) - }) - } - - /** - * Computation of Sum for viewCount and completedCount. - */ - private def sumFunc(list: List[ContentStatus], valFunc: ContentStatus => Int): Int = list.map(x => valFunc(x)).sum - - - /** - * Method to get the content status from the database - * - * Ex: List(Map("courseId" -> "do_43795", batchId -> "batch1", userId->"user001", contentStatus -> Map("C1"->2, "C2" ->1))) - * - */ - def getContentStatusFromDB(eDataBatch: List[Map[String, AnyRef]], metrics: Metrics): Map[String, UserContentConsumption] = { - - val contentConsumption = scala.collection.mutable.Map[String, UserContentConsumption]() - val primaryFields = Map( - config.userId.toLowerCase() -> eDataBatch.map(x => x(config.userId)).distinct, - config.batchId.toLowerCase -> eDataBatch.map(x => x(config.batchId)).distinct, - config.courseId.toLowerCase -> eDataBatch.map(x => x(config.courseId)).distinct - ) - - val records = Option(readFromDB(primaryFields, config.dbKeyspace, config.dbUserContentConsumptionTable, metrics)) - records.map(record => record.groupBy(col => Map(config.batchId -> col.getObject(config.batchId.toLowerCase()).asInstanceOf[String], config.userId -> col.getObject(config.userId.toLowerCase()).asInstanceOf[String], config.courseId -> col.getObject(config.courseId.toLowerCase()).asInstanceOf[String]))) - .foreach(groupedRecords => groupedRecords.map(entry => { - val identifierMap = entry._1 - val consumptionList = entry._2.flatMap(row => Map(row.getObject(config.contentId.toLowerCase()).asInstanceOf[String] -> Map(config.status -> row.getObject(config.status), config.viewcount -> row.getObject(config.viewcount), config.completedcount -> row.getObject(config.completedcount)))) - .map(entry => { - val contentStatus = entry._2.filter(x => x._2 != null) - val contentId = entry._1 - val status = contentStatus.getOrElse(config.status, 1).asInstanceOf[Number].intValue() - val viewCount = contentStatus.getOrElse(config.viewcount, 0).asInstanceOf[Number].intValue() - val completedCount = contentStatus.getOrElse(config.completedcount, 0).asInstanceOf[Number].intValue() - (contentId, ContentStatus(contentId, status, completedCount, viewCount, false)) - }).toMap - - val userId = identifierMap(config.userId) - val batchId = identifierMap(config.batchId) - val courseId = identifierMap(config.courseId) - - val userContentConsumption = UserContentConsumption(userId, batchId, courseId, consumptionList) - contentConsumption += getUCKey(userContentConsumption) -> userContentConsumption - - })) - contentConsumption.toMap - } - - /** - * Content - AUDIT Event Generation using UserContentConsumption - * "eventsFor" - will have the action (or type) for the event to generate. - */ - def contentAuditEvents(userConsumption: UserContentConsumption): List[TelemetryEvent] = { - val userId = userConsumption.userId - val courseId = userConsumption.courseId - val batchId = userConsumption.batchId - val contentsForEvents = userConsumption.contents.filter(c => c._2.eventsFor.nonEmpty).values - contentsForEvents.flatMap(c => { - c.eventsFor.map(action => { - val properties = if (StringUtils.equalsIgnoreCase(action, config.complete)) Array(config.viewcount, config.completedcount) else Array(config.viewcount) - TelemetryEvent( - actor = ActorObject(id = userId), - edata = EventData(props = properties, `type` = action), // action values are "start", "complete". - context = EventContext(cdata = Array(Map("type" -> config.courseBatch, "id" -> batchId).asJava)), - `object` = EventObject(id = c.contentId, `type` = "Content", rollup = Map[String, String]("l1" -> courseId).asJava) - ) - }) - }).toList - } - - def getDBStatus(collectionId: String): String = { - val requestBody = s"""{ - | "request": { - | "filters": { - | "objectType": "Collection", - | "identifier": "$collectionId", - | "status": ["Live", "Unlisted", "Retired"] - | }, - | "fields": ["status"] - | } - |}""".stripMargin - - val response = httpUtil.post(config.searchAPIURL, requestBody) - if (response.status == 200) { - val responseBody = gson.fromJson(response.body, classOf[java.util.Map[String, AnyRef]]) - val result = responseBody.getOrDefault("result", new java.util.HashMap[String, AnyRef]()).asInstanceOf[java.util.Map[String, AnyRef]] - val count = result.getOrDefault("count", 0.asInstanceOf[Number]).asInstanceOf[Number].intValue() - if (count > 0) { - val list = result.getOrDefault("content", new java.util.ArrayList[java.util.Map[String, AnyRef]]()).asInstanceOf[java.util.List[java.util.Map[String, AnyRef]]] - list.asScala.head.get("status").asInstanceOf[String] - } else throw new Exception(s"There are no published or retired collection with id: $collectionId") - } else { - logger.error("search-service error: " + response.body) - throw new Exception("search-service not returning error:" + response.status) - } - } - - def getCollectionStatus(collectionId: String): String = { - val cacheStatus = collectionStatusCache.getNonExpired(collectionId).getOrElse("") - if (StringUtils.isEmpty(cacheStatus)) { - val dbStatus = getDBStatus(collectionId) - collectionStatusCache = collectionStatusCache.putClocked(collectionId, dbStatus)._2 - dbStatus - } else cacheStatus - } -} - diff --git a/activity-aggregate-updater/src/main/scala/org/sunbird/job/aggregate/functions/CollectionProgressCompleteFunction.scala b/activity-aggregate-updater/src/main/scala/org/sunbird/job/aggregate/functions/CollectionProgressCompleteFunction.scala deleted file mode 100644 index a91bf76b4..000000000 --- a/activity-aggregate-updater/src/main/scala/org/sunbird/job/aggregate/functions/CollectionProgressCompleteFunction.scala +++ /dev/null @@ -1,141 +0,0 @@ -package org.sunbird.job.aggregate.functions - -import java.util.UUID - -import com.datastax.driver.core.querybuilder.{QueryBuilder, Select, Update} -import com.google.gson.Gson -import org.apache.flink.api.common.typeinfo.TypeInformation -import org.apache.flink.configuration.Configuration -import org.apache.flink.streaming.api.functions.ProcessFunction -import org.slf4j.LoggerFactory -import org.sunbird.job.cache.RedisConnect -import org.sunbird.job.aggregate.common.DeDupHelper -import org.sunbird.job.dedup.DeDupEngine -import org.sunbird.job.{BaseProcessFunction, Metrics} -import org.sunbird.job.aggregate.domain.{ActorObject, CollectionProgress, EventContext, EventData, EventObject, TelemetryEvent} -import org.sunbird.job.aggregate.task.ActivityAggregateUpdaterConfig -import org.sunbird.job.util.CassandraUtil - -import scala.collection.JavaConverters._ - -class CollectionProgressCompleteFunction(config: ActivityAggregateUpdaterConfig)(implicit val enrolmentCompleteTypeInfo: TypeInformation[List[CollectionProgress]], val stringTypeInfo: TypeInformation[String], @transient var cassandraUtil: CassandraUtil = null) - extends BaseProcessFunction[List[CollectionProgress], String](config) { - - private[this] val logger = LoggerFactory.getLogger(classOf[CollectionProgressCompleteFunction]) - lazy private val gson = new Gson() - var deDupEngine: DeDupEngine = _ - - override def open(parameters: Configuration): Unit = { - super.open(parameters) - cassandraUtil = new CassandraUtil(config.dbHost, config.dbPort) - deDupEngine = new DeDupEngine(config, new RedisConnect(config, Option(config.deDupRedisHost), Option(config.deDupRedisPort)), config.deDupStore, config.deDupExpirySec) - deDupEngine.init() - } - - override def close(): Unit = { - cassandraUtil.close() - deDupEngine.close() - super.close() - } - - override def processElement(events: List[CollectionProgress], context: ProcessFunction[List[CollectionProgress], String]#Context, metrics: Metrics): Unit = { - logger.info("events => "+events) - - val pendingEnrolments = if (config.filterCompletedEnrolments) events.filter {p => - val row = getEnrolment(p.userId, p.courseId, p.batchId)(metrics) - (row != null && row.getInt("status") != 2) - } else events - logger.info("pendingEnrolments =>"+pendingEnrolments) - - val enrolmentQueries = pendingEnrolments.map(enrolmentComplete => getEnrolmentCompleteQuery(enrolmentComplete)) - logger.info("enrolmentQueries => "+enrolmentQueries) - updateDB(config.thresholdBatchWriteSize, enrolmentQueries)(metrics) - pendingEnrolments.foreach(e => { - createIssueCertEvent(e, context)(metrics) - generateAuditEvent(e, context)(metrics) - }) - logger.info("posting events completed") - // Create and update the checksum to DeDup store for the input events. - if (config.dedupEnabled) { - events.map(cp => cp.inputContents.map(c => DeDupHelper.getMessageId(cp.courseId, cp.batchId, cp.userId, c, 2))) - .flatten.foreach(checksum => deDupEngine.storeChecksum(checksum)) - } - } - - override def metricsList(): List[String] = { - List(config.dbReadCount, config.dbUpdateCount, config.enrolmentCompleteCount, config.certIssueEventsCount) - } - - def generateAuditEvent(data: CollectionProgress, context: ProcessFunction[List[CollectionProgress], String]#Context)(implicit metrics: Metrics) = { - val auditEvent = TelemetryEvent( - actor = ActorObject(id = data.userId), - edata = EventData(props = Array("status", "completedon"), `type` = "enrol-complete"), // action values are "start", "complete". - context = EventContext(cdata = Array(Map("type" -> config.courseBatch, "id" -> data.batchId).asJava, Map("type" -> "Course", "id" -> data.courseId).asJava)), - `object` = EventObject(id = data.userId, `type` = "User", rollup = Map[String, String]("l1" -> data.courseId).asJava) - ) - logger.info("audit event =>"+gson.toJson(auditEvent)) - context.output(config.auditEventOutputTag, gson.toJson(auditEvent)) - - } - - def getEnrolment(userId: String, courseId: String, batchId: String)(implicit metrics: Metrics) = { - val selectWhere: Select.Where = QueryBuilder.select().all() - .from(config.dbKeyspace, config.dbUserEnrolmentsTable). - where() - selectWhere.and(QueryBuilder.eq("userid", userId)) - .and(QueryBuilder.eq("courseid", courseId)) - .and(QueryBuilder.eq("batchid", batchId)) - metrics.incCounter(config.dbReadCount) - cassandraUtil.findOne(selectWhere.toString) - } - - def getEnrolmentCompleteQuery(enrolment: CollectionProgress): Update.Where = { - logger.info("Enrolment completed for userId: " + enrolment.userId + " batchId: " + enrolment.batchId) - QueryBuilder.update(config.dbKeyspace, config.dbUserEnrolmentsTable) - .`with`(QueryBuilder.set("status", 2)) - .and(QueryBuilder.set("completedon", enrolment.completedOn)) - .and(QueryBuilder.set("progress", enrolment.progress)) - .and(QueryBuilder.set("contentstatus", enrolment.contentStatus.asJava)) - .and(QueryBuilder.set("datetime", System.currentTimeMillis)) - .where(QueryBuilder.eq("userid", enrolment.userId)) - .and(QueryBuilder.eq("courseid", enrolment.courseId)) - .and(QueryBuilder.eq("batchid", enrolment.batchId)) - } - - /** - * Method to update the specific table in a batch format. - */ - def updateDB(batchSize: Int, queriesList: List[Update.Where])(implicit metrics: Metrics): Unit = { - val groupedQueries = queriesList.grouped(batchSize).toList - groupedQueries.foreach(queries => { - val cqlBatch = QueryBuilder.batch() - queries.map(query => cqlBatch.add(query)) - logger.info("is cassandra cluster available =>"+(null !=cassandraUtil.session)) - val result = cassandraUtil.upsert(cqlBatch.toString) - logger.info("result after update => "+result) - if (result) { - metrics.incCounter(config.dbUpdateCount) - metrics.incCounter(config.enrolmentCompleteCount) - } else { - val msg = "Database update has failed" + cqlBatch.toString - logger.error(msg) - throw new Exception(msg) - } - }) - } - - /** - * Generation of Certificate Issue event for the enrolment completed users to validate and generate certificate. - * @param enrolment - * @param context - * @param metrics - */ - def createIssueCertEvent(enrolment: CollectionProgress, context: ProcessFunction[List[CollectionProgress], String]#Context)(implicit metrics: Metrics): Unit = { - val ets = System.currentTimeMillis - val mid = s"""LP.${ets}.${UUID.randomUUID}""" - val event = s"""{"eid": "BE_JOB_REQUEST","ets": ${ets},"mid": "${mid}","actor": {"id": "Course Certificate Generator","type": "System"},"context": {"pdata": {"ver": "1.0","id": "org.sunbird.platform"}},"object": {"id": "${enrolment.batchId}_${enrolment.courseId}","type": "CourseCertificateGeneration"},"edata": {"userIds": ["${enrolment.userId}"],"action": "issue-certificate","iteration": 1, "trigger": "auto-issue","batchId": "${enrolment.batchId}","reIssue": false,"courseId": "${enrolment.courseId}"}}""" - logger.info("o/p event: "+event) - context.output(config.certIssueOutputTag, event) - metrics.incCounter(config.certIssueEventsCount) - } -} diff --git a/activity-aggregate-updater/src/main/scala/org/sunbird/job/aggregate/functions/CollectionProgressUpdateFunction.scala b/activity-aggregate-updater/src/main/scala/org/sunbird/job/aggregate/functions/CollectionProgressUpdateFunction.scala deleted file mode 100644 index 03ea1ddc0..000000000 --- a/activity-aggregate-updater/src/main/scala/org/sunbird/job/aggregate/functions/CollectionProgressUpdateFunction.scala +++ /dev/null @@ -1,96 +0,0 @@ -package org.sunbird.job.aggregate.functions - -import com.datastax.driver.core.querybuilder.{QueryBuilder, Select, Update} -import org.apache.flink.api.common.typeinfo.TypeInformation -import org.apache.flink.configuration.Configuration -import org.apache.flink.streaming.api.functions.ProcessFunction -import org.slf4j.LoggerFactory -import org.sunbird.job.cache.RedisConnect -import org.sunbird.job.aggregate.common.DeDupHelper -import org.sunbird.job.dedup.DeDupEngine -import org.sunbird.job.aggregate.domain._ -import org.sunbird.job.aggregate.task.ActivityAggregateUpdaterConfig -import org.sunbird.job.util.CassandraUtil -import org.sunbird.job.{BaseProcessFunction, Metrics} - -import scala.collection.JavaConverters._ - -class CollectionProgressUpdateFunction(config: ActivityAggregateUpdaterConfig)(implicit val enrolmentCompleteTypeInfo: TypeInformation[List[CollectionProgress]], val stringTypeInfo: TypeInformation[String], @transient var cassandraUtil: CassandraUtil = null) - extends BaseProcessFunction[List[CollectionProgress], String](config) { - - private[this] val logger = LoggerFactory.getLogger(classOf[CollectionProgressUpdateFunction]) - var deDupEngine: DeDupEngine = _ - - override def open(parameters: Configuration): Unit = { - super.open(parameters) - cassandraUtil = new CassandraUtil(config.dbHost, config.dbPort) - deDupEngine = new DeDupEngine(config, new RedisConnect(config, Option(config.deDupRedisHost), Option(config.deDupRedisPort)), config.deDupStore, config.deDupExpirySec) - deDupEngine.init() - } - - override def close(): Unit = { - cassandraUtil.close() - deDupEngine.close() - super.close() - } - - override def processElement(events: List[CollectionProgress], context: ProcessFunction[List[CollectionProgress], String]#Context, metrics: Metrics): Unit = { - val pendingEnrolments = if (config.filterCompletedEnrolments) events.filter { p => - val row = getEnrolment(p.userId, p.courseId, p.batchId)(metrics) - (row != null && row.getInt("status") != 2) - } else events - val enrolmentQueries = pendingEnrolments.map(collectionProgress => getEnrolmentUpdateQuery(collectionProgress)) - updateDB(config.thresholdBatchWriteSize, enrolmentQueries)(metrics) - // Create and update the checksum to DeDup store for the input events. - if (config.dedupEnabled) { - events.map(cp => cp.inputContents.map(c => DeDupHelper.getMessageId(cp.courseId, cp.batchId, cp.userId, c, 2))) - .flatten.foreach(checksum => deDupEngine.storeChecksum(checksum)) - } - } - - override def metricsList(): List[String] = { - List(config.dbReadCount, config.dbUpdateCount) - } - - def getEnrolment(userId: String, courseId: String, batchId: String)(implicit metrics: Metrics) = { - val selectWhere: Select.Where = QueryBuilder.select().all() - .from(config.dbKeyspace, config.dbUserEnrolmentsTable). - where() - selectWhere.and(QueryBuilder.eq("userid", userId)) - .and(QueryBuilder.eq("courseid", courseId)) - .and(QueryBuilder.eq("batchid", batchId)) - metrics.incCounter(config.dbReadCount) - cassandraUtil.findOne(selectWhere.toString) - } - - def getEnrolmentUpdateQuery(enrolment: CollectionProgress): Update.Where = { - logger.info("Enrolment updated for userId: " + enrolment.userId + " batchId: " + enrolment.batchId) - QueryBuilder.update(config.dbKeyspace, config.dbUserEnrolmentsTable) - .`with`(QueryBuilder.set("status", 1)) - .and(QueryBuilder.set("progress", enrolment.progress)) - .and(QueryBuilder.set("contentstatus", enrolment.contentStatus.asJava)) - .and(QueryBuilder.set("datetime", System.currentTimeMillis)) - .where(QueryBuilder.eq("userid", enrolment.userId)) - .and(QueryBuilder.eq("courseid", enrolment.courseId)) - .and(QueryBuilder.eq("batchid", enrolment.batchId)) - } - - /** - * Method to update the specific table in a batch format. - */ - def updateDB(batchSize: Int, queriesList: List[Update.Where])(implicit metrics: Metrics): Unit = { - val groupedQueries = queriesList.grouped(batchSize).toList - groupedQueries.foreach(queries => { - val cqlBatch = QueryBuilder.batch() - queries.map(query => cqlBatch.add(query)) - val result = cassandraUtil.upsert(cqlBatch.toString) - if (result) { - metrics.incCounter(config.dbUpdateCount) - } else { - val msg = "Database update has failed" + cqlBatch.toString - logger.error(msg) - throw new Exception(msg) - } - }) - } -} diff --git a/activity-aggregate-updater/src/main/scala/org/sunbird/job/aggregate/functions/ContentConsumptionDeDupFunction.scala b/activity-aggregate-updater/src/main/scala/org/sunbird/job/aggregate/functions/ContentConsumptionDeDupFunction.scala deleted file mode 100644 index 1ee934f2f..000000000 --- a/activity-aggregate-updater/src/main/scala/org/sunbird/job/aggregate/functions/ContentConsumptionDeDupFunction.scala +++ /dev/null @@ -1,74 +0,0 @@ -package org.sunbird.job.aggregate.functions - -import java.lang.reflect.Type -import java.security.MessageDigest -import java.util - -import com.google.gson.reflect.TypeToken -import org.apache.commons.lang3.StringUtils -import org.apache.flink.api.common.typeinfo.TypeInformation -import org.apache.flink.configuration.Configuration -import org.apache.flink.streaming.api.functions.ProcessFunction -import org.slf4j.LoggerFactory -import org.sunbird.job.cache.RedisConnect -import org.sunbird.job.aggregate.common.DeDupHelper -import org.sunbird.job.dedup.DeDupEngine -import org.sunbird.job.{BaseProcessFunction, Metrics} -import org.sunbird.job.aggregate.task.ActivityAggregateUpdaterConfig - -import scala.collection.JavaConverters._ - -class ContentConsumptionDeDupFunction(config: ActivityAggregateUpdaterConfig)(implicit val stringTypeInfo: TypeInformation[String]) extends BaseProcessFunction[util.Map[String, AnyRef], String](config) { - - val mapType: Type = new TypeToken[Map[String, AnyRef]]() {}.getType - private[this] val logger = LoggerFactory.getLogger(classOf[ContentConsumptionDeDupFunction]) - var deDupEngine: DeDupEngine = _ - - override def open(parameters: Configuration): Unit = { - super.open(parameters) - deDupEngine = new DeDupEngine(config, new RedisConnect(config, Option(config.deDupRedisHost), Option(config.deDupRedisPort)), config.deDupStore, config.deDupExpirySec) - deDupEngine.init() - } - - override def close(): Unit = { - deDupEngine.close() - super.close() - } - - override def processElement(event: util.Map[String, AnyRef], context: ProcessFunction[util.Map[String, AnyRef], String]#Context, metrics: Metrics): Unit = { - metrics.incCounter(config.totalEventCount) - val eData = event.get(config.eData).asInstanceOf[util.Map[String, AnyRef]].asScala - val isBatchEnrollmentEvent: Boolean = StringUtils.equalsIgnoreCase(eData.getOrElse(config.action, "").asInstanceOf[String], config.batchEnrolmentUpdateCode) - if (isBatchEnrollmentEvent) { - val contents = eData.getOrElse(config.contents, new util.ArrayList[java.util.Map[String, AnyRef]]()).asInstanceOf[util.List[java.util.Map[String, AnyRef]]].asScala - val filteredContents = contents.filter(x => x.get("status") == 2).toList - if (filteredContents.size == 0) - metrics.incCounter(config.skipEventsCount) - else - metrics.incCounter(config.batchEnrolmentUpdateEventCount) - filteredContents.map(c => { - (eData + ("contents" -> List(Map("contentId" -> c.get("contentId"), "status" -> c.get("status"))))).toMap - }).filter(e => discardDuplicates(e)).foreach(d => context.output(config.uniqueConsumptionOutput, d)) - } else metrics.incCounter(config.skipEventsCount) - } - - override def metricsList(): List[String] = { - List(config.totalEventCount, config.skipEventsCount, config.batchEnrolmentUpdateEventCount) - } - - def discardDuplicates(event: Map[String, AnyRef]): Boolean = { - if (config.dedupEnabled) { - val userId = event.getOrElse(config.userId, "").asInstanceOf[String] - val courseId = event.getOrElse(config.courseId, "").asInstanceOf[String] - val batchId = event.getOrElse(config.batchId, "").asInstanceOf[String] - val contents = event.getOrElse(config.contents, List[Map[String,AnyRef]]()).asInstanceOf[List[Map[String, AnyRef]]] - if (contents.nonEmpty) { - val content = contents.head - val contentId = content.getOrElse("contentId", "").asInstanceOf[String] - val status = content.getOrElse("status", 0.asInstanceOf[AnyRef]).asInstanceOf[Number].intValue() - val checksum = DeDupHelper.getMessageId(courseId, batchId, userId, contentId, status) - deDupEngine.isUniqueEvent(checksum) - } else false - } else true - } -} diff --git a/activity-aggregate-updater/src/main/scala/org/sunbird/job/aggregate/task/ActivityAggregateUpdaterConfig.scala b/activity-aggregate-updater/src/main/scala/org/sunbird/job/aggregate/task/ActivityAggregateUpdaterConfig.scala deleted file mode 100644 index f36edaf4c..000000000 --- a/activity-aggregate-updater/src/main/scala/org/sunbird/job/aggregate/task/ActivityAggregateUpdaterConfig.scala +++ /dev/null @@ -1,135 +0,0 @@ -package org.sunbird.job.aggregate.task - -import java.util - -import com.typesafe.config.Config -import org.apache.flink.api.common.typeinfo.TypeInformation -import org.apache.flink.api.java.typeutils.TypeExtractor -import org.apache.flink.streaming.api.scala.OutputTag -import org.sunbird.job.BaseJobConfig -import org.sunbird.job.aggregate.domain.CollectionProgress - -class ActivityAggregateUpdaterConfig(override val config: Config) extends BaseJobConfig(config, "activity-aggregate-updater") { - - private val serialVersionUID = 2905979434303791379L - - implicit val mapTypeInfo: TypeInformation[util.Map[String, AnyRef]] = TypeExtractor.getForClass(classOf[util.Map[String, AnyRef]]) - implicit val scalaMapTypeInfo: TypeInformation[Map[String, AnyRef]] = TypeExtractor.getForClass(classOf[Map[String, AnyRef]]) - implicit val stringTypeInfo: TypeInformation[String] = TypeExtractor.getForClass(classOf[String]) - implicit val enrolmentCompleteTypeInfo: TypeInformation[List[CollectionProgress]] = TypeExtractor.getForClass(classOf[List[CollectionProgress]]) - - // Kafka Topics Configuration - val kafkaInputTopic: String = config.getString("kafka.input.topic") - val kafkaAuditEventTopic: String = config.getString("kafka.output.audit.topic") - val kafkaFailedEventTopic: String = config.getString("kafka.output.failed.topic") - val kafkaCertIssueTopic: String = config.getString("kafka.output.certissue.topic") - - override val kafkaConsumerParallelism: Int = config.getInt("task.consumer.parallelism") - val activityAggregateUpdaterParallelism: Int = config.getInt("task.activity.agg.parallelism") - val deDupProcessParallelism: Int = config.getInt("task.dedup.parallelism") - val enrolmentCompleteParallelism: Int = config.getInt("task.enrolment.complete.parallelism") - - // Metric List - val totalEventCount = "total-events-count" - val failedEventCount = "failed-events-count" - val dbUpdateCount = "db-update-count" - val dbReadCount = "db-read-count" - val cacheHitCount = "cache-hit-count" - val cacheMissCount = "cache-miss-count" - val batchEnrolmentUpdateEventCount = "batch-enrolment-update-count" - val skipEventsCount = "skipped-events-count" - val processedEnrolmentCount = "processed-enrolment-count" - val enrolmentCompleteCount = "enrolment-complete-count" - val certIssueEventsCount = "cert-issue-events-count" - val retiredCCEventsCount = "retired-consumption-events-count" - - // Cassandra Configurations - val dbUserContentConsumptionTable: String = config.getString("lms-cassandra.consumption.table") - val dbUserActivityAggTable: String = config.getString("lms-cassandra.user_activity_agg.table") - val dbUserEnrolmentsTable: String = config.getString("lms-cassandra.user_enrolments.table") - val dbKeyspace: String = config.getString("lms-cassandra.keyspace") - val dbHost: String = config.getString("lms-cassandra.host") - val dbPort: Int = config.getInt("lms-cassandra.port") - - // Redis Configurations - val nodeStore: Int = config.getInt("redis.database.relationCache.id") // Both LeafNodes And Ancestor nodes - val deDupRedisHost: String = config.getString("dedup-redis.host") - val deDupRedisPort: Int = config.getInt("dedup-redis.port") - val deDupStore: Int = config.getInt("dedup-redis.database.index") - val deDupExpirySec: Int = config.getInt("dedup-redis.database.expiry") - - // Tags - val uniqueConsumptionOutputTagName = "unique-consumption-events" - val uniqueConsumptionOutput: OutputTag[Map[String, AnyRef]] = OutputTag[Map[String, AnyRef]](uniqueConsumptionOutputTagName) - val auditEventOutputTagName = "audit-events" - val auditEventOutputTag: OutputTag[String] = OutputTag[String](auditEventOutputTagName) - val failedEventOutputTagName = "failed-events" - val failedEventOutputTag: OutputTag[String] = OutputTag[String](failedEventOutputTagName) - val collectionCompleteOutputTagName = "collection-progress-complete-events" - val collectionCompleteOutputTag: OutputTag[List[CollectionProgress]] = OutputTag[List[CollectionProgress]](collectionCompleteOutputTagName) - val collectionUpdateOutputTagName = "collection-progress-update-events" - val collectionUpdateOutputTag: OutputTag[List[CollectionProgress]] = OutputTag[List[CollectionProgress]](collectionUpdateOutputTagName) - val certIssueOutputTagName = "certificate-issue-events" - val certIssueOutputTag: OutputTag[String] = OutputTag[String](certIssueOutputTagName) - - // constants - val activityType = "activity_type" - val activityId = "activity_id" - val contextId = "context_id" - val activityUser = "user_id" - val aggLastUpdated = "agg_last_updated" - val agg = "agg" - val courseId = "courseId" - val batchId = "batchId" - val contentId = "contentId" - val progress = "progress" - val contents = "contents" - val contentStatus = "contentStatus" - val userId = "userId" - val status = "status" - val unitActivityType = "course-unit" - val courseActivityType = "course" - val leafNodes = "leafnodes" - val ancestors = "ancestors" - val viewcount = "viewcount" - val completedcount = "completedcount" - val complete = "complete" - val eData = "edata" - val action = "action" - val batchEnrolmentUpdateCode = "batch-enrolment-update" - val routerFn = "RouterFn" - val consumptionDeDupFn= "consumption-dedup-process" - val activityAggregateUpdaterFn = "activity-aggregate-updater-fn" - val partition = "partition" - val courseBatch = "CourseBatch" - val collectionProgressUpdateFn = "progress-update-process" - val collectionCompleteFn = "collection-completion-process" - val aggregates = "aggregates" - - // Consumers - val activityAggregateUpdaterConsumer = "activity-aggregate-updater-consumer" - - // Producers - val activityAggregateUpdaterProducer = "activity-aggregate-updater-audit-events-sink" - val enrolmentCompleteEventProducer = "enrolment-complete-audit-sink" - val activityAggFailedEventProducer = "activity-aggregate-updater-failed-sink" - val certIssueEventProducer = "certificate-issue-event-producer" - - //Thresholds - val thresholdBatchReadInterval: Int = config.getInt("threshold.batch.read.interval") - val thresholdBatchReadSize: Int = config.getInt("threshold.batch.read.size") - val thresholdBatchWriteSize: Int = config.getInt("threshold.batch.write.size") - val windowShards: Int = config.getInt("task.window.shards") - - - // Job specific configurations - val moduleAggEnabled: Boolean = config.getBoolean("activity.module.aggs.enabled") - val dedupEnabled: Boolean = config.getBoolean("activity.input.dedup.enabled") - val statusCacheExpirySec: Int = config.getInt("activity.collection.status.cache.expiry") - val filterCompletedEnrolments: Boolean = if (config.hasPath("activity.filter.processed.enrolments")) config.getBoolean("activity.filter.processed.enrolments") else true - - // Other services configuration - val searchServiceBasePath: String = config.getString("service.search.basePath") - val searchAPIURL = searchServiceBasePath + "/v3/search" - -} diff --git a/activity-aggregate-updater/src/main/scala/org/sunbird/job/aggregate/task/ActivityAggregateUpdaterStreamTask.scala b/activity-aggregate-updater/src/main/scala/org/sunbird/job/aggregate/task/ActivityAggregateUpdaterStreamTask.scala deleted file mode 100644 index b947b8523..000000000 --- a/activity-aggregate-updater/src/main/scala/org/sunbird/job/aggregate/task/ActivityAggregateUpdaterStreamTask.scala +++ /dev/null @@ -1,86 +0,0 @@ -package org.sunbird.job.aggregate.task - -import java.io.File -import java.util - -import com.typesafe.config.ConfigFactory -import org.apache.flink.api.common.typeinfo.TypeInformation -import org.apache.flink.api.java.functions.KeySelector -import org.apache.flink.api.java.typeutils.TypeExtractor -import org.apache.flink.api.java.utils.ParameterTool -import org.apache.flink.streaming.api.scala._ -import org.apache.flink.streaming.api.scala.StreamExecutionEnvironment -import org.sunbird.job.connector.FlinkKafkaConnector -import org.sunbird.job.aggregate.domain.CollectionProgress -import org.sunbird.job.aggregate.functions.{ActivityAggregatesFunction, CollectionProgressCompleteFunction, CollectionProgressUpdateFunction, ContentConsumptionDeDupFunction} -import org.sunbird.job.util.{FlinkUtil, HttpUtil} - - -class ActivityAggregateUpdaterStreamTask(config: ActivityAggregateUpdaterConfig, kafkaConnector: FlinkKafkaConnector, httpUtil: HttpUtil) { - def process(): Unit = { - implicit val env: StreamExecutionEnvironment = FlinkUtil.getExecutionContext(config) - implicit val mapTypeInfo: TypeInformation[util.Map[String, AnyRef]] = TypeExtractor.getForClass(classOf[util.Map[String, AnyRef]]) - implicit val stringTypeInfo: TypeInformation[String] = TypeExtractor.getForClass(classOf[String]) - implicit val enrolmentCompleteTypeInfo: TypeInformation[List[CollectionProgress]] = TypeExtractor.getForClass(classOf[List[CollectionProgress]]) - - val progressStream = - env.addSource(kafkaConnector.kafkaMapSource(config.kafkaInputTopic)).name(config.activityAggregateUpdaterConsumer) - .uid(config.activityAggregateUpdaterConsumer).setParallelism(config.kafkaConsumerParallelism) - .rebalance - .process(new ContentConsumptionDeDupFunction(config)).name(config.consumptionDeDupFn) - .uid(config.consumptionDeDupFn).setParallelism(config.deDupProcessParallelism) - .getSideOutput(config.uniqueConsumptionOutput) - .keyBy(new ActivityAggregatorKeySelector(config)) - .countWindow(config.thresholdBatchReadSize) - .process(new ActivityAggregatesFunction(config, httpUtil)) - .name(config.activityAggregateUpdaterFn) - .uid(config.activityAggregateUpdaterFn) - .setParallelism(config.activityAggregateUpdaterParallelism) - - progressStream.getSideOutput(config.auditEventOutputTag).addSink(kafkaConnector.kafkaStringSink(config.kafkaAuditEventTopic)) - .name(config.activityAggregateUpdaterProducer).uid(config.activityAggregateUpdaterProducer) - progressStream.getSideOutput(config.failedEventOutputTag).addSink(kafkaConnector.kafkaStringSink(config.kafkaFailedEventTopic)) - .name(config.activityAggFailedEventProducer).uid(config.activityAggFailedEventProducer) - - // TODO: set separate parallelism for below task. - progressStream.getSideOutput(config.collectionUpdateOutputTag).process(new CollectionProgressUpdateFunction(config)) - .name(config.collectionProgressUpdateFn).uid(config.collectionProgressUpdateFn).setParallelism(config.enrolmentCompleteParallelism) - val enrolmentCompleteStream = progressStream.getSideOutput(config.collectionCompleteOutputTag).process(new CollectionProgressCompleteFunction(config)) - .name(config.collectionCompleteFn).uid(config.collectionCompleteFn).setParallelism(config.enrolmentCompleteParallelism) - - enrolmentCompleteStream.getSideOutput(config.certIssueOutputTag).addSink(kafkaConnector.kafkaStringSink(config.kafkaCertIssueTopic)) - .name(config.certIssueEventProducer).uid(config.certIssueEventProducer) - enrolmentCompleteStream.getSideOutput(config.auditEventOutputTag).addSink(kafkaConnector.kafkaStringSink(config.kafkaAuditEventTopic)) - .name(config.enrolmentCompleteEventProducer).uid(config.enrolmentCompleteEventProducer) - - env.execute(config.jobName) - } - - -} - -// $COVERAGE-OFF$ Disabling scoverage as the below code can only be invoked within flink cluster -object ActivityAggregateUpdaterStreamTask { - - def main(args: Array[String]): Unit = { - val configFilePath = Option(ParameterTool.fromArgs(args).get("config.file.path")) - val config = configFilePath.map { - path => ConfigFactory.parseFile(new File(path)).resolve() - }.getOrElse(ConfigFactory.load("activity-aggregate-updater.conf").withFallback(ConfigFactory.systemEnvironment())) - val courseAggregator = new ActivityAggregateUpdaterConfig(config) - val kafkaUtil = new FlinkKafkaConnector(courseAggregator) - val httpUtil = new HttpUtil - val task = new ActivityAggregateUpdaterStreamTask(courseAggregator, kafkaUtil, httpUtil) - task.process() - } - -} -// $COVERAGE-ON$ - -class ActivityAggregatorKeySelector(config: ActivityAggregateUpdaterConfig) extends KeySelector[Map[String, AnyRef], Int] { - private val serialVersionUID = 7267989625042068736L - private val shards = config.windowShards - override def getKey(in: Map[String, AnyRef]): Int = { - in.getOrElse(config.userId, "").asInstanceOf[String].hashCode % shards - } -} diff --git a/activity-aggregate-updater/src/test/resources/logback-test.xml b/activity-aggregate-updater/src/test/resources/logback-test.xml deleted file mode 100644 index 2e5cb5e09..000000000 --- a/activity-aggregate-updater/src/test/resources/logback-test.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n - - - - - - - - - - - - - - diff --git a/activity-aggregate-updater/src/test/resources/test.conf b/activity-aggregate-updater/src/test/resources/test.conf deleted file mode 100644 index f036ad5e2..000000000 --- a/activity-aggregate-updater/src/test/resources/test.conf +++ /dev/null @@ -1,53 +0,0 @@ -include "base-test.conf" - -kafka { - input.topic = "sunbirddev.coursebatch.job.request" - output.audit.topic = "sunbirddev.telemetry.raw" - output.failed.topic = "sunbirddev.activity.agg.failed" - output.certissue.topic = "sunbirddev.issue.certificate.request" - groupId = "sunbirddev-activity-aggregate-updater-group" -} - -task { - window.shards = 1 - consumer.parallelism = 1 - dedup.parallelism = 1 - activity.agg.parallelism = 1 - enrolment.complete.parallelism = 1 -} - -lms-cassandra { - keyspace = "sunbird_courses" - consumption.table = "user_content_consumption" - user_activity_agg.table = "user_activity_agg" - user_enrolments.table = "user_enrolments" -} - -redis { - database { - relationCache.id = 10 - } -} - -threshold.batch.read.interval = 60 // In sec -threshold.batch.read.size = 1 -threshold.batch.write.size = 5 - -dedup-redis { - host = localhost - port = 6340 - database.index = 13 - database.expiry = 600 -} - -activity { - module.aggs.enabled = true - input.dedup.enabled = true - collection.status.cache.expiry = 3600 -} - -service { - search { - basePath = "http://search-service:9000" - } -} \ No newline at end of file diff --git a/activity-aggregate-updater/src/test/resources/test.cql b/activity-aggregate-updater/src/test/resources/test.cql deleted file mode 100644 index 5fff30d90..000000000 --- a/activity-aggregate-updater/src/test/resources/test.cql +++ /dev/null @@ -1,79 +0,0 @@ -CREATE KEYSPACE sunbird_courses with replication = {'class':'SimpleStrategy','replication_factor':1}; - -CREATE TABLE sunbird_courses.user_content_consumption ( - userid text, - courseid text, - batchid text, - contentid text, - completedcount int, - datetime timestamp, - lastaccesstime text, - lastcompletedtime text, - lastupdatedtime text, - progress int, - status int, - viewcount int, - PRIMARY KEY (userid, courseid, batchid, contentid) -) WITH CLUSTERING ORDER BY (courseid ASC, batchid ASC, contentid ASC); - -// EVENT_1 Testcase data -INSERT INTO sunbird_courses.user_content_consumption(userid, contentid, batchid,courseid,progress,status,viewcount,completedcount) VALUES ('8454cb21-3ce9-4e30-85b5-fade097880d8','do_11260735471149056012299','0126083288437637121','do_1127212344324751361295',100, 2, 3,1) ; -INSERT INTO sunbird_courses.user_content_consumption(userid, contentid, batchid,courseid,progress,status,viewcount,completedcount) VALUES ('8454cb21-3ce9-4e30-85b5-fade097880d8','do_11260735471149056012300','0126083288437637121','do_1127212344324751361295',100, 2, 2,2) ; -INSERT INTO sunbird_courses.user_content_consumption(userid, contentid, batchid,courseid,progress,status,viewcount,completedcount) VALUES ('8454cb21-3ce9-4e30-85b5-fade097880d8','do_11260735471149056012301','0126083288437637121','do_1127212344324751361295',0, 1,0,1) ; - -//Event_2 Testcase Data -INSERT INTO sunbird_courses.user_content_consumption(userid, contentid, batchid,courseid,progress,status,viewcount,completedcount) VALUES ('user001','do_R1','Batch1','course001',100, 2,1,0) ; -INSERT INTO sunbird_courses.user_content_consumption(userid, contentid, batchid,courseid,progress,status,viewcount,completedcount) VALUES ('user001','do_R2','Batch1','course001',100, 2,1,0) ; -INSERT INTO sunbird_courses.user_content_consumption(userid, contentid, batchid,courseid,progress,status,viewcount,completedcount) VALUES ('user001','do_R3','Batch1','course001',100, 2,1,0) ; - - - -CREATE TABLE IF NOT EXISTS sunbird_courses.user_activity_agg ( - activity_id text, - user_id text, - activity_type text, - context_id text, - agg Map, - aggregates Map, - agg_last_updated Map, - PRIMARY KEY ((activity_type, activity_id), context_id, user_id) -); - -CREATE TABLE sunbird_courses.user_enrolments ( - userid text, - courseid text, - batchid text, - active boolean, - addedby text, - certificates list>>, - completedon timestamp, - completionpercentage int, - contentstatus map, - datetime timestamp, - enrolleddate text, - issued_certificates list>>, - lastreadcontentid text, - lastreadcontentstatus int, - progress int, - status int, - PRIMARY KEY (userid, courseid, batchid) -) WITH CLUSTERING ORDER BY (courseid ASC, batchid ASC) - AND bloom_filter_fp_chance = 0.01 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} - AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 - AND default_time_to_live = 0 - AND gc_grace_seconds = 864000 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 0 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; -CREATE INDEX inx_ues_status ON sunbird_courses.user_enrolments (status); -CREATE INDEX inx_ues_certs ON sunbird_courses.user_enrolments (values(certificates)); - - -INSERT INTO sunbird_courses.user_enrolments(userid, courseid, batchid, status) VALUES ('user001', 'course001', 'Batch1', 1) diff --git a/activity-aggregate-updater/src/test/scala/org/sunbird/job/fixture/EventFixture.scala b/activity-aggregate-updater/src/test/scala/org/sunbird/job/fixture/EventFixture.scala deleted file mode 100644 index a26f3426b..000000000 --- a/activity-aggregate-updater/src/test/scala/org/sunbird/job/fixture/EventFixture.scala +++ /dev/null @@ -1,194 +0,0 @@ -package org.sunbird.job.fixture - -object EventFixture { - - /** - * case-1. Inserting a first course which is having 3 leaf nodes in the redis database and cassandra database - * does not contains this course id. - * courseId = - * ============BE_JOB_REQUEST_CONTENTS========== - * do_1127212344324751361295 - course - * do_course_unit1 - unit1 - * do_11260735471149056012299 - resource - * do_course_unit2 - unit2 - * do_11260735471149056012300 - resource - * do_course_unit3 - unit3 - * do_11260735471149056012301 - resource - * do_11260735471149056012300 -resource - * - * - * ============== content status in the event ====== - * do_11260735471149056012299 - 2 - * do_11260735471149056012301 - 1 - * do_11260735471149056012300 - 1 - * ============== content status in the database(content-consumption) - * do_11260735471149056012299 - 2 - * do_11260735471149056012301 - 1 - * do_11260735471149056012300 - 2 - * - * ============== Computation ============== - * unit level computation - * do_11260735471149056012299:ansestor -> do_course_unit1, do_1127212344324751361295 - * do_course_unit1:do_1127212344324751361295:leafnodes: do_11260735471149056012299 - * lefNodesSize = 1, completed = 1 - * 1/1 = 100% - * - * do_11260735471149056012301:ansestor -> do_course_unit3,do_1127212344324751361295 - * do_course_unit3:do_1127212344324751361295:leafNodes -> do_11260735471149056012301,do_11260735471149056012300 - * leafNodesSize = 2,completed 1 - * 1/2 = 50% - * - * do_11260735471149056012300:ansestor -> do_course_unit3,do_course_unit2,do_1127212344324751361295 - * do_course_unit3:do_1127212344324751361295:leafNodes -> do_11260735471149056012301,do_11260735471149056012300 - * leafNodesSize = 2, completed = 1 - * 1/2 = 50% - * - * do_course_unit2:do_1127212344324751361295:leafNodes -> do_11260735471149056012300 - * leafNodesSize = 1, completed = 1 - * 1/1 = 100% - * - * course level progress computation - * do_1127212344324751361295:leafNodes = do_11260735471149056012299, do_11260735471149056012301, do_11260735471149056012300 - */ - - val EVENT_1: String = - """ - |{"eid":"BE_JOB_REQUEST","ets":1563788371969,"mid":"LMS.1563788371969.590c5fa0-0ce8-46ed-bf6c-681c0a1fdac8","actor":{"type":"System","id":"Course Batch Updater"},"context":{"pdata":{"ver":"1.0","id":"org.sunbird.platform"}},"object":{"type":"CourseBatchEnrolment","id":"0126083288437637121_8454cb21-3ce9-4e30-85b5-fade097880d8"},"edata":{"contents":[{"contentId":"do_11260735471149056012299","status":2},{"contentId":"do_11260735471149056012300","status":1},{"contentId":"do_11260735471149056012301","status":1}],"action":"batch-enrolment-update","iteration":1,"batchId":"0126083288437637121","userId":"8454cb21-3ce9-4e30-85b5-fade097880d8","courseId":"do_1127212344324751361295"}} - |""".stripMargin - - val courseLeafNodes = Map("do_1127212344324751361295:do_1127212344324751361295:leafnodes" -> List("do_11260735471149056012299", "do_11260735471149056012300", "do_11260735471149056012301")) - val unitLeafNodes_1 = Map("do_1127212344324751361295:do_course_unit1:leafnodes" -> List("do_11260735471149056012299")) - val unitLeafNodes_2 = Map("do_1127212344324751361295:do_course_unit2:leafnodes" -> List("do_11260735471149056012300")) - val unitLeafNodes_3 = Map("do_1127212344324751361295:do_course_unit3:leafnodes" -> List("do_11260735471149056012301", "do_11260735471149056012300")) - - val ancestorsResource_1 = Map("do_1127212344324751361295:do_11260735471149056012299:ancestors" -> List("do_course_unit1", "do_1127212344324751361295")) - val ancestorsResource_2 = Map("do_1127212344324751361295:do_11260735471149056012300:ancestors" -> List("do_course_unit2", "do_course_unit3", "do_1127212344324751361295")) - val ancestorsResource_3 = Map("do_1127212344324751361295:do_11260735471149056012301:ancestors" -> List("do_course_unit3", "do_1127212344324751361295")) - - val CASE_1:Map[String, AnyRef] = Map("event" -> EVENT_1, "cacheData" -> List(courseLeafNodes, - unitLeafNodes_1, unitLeafNodes_2,unitLeafNodes_3, ancestorsResource_1,ancestorsResource_2,ancestorsResource_3)) - - - - val EVENT_2: String = - """ - |{"eid":"BE_JOB_REQUEST","ets":1563788371969,"mid":"LMS.1563788371969.590c5fa0-0ce8-46ed-bf6c-681c0a1fdac8","actor":{"type":"System","id":"Course Batch Updater"},"context":{"pdata":{"ver":"1.0","id":"org.sunbird.platform"}},"object":{"type":"CourseBatchEnrolment","id":"0126083288437637121_8454cb21-3ce9-4e30-85b5-fade097880d8"},"edata":{"contents":[{"contentId":"do_R1","status":2},{"contentId":"do_R2","status":1},{"contentId":"do_R3","status":2}],"action":"batch-enrolment-update","iteration":1,"batchId":"Batch1","userId":"user001","courseId":"course001"}} - |""".stripMargin - - /** **** course structure **** - * - * case2: When all resource progress is 2 in the content-consumption table - * - * course001 - course - * unit1 - unit1 - * do_R1 - Resource - * do_R3 - Resource - * unit2 - Unit2 - * do_R2 - Resource - * do_R3 - Resource - * - * ============== content status in the event ====== - * do_R1 - 1 - * do_R3 - 1 - * do_R2 - 1 - * ============== content status in the database(content-consumption) - * do_R1 - 2 - * do_R2 - 2 - * do_R3 - 2 - * - * //Unit Level - * course001:do_R1:ansestor => unit1,course001 - * unit1:leafNodes -> do_R1,do_R3 - * output:leafNodesSize = 2, completed=2 - * course001:do_R2:ansestor => unit2,course001 - * unit2:leafNodes -> do_R2,do_R3 - * output:leafNodesSize = 2, completed = 2 - * course001:do_R3:ansestor => unit1, unit2,course001 - * unit1:leafNodes -> do_R1,do_R3 - * unit2:leafNodes -> do_R2,do_R3 - * output:leafNodes=2, completed=2 - * // CourseLevel - * output:LeafNodes =3, Completed =3 - * - * - */ - val e2_courseLeafNodes = Map("course001:course001:leafnodes" -> List("do_R1", "do_R3", "do_R2")) - val e2_unitLeafNodes_1 = Map("course001:unit1:leafnodes" -> List("do_R1", "do_R3")) - val e2_unitLeafNodes_2 = Map("course001:unit2:leafnodes" -> List("do_R2", "do_R3")) - - val e2_ancestorsResource_1 = Map("course001:do_R1:ancestors" -> List("unit1", "course001")) - val e2_ancestorsResource_2 = Map("course001:do_R3:ancestors" -> List("unit1", "unit2", "course001")) - val e2_ancestorsResource_3 = Map("course001:do_R2:ancestors" -> List("unit2", "course001")) - - val CASE_2:Map[String, AnyRef] = Map("event" -> EVENT_2, "cacheData" -> List(e2_courseLeafNodes, - e2_unitLeafNodes_1, e2_unitLeafNodes_2,e2_ancestorsResource_1, e2_ancestorsResource_2,e2_ancestorsResource_3)) - - /** * - * - * Case3: When resource data is not available in the content_consumption table and user_activity_agg - * - * C11 - course - * unit11 - unit1 - * R11 - Resource - * R22 - Resource - * unit22 - Unit2 - * R11 - Resource - * - * ============== content status in the event ====== - * R11 - 2 - * R22 - 2 - * ============== content status in the database(content-consumption) - * Data is not available - * - * //Unit Level - * C11:R11:ansestor => unit11,unit22,C11 - * unit11:leafNodes -> R11,R22 - * output:leafNodesSize = 2, completed=2 - * unit11:leafNodes -> R11 - * output:leafNodesSize = 1, completed=1 - * C11:R22:ansestor => unit11,C11 - * unit11:leafNodes -> R11,R22 - * output:leafNodesSize = 2, completed = 2 - * // CourseLevel - * output:LeafNodes =2, Completed =2 - * - */ - - val e3_courseLeafNodes = Map("C11:C11:leafnodes" -> List("R11", "R22")) - val e3_unitLeafNodes_1 = Map("C11:unit11:leafnodes" -> List("R11", "R22")) - val e3_unitLeafNodes_2 = Map("C11:unit22:leafnodes" -> List("R11")) - - val e3_ancestorsResource_1 = Map("C11:R11:ancestors" -> List("unit11", "C11")) - val e3_ancestorsResource_2 = Map("C11:R11:ancestors" -> List("unit22", "C11")) - val e3_ancestorsResource_3 = Map("C11:R22:ancestors" -> List("unit11", "C11")) - - val EVENT_3: String = - """ - |{"eid":"BE_JOB_REQUEST","ets":1563788371969,"mid":"LMS.1563788371969.590c5fa0-0ce8-46ed-bf6c-681c0a1fdac8","actor":{"type":"System","id":"Course Batch Updater"},"context":{"pdata":{"ver":"1.0","id":"org.sunbird.platform"}},"object":{"type":"CourseBatchEnrolment","id":"0126083288437637121_8454cb21-3ce9-4e30-85b5-fade097880d8"},"edata":{"contents":[{"contentId":"R11","status":2},{"contentId":"R22","status":2}],"action":"batch-enrolment-update","iteration":1,"batchId":"B11","userId":"U11","courseId":"C11"}} - |""".stripMargin - - val CASE_3:Map[String, AnyRef] = Map("event" -> EVENT_3, "cacheData" -> List(e3_courseLeafNodes, e3_unitLeafNodes_1, e3_unitLeafNodes_2,e3_ancestorsResource_1, e3_ancestorsResource_2, e3_ancestorsResource_3) ) - - val EVENT_4: String = - """ - |{"eid":"BE_JOB_REQUEST","ets":1563788371969,"mid":"LMS.1563788371969.590c5fa0-0ce8-46ed-bf6c-681c0a1fdac8","actor":{"type":"System","id":"Course Batch Updater"},"context":{"pdata":{"ver":"1.0","id":"org.sunbird.platform"}},"object":{"type":"CourseBatchEnrolment","id":"0126083288437637121_8454cb21-3ce9-4e30-85b5-fade097880d8"},"edata":{"contents":[{"contentId":"do_11260735471149056012299","status":2},{"contentId":"do_11260735471149056012300","status":1},{"contentId":"do_11260735471149056012301","status":1}],"action":"batch-enrolment-update","iteration":1,"batchId":"0126083288437637121","userId":"8454cb21-3ce9-4e30-85b5-fade097880d8","courseId":"do_1127212344324751361295"}} - |""".stripMargin - - val EVENT_5: String = - """ - |{"eid":"BE_JOB_REQUEST","ets":1563788371969,"mid":"LMS.1563788371969.590c5fa0-0ce8-46ed-bf6c-681c0a1fdac8","actor":{"type":"System","id":"Course Batch Updater"},"context":{"pdata":{"ver":"1.0","id":"org.sunbird.platform"}},"object":{"type":"CourseBatchEnrolment","id":"0126083288437637121_8454cb21-3ce9-4e30-85b5-fade097880d8"},"edata":{"contents":[{"contentId":"do_11260735471149056012299","status":2},{"contentId":"do_11260735471149056012300","status":1},{"contentId":"do_11260735471149056012301","status":1}],"action":"batch-update","iteration":1,"batchId":"0126083288437637121","userId":"8454cb21-3ce9-4e30-85b5-fade097880d8","courseId":"do_1127212344324751361295"}} - |""".stripMargin - - val CC_EVENT1: String = - """ - |{"eid":"BE_JOB_REQUEST","ets":1563788371969,"mid":"LMS.1563788371969.590c5fa0-0ce8-46ed-bf6c-681c0a1fdac81","actor":{"type":"System","id":"Course Batch Updater"},"context":{"pdata":{"ver":"1.0","id":"org.sunbird.platform"}},"object":{"type":"CourseBatchEnrolment","id":"0126083288437637121_8454cb21-3ce9-4e30-85b5-fade097880d8"},"edata":{"contents":[{"contentId":"do_R1","status":2}],"action":"batch-enrolment-update","iteration":1,"batchId":"Batch1","userId":"user001","courseId":"course001"}} - |""".stripMargin - val CC_EVENT2: String = - """ - |{"eid":"BE_JOB_REQUEST","ets":1563788371969,"mid":"LMS.1563788371969.590c5fa0-0ce8-46ed-bf6c-681c0a1fdac82","actor":{"type":"System","id":"Course Batch Updater"},"context":{"pdata":{"ver":"1.0","id":"org.sunbird.platform"}},"object":{"type":"CourseBatchEnrolment","id":"0126083288437637121_8454cb21-3ce9-4e30-85b5-fade097880d8"},"edata":{"contents":[{"contentId":"do_R2","status":2}],"action":"batch-enrolment-update","iteration":1,"batchId":"Batch1","userId":"user001","courseId":"course001"}} - |""".stripMargin - val CC_EVENT3: String = - """ - |{"eid":"BE_JOB_REQUEST","ets":1563788371969,"mid":"LMS.1563788371969.590c5fa0-0ce8-46ed-bf6c-681c0a1fdac83","actor":{"type":"System","id":"Course Batch Updater"},"context":{"pdata":{"ver":"1.0","id":"org.sunbird.platform"}},"object":{"type":"CourseBatchEnrolment","id":"0126083288437637121_8454cb21-3ce9-4e30-85b5-fade097880d8"},"edata":{"contents":[{"contentId":"do_R3","status":2}],"action":"batch-enrolment-update","iteration":1,"batchId":"Batch1","userId":"user001","courseId":"course001"}} - |""".stripMargin -} \ No newline at end of file diff --git a/activity-aggregate-updater/src/test/scala/org/sunbird/job/spec/ActivityAggregateUpdaterTaskTestSpec.scala b/activity-aggregate-updater/src/test/scala/org/sunbird/job/spec/ActivityAggregateUpdaterTaskTestSpec.scala deleted file mode 100644 index 118c9b037..000000000 --- a/activity-aggregate-updater/src/test/scala/org/sunbird/job/spec/ActivityAggregateUpdaterTaskTestSpec.scala +++ /dev/null @@ -1,220 +0,0 @@ -package org.sunbird.job.spec - -import java.util - -import com.datastax.driver.core.Row -import com.google.gson.Gson -import com.typesafe.config.{Config, ConfigFactory} -import org.apache.flink.api.common.typeinfo.TypeInformation -import org.apache.flink.api.java.typeutils.TypeExtractor -import org.apache.flink.runtime.testutils.MiniClusterResourceConfiguration -import org.apache.flink.streaming.api.functions.source.SourceFunction -import org.apache.flink.streaming.api.functions.source.SourceFunction.SourceContext -import org.apache.flink.test.util.MiniClusterWithClientResource -import org.cassandraunit.CQLDataLoader -import org.cassandraunit.dataset.cql.FileCQLDataSet -import org.cassandraunit.utils.EmbeddedCassandraServerHelper -import org.mockito.Mockito -import org.mockito.Mockito._ -import org.sunbird.job.cache.RedisConnect -import org.sunbird.job.connector.FlinkKafkaConnector -import org.sunbird.job.fixture.EventFixture -import org.sunbird.job.aggregate.task.{ActivityAggregateUpdaterConfig, ActivityAggregateUpdaterStreamTask} -import org.sunbird.job.util.{CassandraUtil, HTTPResponse, HttpUtil} -import org.sunbird.spec.{BaseMetricsReporter, BaseTestSpec} -import redis.clients.jedis.Jedis -import redis.embedded.RedisServer - -import scala.collection.mutable -import scala.collection.JavaConverters._ - -class ActivityAggregateUpdaterTaskTestSpec extends BaseTestSpec { - - implicit val mapTypeInfo: TypeInformation[util.Map[String, AnyRef]] = TypeExtractor.getForClass(classOf[util.Map[String, AnyRef]]) - - val flinkCluster = new MiniClusterWithClientResource(new MiniClusterResourceConfiguration.Builder() - .setConfiguration(testConfiguration()) - .setNumberSlotsPerTaskManager(1) - .setNumberTaskManagers(1) - .build) - - var redisServer: RedisServer = _ - redisServer = new RedisServer(6340) - redisServer.start() - var jedis: Jedis = _ - val mockKafkaUtil: FlinkKafkaConnector = mock[FlinkKafkaConnector](Mockito.withSettings().serializable()) - val gson = new Gson() - val config: Config = ConfigFactory.load("test.conf") - val courseAggregatorConfig: ActivityAggregateUpdaterConfig = new ActivityAggregateUpdaterConfig(config) - val mockHttpUtil: HttpUtil = mock[HttpUtil](Mockito.withSettings().serializable()) - - var cassandraUtil: CassandraUtil = _ - - val requestBody = s"""{ - | "request": { - | "filters": { - | "objectType": "Collection", - | "identifier": "course001", - | "status": ["Live", "Unlisted", "Retired"] - | }, - | "fields": ["status"] - | } - |}""".stripMargin - - override protected def beforeAll(): Unit = { - super.beforeAll() - val redisConnect = new RedisConnect(courseAggregatorConfig) - jedis = redisConnect.getConnection(courseAggregatorConfig.nodeStore) - EmbeddedCassandraServerHelper.startEmbeddedCassandra(80000L) - cassandraUtil = new CassandraUtil(courseAggregatorConfig.dbHost, courseAggregatorConfig.dbPort) - val session = cassandraUtil.session - - val dataLoader = new CQLDataLoader(session) - dataLoader.load(new FileCQLDataSet(getClass.getResource("/test.cql").getPath, true, true)) - // Clear the metrics - testCassandraUtil(cassandraUtil) - BaseMetricsReporter.gaugeMetrics.clear() - jedis.flushDB() - flinkCluster.before() - updateRedis(jedis, EventFixture.CASE_1.asInstanceOf[Map[String, AnyRef]]) - updateRedis(jedis, EventFixture.CASE_2.asInstanceOf[Map[String, AnyRef]]) - updateRedis(jedis, EventFixture.CASE_3.asInstanceOf[Map[String, AnyRef]]) - } - - override protected def afterAll(): Unit = { - super.afterAll() - try { - EmbeddedCassandraServerHelper.cleanEmbeddedCassandra() - redisServer.stop() - } catch { - case ex: Exception => { - } - } - flinkCluster.after() - } - - def initialize() { - when(mockKafkaUtil.kafkaMapSource(courseAggregatorConfig.kafkaInputTopic)).thenReturn(new CompleteContentConsumptionMapSource) - when(mockKafkaUtil.kafkaStringSink(courseAggregatorConfig.kafkaAuditEventTopic)).thenReturn(new AuditEventSink) - when(mockKafkaUtil.kafkaStringSink(courseAggregatorConfig.kafkaFailedEventTopic)).thenReturn(new FailedEventSink) - when(mockKafkaUtil.kafkaStringSink(courseAggregatorConfig.kafkaCertIssueTopic)).thenReturn(new CertificateIssuedEventsSink) - } - - "Activity Aggregator " should " compute and update enrolment as completed when all the content consumption data processed" in { - initialize() - new ActivityAggregateUpdaterStreamTask(courseAggregatorConfig, mockKafkaUtil, new HttpUtil).process() - BaseMetricsReporter.gaugeMetrics(s"${courseAggregatorConfig.jobName}.${courseAggregatorConfig.totalEventCount}").getValue() should be(3) - BaseMetricsReporter.gaugeMetrics(s"${courseAggregatorConfig.jobName}.${courseAggregatorConfig.batchEnrolmentUpdateEventCount}").getValue() should be(3) - BaseMetricsReporter.gaugeMetrics(s"${courseAggregatorConfig.jobName}.${courseAggregatorConfig.dbReadCount}").getValue() should be(3) - BaseMetricsReporter.gaugeMetrics(s"${courseAggregatorConfig.jobName}.${courseAggregatorConfig.dbUpdateCount}").getValue() should be(6) - BaseMetricsReporter.gaugeMetrics(s"${courseAggregatorConfig.jobName}.${courseAggregatorConfig.cacheHitCount}").getValue() should be(18) - BaseMetricsReporter.gaugeMetrics(s"${courseAggregatorConfig.jobName}.${courseAggregatorConfig.processedEnrolmentCount}").getValue() should be(3) - BaseMetricsReporter.gaugeMetrics(s"${courseAggregatorConfig.jobName}.${courseAggregatorConfig.enrolmentCompleteCount}").getValue() should be(1) - BaseMetricsReporter.gaugeMetrics(s"${courseAggregatorConfig.jobName}.${courseAggregatorConfig.failedEventCount}").getValue() should be(0) - BaseMetricsReporter.gaugeMetrics(s"${courseAggregatorConfig.jobName}.${courseAggregatorConfig.skipEventsCount}").getValue() should be(0) - BaseMetricsReporter.gaugeMetrics(s"${courseAggregatorConfig.jobName}.${courseAggregatorConfig.cacheMissCount}").getValue() should be(0) - - AuditEventSink.values.size() should be(4) - AuditEventSink.values.forEach(event => { - println("AUDIT_TELEMETRY_EVENT: " + event) - }) - jedis.select(courseAggregatorConfig.deDupStore) - val deDupKeys = jedis.keys("*") - println("DeDup Keys:" + deDupKeys) - deDupKeys.size() should be (3) - jedis.select(courseAggregatorConfig.nodeStore) - } - - "Activity Aggregator " should " throw exception when the cache not available for root collection" in { - jedis.select(courseAggregatorConfig.nodeStore) - jedis.flushAll() - when(mockHttpUtil.post(courseAggregatorConfig.searchAPIURL, requestBody)).thenReturn(HTTPResponse(200, """{"id":"api.v1.search","ver":"1.0","ts":"2020-12-16T12:37:40.283Z","params":{"resmsgid":"7c4cf0b0-3f9b-11eb-9b0c-abcfbdf41bc3","msgid":"7c4b1bf0-3f9b-11eb-9b0c-abcfbdf41bc3","status":"successful","err":null,"errmsg":null},"responseCode":"OK","result":{"count":1,"content":[{"identifier":"course001","objectType":"Content","status":"Live"}]}}""")) - initialize() - - val activityAggTask = new ActivityAggregateUpdaterStreamTask(courseAggregatorConfig, mockKafkaUtil, mockHttpUtil) - the [Exception] thrownBy { - activityAggTask.process() - } should have message "Job execution failed." - - // De-dup should not save the keys for which the processing failed. - // This will help in processing the same data after restart. - jedis.select(courseAggregatorConfig.deDupStore) - jedis.keys("*").size() should be (0) - jedis.select(courseAggregatorConfig.nodeStore) - - FailedEventSink.values.forEach(event => { - println("FAILED_EVENT_DATA: " + event) - }) - // failedEventSink.values.size() should be (2) - } - - ignore should " skip the retired collection consumption events" in { - jedis.select(courseAggregatorConfig.nodeStore) - jedis.flushAll() - reset(mockHttpUtil) - when(mockHttpUtil.post(courseAggregatorConfig.searchAPIURL, requestBody)).thenReturn(HTTPResponse(200, """{"id":"api.v1.search","ver":"1.0","ts":"2020-12-16T12:37:40.283Z","params":{"resmsgid":"7c4cf0b0-3f9b-11eb-9b0c-abcfbdf41bc3","msgid":"7c4b1bf0-3f9b-11eb-9b0c-abcfbdf41bc3","status":"successful","err":null,"errmsg":null},"responseCode":"OK","result":{"count":1,"content":[{"identifier":"course001","objectType":"Content","status":"Retired"}]}}""")) - initialize() - - val activityAggTask = new ActivityAggregateUpdaterStreamTask(courseAggregatorConfig, mockKafkaUtil, mockHttpUtil) - activityAggTask.process() - - jedis.select(courseAggregatorConfig.deDupStore) - jedis.keys("*").size() should be (0) - jedis.select(courseAggregatorConfig.nodeStore) - - BaseMetricsReporter.gaugeMetrics(s"${courseAggregatorConfig.jobName}.${courseAggregatorConfig.retiredCCEventsCount}").getValue() should be(3) - - } - - def testCassandraUtil(cassandraUtil: CassandraUtil): Unit = { - cassandraUtil.reconnect() - } - - def updateRedis(jedis: Jedis, testData: Map[String, AnyRef]) { - testData.get("cacheData").map(data => { - data.asInstanceOf[List[Map[String, AnyRef]]].map(cacheData => { - cacheData.map(x => { - x._2.asInstanceOf[List[String]].foreach(d => { - jedis.sadd(x._1, d) - }) - }) - }) - }) - } - - def readFromCassandra(event: String): util.List[Row] = { - val event1_primaryCols = getPrimaryCols(gson.fromJson(event, new util.LinkedHashMap[String, AnyRef]().getClass).asInstanceOf[util.Map[String, AnyRef]].asScala.asJava) - val query = s"select * from sunbird_courses.user_activity_agg where context_id='cb:${event1_primaryCols.get("batchid").get}' and user_id='${event1_primaryCols.get("userid").get}' ALLOW FILTERING;" - cassandraUtil.find(query) - } - - def readFromContentConsumptionTable(event: String): util.List[Row] = { - val event1_primaryCols = getPrimaryCols(gson.fromJson(event, new util.LinkedHashMap[String, AnyRef]().getClass).asInstanceOf[util.Map[String, AnyRef]].asScala.asJava) - val query = s"select * from sunbird_courses.user_content_consumption where userid='${event1_primaryCols.get("userid").get}' and batchid='${event1_primaryCols.get("batchid").get}' and courseid='${event1_primaryCols.get("courseid").get}' ALLOW FILTERING;" - cassandraUtil.find(query) - } - - - def getPrimaryCols(event: util.Map[String, AnyRef]): mutable.Map[String, String] = { - val eventData = event.get("edata").asInstanceOf[util.Map[String, AnyRef]] - val primaryFields = List("userid", "courseid", "batchid") - eventData.asScala.map(v => (v._1.toLowerCase, v._2)).filter(x => primaryFields.contains(x._1)).asInstanceOf[mutable.Map[String, String]] - } -} - -private class CompleteContentConsumptionMapSource extends SourceFunction[util.Map[String, AnyRef]] { - - override def run(ctx: SourceContext[util.Map[String, AnyRef]]) { - ctx.collect(jsonToMap(EventFixture.CC_EVENT1)) - ctx.collect(jsonToMap(EventFixture.CC_EVENT2)) - ctx.collect(jsonToMap(EventFixture.CC_EVENT3)) - } - - override def cancel() = {} - - def jsonToMap(json: String): util.Map[String, AnyRef] = { - val gson = new Gson() - gson.fromJson(json, new util.LinkedHashMap[String, AnyRef]().getClass).asInstanceOf[util.Map[String, AnyRef]] - } - -} diff --git a/activity-aggregate-updater/src/test/scala/org/sunbird/job/spec/BaseActivityAggregateTestSpec.scala b/activity-aggregate-updater/src/test/scala/org/sunbird/job/spec/BaseActivityAggregateTestSpec.scala deleted file mode 100644 index cd881c3ab..000000000 --- a/activity-aggregate-updater/src/test/scala/org/sunbird/job/spec/BaseActivityAggregateTestSpec.scala +++ /dev/null @@ -1,59 +0,0 @@ -package org.sunbird.job.spec - -import java.util - -import org.apache.flink.streaming.api.functions.sink.SinkFunction - - -class AuditEventSink extends SinkFunction[String] { - - override def invoke(value: String): Unit = { - synchronized { - AuditEventSink.values.add(value) - } - } -} - -object AuditEventSink { - val values: util.List[String] = new util.ArrayList() -} - -class FailedEventSink extends SinkFunction[String] { - - override def invoke(value: String): Unit = { - synchronized { - FailedEventSink.values.add(value) - } - } -} - -object FailedEventSink { - val values: util.List[String] = new util.ArrayList() -} - -class SuccessEvent extends SinkFunction[String] { - - override def invoke(value: String): Unit = { - synchronized { - SuccessEventSink.values.add(value) - } - } -} - -object SuccessEventSink { - val values: util.List[String] = new util.ArrayList() -} - - -class CertificateIssuedEventsSink extends SinkFunction[String] { - - override def invoke(value: String): Unit = { - synchronized { - CertificateIssuedEvents.values.add(value) - } - } -} - -object CertificateIssuedEvents { - val values: util.List[String] = new util.ArrayList() -} diff --git a/asset-enrichment/src/main/resources/asset-enrichment.conf b/asset-enrichment/src/main/resources/asset-enrichment.conf index a3dfaba67..2f16eabf7 100644 --- a/asset-enrichment/src/main/resources/asset-enrichment.conf +++ b/asset-enrichment/src/main/resources/asset-enrichment.conf @@ -33,4 +33,16 @@ content { thumbnail.max { sample = 5 size.pixel = 150 -} \ No newline at end of file +} + +cloudstorage.metadata.replace_absolute_path=false +cloudstorage.relative_path_prefix= "CONTENT_STORAGE_BASE_PATH" +cloudstorage.read_base_path="https://sunbirddev.blob.core.windows.net" +cloudstorage.write_base_path=["https://sunbirddev.blob.core.windows.net","https://obj.dev.sunbird.org"] +cloudstorage.metadata.list=["appIcon","posterImage","artifactUrl","downloadUrl","variants","previewUrl","pdfUrl", "streamingUrl", "toc_url"] + +cloud_storage_type="" +cloud_storage_key="" +cloud_storage_secret="" +cloud_storage_container="" +cloud_storage_endpoint="" \ No newline at end of file diff --git a/asset-enrichment/src/main/scala/org/sunbird/job/assetenricment/functions/ImageEnrichmentFunction.scala b/asset-enrichment/src/main/scala/org/sunbird/job/assetenricment/functions/ImageEnrichmentFunction.scala index 6c337f9a1..9ad7418f3 100644 --- a/asset-enrichment/src/main/scala/org/sunbird/job/assetenricment/functions/ImageEnrichmentFunction.scala +++ b/asset-enrichment/src/main/scala/org/sunbird/job/assetenricment/functions/ImageEnrichmentFunction.scala @@ -26,7 +26,7 @@ class ImageEnrichmentFunction(config: AssetEnrichmentConfig, override def open(parameters: Configuration): Unit = { super.open(parameters) - neo4JUtil = new Neo4JUtil(config.graphRoutePath, config.graphName) + neo4JUtil = new Neo4JUtil(config.graphRoutePath, config.graphName, config) } override def close(): Unit = { diff --git a/asset-enrichment/src/main/scala/org/sunbird/job/assetenricment/functions/VideoEnrichmentFunction.scala b/asset-enrichment/src/main/scala/org/sunbird/job/assetenricment/functions/VideoEnrichmentFunction.scala index cf989d446..f94a6cfbd 100644 --- a/asset-enrichment/src/main/scala/org/sunbird/job/assetenricment/functions/VideoEnrichmentFunction.scala +++ b/asset-enrichment/src/main/scala/org/sunbird/job/assetenricment/functions/VideoEnrichmentFunction.scala @@ -25,7 +25,7 @@ class VideoEnrichmentFunction(config: AssetEnrichmentConfig, override def open(parameters: Configuration): Unit = { super.open(parameters) - neo4JUtil = new Neo4JUtil(config.graphRoutePath, config.graphName) + neo4JUtil = new Neo4JUtil(config.graphRoutePath, config.graphName, config) } override def close(): Unit = { diff --git a/asset-enrichment/src/test/scala/org/sunbird/job/spec/AssetEnrichmentTaskTestSpec.scala b/asset-enrichment/src/test/scala/org/sunbird/job/spec/AssetEnrichmentTaskTestSpec.scala index d958a8890..973f48f31 100644 --- a/asset-enrichment/src/test/scala/org/sunbird/job/spec/AssetEnrichmentTaskTestSpec.scala +++ b/asset-enrichment/src/test/scala/org/sunbird/job/spec/AssetEnrichmentTaskTestSpec.scala @@ -80,7 +80,7 @@ class AssetEnrichmentTaskTestSpec extends BaseTestSpec { new VideoEnrichmentFunction(jobConfig).enrichVideo(asset)(jobConfig, youTubeUtil, cloudUtil, mockNeo4JUtil) asset.get("thumbnail", "").asInstanceOf[String] should be("https://i.ytimg.com/vi/-SgZ3Enpau8/mqdefault.jpg") asset.get("status", "").asInstanceOf[String] should be("Live") - asset.get("duration", "0").asInstanceOf[String] should be("273") + asset.get("duration", "0").asInstanceOf[String] should be("274") } "validateForArtifactUrl" should "validate for content upload context driven" in { diff --git a/audit-event-generator/src/test/scala/org/sunbird/job/fixture/EventFixture.scala b/audit-event-generator/src/test/scala/org/sunbird/job/fixture/EventFixture.scala index 08797b294..0385a90d9 100644 --- a/audit-event-generator/src/test/scala/org/sunbird/job/fixture/EventFixture.scala +++ b/audit-event-generator/src/test/scala/org/sunbird/job/fixture/EventFixture.scala @@ -14,7 +14,7 @@ object EventFixture { val EVENT_3: String = """ - |{"ets":1552464380225,"channel":"in.ekstep","transactionData":{"properties":{"s3Key":{"ov":null,"nv":"content/do_11271778298376192013/artifact/pdf_1552464372724.pdf"},"size":{"ov":null,"nv":433994.0},"artifactUrl":{"ov":null,"nv":"https://ekstep-public-dev.s3-ap-south-1.amazonaws.com/content/do_11271778298376192013/artifact/pdf_1552464372724.pdf"},"lastUpdatedOn":{"ov":"2019-03-13T13:25:43.129+0530","nv":"2019-03-13T13:36:20.093+0530"},"versionKey":{"ov":"1552463743129","nv":"1552464380093"}}},"label":"Resource Content 1","nodeType":"DATA_NODE","userId":"ANONYMOUS","createdOn":"2019-03-13T13:36:20.223+0530","objectType":"Content","nodeUniqueId":"do_11271778298376192013","requestId":null,"operationType":"UPDATE","nodeGraphId":590883,"graphId":"domain"} + |{"ets":1552464380225,"channel":"in.ekstep","transactionData":{"properties":{"s3Key":{"ov":null,"nv":"content/do_11271778298376192013/artifact/pdf_1552464372724.pdf"},"size":{"ov":null,"nv":433994.0},"artifactUrl":{"ov":null,"nv":"https://sunbirddevbbpublic.blob.core.windows.net/sunbird-content-staging-knowlg/content/assets/do_21385816669265920016/pdf_233.pdf"},"lastUpdatedOn":{"ov":"2019-03-13T13:25:43.129+0530","nv":"2019-03-13T13:36:20.093+0530"},"versionKey":{"ov":"1552463743129","nv":"1552464380093"}}},"label":"Resource Content 1","nodeType":"DATA_NODE","userId":"ANONYMOUS","createdOn":"2019-03-13T13:36:20.223+0530","objectType":"Content","nodeUniqueId":"do_11271778298376192013","requestId":null,"operationType":"UPDATE","nodeGraphId":590883,"graphId":"domain"} |""".stripMargin val EVENT_4: String = diff --git a/auto-creator-v2/src/main/resources/auto-creator-v2.conf b/auto-creator-v2/src/main/resources/auto-creator-v2.conf index d99e37c2a..6b4d4a5a5 100644 --- a/auto-creator-v2/src/main/resources/auto-creator-v2.conf +++ b/auto-creator-v2/src/main/resources/auto-creator-v2.conf @@ -32,3 +32,15 @@ service { source { baseUrl = "https://dev.sunbirded.org/api" } + +cloudstorage.metadata.replace_absolute_path=false +cloudstorage.relative_path_prefix= "CONTENT_STORAGE_BASE_PATH" +cloudstorage.read_base_path="https://sunbirddev.blob.core.windows.net" +cloudstorage.write_base_path=["https://sunbirddev.blob.core.windows.net","https://obj.dev.sunbird.org"] +cloudstorage.metadata.list=["appIcon","posterImage","artifactUrl","downloadUrl","variants","previewUrl","pdfUrl", "streamingUrl", "toc_url"] + +cloud_storage_type="" +cloud_storage_key="" +cloud_storage_secret="" +cloud_storage_container="" +cloud_storage_endpoint="" diff --git a/auto-creator-v2/src/main/scala/org/sunbird/job/autocreatorv2/functions/AutoCreatorFunction.scala b/auto-creator-v2/src/main/scala/org/sunbird/job/autocreatorv2/functions/AutoCreatorFunction.scala index f2cee1a96..d7f3d4530 100644 --- a/auto-creator-v2/src/main/scala/org/sunbird/job/autocreatorv2/functions/AutoCreatorFunction.scala +++ b/auto-creator-v2/src/main/scala/org/sunbird/job/autocreatorv2/functions/AutoCreatorFunction.scala @@ -30,8 +30,8 @@ class AutoCreatorFunction(config: AutoCreatorV2Config, httpUtil: HttpUtil, override def open(parameters: Configuration): Unit = { super.open(parameters) - cassandraUtil = new CassandraUtil(config.cassandraHost, config.cassandraPort) - neo4JUtil = new Neo4JUtil(config.graphRoutePath, config.graphName) + cassandraUtil = new CassandraUtil(config.cassandraHost, config.cassandraPort, config) + neo4JUtil = new Neo4JUtil(config.graphRoutePath, config.graphName, config) cloudStorageUtil = new CloudStorageUtil(config) } diff --git a/auto-creator-v2/src/test/resources/test.conf b/auto-creator-v2/src/test/resources/test.conf index 39d34af6d..8d56b3ef4 100644 --- a/auto-creator-v2/src/test/resources/test.conf +++ b/auto-creator-v2/src/test/resources/test.conf @@ -34,4 +34,9 @@ service { source { baseUrl = "https://dev.sunbirded.org/api" -} \ No newline at end of file +} + +cloudstorage.metadata.replace_absolute_path=true +cloudstorage.read_base_path="https://sunbirddev.blob.core.windows.net" +cloudstorage.write_base_path=["https://sunbirddev.blob.core.windows.net","https://obj.dev.sunbird.org"] +cloudstorage.metadata.list=["appIcon","posterImage","artifactUrl","downloadUrl","variants","previewUrl","pdfUrl", "streamingUrl", "toc_url"] \ No newline at end of file diff --git a/auto-creator-v2/src/test/scala/org/sunbird/job/autocreatorv2/spec/helper/AutoCreatorSpec.scala b/auto-creator-v2/src/test/scala/org/sunbird/job/autocreatorv2/spec/helper/AutoCreatorSpec.scala index 6b1488e0d..219969dd7 100644 --- a/auto-creator-v2/src/test/scala/org/sunbird/job/autocreatorv2/spec/helper/AutoCreatorSpec.scala +++ b/auto-creator-v2/src/test/scala/org/sunbird/job/autocreatorv2/spec/helper/AutoCreatorSpec.scala @@ -31,7 +31,7 @@ class AutoCreatorSpec extends FlatSpec with BeforeAndAfterAll with Matchers with override protected def beforeAll(): Unit = { super.beforeAll() EmbeddedCassandraServerHelper.startEmbeddedCassandra(80000L) - cassandraUtil = new CassandraUtil(jobConfig.cassandraHost, jobConfig.cassandraPort) + cassandraUtil = new CassandraUtil(jobConfig.cassandraHost, jobConfig.cassandraPort, jobConfig) val session = cassandraUtil.session val dataLoader = new CQLDataLoader(session) dataLoader.load(new FileCQLDataSet(getClass.getResource("/test.cql").getPath, true, true)) diff --git a/auto-creator-v2/src/test/scala/org/sunbird/job/autocreatorv2/spec/helper/ObjectUpdaterSpec.scala b/auto-creator-v2/src/test/scala/org/sunbird/job/autocreatorv2/spec/helper/ObjectUpdaterSpec.scala index 1240eb70f..0c6b8d8af 100644 --- a/auto-creator-v2/src/test/scala/org/sunbird/job/autocreatorv2/spec/helper/ObjectUpdaterSpec.scala +++ b/auto-creator-v2/src/test/scala/org/sunbird/job/autocreatorv2/spec/helper/ObjectUpdaterSpec.scala @@ -31,7 +31,7 @@ class ObjectUpdaterSpec extends FlatSpec with BeforeAndAfterAll with Matchers wi override protected def beforeAll(): Unit = { super.beforeAll() EmbeddedCassandraServerHelper.startEmbeddedCassandra(80000L) - cassandraUtil = new CassandraUtil(jobConfig.cassandraHost, jobConfig.cassandraPort) + cassandraUtil = new CassandraUtil(jobConfig.cassandraHost, jobConfig.cassandraPort, jobConfig) val session = cassandraUtil.session val dataLoader = new CQLDataLoader(session) dataLoader.load(new FileCQLDataSet(getClass.getResource("/test.cql").getPath, true, true)) diff --git a/relation-cache-updater/pom.xml b/cassandra-data-migration/pom.xml similarity index 92% rename from relation-cache-updater/pom.xml rename to cassandra-data-migration/pom.xml index 25a77c1c1..4d0cc6db5 100644 --- a/relation-cache-updater/pom.xml +++ b/cassandra-data-migration/pom.xml @@ -9,12 +9,12 @@ knowledge-platform-jobs 1.0 - relation-cache-updater + cassandra-data-migration 1.0.0 jar - relation-cache-updater + cassandra-data-migration - Relation Cache Updater Flink Job + Cassandra Data Migration Flink Job @@ -66,18 +66,6 @@ test tests - - org.cassandraunit - cassandra-unit - 3.11.2.0 - test - - - it.ozimov - embedded-redis - 0.7.1 - test - org.scalatest scalatest_${scala.version} @@ -90,11 +78,17 @@ 3.3.3 test + + org.cassandraunit + cassandra-unit + 3.11.2.0 + test + - src/main/scala - src/test/scala + src/main/scala + src/test/scala org.apache.maven.plugins @@ -104,7 +98,6 @@ 11 - org.apache.maven.plugins maven-shade-plugin @@ -135,9 +128,10 @@ + - org.sunbird.job.relationcache.task.RelationCacheUpdaterStreamTask + org.sunbird.job.task.AutoCreatorV2StreamTask ${project.build.directory}/surefire-reports . - relation-cache-updater-testsuite.txt + cassandra-data-migration-testsuite.txt diff --git a/cassandra-data-migration/src/main/resources/cassandra-data-migration.conf b/cassandra-data-migration/src/main/resources/cassandra-data-migration.conf new file mode 100644 index 000000000..e096edc04 --- /dev/null +++ b/cassandra-data-migration/src/main/resources/cassandra-data-migration.conf @@ -0,0 +1,18 @@ +include "base-config.conf" + +kafka { + input.topic = "sunbirddev.cassandra.data.migration.request" + failed.topic = "sunbirddev.cassandra.data.migration.job.request.failed" + groupId = "sunbirddev-cassandra-data-migration-group" +} + +task { + consumer.parallelism = 1 + parallelism = 1 +} + +migrate { + key_value_strings_to_migrate = { + "https://sunbirddev.blob.core.windows.net": "https://ekstep-public-dev.s3-ap-south-1.amazonaws.com" + } +} diff --git a/credential-generator/collection-cert-pre-processor/src/main/resources/log4j.properties b/cassandra-data-migration/src/main/resources/log4j.properties similarity index 91% rename from credential-generator/collection-cert-pre-processor/src/main/resources/log4j.properties rename to cassandra-data-migration/src/main/resources/log4j.properties index cacd49c79..6217bab54 100644 --- a/credential-generator/collection-cert-pre-processor/src/main/resources/log4j.properties +++ b/cassandra-data-migration/src/main/resources/log4j.properties @@ -1,6 +1,6 @@ # log4j.appender.file=org.apache.log4j.FileAppender log4j.appender.file=org.apache.log4j.RollingFileAppender -log4j.appender.file.file=course-metrics-updater.log +log4j.appender.file.file=auto-creator-v2.log log4j.appender.file.append=true log4j.appender.file.layout=org.apache.log4j.PatternLayout log4j.appender.file.MaxFileSize=256KB diff --git a/cassandra-data-migration/src/main/scala/org/sunbird/job/migration/domain/Event.scala b/cassandra-data-migration/src/main/scala/org/sunbird/job/migration/domain/Event.scala new file mode 100644 index 000000000..6474a0a35 --- /dev/null +++ b/cassandra-data-migration/src/main/scala/org/sunbird/job/migration/domain/Event.scala @@ -0,0 +1,34 @@ +package org.sunbird.job.migration.domain + +import org.apache.commons.lang3.StringUtils +import org.sunbird.job.domain.reader.JobRequest +import org.sunbird.job.task.CassandraDataMigrationConfig + +class Event(eventMap: java.util.Map[String, Any], partition: Int, offset: Long) extends JobRequest(eventMap, partition, offset) { + + val jobName = "cassandra-data-migration" + + def eData: Map[String, AnyRef] = readOrDefault("edata", Map()).asInstanceOf[Map[String, AnyRef]] + + def action: String = readOrDefault[String]("edata.action", "") + + def keyspace: String = readOrDefault[String]("edata.keyspace", "") + + def table: String = readOrDefault[String]("edata.table", "") + + def column: String = readOrDefault[String]("edata.column", "") + + def columnType: String = readOrDefault[String]("edata.columnType", "") + + def primaryKeyColumn: String = readOrDefault[String]("edata.primaryKeyColumn", "") + + def primaryKeyColumnType: String = readOrDefault[String]("edata.primaryKeyColumnType", "") + + + def isValid(): Boolean = { + (StringUtils.equals("migrate-cassandra", action) && StringUtils.isNotBlank(column) && StringUtils.isNotBlank(columnType) + && StringUtils.isNotBlank(table) && StringUtils.isNotBlank(keyspace) && StringUtils.isNotBlank(primaryKeyColumn) + && StringUtils.isNotBlank(primaryKeyColumnType)) + } + +} \ No newline at end of file diff --git a/cassandra-data-migration/src/main/scala/org/sunbird/job/migration/functions/CassandraDataMigrationFunction.scala b/cassandra-data-migration/src/main/scala/org/sunbird/job/migration/functions/CassandraDataMigrationFunction.scala new file mode 100644 index 000000000..5b14172ab --- /dev/null +++ b/cassandra-data-migration/src/main/scala/org/sunbird/job/migration/functions/CassandraDataMigrationFunction.scala @@ -0,0 +1,45 @@ +package org.sunbird.job.migration.functions + +import org.apache.flink.api.common.typeinfo.TypeInformation +import org.apache.flink.configuration.Configuration +import org.apache.flink.streaming.api.functions.ProcessFunction +import org.slf4j.LoggerFactory +import org.sunbird.job.task.CassandraDataMigrationConfig +import org.sunbird.job.domain.`object`.DefinitionCache +import org.sunbird.job.migration.domain.Event +import org.sunbird.job.migration.helpers.CassandraDataMigrator +import org.sunbird.job.util._ +import org.sunbird.job.{BaseProcessFunction, Metrics} + +import java.util + +class CassandraDataMigrationFunction(config: CassandraDataMigrationConfig, + @transient var neo4JUtil: Neo4JUtil = null, + @transient var cassandraUtil: CassandraUtil = null) + (implicit mapTypeInfo: TypeInformation[util.Map[String, AnyRef]], stringTypeInfo: TypeInformation[String]) + extends BaseProcessFunction[Event, String](config) with CassandraDataMigrator { + + private[this] lazy val logger = LoggerFactory.getLogger(classOf[CassandraDataMigrationFunction]) + lazy val defCache: DefinitionCache = new DefinitionCache() + + override def metricsList(): List[String] = { + List(config.totalEventsCount, config.successEventCount, config.failedEventCount, config.skippedEventCount) + } + + override def open(parameters: Configuration): Unit = { + super.open(parameters) + cassandraUtil = new CassandraUtil(config.cassandraHost, config.cassandraPort, config) + } + + override def close(): Unit = { + super.close() + cassandraUtil.close() + } + + override def processElement(event: Event, context: ProcessFunction[Event, String]#Context, metrics: Metrics): Unit = { + logger.info("CassandraDataMigrationFunction:: processElement:: event:: " + event) + if (event.isValid()) + migrateData(event, config)(cassandraUtil) + else logger.info("CassandraDataMigrationFunction:: processElement:: event SKIPPED!! :: " + event) + } +} diff --git a/cassandra-data-migration/src/main/scala/org/sunbird/job/migration/helpers/CassandraDataMigrator.scala b/cassandra-data-migration/src/main/scala/org/sunbird/job/migration/helpers/CassandraDataMigrator.scala new file mode 100644 index 000000000..e9f6e281d --- /dev/null +++ b/cassandra-data-migration/src/main/scala/org/sunbird/job/migration/helpers/CassandraDataMigrator.scala @@ -0,0 +1,78 @@ +package org.sunbird.job.migration.helpers + +import com.datastax.driver.core.Row +import com.datastax.driver.core.querybuilder.{Clause, QueryBuilder} +import org.apache.commons.lang3.StringUtils +import org.slf4j.LoggerFactory +import org.sunbird.job.exception.InvalidInputException +import org.sunbird.job.migration.domain.Event +import org.sunbird.job.task.CassandraDataMigrationConfig +import org.sunbird.job.util._ + +trait CassandraDataMigrator { + + private[this] val logger = LoggerFactory.getLogger(classOf[CassandraDataMigrator]) + + def migrateData(event: Event, config: CassandraDataMigrationConfig)(implicit cassandraUtil: CassandraUtil): Unit = { + + // select primary key Column rows from table to migrate + val primaryKeys = readPrimaryKeysFromCassandra(event) + logger.info(s"CassandraDataMigrator:: migrateData:: After fetching primary keys. Keys Count:: " + primaryKeys.size()) + primaryKeys.forEach(col => { + val primaryKey = event.primaryKeyColumnType.toLowerCase match { + case "uuid" => col.getUUID(event.primaryKeyColumn) + case _ => col.getString(event.primaryKeyColumn) + } + val row = readColumnDataFromCassandra(primaryKey, event)(cassandraUtil) + if(row != null) { + val fetchedData: String = row.getString(event.column) + logger.info(s"CassandraDataMigrator:: migrateData:: Fetched ${event.column} in Cassandra For $primaryKey :: $fetchedData") + + val migratedData = StringUtils.replaceEach(fetchedData, config.keyValueMigrateStrings.keySet().toArray().map(_.asInstanceOf[String]), config.keyValueMigrateStrings.values().toArray().map(_.asInstanceOf[String])) + + // Pass updated data to row using primaryKey field + updateMigratedDataToCassandra(migratedData, primaryKey, event) (cassandraUtil) + } + }) + + } + + def readPrimaryKeysFromCassandra(event: Event)(implicit cassandraUtil: CassandraUtil): java.util.List[Row] = { + val query = s"""select ${event.primaryKeyColumn} from ${event.keyspace}.${event.table} ALLOW FILTERING;""" + cassandraUtil.find(query) + } + + def readColumnDataFromCassandra(primaryKey: AnyRef, event: Event)(implicit cassandraUtil: CassandraUtil): Row = { + val query = event.primaryKeyColumnType.toLowerCase match { + case "uuid" => event.columnType.toLowerCase match { + case "blob" => s"""select blobAsText(${event.column}) as ${event.column} from ${event.keyspace}.${event.table} where ${event.primaryKeyColumn}=$primaryKey ALLOW FILTERING;""" + case _ => s"""select ${event.column} from ${event.keyspace}.${event.table} where ${event.primaryKeyColumn}=$primaryKey ALLOW FILTERING;""" + } + case _ => event.columnType.toLowerCase match { + case "blob" => s"""select blobAsText(${event.column}) as ${event.column} from ${event.keyspace}.${event.table} where ${event.primaryKeyColumn}='$primaryKey' ALLOW FILTERING;""" + case _ => s"""select ${event.column} from ${event.keyspace}.${event.table} where ${event.primaryKeyColumn}='$primaryKey' ALLOW FILTERING;""" + } + } + + cassandraUtil.findOne(query) + } + + def updateMigratedDataToCassandra(migratedData: String, primaryKey: AnyRef, event: Event)(implicit cassandraUtil: CassandraUtil): Unit = { + val update = QueryBuilder.update(event.keyspace, event.table) + val clause: Clause = QueryBuilder.eq(event.primaryKeyColumn, primaryKey) + update.where.and(clause) + event.columnType.toLowerCase match { + case "blob" => update.`with`(QueryBuilder.set(event.column, QueryBuilder.fcall("textAsBlob", migratedData))) + case _ => update.`with`(QueryBuilder.set(event.column, migratedData)) + } + + logger.info(s"CassandraDataMigrator:: updateMigratedDataToCassandra:: Updating ${event.column} in Cassandra For $primaryKey :: ${update}") + val result = cassandraUtil.update(update) + if (result) logger.info(s"CassandraDataMigrator:: updateMigratedDataToCassandra:: ${event.column} Updated Successfully For $primaryKey") + else { + logger.error(s"CassandraDataMigrator:: updateMigratedDataToCassandra:: ${event.column} Update Failed For $primaryKey") + throw new InvalidInputException(s"${event.column} Update Failed For $primaryKey") + } + } + +} diff --git a/cassandra-data-migration/src/main/scala/org/sunbird/job/task/CassandraDataMigrationConfig.scala b/cassandra-data-migration/src/main/scala/org/sunbird/job/task/CassandraDataMigrationConfig.scala new file mode 100644 index 000000000..2e8e93b8b --- /dev/null +++ b/cassandra-data-migration/src/main/scala/org/sunbird/job/task/CassandraDataMigrationConfig.scala @@ -0,0 +1,40 @@ +package org.sunbird.job.task + +import java.util +import com.typesafe.config.Config +import org.apache.flink.api.common.typeinfo.TypeInformation +import org.apache.flink.api.java.typeutils.TypeExtractor +import org.sunbird.job.BaseJobConfig + + +class CassandraDataMigrationConfig(override val config: Config) extends BaseJobConfig(config, "cassandra-data-migraton") { + + implicit val mapTypeInfo: TypeInformation[util.Map[String, AnyRef]] = TypeExtractor.getForClass(classOf[util.Map[String, AnyRef]]) + implicit val stringTypeInfo: TypeInformation[String] = TypeExtractor.getForClass(classOf[String]) + + // Kafka Topics Configuration + val kafkaInputTopic: String = config.getString("kafka.input.topic") + override val kafkaConsumerParallelism: Int = config.getInt("task.consumer.parallelism") + override val parallelism: Int = config.getInt("task.parallelism") + + // Metric List + val totalEventsCount = "total-events-count" + val successEventCount = "success-events-count" + val failedEventCount = "failed-events-count" + val skippedEventCount = "skipped-events-count" + + // Consumers + val eventConsumer = "cassandra-data-migration-consumer" + val cassandraDataMigrationFunction = "cassandra-data-migration-process" + val cassandraDataMigrationEventProducer = "cassandra-data-migration-producer" + + val configVersion = "1.0" + + // DB Config + val cassandraHost: String = config.getString("lms-cassandra.host") + val cassandraPort: Int = config.getInt("lms-cassandra.port") + + val keyValueMigrateStrings: util.Map[String, String] = config.getAnyRef("migrate.key_value_strings_to_migrate").asInstanceOf[util.Map[String, String]] + + def getConfig() = config +} diff --git a/cassandra-data-migration/src/main/scala/org/sunbird/job/task/CassandraDataMigrationStreamTask.scala b/cassandra-data-migration/src/main/scala/org/sunbird/job/task/CassandraDataMigrationStreamTask.scala new file mode 100644 index 000000000..e70cad4a3 --- /dev/null +++ b/cassandra-data-migration/src/main/scala/org/sunbird/job/task/CassandraDataMigrationStreamTask.scala @@ -0,0 +1,54 @@ +package org.sunbird.job.task + +import com.typesafe.config.ConfigFactory +import org.apache.flink.api.common.typeinfo.TypeInformation +import org.apache.flink.api.java.typeutils.TypeExtractor +import org.apache.flink.api.java.utils.ParameterTool +import org.apache.flink.streaming.api.scala.StreamExecutionEnvironment +import org.slf4j.LoggerFactory +import org.sunbird.job.connector.FlinkKafkaConnector +import org.sunbird.job.migration.domain.Event +import org.sunbird.job.migration.functions.CassandraDataMigrationFunction +import org.sunbird.job.util.FlinkUtil + +import java.io.File +import java.util + + +class CassandraDataMigrationStreamTask(config: CassandraDataMigrationConfig, kafkaConnector: FlinkKafkaConnector) { + private[this] val logger = LoggerFactory.getLogger(classOf[CassandraDataMigrationStreamTask]) + + def process(): Unit = { + implicit val env: StreamExecutionEnvironment = FlinkUtil.getExecutionContext(config) + implicit val eventTypeInfo: TypeInformation[Event] = TypeExtractor.getForClass(classOf[Event]) + implicit val mapTypeInfo: TypeInformation[util.Map[String, AnyRef]] = TypeExtractor.getForClass(classOf[util.Map[String, AnyRef]]) + implicit val stringTypeInfo: TypeInformation[String] = TypeExtractor.getForClass(classOf[String]) + + val cassandraDataMigratorStream = env.addSource(kafkaConnector.kafkaJobRequestSource[Event](config.kafkaInputTopic)).name(config.eventConsumer) + .uid(config.eventConsumer).setParallelism(config.kafkaConsumerParallelism) + .rebalance + .process(new CassandraDataMigrationFunction(config)) + .name(config.cassandraDataMigrationFunction) + .uid(config.cassandraDataMigrationFunction) + .setParallelism(config.parallelism) + + env.execute(config.jobName) + } +} + +// $COVERAGE-OFF$ Disabling scoverage as the below code can only be invoked within flink cluster +object CassandraDataMigrationStreamTask { + + def main(args: Array[String]): Unit = { + val configFilePath = Option(ParameterTool.fromArgs(args).get("config.file.path")) + val config = configFilePath.map { + path => ConfigFactory.parseFile(new File(path)).resolve() + }.getOrElse(ConfigFactory.load("cassandra-data-migration.conf").withFallback(ConfigFactory.systemEnvironment())) + val cassandraDataMigratorConfig = new CassandraDataMigrationConfig(config) + val kafkaUtil = new FlinkKafkaConnector(cassandraDataMigratorConfig) + val task = new CassandraDataMigrationStreamTask(cassandraDataMigratorConfig, kafkaUtil) + task.process() + } +} + +// $COVERAGE-ON$ diff --git a/cassandra-data-migration/src/test/resources/test.conf b/cassandra-data-migration/src/test/resources/test.conf new file mode 100644 index 000000000..9746e85d4 --- /dev/null +++ b/cassandra-data-migration/src/test/resources/test.conf @@ -0,0 +1,26 @@ +include "base-config.conf" + +kafka { + input.topic = "sunbirddev.cassandra.data.migration.request" + failed.topic = "sunbirddev.cassandra.data.migration.job.request.failed" + groupId = "sunbirddev-cassandra-data-migration-group" +} + +task { + consumer.parallelism = 1 + parallelism = 1 + content-auto-creator.parallelism = 1 +} + +lms-cassandra { + keyspace = "hierarchy_store" + table = "content_hierarchy" + host = "localhost" + port = "9142" +} + +migrate { + key_value_strings_to_migrate = { + "https://sunbirddev.blob.core.windows.net": "https://ekstep-public-dev.s3-ap-south-1.amazonaws.com" + } +} diff --git a/cassandra-data-migration/src/test/resources/test.cql b/cassandra-data-migration/src/test/resources/test.cql new file mode 100644 index 000000000..c64b35f10 --- /dev/null +++ b/cassandra-data-migration/src/test/resources/test.cql @@ -0,0 +1,31 @@ +CREATE KEYSPACE IF NOT EXISTS hierarchy_store WITH replication = { + 'class': 'SimpleStrategy', + 'replication_factor': '1' +}; + +CREATE TABLE IF NOT EXISTS hierarchy_store.content_hierarchy ( + identifier text, + hierarchy text, + relational_metadata text, + PRIMARY KEY (identifier) +); + +CREATE KEYSPACE IF NOT EXISTS dialcodes WITH replication = { + 'class': 'SimpleStrategy', + 'replication_factor': '1' +}; + +CREATE TABLE IF NOT EXISTS dialcodes.dialcode_batch ( + processid uuid PRIMARY KEY, + channel text, + config map, + created_on timestamp, + dialcodes list, + publisher text, + status int, + url text +); + +INSERT INTO hierarchy_store.content_hierarchy(identifier, hierarchy) VALUES ('do_4567', '{"ownershipType":["createdBy"],"copyright":"Sunbird","downloadUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11305855864948326411234/course-hierarchy-test-1_1594062643205_do_11305855864948326411234_1.0_spine.ecar","channel":"b00bc992ef25f1a9a8d63291e20efc8d","organisation":["Sunbird"],"language":["English"],"variants":{"online":{"ecarUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11305855864948326411234/course-hierarchy-test-1_1594062643394_do_11305855864948326411234_1.0_online.ecar","size":7399.0},"spine":{"ecarUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11305855864948326411234/course-hierarchy-test-1_1594062643205_do_11305855864948326411234_1.0_spine.ecar","size":40995.0}},"mimeType":"application/vnd.ekstep.content-collection","leafNodes":["do_1130314841730334721104","do_1130314849898332161107","do_1130314857102131201110","do_1130314847650037761106","do_1130314851303178241108","do_1130314845426565121105"],"objectType":"Content","appIcon":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11305855864948326411234/artifact/2a4b8abd789184932399d222d03d9b5c.thumb.jpg","collections":[],"children":[{"ownershipType":["createdBy"],"parent":"do_11305855864948326411234","copyright":"Sunbird","code":"do_11305855931318272011245","channel":"b00bc992ef25f1a9a8d63291e20efc8d","language":["English"],"mimeType":"application/vnd.ekstep.content-collection","idealScreenSize":"normal","createdOn":"2020-07-06T19:06:56.160+0000","objectType":"Content","children":[{"ownershipType":["createdBy"],"parent":"do_11305855931318272011245","copyright":"Sunbird","code":"b99980a2-63dd-4d55-9c35-5c5a70e0c505","channel":"b00bc992ef25f1a9a8d63291e20efc8d","language":["English"],"mimeType":"application/vnd.ekstep.content-collection","idealScreenSize":"normal","createdOn":"2020-07-06T19:06:56.153+0000","objectType":"Content","children":[{"ownershipType":["createdBy"],"parent":"do_11305855931312537611237","copyright":"Sunbird","code":"3e43357b-05ad-455e-bbb3-2911be50cc71","channel":"b00bc992ef25f1a9a8d63291e20efc8d","language":["English"],"mimeType":"application/vnd.ekstep.content-collection","idealScreenSize":"normal","createdOn":"2020-07-06T19:06:56.159+0000","objectType":"Content","children":[{"ownershipType":["createdBy"],"parent":"do_11305855931317452811243","copyright":"Sunbird","code":"82426726-038e-47d1-9403-813a083278d2","channel":"b00bc992ef25f1a9a8d63291e20efc8d","language":["English"],"mimeType":"application/vnd.ekstep.content-collection","idealScreenSize":"normal","createdOn":"2020-07-06T19:06:56.156+0000","objectType":"Content","children":[{"ownershipType":["createdBy"],"parent":"do_11305855931314995211239","copyright":"Sunbird","code":"9e2e3a6e-8a76-433d-85c7-d4ac3d732157","channel":"b00bc992ef25f1a9a8d63291e20efc8d","language":["English"],"mimeType":"application/vnd.ekstep.content-collection","idealScreenSize":"normal","createdOn":"2020-07-06T19:06:56.157+0000","objectType":"Content","children":[{"ownershipType":["createdBy"],"previewUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_1130314841730334721104/artifact/sftbr-04u.pdf","downloadUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_1130314841730334721104/prad-pdf-content-1_1590758122228_do_1130314841730334721104_2.0.ecar","channel":"in.ekstep","questions":[],"language":["English"],"variants":{"spine":{"ecarUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_1130314841730334721104/prad-pdf-content-1_1590758122675_do_1130314841730334721104_2.0_spine.ecar","size":943.0}},"mimeType":"application/pdf","usesContent":[],"artifactUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_1130314841730334721104/artifact/sftbr-04u.pdf","contentEncoding":"identity","contentType":"Resource","identifier":"do_1130314841730334721104","audience":["Learner"],"visibility":"Default","mediaType":"content","itemSets":[],"osId":"org.ekstep.quiz.app","lastPublishedBy":"System","version":2,"pragma":["external"],"prevState":"Live","license":"CC BY 4.0","lastPublishedOn":"2020-05-29T13:15:18.492+0000","size":736557.0,"concepts":[],"name":"prad PDF Content-1","status":"Live","code":"test-Resourcce","prevStatus":"Processing","methods":[],"streamingUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_1130314841730334721104/artifact/sftbr-04u.pdf","idealScreenSize":"normal","createdOn":"2020-05-29T13:02:25.342+0000","contentDisposition":"inline","lastUpdatedOn":"2020-05-29T13:15:17.957+0000","SYS_INTERNAL_LAST_UPDATED_ON":"2020-05-29T13:15:23.067+0000","dialcodeRequired":"No","lastStatusChangedOn":"2020-05-29T13:15:23.061+0000","os":["All"],"cloudStorageKey":"content/do_1130314841730334721104/artifact/sftbr-04u.pdf","libraries":[],"pkgVersion":2.0,"versionKey":"1590758117957","idealScreenDensity":"hdpi","s3Key":"ecar_files/do_1130314841730334721104/prad-pdf-content-1_1590758122228_do_1130314841730334721104_2.0.ecar","framework":"NCF","compatibilityLevel":4,"index":1,"depth":6,"parent":"do_11305855931315814411241"},{"ownershipType":["createdBy"],"previewUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_1130314845426565121105/artifact/sftbr-04u.pdf","downloadUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_1130314845426565121105/prad-pdf-content-2_1590758131556_do_1130314845426565121105_1.0.ecar","channel":"in.ekstep","questions":[],"language":["English"],"variants":{"spine":{"ecarUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_1130314845426565121105/prad-pdf-content-2_1590758132007_do_1130314845426565121105_1.0_spine.ecar","size":859.0}},"mimeType":"application/pdf","usesContent":[],"artifactUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_1130314845426565121105/artifact/sftbr-04u.pdf","contentEncoding":"identity","contentType":"Resource","identifier":"do_1130314845426565121105","audience":["Learner"],"visibility":"Default","mediaType":"content","itemSets":[],"osId":"org.ekstep.quiz.app","lastPublishedBy":"System","version":2,"pragma":["external"],"prevState":"Draft","license":"CC BY 4.0","lastPublishedOn":"2020-05-29T13:15:31.543+0000","size":736471.0,"concepts":[],"name":"prad PDF Content-2","status":"Live","code":"test-Resourcce","prevStatus":"Processing","methods":[],"streamingUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_1130314845426565121105/artifact/sftbr-04u.pdf","idealScreenSize":"normal","createdOn":"2020-05-29T13:03:10.463+0000","contentDisposition":"inline","lastUpdatedOn":"2020-05-29T13:15:31.107+0000","SYS_INTERNAL_LAST_UPDATED_ON":"2020-05-29T13:15:32.306+0000","dialcodeRequired":"No","lastStatusChangedOn":"2020-05-29T13:15:32.302+0000","os":["All"],"cloudStorageKey":"content/do_1130314845426565121105/artifact/sftbr-04u.pdf","libraries":[],"pkgVersion":1.0,"versionKey":"1590758131107","idealScreenDensity":"hdpi","s3Key":"ecar_files/do_1130314845426565121105/prad-pdf-content-2_1590758131556_do_1130314845426565121105_1.0.ecar","framework":"NCF","compatibilityLevel":4,"index":2,"depth":6,"parent":"do_11305855931315814411241"}],"contentDisposition":"inline","lastUpdatedOn":"2020-07-06T19:10:42.581+0000","contentEncoding":"gzip","contentType":"CourseUnit","dialcodeRequired":"No","identifier":"do_11305855931315814411241","lastStatusChangedOn":"2020-07-06T19:06:56.157+0000","audience":["Learner"],"os":["All"],"visibility":"Parent","index":1,"mediaType":"content","osId":"org.ekstep.launcher","languageCode":["en"],"versionKey":"1594062416157","license":"CC BY 4.0","idealScreenDensity":"hdpi","framework":"NCFCOPY","depth":5,"compatibilityLevel":1,"name":"Unit - 1.1.1.1.1","status":"Live","lastPublishedOn":"2020-07-06T19:10:42.996+0000","pkgVersion":1.0,"leafNodesCount":2,"leafNodes":["do_1130314841730334721104","do_1130314845426565121105"],"downloadUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11305855864948326411234/course-hierarchy-test-1_1594062643205_do_11305855864948326411234_1.0_spine.ecar","variants":"{\"online\":{\"ecarUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11305855864948326411234/course-hierarchy-test-1_1594062643394_do_11305855864948326411234_1.0_online.ecar\",\"size\":7399.0},\"spine\":{\"ecarUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11305855864948326411234/course-hierarchy-test-1_1594062643205_do_11305855864948326411234_1.0_spine.ecar\",\"size\":40995.0}}"}],"contentDisposition":"inline","lastUpdatedOn":"2020-07-06T19:10:42.581+0000","contentEncoding":"gzip","contentType":"CourseUnit","dialcodeRequired":"No","identifier":"do_11305855931314995211239","lastStatusChangedOn":"2020-07-06T19:06:56.156+0000","audience":["Learner"],"os":["All"],"visibility":"Parent","index":1,"mediaType":"content","osId":"org.ekstep.launcher","languageCode":["en"],"versionKey":"1594062416156","license":"CC BY 4.0","idealScreenDensity":"hdpi","framework":"NCFCOPY","depth":4,"compatibilityLevel":1,"name":"Unit - 1.1.1.1","status":"Live","lastPublishedOn":"2020-07-06T19:10:42.996+0000","pkgVersion":1.0,"leafNodesCount":2,"leafNodes":["do_1130314841730334721104","do_1130314845426565121105"],"downloadUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11305855864948326411234/course-hierarchy-test-1_1594062643205_do_11305855864948326411234_1.0_spine.ecar","variants":"{\"online\":{\"ecarUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11305855864948326411234/course-hierarchy-test-1_1594062643394_do_11305855864948326411234_1.0_online.ecar\",\"size\":7399.0},\"spine\":{\"ecarUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11305855864948326411234/course-hierarchy-test-1_1594062643205_do_11305855864948326411234_1.0_spine.ecar\",\"size\":40995.0}}"}],"contentDisposition":"inline","lastUpdatedOn":"2020-07-06T19:10:42.581+0000","contentEncoding":"gzip","contentType":"CourseUnit","dialcodeRequired":"No","identifier":"do_11305855931317452811243","lastStatusChangedOn":"2020-07-06T19:06:56.159+0000","audience":["Learner"],"os":["All"],"visibility":"Parent","index":1,"mediaType":"content","osId":"org.ekstep.launcher","languageCode":["en"],"versionKey":"1594062416159","license":"CC BY 4.0","idealScreenDensity":"hdpi","framework":"NCFCOPY","depth":3,"compatibilityLevel":1,"name":"Unit - 1.1.1","status":"Live","lastPublishedOn":"2020-07-06T19:10:42.996+0000","pkgVersion":1.0,"leafNodesCount":2,"leafNodes":["do_1130314841730334721104","do_1130314845426565121105"],"downloadUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11305855864948326411234/course-hierarchy-test-1_1594062643205_do_11305855864948326411234_1.0_spine.ecar","variants":"{\"online\":{\"ecarUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11305855864948326411234/course-hierarchy-test-1_1594062643394_do_11305855864948326411234_1.0_online.ecar\",\"size\":7399.0},\"spine\":{\"ecarUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11305855864948326411234/course-hierarchy-test-1_1594062643205_do_11305855864948326411234_1.0_spine.ecar\",\"size\":40995.0}}"}],"contentDisposition":"inline","lastUpdatedOn":"2020-07-06T19:10:42.581+0000","contentEncoding":"gzip","contentType":"CourseUnit","dialcodeRequired":"No","identifier":"do_11305855931312537611237","lastStatusChangedOn":"2020-07-06T19:06:56.153+0000","audience":["Learner"],"os":["All"],"visibility":"Parent","index":1,"mediaType":"content","osId":"org.ekstep.launcher","languageCode":["en"],"versionKey":"1594062416153","license":"CC BY 4.0","idealScreenDensity":"hdpi","framework":"NCFCOPY","depth":2,"compatibilityLevel":1,"name":"Unit - 1.1","status":"Live","lastPublishedOn":"2020-07-06T19:10:42.996+0000","pkgVersion":1.0,"leafNodesCount":2,"leafNodes":["do_1130314841730334721104","do_1130314845426565121105"],"downloadUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11305855864948326411234/course-hierarchy-test-1_1594062643205_do_11305855864948326411234_1.0_spine.ecar","variants":"{\"online\":{\"ecarUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11305855864948326411234/course-hierarchy-test-1_1594062643394_do_11305855864948326411234_1.0_online.ecar\",\"size\":7399.0},\"spine\":{\"ecarUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11305855864948326411234/course-hierarchy-test-1_1594062643205_do_11305855864948326411234_1.0_spine.ecar\",\"size\":40995.0}}"}],"contentDisposition":"inline","lastUpdatedOn":"2020-07-06T19:10:42.581+0000","contentEncoding":"gzip","contentType":"CourseUnit","dialcodeRequired":"No","identifier":"do_11305855931318272011245","lastStatusChangedOn":"2020-07-06T19:06:56.160+0000","audience":["Learner"],"os":["All"],"visibility":"Parent","index":1,"mediaType":"content","osId":"org.ekstep.launcher","languageCode":["en"],"versionKey":"1594062416160","license":"CC BY 4.0","idealScreenDensity":"hdpi","framework":"NCFCOPY","depth":1,"compatibilityLevel":1,"name":"Unit - 1","status":"Live","lastPublishedOn":"2020-07-06T19:10:42.996+0000","pkgVersion":1.0,"leafNodesCount":2,"leafNodes":["do_1130314841730334721104","do_1130314845426565121105"],"downloadUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11305855864948326411234/course-hierarchy-test-1_1594062643205_do_11305855864948326411234_1.0_spine.ecar","variants":"{\"online\":{\"ecarUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11305855864948326411234/course-hierarchy-test-1_1594062643394_do_11305855864948326411234_1.0_online.ecar\",\"size\":7399.0},\"spine\":{\"ecarUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11305855864948326411234/course-hierarchy-test-1_1594062643205_do_11305855864948326411234_1.0_spine.ecar\",\"size\":40995.0}}"},{"ownershipType":["createdBy"],"parent":"do_11305855864948326411234","copyright":"Sunbird","code":"do_11305856007005798411326","channel":"b00bc992ef25f1a9a8d63291e20efc8d","language":["English"],"mimeType":"application/vnd.ekstep.content-collection","idealScreenSize":"normal","createdOn":"2020-07-06T19:08:28.552+0000","objectType":"Content","children":[{"ownershipType":["createdBy"],"parent":"do_11305856007005798411326","copyright":"Sunbird","code":"6dd56b10-389a-49d1-a413-d11494856293","channel":"b00bc992ef25f1a9a8d63291e20efc8d","language":["English"],"mimeType":"application/vnd.ekstep.content-collection","idealScreenSize":"normal","createdOn":"2020-07-06T19:08:28.555+0000","objectType":"Content","children":[{"ownershipType":["createdBy"],"parent":"do_11305856007008256011330","copyright":"Sunbird","code":"55c16590-ec0e-4c24-83c8-aed18e0b2d8d","channel":"b00bc992ef25f1a9a8d63291e20efc8d","language":["English"],"mimeType":"application/vnd.ekstep.content-collection","idealScreenSize":"normal","createdOn":"2020-07-06T19:08:28.554+0000","objectType":"Content","children":[{"ownershipType":["createdBy"],"parent":"do_11305856007007436811328","copyright":"Sunbird","code":"70a1ac3b-e26c-4d92-9058-674286e597c5","channel":"b00bc992ef25f1a9a8d63291e20efc8d","language":["English"],"mimeType":"application/vnd.ekstep.content-collection","idealScreenSize":"normal","createdOn":"2020-07-06T19:08:28.550+0000","objectType":"Content","children":[{"ownershipType":["createdBy"],"parent":"do_11305856007004160011324","copyright":"Sunbird","code":"ae34bd17-cac9-4198-b84e-244a1312bbd9","channel":"b00bc992ef25f1a9a8d63291e20efc8d","language":["English"],"mimeType":"application/vnd.ekstep.content-collection","idealScreenSize":"normal","createdOn":"2020-07-06T19:08:28.556+0000","objectType":"Content","children":[{"ownershipType":["createdBy"],"previewUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_1130314847650037761106/artifact/sftbr-04u.pdf","downloadUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_1130314847650037761106/prad-pdf-content-3_1590758142406_do_1130314847650037761106_1.0.ecar","channel":"in.ekstep","questions":[],"language":["English"],"variants":{"spine":{"ecarUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_1130314847650037761106/prad-pdf-content-3_1590758142803_do_1130314847650037761106_1.0_spine.ecar","size":859.0}},"mimeType":"application/pdf","usesContent":[],"artifactUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_1130314847650037761106/artifact/sftbr-04u.pdf","contentEncoding":"identity","contentType":"Resource","identifier":"do_1130314847650037761106","audience":["Learner"],"visibility":"Default","mediaType":"content","itemSets":[],"osId":"org.ekstep.quiz.app","lastPublishedBy":"System","version":2,"pragma":["external"],"prevState":"Draft","license":"CC BY 4.0","lastPublishedOn":"2020-05-29T13:15:41.207+0000","size":736473.0,"concepts":[],"name":"prad PDF Content-3","status":"Live","code":"test-Resourcce","prevStatus":"Processing","methods":[],"streamingUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_1130314847650037761106/artifact/sftbr-04u.pdf","idealScreenSize":"normal","createdOn":"2020-05-29T13:03:37.605+0000","contentDisposition":"inline","lastUpdatedOn":"2020-05-29T13:15:40.816+0000","SYS_INTERNAL_LAST_UPDATED_ON":"2020-05-29T13:15:43.114+0000","dialcodeRequired":"No","lastStatusChangedOn":"2020-05-29T13:15:43.110+0000","os":["All"],"cloudStorageKey":"content/do_1130314847650037761106/artifact/sftbr-04u.pdf","libraries":[],"pkgVersion":1.0,"versionKey":"1590758140816","idealScreenDensity":"hdpi","s3Key":"ecar_files/do_1130314847650037761106/prad-pdf-content-3_1590758142406_do_1130314847650037761106_1.0.ecar","framework":"NCF","compatibilityLevel":4,"index":1,"depth":6,"parent":"do_11305856007009075211332"},{"ownershipType":["createdBy"],"previewUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_1130314849898332161107/artifact/sftbr-04u.pdf","downloadUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_1130314849898332161107/prad-pdf-content-4_1590758137227_do_1130314849898332161107_1.0.ecar","channel":"in.ekstep","questions":[],"language":["English"],"variants":{"spine":{"ecarUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_1130314849898332161107/prad-pdf-content-4_1590758137643_do_1130314849898332161107_1.0_spine.ecar","size":860.0}},"mimeType":"application/pdf","usesContent":[],"artifactUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_1130314849898332161107/artifact/sftbr-04u.pdf","contentEncoding":"identity","contentType":"Resource","identifier":"do_1130314849898332161107","audience":["Learner"],"visibility":"Default","mediaType":"content","itemSets":[],"osId":"org.ekstep.quiz.app","lastPublishedBy":"System","version":2,"pragma":["external"],"prevState":"Draft","license":"CC BY 4.0","lastPublishedOn":"2020-05-29T13:15:32.747+0000","size":736472.0,"concepts":[],"name":"prad PDF Content-4","status":"Live","code":"test-Resourcce","prevStatus":"Processing","methods":[],"streamingUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_1130314849898332161107/artifact/sftbr-04u.pdf","idealScreenSize":"normal","createdOn":"2020-05-29T13:04:05.049+0000","contentDisposition":"inline","lastUpdatedOn":"2020-05-29T13:15:32.376+0000","SYS_INTERNAL_LAST_UPDATED_ON":"2020-05-29T13:15:37.968+0000","dialcodeRequired":"No","lastStatusChangedOn":"2020-05-29T13:15:37.963+0000","os":["All"],"cloudStorageKey":"content/do_1130314849898332161107/artifact/sftbr-04u.pdf","libraries":[],"pkgVersion":1.0,"versionKey":"1590758132376","idealScreenDensity":"hdpi","s3Key":"ecar_files/do_1130314849898332161107/prad-pdf-content-4_1590758137227_do_1130314849898332161107_1.0.ecar","framework":"NCF","compatibilityLevel":4,"index":2,"depth":6,"parent":"do_11305856007009075211332"}],"contentDisposition":"inline","lastUpdatedOn":"2020-07-06T19:10:42.581+0000","contentEncoding":"gzip","contentType":"CourseUnit","dialcodeRequired":"No","identifier":"do_11305856007009075211332","lastStatusChangedOn":"2020-07-06T19:08:28.556+0000","audience":["Learner"],"os":["All"],"visibility":"Parent","index":1,"mediaType":"content","osId":"org.ekstep.launcher","languageCode":["en"],"versionKey":"1594062508556","license":"CC BY 4.0","idealScreenDensity":"hdpi","framework":"NCFCOPY","depth":5,"compatibilityLevel":1,"name":"Unit - 2.1.1.1.1","status":"Live","lastPublishedOn":"2020-07-06T19:10:42.996+0000","pkgVersion":1.0,"leafNodesCount":2,"leafNodes":["do_1130314849898332161107","do_1130314847650037761106"],"downloadUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11305855864948326411234/course-hierarchy-test-1_1594062643205_do_11305855864948326411234_1.0_spine.ecar","variants":"{\"online\":{\"ecarUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11305855864948326411234/course-hierarchy-test-1_1594062643394_do_11305855864948326411234_1.0_online.ecar\",\"size\":7399.0},\"spine\":{\"ecarUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11305855864948326411234/course-hierarchy-test-1_1594062643205_do_11305855864948326411234_1.0_spine.ecar\",\"size\":40995.0}}"}],"contentDisposition":"inline","lastUpdatedOn":"2020-07-06T19:10:42.581+0000","contentEncoding":"gzip","contentType":"CourseUnit","dialcodeRequired":"No","identifier":"do_11305856007004160011324","lastStatusChangedOn":"2020-07-06T19:08:28.550+0000","audience":["Learner"],"os":["All"],"visibility":"Parent","index":1,"mediaType":"content","osId":"org.ekstep.launcher","languageCode":["en"],"versionKey":"1594062508550","license":"CC BY 4.0","idealScreenDensity":"hdpi","framework":"NCFCOPY","depth":4,"compatibilityLevel":1,"name":"Unit - 2.1.1.1","status":"Live","lastPublishedOn":"2020-07-06T19:10:42.996+0000","pkgVersion":1.0,"leafNodesCount":2,"leafNodes":["do_1130314849898332161107","do_1130314847650037761106"],"downloadUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11305855864948326411234/course-hierarchy-test-1_1594062643205_do_11305855864948326411234_1.0_spine.ecar","variants":"{\"online\":{\"ecarUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11305855864948326411234/course-hierarchy-test-1_1594062643394_do_11305855864948326411234_1.0_online.ecar\",\"size\":7399.0},\"spine\":{\"ecarUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11305855864948326411234/course-hierarchy-test-1_1594062643205_do_11305855864948326411234_1.0_spine.ecar\",\"size\":40995.0}}"}],"contentDisposition":"inline","lastUpdatedOn":"2020-07-06T19:10:42.581+0000","contentEncoding":"gzip","contentType":"CourseUnit","dialcodeRequired":"No","identifier":"do_11305856007007436811328","lastStatusChangedOn":"2020-07-06T19:08:28.554+0000","audience":["Learner"],"os":["All"],"visibility":"Parent","index":1,"mediaType":"content","osId":"org.ekstep.launcher","languageCode":["en"],"versionKey":"1594062508554","license":"CC BY 4.0","idealScreenDensity":"hdpi","framework":"NCFCOPY","depth":3,"compatibilityLevel":1,"name":"Unit - 2.1.1","status":"Live","lastPublishedOn":"2020-07-06T19:10:42.996+0000","pkgVersion":1.0,"leafNodesCount":2,"leafNodes":["do_1130314849898332161107","do_1130314847650037761106"],"downloadUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11305855864948326411234/course-hierarchy-test-1_1594062643205_do_11305855864948326411234_1.0_spine.ecar","variants":"{\"online\":{\"ecarUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11305855864948326411234/course-hierarchy-test-1_1594062643394_do_11305855864948326411234_1.0_online.ecar\",\"size\":7399.0},\"spine\":{\"ecarUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11305855864948326411234/course-hierarchy-test-1_1594062643205_do_11305855864948326411234_1.0_spine.ecar\",\"size\":40995.0}}"}],"contentDisposition":"inline","lastUpdatedOn":"2020-07-06T19:10:42.581+0000","contentEncoding":"gzip","contentType":"CourseUnit","dialcodeRequired":"No","identifier":"do_11305856007008256011330","lastStatusChangedOn":"2020-07-06T19:08:28.555+0000","audience":["Learner"],"os":["All"],"visibility":"Parent","index":1,"mediaType":"content","osId":"org.ekstep.launcher","languageCode":["en"],"versionKey":"1594062508555","license":"CC BY 4.0","idealScreenDensity":"hdpi","framework":"NCFCOPY","depth":2,"compatibilityLevel":1,"name":"Unit - 2.1","status":"Live","lastPublishedOn":"2020-07-06T19:10:42.996+0000","pkgVersion":1.0,"leafNodesCount":2,"leafNodes":["do_1130314849898332161107","do_1130314847650037761106"],"downloadUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11305855864948326411234/course-hierarchy-test-1_1594062643205_do_11305855864948326411234_1.0_spine.ecar","variants":"{\"online\":{\"ecarUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11305855864948326411234/course-hierarchy-test-1_1594062643394_do_11305855864948326411234_1.0_online.ecar\",\"size\":7399.0},\"spine\":{\"ecarUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11305855864948326411234/course-hierarchy-test-1_1594062643205_do_11305855864948326411234_1.0_spine.ecar\",\"size\":40995.0}}"}],"contentDisposition":"inline","lastUpdatedOn":"2020-07-06T19:10:42.581+0000","contentEncoding":"gzip","contentType":"CourseUnit","dialcodeRequired":"No","identifier":"do_11305856007005798411326","lastStatusChangedOn":"2020-07-06T19:08:28.552+0000","audience":["Learner"],"os":["All"],"visibility":"Parent","index":2,"mediaType":"content","osId":"org.ekstep.launcher","languageCode":["en"],"versionKey":"1594062508552","license":"CC BY 4.0","idealScreenDensity":"hdpi","framework":"NCFCOPY","depth":1,"compatibilityLevel":1,"name":"Unit - 2","status":"Live","lastPublishedOn":"2020-07-06T19:10:42.996+0000","pkgVersion":1.0,"leafNodesCount":2,"leafNodes":["do_1130314849898332161107","do_1130314847650037761106"],"downloadUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11305855864948326411234/course-hierarchy-test-1_1594062643205_do_11305855864948326411234_1.0_spine.ecar","variants":"{\"online\":{\"ecarUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11305855864948326411234/course-hierarchy-test-1_1594062643394_do_11305855864948326411234_1.0_online.ecar\",\"size\":7399.0},\"spine\":{\"ecarUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11305855864948326411234/course-hierarchy-test-1_1594062643205_do_11305855864948326411234_1.0_spine.ecar\",\"size\":40995.0}}"},{"ownershipType":["createdBy"],"parent":"do_11305855864948326411234","copyright":"Sunbird","code":"0cc610ce-b54e-469c-87b5-8aadbe55ead2","channel":"b00bc992ef25f1a9a8d63291e20efc8d","language":["English"],"mimeType":"application/vnd.ekstep.content-collection","idealScreenSize":"normal","createdOn":"2020-07-06T19:09:35.086+0000","objectType":"Content","children":[{"ownershipType":["createdBy"],"parent":"do_11305856061510451211340","copyright":"Sunbird","code":"73c4c338-5713-47c9-9d13-18b36952da43","channel":"b00bc992ef25f1a9a8d63291e20efc8d","language":["English"],"mimeType":"application/vnd.ekstep.content-collection","idealScreenSize":"normal","createdOn":"2020-07-06T19:09:35.098+0000","objectType":"Content","children":[{"ownershipType":["createdBy"],"parent":"do_11305856061520281611348","copyright":"Sunbird","code":"ac62c605-e41a-43a0-82e8-ca6930a17442","channel":"b00bc992ef25f1a9a8d63291e20efc8d","language":["English"],"mimeType":"application/vnd.ekstep.content-collection","idealScreenSize":"normal","createdOn":"2020-07-06T19:09:35.091+0000","objectType":"Content","children":[{"ownershipType":["createdBy"],"parent":"do_11305856061514547211344","copyright":"Sunbird","code":"beb2a366-c3ab-49aa-90e3-bac853837607","channel":"b00bc992ef25f1a9a8d63291e20efc8d","language":["English"],"mimeType":"application/vnd.ekstep.content-collection","idealScreenSize":"normal","createdOn":"2020-07-06T19:09:35.093+0000","objectType":"Content","children":[{"ownershipType":["createdBy"],"parent":"do_11305856061516185611346","copyright":"Sunbird","code":"dabe5332-cb83-45b5-9818-3b3b76b03299","channel":"b00bc992ef25f1a9a8d63291e20efc8d","language":["English"],"mimeType":"application/vnd.ekstep.content-collection","idealScreenSize":"normal","createdOn":"2020-07-06T19:09:35.090+0000","objectType":"Content","children":[{"ownershipType":["createdBy"],"previewUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_1130314841730334721104/artifact/sftbr-04u.pdf","downloadUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_1130314841730334721104/prad-pdf-content-1_1590758122228_do_1130314841730334721104_2.0.ecar","channel":"in.ekstep","questions":[],"language":["English"],"variants":{"spine":{"ecarUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_1130314841730334721104/prad-pdf-content-1_1590758122675_do_1130314841730334721104_2.0_spine.ecar","size":943.0}},"mimeType":"application/pdf","usesContent":[],"artifactUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_1130314841730334721104/artifact/sftbr-04u.pdf","contentEncoding":"identity","contentType":"Resource","identifier":"do_1130314841730334721104","audience":["Learner"],"visibility":"Default","mediaType":"content","itemSets":[],"osId":"org.ekstep.quiz.app","lastPublishedBy":"System","version":2,"pragma":["external"],"prevState":"Live","license":"CC BY 4.0","lastPublishedOn":"2020-05-29T13:15:18.492+0000","size":736557.0,"concepts":[],"name":"prad PDF Content-1","status":"Live","code":"test-Resourcce","prevStatus":"Processing","methods":[],"streamingUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_1130314841730334721104/artifact/sftbr-04u.pdf","idealScreenSize":"normal","createdOn":"2020-05-29T13:02:25.342+0000","contentDisposition":"inline","lastUpdatedOn":"2020-05-29T13:15:17.957+0000","SYS_INTERNAL_LAST_UPDATED_ON":"2020-05-29T13:15:23.067+0000","dialcodeRequired":"No","lastStatusChangedOn":"2020-05-29T13:15:23.061+0000","os":["All"],"cloudStorageKey":"content/do_1130314841730334721104/artifact/sftbr-04u.pdf","libraries":[],"pkgVersion":2.0,"versionKey":"1590758117957","idealScreenDensity":"hdpi","s3Key":"ecar_files/do_1130314841730334721104/prad-pdf-content-1_1590758122228_do_1130314841730334721104_2.0.ecar","framework":"NCF","compatibilityLevel":4,"index":1,"depth":6,"parent":"do_11305856061513728011342"},{"ownershipType":["createdBy"],"previewUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_1130314847650037761106/artifact/sftbr-04u.pdf","downloadUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_1130314847650037761106/prad-pdf-content-3_1590758142406_do_1130314847650037761106_1.0.ecar","channel":"in.ekstep","questions":[],"language":["English"],"variants":{"spine":{"ecarUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_1130314847650037761106/prad-pdf-content-3_1590758142803_do_1130314847650037761106_1.0_spine.ecar","size":859.0}},"mimeType":"application/pdf","usesContent":[],"artifactUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_1130314847650037761106/artifact/sftbr-04u.pdf","contentEncoding":"identity","contentType":"Resource","identifier":"do_1130314847650037761106","audience":["Learner"],"visibility":"Default","mediaType":"content","itemSets":[],"osId":"org.ekstep.quiz.app","lastPublishedBy":"System","version":2,"pragma":["external"],"prevState":"Draft","license":"CC BY 4.0","lastPublishedOn":"2020-05-29T13:15:41.207+0000","size":736473.0,"concepts":[],"name":"prad PDF Content-3","status":"Live","code":"test-Resourcce","prevStatus":"Processing","methods":[],"streamingUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_1130314847650037761106/artifact/sftbr-04u.pdf","idealScreenSize":"normal","createdOn":"2020-05-29T13:03:37.605+0000","contentDisposition":"inline","lastUpdatedOn":"2020-05-29T13:15:40.816+0000","SYS_INTERNAL_LAST_UPDATED_ON":"2020-05-29T13:15:43.114+0000","dialcodeRequired":"No","lastStatusChangedOn":"2020-05-29T13:15:43.110+0000","os":["All"],"cloudStorageKey":"content/do_1130314847650037761106/artifact/sftbr-04u.pdf","libraries":[],"pkgVersion":1.0,"versionKey":"1590758140816","idealScreenDensity":"hdpi","s3Key":"ecar_files/do_1130314847650037761106/prad-pdf-content-3_1590758142406_do_1130314847650037761106_1.0.ecar","framework":"NCF","compatibilityLevel":4,"index":2,"depth":6,"parent":"do_11305856061513728011342"},{"ownershipType":["createdBy"],"previewUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_1130314851303178241108/artifact/sftbr-04u.pdf","downloadUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_1130314851303178241108/prad-pdf-content-5_1590758139833_do_1130314851303178241108_1.0.ecar","channel":"in.ekstep","questions":[],"language":["English"],"variants":{"spine":{"ecarUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_1130314851303178241108/prad-pdf-content-5_1590758140392_do_1130314851303178241108_1.0_spine.ecar","size":857.0}},"mimeType":"application/pdf","usesContent":[],"artifactUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_1130314851303178241108/artifact/sftbr-04u.pdf","contentEncoding":"identity","contentType":"Resource","identifier":"do_1130314851303178241108","audience":["Learner"],"visibility":"Default","mediaType":"content","itemSets":[],"osId":"org.ekstep.quiz.app","lastPublishedBy":"System","version":2,"pragma":["external"],"prevState":"Draft","license":"CC BY 4.0","lastPublishedOn":"2020-05-29T13:15:39.820+0000","size":736469.0,"concepts":[],"name":"prad PDF Content-5","status":"Live","code":"test-Resourcce","prevStatus":"Processing","methods":[],"streamingUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_1130314851303178241108/artifact/sftbr-04u.pdf","idealScreenSize":"normal","createdOn":"2020-05-29T13:04:22.199+0000","contentDisposition":"inline","lastUpdatedOn":"2020-05-29T13:15:39.308+0000","SYS_INTERNAL_LAST_UPDATED_ON":"2020-05-29T13:15:40.726+0000","dialcodeRequired":"No","lastStatusChangedOn":"2020-05-29T13:15:40.722+0000","os":["All"],"cloudStorageKey":"content/do_1130314851303178241108/artifact/sftbr-04u.pdf","libraries":[],"pkgVersion":1.0,"versionKey":"1590758139308","idealScreenDensity":"hdpi","s3Key":"ecar_files/do_1130314851303178241108/prad-pdf-content-5_1590758139833_do_1130314851303178241108_1.0.ecar","framework":"NCF","compatibilityLevel":4,"index":3,"depth":6,"parent":"do_11305856061513728011342"},{"ownershipType":["createdBy"],"previewUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_1130314857102131201110/artifact/sftbr-04u.pdf","downloadUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_1130314857102131201110/prad-pdf-content-6_1590758138471_do_1130314857102131201110_1.0.ecar","channel":"in.ekstep","questions":[],"language":["English"],"variants":{"spine":{"ecarUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_1130314857102131201110/prad-pdf-content-6_1590758138908_do_1130314857102131201110_1.0_spine.ecar","size":857.0}},"mimeType":"application/pdf","usesContent":[],"artifactUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_1130314857102131201110/artifact/sftbr-04u.pdf","contentEncoding":"identity","contentType":"Resource","identifier":"do_1130314857102131201110","audience":["Learner"],"visibility":"Default","mediaType":"content","itemSets":[],"osId":"org.ekstep.quiz.app","lastPublishedBy":"System","version":2,"pragma":["external"],"prevState":"Draft","license":"CC BY 4.0","lastPublishedOn":"2020-05-29T13:15:38.461+0000","size":736468.0,"concepts":[],"name":"prad PDF Content-6","status":"Live","code":"test-Resourcce","prevStatus":"Processing","methods":[],"streamingUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_1130314857102131201110/artifact/sftbr-04u.pdf","idealScreenSize":"normal","createdOn":"2020-05-29T13:05:32.987+0000","contentDisposition":"inline","lastUpdatedOn":"2020-05-29T13:15:38.046+0000","SYS_INTERNAL_LAST_UPDATED_ON":"2020-05-29T13:15:39.231+0000","dialcodeRequired":"No","lastStatusChangedOn":"2020-05-29T13:15:39.226+0000","os":["All"],"cloudStorageKey":"content/do_1130314857102131201110/artifact/sftbr-04u.pdf","libraries":[],"pkgVersion":1.0,"versionKey":"1590758138046","idealScreenDensity":"hdpi","s3Key":"ecar_files/do_1130314857102131201110/prad-pdf-content-6_1590758138471_do_1130314857102131201110_1.0.ecar","framework":"NCF","compatibilityLevel":4,"index":4,"depth":6,"parent":"do_11305856061513728011342"}],"contentDisposition":"inline","lastUpdatedOn":"2020-07-06T19:10:42.581+0000","contentEncoding":"gzip","contentType":"CourseUnit","dialcodeRequired":"No","identifier":"do_11305856061513728011342","lastStatusChangedOn":"2020-07-06T19:09:35.090+0000","audience":["Learner"],"os":["All"],"visibility":"Parent","index":1,"mediaType":"content","osId":"org.ekstep.launcher","languageCode":["en"],"versionKey":"1594062575090","license":"CC BY 4.0","idealScreenDensity":"hdpi","framework":"NCFCOPY","depth":5,"compatibilityLevel":1,"name":"Unit - 3.1.1.1.1","status":"Live","lastPublishedOn":"2020-07-06T19:10:42.996+0000","pkgVersion":1.0,"leafNodesCount":4,"leafNodes":["do_1130314841730334721104","do_1130314857102131201110","do_1130314847650037761106","do_1130314851303178241108"],"downloadUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11305855864948326411234/course-hierarchy-test-1_1594062643205_do_11305855864948326411234_1.0_spine.ecar","variants":"{\"online\":{\"ecarUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11305855864948326411234/course-hierarchy-test-1_1594062643394_do_11305855864948326411234_1.0_online.ecar\",\"size\":7399.0},\"spine\":{\"ecarUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11305855864948326411234/course-hierarchy-test-1_1594062643205_do_11305855864948326411234_1.0_spine.ecar\",\"size\":40995.0}}"}],"contentDisposition":"inline","lastUpdatedOn":"2020-07-06T19:10:42.581+0000","contentEncoding":"gzip","contentType":"CourseUnit","dialcodeRequired":"No","identifier":"do_11305856061516185611346","lastStatusChangedOn":"2020-07-06T19:09:35.093+0000","audience":["Learner"],"os":["All"],"visibility":"Parent","index":1,"mediaType":"content","osId":"org.ekstep.launcher","languageCode":["en"],"versionKey":"1594062575093","license":"CC BY 4.0","idealScreenDensity":"hdpi","framework":"NCFCOPY","depth":4,"compatibilityLevel":1,"name":"Unit - 3.1.1.1","status":"Live","lastPublishedOn":"2020-07-06T19:10:42.996+0000","pkgVersion":1.0,"leafNodesCount":4,"leafNodes":["do_1130314841730334721104","do_1130314857102131201110","do_1130314847650037761106","do_1130314851303178241108"],"downloadUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11305855864948326411234/course-hierarchy-test-1_1594062643205_do_11305855864948326411234_1.0_spine.ecar","variants":"{\"online\":{\"ecarUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11305855864948326411234/course-hierarchy-test-1_1594062643394_do_11305855864948326411234_1.0_online.ecar\",\"size\":7399.0},\"spine\":{\"ecarUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11305855864948326411234/course-hierarchy-test-1_1594062643205_do_11305855864948326411234_1.0_spine.ecar\",\"size\":40995.0}}"}],"contentDisposition":"inline","lastUpdatedOn":"2020-07-06T19:10:42.581+0000","contentEncoding":"gzip","contentType":"CourseUnit","dialcodeRequired":"No","identifier":"do_11305856061514547211344","lastStatusChangedOn":"2020-07-06T19:09:35.091+0000","audience":["Learner"],"os":["All"],"visibility":"Parent","index":1,"mediaType":"content","osId":"org.ekstep.launcher","languageCode":["en"],"versionKey":"1594062575091","license":"CC BY 4.0","idealScreenDensity":"hdpi","framework":"NCFCOPY","depth":3,"compatibilityLevel":1,"name":"Unit - 3.1.1","status":"Live","lastPublishedOn":"2020-07-06T19:10:42.996+0000","pkgVersion":1.0,"leafNodesCount":4,"leafNodes":["do_1130314841730334721104","do_1130314857102131201110","do_1130314847650037761106","do_1130314851303178241108"],"downloadUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11305855864948326411234/course-hierarchy-test-1_1594062643205_do_11305855864948326411234_1.0_spine.ecar","variants":"{\"online\":{\"ecarUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11305855864948326411234/course-hierarchy-test-1_1594062643394_do_11305855864948326411234_1.0_online.ecar\",\"size\":7399.0},\"spine\":{\"ecarUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11305855864948326411234/course-hierarchy-test-1_1594062643205_do_11305855864948326411234_1.0_spine.ecar\",\"size\":40995.0}}"}],"contentDisposition":"inline","lastUpdatedOn":"2020-07-06T19:10:42.581+0000","contentEncoding":"gzip","contentType":"CourseUnit","dialcodeRequired":"No","identifier":"do_11305856061520281611348","lastStatusChangedOn":"2020-07-06T19:09:35.098+0000","audience":["Learner"],"os":["All"],"visibility":"Parent","index":1,"mediaType":"content","osId":"org.ekstep.launcher","languageCode":["en"],"versionKey":"1594062575098","license":"CC BY 4.0","idealScreenDensity":"hdpi","framework":"NCFCOPY","depth":2,"compatibilityLevel":1,"name":"Unit - 3.1","status":"Live","lastPublishedOn":"2020-07-06T19:10:42.996+0000","pkgVersion":1.0,"leafNodesCount":4,"leafNodes":["do_1130314841730334721104","do_1130314857102131201110","do_1130314847650037761106","do_1130314851303178241108"],"downloadUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11305855864948326411234/course-hierarchy-test-1_1594062643205_do_11305855864948326411234_1.0_spine.ecar","variants":"{\"online\":{\"ecarUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11305855864948326411234/course-hierarchy-test-1_1594062643394_do_11305855864948326411234_1.0_online.ecar\",\"size\":7399.0},\"spine\":{\"ecarUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11305855864948326411234/course-hierarchy-test-1_1594062643205_do_11305855864948326411234_1.0_spine.ecar\",\"size\":40995.0}}"}],"contentDisposition":"inline","lastUpdatedOn":"2020-07-06T19:10:42.581+0000","contentEncoding":"gzip","contentType":"CourseUnit","dialcodeRequired":"No","identifier":"do_11305856061510451211340","lastStatusChangedOn":"2020-07-06T19:09:35.086+0000","audience":["Learner"],"os":["All"],"visibility":"Parent","index":3,"mediaType":"content","osId":"org.ekstep.launcher","languageCode":["en"],"versionKey":"1594062575086","license":"CC BY 4.0","idealScreenDensity":"hdpi","framework":"NCFCOPY","depth":1,"compatibilityLevel":1,"name":"Unit - 3","status":"Live","lastPublishedOn":"2020-07-06T19:10:42.996+0000","pkgVersion":1.0,"leafNodesCount":4,"leafNodes":["do_1130314841730334721104","do_1130314857102131201110","do_1130314847650037761106","do_1130314851303178241108"],"downloadUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11305855864948326411234/course-hierarchy-test-1_1594062643205_do_11305855864948326411234_1.0_spine.ecar","variants":"{\"online\":{\"ecarUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11305855864948326411234/course-hierarchy-test-1_1594062643394_do_11305855864948326411234_1.0_online.ecar\",\"size\":7399.0},\"spine\":{\"ecarUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11305855864948326411234/course-hierarchy-test-1_1594062643205_do_11305855864948326411234_1.0_spine.ecar\",\"size\":40995.0}}"}],"contentEncoding":"gzip","lockKey":"98909e87-3361-4c6c-9b68-136fbead2bef","mimeTypesCount":"{\"application/pdf\":8,\"application/vnd.ekstep.content-collection\":15}","totalCompressedSize":5891940.0,"contentType":"Course","identifier":"do_11305855864948326411234","lastUpdatedBy":"95e4942d-cbe8-477d-aebd-ad8e6de4bfc8","audience":["Learner"],"toc_url":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11305855864948326411234/artifact/do_11305855864948326411234_toc.json","visibility":"Default","contentTypesCount":"{\"CourseUnit\":15,\"Resource\":8}","childNodes":["do_11305856007009075211332","do_11305856061513728011342","do_11305855931312537611237","do_1130314841730334721104","do_11305856061520281611348","do_11305856007005798411326","do_11305856007008256011330","do_1130314847650037761106","do_1130314851303178241108","do_11305855931317452811243","do_11305855931314995211239","do_11305856061514547211344","do_1130314849898332161107","do_11305855931315814411241","do_11305856007007436811328","do_1130314857102131201110","do_11305855931318272011245","do_11305856061510451211340","do_11305856061516185611346","do_1130314845426565121105","do_11305856007004160011324"],"consumerId":"273f3b18-5dda-4a27-984a-060c7cd398d3","mediaType":"content","osId":"org.ekstep.quiz.app","lastPublishedBy":"Ekstep","version":2,"prevState":"Draft","license":"CC BY 4.0","size":40995.0,"lastPublishedOn":"2020-07-06T19:10:42.996+0000","name":"Course Hierarchy Test - 1","status":"Live","code":"org.sunbird.pZaKxV","prevStatus":"Processing","description":"Enter description for Course","posterImage":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11299104587967692816/artifact/2a4b8abd789184932399d222d03d9b5c.jpg","idealScreenSize":"normal","createdOn":"2020-07-06T19:05:35.144+0000","copyrightYear":2020,"contentDisposition":"inline","lastUpdatedOn":"2020-07-06T19:10:42.581+0000","SYS_INTERNAL_LAST_UPDATED_ON":"2020-07-06T19:10:43.861+0000","dialcodeRequired":"No","creator":"Reviewer User","lastStatusChangedOn":"2020-07-06T19:10:43.856+0000","createdFor":["ORG_001"],"os":["All"],"pkgVersion":1.0,"versionKey":"1594062642581","idealScreenDensity":"hdpi","s3Key":"ecar_files/do_11305855864948326411234/course-hierarchy-test-1_1594062643205_do_11305855864948326411234_1.0_spine.ecar","depth":0,"framework":"NCFCOPY","createdBy":"95e4942d-cbe8-477d-aebd-ad8e6de4bfc8","leafNodesCount":6,"compatibilityLevel":4,"usedByContent":[],"board":"NCERT","resourceType":"Course","reservedDialcodes":{"K2D9J9":0},"c_sunbird_dev_private_batch_count":0,"c_sunbird_dev_open_batch_count":1,"batches":[{"createdFor":["ORG_001"],"endDate":null,"name":"Course Hierarchy Test - 1","batchId":"0130585642325770243","enrollmentType":"open","enrollmentEndDate":null,"startDate":"2020-07-06","status":0}]}'); + +INSERT INTO dialcodes.dialcode_batch(processid, channel, url) VALUES (uuid(), '123456', 'https://preprodall.blob.core.windows.net/dial/01272777697873100812/do_21312279009695334411163_english_class_1_mathematics_1601903174498.zip'); diff --git a/cassandra-data-migration/src/test/scala/org/sunbird/job/migration/fixture/EventFixture.scala b/cassandra-data-migration/src/test/scala/org/sunbird/job/migration/fixture/EventFixture.scala new file mode 100644 index 000000000..49378130d --- /dev/null +++ b/cassandra-data-migration/src/test/scala/org/sunbird/job/migration/fixture/EventFixture.scala @@ -0,0 +1,26 @@ +package org.sunbird.job.migration.fixture + +object EventFixture { + + val EVENT_1: String = + """ + |{"eid":"BE_JOB_REQUEST","ets":1619527882745,"mid":"LP.1619527882745.32dc378a-430f-49f6-83b5-bd73b767ad36","actor":{"id":"cassandra-migration","type":"System"},"context":{"channel":"ORG_001","pdata":{"id":"org.sunbird.platform","ver":"1.0"},"env":"dev"},"object":{"id":"","ver":""},"edata":{"keyspace":"hierarchy_store", "table":"content_hierarchy", "column": "hierarchy", "columnType":"String", "primaryKeyColumn": "identifier", "primaryKeyColumnType": "String", "action":"migrate-cassandra","iteration":1}} + |""".stripMargin + + val EVENT_2: String = + """ + |{"eid":"BE_JOB_REQUEST","ets":1619527882745,"mid":"LP.1619527882745.32dc378a-430f-49f6-83b5-bd73b767ad36","actor":{"id":"cassandra-migration","type":"System"},"context":{"channel":"ORG_001","pdata":{"id":"org.sunbird.platform","ver":"1.0"},"env":"dev"},"object":{"id":"","ver":""},"edata":{"keyspace":"dummy","table":"dummy", "column": "dummy", "action":"dummy","iteration":1}} + |""".stripMargin + + val EVENT_3: String = + """ + |{"eid":"BE_JOB_REQUEST","ets":1619527882745,"mid":"LP.1619527882745.32dc378a-430f-49f6-83b5-bd73b767ad36","actor":{"id":"cassandra-data-migration","type":"System"},"context":{"channel":"ORG_001","pdata":{"id":"org.sunbird.platform","ver":"1.0"},"env":"dev"},"edata":{"column":"url", "columnType":"String", "table": "dialcode_images", "keyspace": "dialcodes", "primaryKeyColumn": "filename", "primaryKeyColumnType": "String", "action":"migrate-cassandra","iteration":1}} + |""".stripMargin + + val EVENT_4: String = + """ + |{"eid":"BE_JOB_REQUEST","ets":1619527882745,"mid":"LP.1619527882745.32dc378a-430f-49f6-83b5-bd73b767ad36","actor":{"id":"cassandra-data-migration","type":"System"},"context":{"channel":"ORG_001","pdata":{"id":"org.sunbird.platform","ver":"1.0"},"env":"dev"},"edata":{"column":"url", "columnType":"String", "table": "dialcode_batch", "keyspace": "dialcodes", "primaryKeyColumn": "processid", "primaryKeyColumnType": "UUID", "action":"migrate-cassandra","iteration":1}} + |""".stripMargin + + +} \ No newline at end of file diff --git a/cassandra-data-migration/src/test/scala/org/sunbird/job/migration/helpers/CassandraDataMigratorSpec.scala b/cassandra-data-migration/src/test/scala/org/sunbird/job/migration/helpers/CassandraDataMigratorSpec.scala new file mode 100644 index 000000000..998218eff --- /dev/null +++ b/cassandra-data-migration/src/test/scala/org/sunbird/job/migration/helpers/CassandraDataMigratorSpec.scala @@ -0,0 +1,58 @@ +package org.sunbird.job.migration.helpers + +import com.typesafe.config.{Config, ConfigFactory} +import org.cassandraunit.CQLDataLoader +import org.cassandraunit.dataset.cql.FileCQLDataSet +import org.cassandraunit.utils.EmbeddedCassandraServerHelper +import org.sunbird.job.migration.domain.Event +import org.sunbird.job.migration.fixture.EventFixture +import org.sunbird.job.task.CassandraDataMigrationConfig +import org.sunbird.job.util.{CassandraUtil, JSONUtil} +import org.sunbird.spec.BaseTestSpec + +import java.util + +class CassandraDataMigratorSpec extends BaseTestSpec { + + val config: Config = ConfigFactory.load("test.conf") + var cassandraUtil: CassandraUtil = _ + val jobConfig: CassandraDataMigrationConfig = new CassandraDataMigrationConfig(config) + + override protected def beforeAll(): Unit = { + super.beforeAll() + EmbeddedCassandraServerHelper.startEmbeddedCassandra(80000L) + cassandraUtil = new CassandraUtil(jobConfig.cassandraHost, jobConfig.cassandraPort, jobConfig) + val session = cassandraUtil.session + + val dataLoader = new CQLDataLoader(session); + dataLoader.load(new FileCQLDataSet(getClass.getResource("/test.cql").getPath, true, true)); + // Clear the metrics + testCassandraUtil(cassandraUtil) + } + + override protected def afterAll(): Unit = { + super.afterAll() + try { + EmbeddedCassandraServerHelper.cleanEmbeddedCassandra() + } catch { + case ex: Exception => { + } + } + } + + + "CassandraDataMigrator " should "update the string " in { + val content_id = "do_4567" + val event = new Event(JSONUtil.deserialize[util.Map[String, Any]](EventFixture.EVENT_1), 0, 10) + new TestCassandraDataMigrator().migrateData(event, jobConfig)(cassandraUtil) + val row = new TestCassandraDataMigrator().readColumnDataFromCassandra(content_id, event)(cassandraUtil) + val migratedData: String = row.getString(event.column) + assert(migratedData.contains("\""+jobConfig.keyValueMigrateStrings.values().toArray().head)) + } + + def testCassandraUtil(cassandraUtil: CassandraUtil): Unit = { + cassandraUtil.reconnect() + } + + class TestCassandraDataMigrator extends CassandraDataMigrator {} +} diff --git a/cassandra-data-migration/src/test/scala/org/sunbird/job/migration/task/CassandraDataMigrationTaskTestSpec.scala b/cassandra-data-migration/src/test/scala/org/sunbird/job/migration/task/CassandraDataMigrationTaskTestSpec.scala new file mode 100644 index 000000000..74b3aaf67 --- /dev/null +++ b/cassandra-data-migration/src/test/scala/org/sunbird/job/migration/task/CassandraDataMigrationTaskTestSpec.scala @@ -0,0 +1,73 @@ +package org.sunbird.job.migration.task + +import com.typesafe.config.{Config, ConfigFactory} +import org.apache.flink.api.common.typeinfo.TypeInformation +import org.apache.flink.api.java.typeutils.TypeExtractor +import org.apache.flink.runtime.testutils.MiniClusterResourceConfiguration +import org.apache.flink.streaming.api.functions.source.SourceFunction +import org.apache.flink.streaming.api.functions.source.SourceFunction.SourceContext +import org.apache.flink.test.util.MiniClusterWithClientResource +import org.cassandraunit.CQLDataLoader +import org.cassandraunit.dataset.cql.FileCQLDataSet +import org.cassandraunit.utils.EmbeddedCassandraServerHelper +import org.mockito.Mockito +import org.mockito.Mockito.when +import org.sunbird.job.connector.FlinkKafkaConnector +import org.sunbird.job.migration.domain.Event +import org.sunbird.job.migration.fixture.EventFixture +import org.sunbird.job.task.{CassandraDataMigrationConfig, CassandraDataMigrationStreamTask} +import org.sunbird.job.util.{CassandraUtil, JSONUtil} +import org.sunbird.spec.{BaseMetricsReporter, BaseTestSpec} + +import java.util + +class CassandraDataMigrationTaskTestSpec extends BaseTestSpec { + + implicit val mapTypeInfo: TypeInformation[java.util.Map[String, AnyRef]] = TypeExtractor.getForClass(classOf[java.util.Map[String, AnyRef]]) + + val flinkCluster = new MiniClusterWithClientResource(new MiniClusterResourceConfiguration.Builder() + .setConfiguration(testConfiguration()) + .setNumberSlotsPerTaskManager(1) + .setNumberTaskManagers(1) + .build) + val mockKafkaUtil: FlinkKafkaConnector = mock[FlinkKafkaConnector](Mockito.withSettings().serializable()) + val config: Config = ConfigFactory.load("test.conf").withFallback(ConfigFactory.systemEnvironment()) + val jobConfig: CassandraDataMigrationConfig = new CassandraDataMigrationConfig(config) + var cassandraUtils: CassandraUtil = _ + + var currentMilliSecond = 1605816926271L + + override protected def beforeAll(): Unit = { + BaseMetricsReporter.gaugeMetrics.clear() + EmbeddedCassandraServerHelper.startEmbeddedCassandra(80000L) + cassandraUtils = new CassandraUtil(jobConfig.cassandraHost, jobConfig.cassandraPort, jobConfig) + val session = cassandraUtils.session + val dataLoader = new CQLDataLoader(session) + dataLoader.load(new FileCQLDataSet(getClass.getResource("/test.cql").getPath, true, true)) + flinkCluster.before() + super.beforeAll() + } + + override protected def afterAll(): Unit = { + flinkCluster.after() + super.afterAll() + } + + "CassandraDataMigrationTask" should "generate event" in { + when(mockKafkaUtil.kafkaJobRequestSource[Event](jobConfig.kafkaInputTopic)).thenReturn(new CassandraDataMigrationMapSource) + new CassandraDataMigrationStreamTask(jobConfig, mockKafkaUtil).process() + } +} + +class CassandraDataMigrationMapSource extends SourceFunction[Event] { + + override def run(ctx: SourceContext[Event]): Unit = { + // Valid event + ctx.collect(new Event(JSONUtil.deserialize[util.Map[String, Any]](EventFixture.EVENT_1), 0, 10)) + + // Invalid event + ctx.collect(new Event(JSONUtil.deserialize[util.Map[String, Any]](EventFixture.EVENT_2), 0, 10)) + } + + override def cancel(): Unit = {} +} \ No newline at end of file diff --git a/content-auto-creator/src/main/resources/content-auto-creator.conf b/content-auto-creator/src/main/resources/content-auto-creator.conf index 987920ec1..0b87fbd97 100644 --- a/content-auto-creator/src/main/resources/content-auto-creator.conf +++ b/content-auto-creator/src/main/resources/content-auto-creator.conf @@ -25,7 +25,7 @@ content_auto_creator { allowed_object_types=["Content"] allowed_content_stages=["create","upload","review","publish"] content_mandatory_fields=["name","code","mimeType","primaryCategory","artifactUrl","lastPublishedBy"] - content_props_to_removed=["identifier","downloadUrl","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","badgeAssertions","leafNodes","sYS_INTERNAL_LAST_UPDATED_ON","previewUrl","channel","objectType","visibility","version","pragma","prevStatus","streamingUrl","idealScreenSize","contentDisposition","lastStatusChangedOn","idealScreenDensity","lastSubmittedOn","publishError","flaggedBy","flags","lastFlaggedOn","publisher","lastUpdatedBy","lastSubmittedBy","uploadError","lockKey","publish_type","reviewError","totalCompressedSize","origin","originData","importError","questions"] + content_props_to_removed=["identifier","downloadUrl","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","badgeAssertions","leafNodes","sYS_INTERNAL_LAST_UPDATED_ON","previewUrl","channel","objectType","visibility","version","pragma","prevStatus","streamingUrl","idealScreenSize","contentDisposition","lastStatusChangedOn","idealScreenDensity","lastSubmittedOn","publishError","flaggedBy","flags","lastFlaggedOn","publisher","lastUpdatedBy","lastSubmittedBy","uploadError","lockKey","publish_type","reviewError","totalCompressedSize","origin","originData","importError","questions","posterImage"] bulk_upload_mime_types=["video/mp4"] artifact_upload_max_size=52428800 content_create_props=["name","code","mimeType","contentType","framework","processId","primaryCategory"] @@ -56,3 +56,14 @@ cloud_storage { } } +cloudstorage.metadata.replace_absolute_path=false +cloudstorage.relative_path_prefix= "CONTENT_STORAGE_BASE_PATH" +cloudstorage.read_base_path="https://sunbirddev.blob.core.windows.net" +cloudstorage.write_base_path=["https://sunbirddev.blob.core.windows.net","https://obj.dev.sunbird.org"] +cloudstorage.metadata.list=["appIcon","posterImage","artifactUrl","downloadUrl","variants","previewUrl","pdfUrl", "streamingUrl", "toc_url"] + +cloud_storage_type="" +cloud_storage_key="" +cloud_storage_secret="" +cloud_storage_container="" +cloud_storage_endpoint="" diff --git a/content-auto-creator/src/main/scala/org/sunbird/job/contentautocreator/functions/ContentAutoCreatorFunction.scala b/content-auto-creator/src/main/scala/org/sunbird/job/contentautocreator/functions/ContentAutoCreatorFunction.scala index 68b0394eb..c40e99b3e 100644 --- a/content-auto-creator/src/main/scala/org/sunbird/job/contentautocreator/functions/ContentAutoCreatorFunction.scala +++ b/content-auto-creator/src/main/scala/org/sunbird/job/contentautocreator/functions/ContentAutoCreatorFunction.scala @@ -30,7 +30,7 @@ class ContentAutoCreatorFunction(config: ContentAutoCreatorConfig, httpUtil: Htt override def open(parameters: Configuration): Unit = { super.open(parameters) - neo4JUtil = new Neo4JUtil(config.graphRoutePath, config.graphName) + neo4JUtil = new Neo4JUtil(config.graphRoutePath, config.graphName, config) cloudStorageUtil = new CloudStorageUtil(config) } diff --git a/content-auto-creator/src/main/scala/org/sunbird/job/contentautocreator/helpers/ContentAutoCreator.scala b/content-auto-creator/src/main/scala/org/sunbird/job/contentautocreator/helpers/ContentAutoCreator.scala index fb3a98f23..da6009fd6 100644 --- a/content-auto-creator/src/main/scala/org/sunbird/job/contentautocreator/helpers/ContentAutoCreator.scala +++ b/content-auto-creator/src/main/scala/org/sunbird/job/contentautocreator/helpers/ContentAutoCreator.scala @@ -346,7 +346,7 @@ trait ContentAutoCreator extends ContentCollectionUpdater { } private def publishContent(channel: String, identifier: String, lastPublishedBy: String, config: ContentAutoCreatorConfig, httpUtil: HttpUtil): Unit = { - val requestUrl = s"${config.learningServiceBaseUrl}/content/v3/publish/" + identifier + val requestUrl = s"${config.contentServiceBaseUrl}/content/v4/publish/" + identifier val reqMap = new java.util.HashMap[String, AnyRef]() { put(ContentAutoCreatorConstants.REQUEST, new java.util.HashMap[String, AnyRef]() { put(ContentAutoCreatorConstants.CONTENT, new java.util.HashMap[String, AnyRef]() { @@ -422,6 +422,7 @@ trait ContentAutoCreator extends ContentCollectionUpdater { } private def getErrorDetails(httpResponse: HTTPResponse): String = { + logger.info("ContentAutoCreator:: getErrorDetails:: httpResponse.body:: " + httpResponse.body) val response = JSONUtil.deserialize[Map[String, AnyRef]](httpResponse.body) if (null != response) " | Response Code :" + httpResponse.status + " | Result : " + response.getOrElse("result", Map[String, AnyRef]()).asInstanceOf[Map[String, AnyRef]] + " | Error Message : " + response.getOrElse("params", Map[String, AnyRef]()).asInstanceOf[Map[String, AnyRef]] else " | Null Response Received." diff --git a/content-auto-creator/src/main/scala/org/sunbird/job/contentautocreator/task/ContentAutoCreatorConfig.scala b/content-auto-creator/src/main/scala/org/sunbird/job/contentautocreator/task/ContentAutoCreatorConfig.scala index 5c50d1864..c7693f5f2 100644 --- a/content-auto-creator/src/main/scala/org/sunbird/job/contentautocreator/task/ContentAutoCreatorConfig.scala +++ b/content-auto-creator/src/main/scala/org/sunbird/job/contentautocreator/task/ContentAutoCreatorConfig.scala @@ -57,7 +57,6 @@ class ContentAutoCreatorConfig(override val config: Config) extends BaseJobConfi val overrideManifestProps: List[String] = if (config.hasPath("object.override_manifest_props")) config.getStringList("object.override_manifest_props").asScala.toList else List("variants", "downloadUrl", "previewUrl", "pdfUrl", "lastPublishedBy") val contentServiceBaseUrl : String = config.getString("service.content_service.basePath") val searchServiceBaseUrl : String = config.getString("service.search.basePath") - val learningServiceBaseUrl : String = config.getString("service.learning_service.basePath") val allowedContentStages: List[String] = if (config.hasPath("content_auto_creator.allowed_content_stages")) config.getStringList("content_auto_creator.allowed_content_stages").asScala.toList else List("create", "upload", "review", "publish") val allowedContentObjectTypes: List[String] = if (config.hasPath("content_auto_creator.allowed_object_types")) config.getStringList("content_auto_creator.allowed_object_types").asScala.toList else List("Content") diff --git a/content-auto-creator/src/test/scala/org/sunbird/job/contentautocreator/spec/helper/ContentAutoCreatorSpec.scala b/content-auto-creator/src/test/scala/org/sunbird/job/contentautocreator/spec/helper/ContentAutoCreatorSpec.scala index 558c78567..cd6e5a21f 100644 --- a/content-auto-creator/src/test/scala/org/sunbird/job/contentautocreator/spec/helper/ContentAutoCreatorSpec.scala +++ b/content-auto-creator/src/test/scala/org/sunbird/job/contentautocreator/spec/helper/ContentAutoCreatorSpec.scala @@ -60,7 +60,7 @@ class ContentAutoCreatorSpec extends FlatSpec with Matchers with MockitoSugar { when(mockHttpUtil.patch(contains("/content/v4/update"), anyString, any())).thenReturn(HTTPResponse(200, createResponse)) when(mockHttpUtil.postFilePath(contains("/content/v4/upload"), anyString, anyString, any())).thenReturn(HTTPResponse(200, uploadResponse)) when(mockHttpUtil.post(contains("/content/v4/review"), anyString, any())).thenReturn(HTTPResponse(200, reviewResponse)) - when(mockHttpUtil.post(contains("/content/v3/publish"), anyString, any())).thenReturn(HTTPResponse(200, publishResponse)) + when(mockHttpUtil.post(contains("/content/v4/publish"), anyString, any())).thenReturn(HTTPResponse(200, publishResponse)) val metadata = new util.HashMap[String, AnyRef]() { put("identifier", "do_21344892893869670417014"); put("IL_UNIQUE_ID", "do_21344892893869670417014"); @@ -73,19 +73,19 @@ class ContentAutoCreatorSpec extends FlatSpec with Matchers with MockitoSugar { assert(isContentPublished) } - "process" should "not throw exception for sunbirdEvent" in { - val contentResponse = """{"responseCode":"OK","result":{"content":{"identifier":"do_2134462034258575361402","ownershipType":["createdBy"],"unitIdentifiers":["do_2134460300692275201128"],"copyright":"Test axis,2076","organisationId":"da0d83d6-0692-4d94-95ae-7499d5e0a5bd","keywords":["Nadiya"],"subject":["Hindi"],"targetMediumIds":["ekstep_ncert_k-12_medium_english"],"channel":"01329314824202649627","language":["English"],"source":"https://dockstaging.sunbirded.org/api/content/v1/read/do_2134462034258575361402","mimeType":"video/mp4","targetGradeLevelIds":["ekstep_ncert_k-12_gradelevel_class2"],"objectType":"Content","appIcon":"https://stagingdock.blob.core.windows.net/sunbird-content-dock/content/do_2134462034258575361402/artifact/rhinocerous.thumb.jpg","primaryCategory":"Teacher Resource","appId":"staging.dock.portal","contentEncoding":"identity","artifactUrl":"https://file-examples.com/wp-content/uploads/2017/04/file_example_MP4_480_1_5MG.mp4","contentType":"MarkingSchemeRubric","trackable":{"enabled":"No","autoBatch":"No"},"identifier":"do_2134462034258575361402","audience":["Student"],"subjectIds":["ekstep_ncert_k-12_subject_hindi"],"visibility":"Default","author":"classmate5","mediaType":"content","osId":"org.ekstep.quiz.app","languageCode":["en"],"lastPublishedBy":"1cf88ea3-083d-4fdf-84be-3628e63ce7f0","version":2,"se_subjects":["Hindi"],"license":"CC BY 4.0","prevState":"Review","size":13992641,"lastPublishedOn":"2022-01-05T12:05:03.920+0000","name":"content_262","topic":["मेरे बचपन के दिन"],"attributions":["kayal"],"targetBoardIds":["ekstep_ncert_k-12_board_cbse"],"status":"Live","topicsIds":["ekstep_ncert_k-12_topic_8696da1edbd3ce327d2a0822f75bb44c7e4fecf8"],"code":"837d2b38-dd5b-10ff-f210-c93bd19adcb3","interceptionPoints":{},"credentials":{"enabled":"No"},"prevStatus":"Draft","description":"MP4","posterImage":"https://stagingdock.blob.core.windows.net/sunbird-content-dock/content/do_2134462034258575361402/artifact/rhinocerous.jpg","idealScreenSize":"normal","createdOn":"2022-01-05T12:04:57.124+0000","targetSubjectIds":["ekstep_ncert_k-12_subject_hindi"],"processId":"be4d7bf6-364c-42e7-bc0d-adea66117458","contentDisposition":"inline","lastUpdatedOn":"2022-01-05T12:05:05.269+0000","collectionId":"do_2134460300682690561125","dialcodeRequired":"No","lastStatusChangedOn":"2022-01-05T12:05:05.269+0000","creator":"cbsestaging26","os":["All"],"se_FWIds":["ekstep_ncert_k-12"],"targetFWIds":["ekstep_ncert_k-12"],"pkgVersion":1,"versionKey":"1641384302775","idealScreenDensity":"hdpi","framework":"ekstep_ncert_k-12","lastSubmittedOn":"2022-01-05T12:05:02.768+0000","createdBy":"530b19ea-dc8d-4cc7-a4b5-0c0214c8113a","se_topics":["मेरे बचपन के दिन"],"compatibilityLevel":1,"programId":"8f514bc0-6de9-11ec-9e9f-9f0c75510617","createdFor":["01329314824202649627"],"status":"Draft"}}}""" + ignore should "not throw exception for sunbirdEvent" in { + val contentResponse = """{"responseCode":"OK","result":{"content":{"identifier":"do_2134462034258575361402","ownershipType":["createdBy"],"unitIdentifiers":["do_2134460300692275201128"],"copyright":"Test axis,2076","organisationId":"da0d83d6-0692-4d94-95ae-7499d5e0a5bd","keywords":["Nadiya"],"subject":["Hindi"],"targetMediumIds":["ekstep_ncert_k-12_medium_english"],"channel":"01329314824202649627","language":["English"],"source":"https://dockstaging.sunbirded.org/api/content/v1/read/do_2134462034258575361402","mimeType":"video/mp4","targetGradeLevelIds":["ekstep_ncert_k-12_gradelevel_class2"],"objectType":"Content","appIcon":"https://stagingdock.blob.core.windows.net/sunbird-content-dock/content/do_2134462034258575361402/artifact/rhinocerous.thumb.jpg","primaryCategory":"Teacher Resource","appId":"staging.dock.portal","contentEncoding":"identity","artifactUrl":"https://sunbirddevbbpublic.blob.core.windows.net//content/assets/do_21369942869119795219/kors-smaapn.mp4","contentType":"MarkingSchemeRubric","trackable":{"enabled":"No","autoBatch":"No"},"identifier":"do_2134462034258575361402","audience":["Student"],"subjectIds":["ekstep_ncert_k-12_subject_hindi"],"visibility":"Default","author":"classmate5","mediaType":"content","osId":"org.ekstep.quiz.app","languageCode":["en"],"lastPublishedBy":"1cf88ea3-083d-4fdf-84be-3628e63ce7f0","version":2,"se_subjects":["Hindi"],"license":"CC BY 4.0","prevState":"Review","size":13992641,"lastPublishedOn":"2022-01-05T12:05:03.920+0000","name":"content_262","topic":["मेरे बचपन के दिन"],"attributions":["kayal"],"targetBoardIds":["ekstep_ncert_k-12_board_cbse"],"status":"Live","topicsIds":["ekstep_ncert_k-12_topic_8696da1edbd3ce327d2a0822f75bb44c7e4fecf8"],"code":"837d2b38-dd5b-10ff-f210-c93bd19adcb3","interceptionPoints":{},"credentials":{"enabled":"No"},"prevStatus":"Draft","description":"MP4","posterImage":"https://stagingdock.blob.core.windows.net/sunbird-content-dock/content/do_2134462034258575361402/artifact/rhinocerous.jpg","idealScreenSize":"normal","createdOn":"2022-01-05T12:04:57.124+0000","targetSubjectIds":["ekstep_ncert_k-12_subject_hindi"],"processId":"be4d7bf6-364c-42e7-bc0d-adea66117458","contentDisposition":"inline","lastUpdatedOn":"2022-01-05T12:05:05.269+0000","collectionId":"do_2134460300682690561125","dialcodeRequired":"No","lastStatusChangedOn":"2022-01-05T12:05:05.269+0000","creator":"cbsestaging26","os":["All"],"se_FWIds":["ekstep_ncert_k-12"],"targetFWIds":["ekstep_ncert_k-12"],"pkgVersion":1,"versionKey":"1641384302775","idealScreenDensity":"hdpi","framework":"ekstep_ncert_k-12","lastSubmittedOn":"2022-01-05T12:05:02.768+0000","createdBy":"530b19ea-dc8d-4cc7-a4b5-0c0214c8113a","se_topics":["मेरे बचपन के दिन"],"compatibilityLevel":1,"programId":"8f514bc0-6de9-11ec-9e9f-9f0c75510617","createdFor":["01329314824202649627"],"status":"Draft"}}}""" val createResponse = """{"responseCode":"OK","result":{"identifier":"do_2134462034258575361402", "versionKey":"1587624624051"}}""" - val uploadResponse = """{"responseCode":"OK","result":{"identifier":"do_2134462034258575361402", "artifactUrl": "https://file-examples.com/wp-content/uploads/2017/04/file_example_MP4_480_1_5MG.mp4"}}}""" + val uploadResponse = """{"responseCode":"OK","result":{"identifier":"do_2134462034258575361402", "artifactUrl": "https://sunbirddevbbpublic.blob.core.windows.net//content/assets/do_21369942869119795219/kors-smaapn.mp4"}}}""" val reviewResponse = """{"responseCode":"OK","result":{"node_id":"do_2134462034258575361402"}}""" val publishResponse = """{"responseCode":"OK","result":{"publishStatus":"Success"}}""" val httpUtil = new HttpUtil val downloadPath = "/tmp/content" + File.separator + "_temp_" + System.currentTimeMillis val appIconUrl = "https://stagingdock.blob.core.windows.net/sunbird-content-dock/content/do_2134462034258575361402/artifact/rhinocerous.thumb.jpg" - val artifactUrl = "https://file-examples.com/wp-content/uploads/2017/04/file_example_MP4_480_1_5MG.mp4" + val artifactUrl = "https://sunbirddevbbpublic.blob.core.windows.net//content/assets/do_21369942869119795219/kors-smaapn.mp4" - val sunbirdEvent = "{\"eid\":\"BE_JOB_REQUEST\",\"ets\":1641391738147,\"mid\":\"LP.1641391738147.eb3b7a96-259f-4b46-b386-d0bec1873a57\",\"actor\":{\"id\":\"Auto Creator\",\"type\":\"System\"},\"context\":{\"pdata\":{\"id\":\"org.sunbird.platform\",\"ver\":\"1.0\",\"env\":\"staging\"},\"channel\":\"01329314824202649627\"},\"object\":{\"id\":\"do_2134462034258575361402\",\"ver\":\"1641384302775\"},\"edata\":{\"action\":\"auto-create\",\"originData\":{},\"iteration\":1,\"metadata\":{\"ownershipType\":[\"createdBy\"],\"unitIdentifiers\":[\"do_2134460300692275201128\"],\"copyright\":\"Test axis,2076\",\"organisationId\":\"da0d83d6-0692-4d94-95ae-7499d5e0a5bd\",\"keywords\":[\"Nadiya\"],\"subject\":[\"Hindi\"],\"targetMediumIds\":[\"ekstep_ncert_k-12_medium_english\"],\"channel\":\"01329314824202649627\",\"language\":[\"English\"],\"source\":\"https://dockstaging.sunbirded.org/api/content/v1/read/do_2134462034258575361402\",\"mimeType\":\"video/mp4\",\"targetGradeLevelIds\":[\"ekstep_ncert_k-12_gradelevel_class2\"],\"objectType\":\"Content\",\"appIcon\":\"https://stagingdock.blob.core.windows.net/sunbird-content-dock/content/do_2134462034258575361402/artifact/rhinocerous.thumb.jpg\",\"primaryCategory\":\"Teacher Resource\",\"appId\":\"staging.dock.portal\",\"contentEncoding\":\"identity\",\"artifactUrl\":\"https://file-examples.com/wp-content/uploads/2017/04/file_example_MP4_480_1_5MG.mp4\",\"contentType\":\"MarkingSchemeRubric\",\"trackable\":{\"enabled\":\"No\",\"autoBatch\":\"No\"},\"identifier\":\"do_2134462034258575361402\",\"audience\":[\"Student\"],\"subjectIds\":[\"ekstep_ncert_k-12_subject_hindi\"],\"visibility\":\"Default\",\"author\":\"classmate5\",\"mediaType\":\"content\",\"osId\":\"org.ekstep.quiz.app\",\"languageCode\":[\"en\"],\"lastPublishedBy\":\"1cf88ea3-083d-4fdf-84be-3628e63ce7f0\",\"version\":2,\"se_subjects\":[\"Hindi\"],\"license\":\"CC BY 4.0\",\"prevState\":\"Review\",\"size\":13992641,\"lastPublishedOn\":\"2022-01-05T12:05:03.920+0000\",\"name\":\"content_262\",\"topic\":[\"मेरे बचपन के दिन\"],\"attributions\":[\"kayal\"],\"targetBoardIds\":[\"ekstep_ncert_k-12_board_cbse\"],\"status\":\"Live\",\"topicsIds\":[\"ekstep_ncert_k-12_topic_8696da1edbd3ce327d2a0822f75bb44c7e4fecf8\"],\"code\":\"837d2b38-dd5b-10ff-f210-c93bd19adcb3\",\"interceptionPoints\":{},\"credentials\":{\"enabled\":\"No\"},\"prevStatus\":\"Draft\",\"description\":\"MP4\",\"posterImage\":\"https://stagingdock.blob.core.windows.net/sunbird-content-dock/content/do_2134462034258575361402/artifact/rhinocerous.jpg\",\"idealScreenSize\":\"normal\",\"createdOn\":\"2022-01-05T12:04:57.124+0000\",\"targetSubjectIds\":[\"ekstep_ncert_k-12_subject_hindi\"],\"processId\":\"be4d7bf6-364c-42e7-bc0d-adea66117458\",\"contentDisposition\":\"inline\",\"lastUpdatedOn\":\"2022-01-05T12:05:05.269+0000\",\"collectionId\":\"do_2134460300682690561125\",\"dialcodeRequired\":\"No\",\"lastStatusChangedOn\":\"2022-01-05T12:05:05.269+0000\",\"creator\":\"cbsestaging26\",\"os\":[\"All\"],\"se_FWIds\":[\"ekstep_ncert_k-12\"],\"targetFWIds\":[\"ekstep_ncert_k-12\"],\"pkgVersion\":1,\"versionKey\":\"1641384302775\",\"idealScreenDensity\":\"hdpi\",\"framework\":\"ekstep_ncert_k-12\",\"lastSubmittedOn\":\"2022-01-05T12:05:02.768+0000\",\"createdBy\":\"530b19ea-dc8d-4cc7-a4b5-0c0214c8113a\",\"se_topics\":[\"मेरे बचपन के दिन\"],\"compatibilityLevel\":1,\"programId\":\"8f514bc0-6de9-11ec-9e9f-9f0c75510617\",\"createdFor\":[\"01329314824202649627\"]},\"repository\":\"https://dockstaging.sunbirded.org/api/content/v1/read/do_2134462034258575361402\",\"collection\":[{\"identifier\":\"do_21344602761084928012178\",\"unitId\":\"do_21344602911420416012179\"}],\"objectType\":\"Content\",\"stage\":\"\"}}" + val sunbirdEvent = "{\"eid\":\"BE_JOB_REQUEST\",\"ets\":1641391738147,\"mid\":\"LP.1641391738147.eb3b7a96-259f-4b46-b386-d0bec1873a57\",\"actor\":{\"id\":\"Auto Creator\",\"type\":\"System\"},\"context\":{\"pdata\":{\"id\":\"org.sunbird.platform\",\"ver\":\"1.0\",\"env\":\"staging\"},\"channel\":\"01329314824202649627\"},\"object\":{\"id\":\"do_2134462034258575361402\",\"ver\":\"1641384302775\"},\"edata\":{\"action\":\"auto-create\",\"originData\":{},\"iteration\":1,\"metadata\":{\"ownershipType\":[\"createdBy\"],\"unitIdentifiers\":[\"do_2134460300692275201128\"],\"copyright\":\"Test axis,2076\",\"organisationId\":\"da0d83d6-0692-4d94-95ae-7499d5e0a5bd\",\"keywords\":[\"Nadiya\"],\"subject\":[\"Hindi\"],\"targetMediumIds\":[\"ekstep_ncert_k-12_medium_english\"],\"channel\":\"01329314824202649627\",\"language\":[\"English\"],\"source\":\"https://dockstaging.sunbirded.org/api/content/v1/read/do_2134462034258575361402\",\"mimeType\":\"video/mp4\",\"targetGradeLevelIds\":[\"ekstep_ncert_k-12_gradelevel_class2\"],\"objectType\":\"Content\",\"appIcon\":\"https://stagingdock.blob.core.windows.net/sunbird-content-dock/content/do_2134462034258575361402/artifact/rhinocerous.thumb.jpg\",\"primaryCategory\":\"Teacher Resource\",\"appId\":\"staging.dock.portal\",\"contentEncoding\":\"identity\",\"artifactUrl\":\"https://sunbirddevbbpublic.blob.core.windows.net//content/assets/do_21369942869119795219/kors-smaapn.mp4\",\"contentType\":\"MarkingSchemeRubric\",\"trackable\":{\"enabled\":\"No\",\"autoBatch\":\"No\"},\"identifier\":\"do_2134462034258575361402\",\"audience\":[\"Student\"],\"subjectIds\":[\"ekstep_ncert_k-12_subject_hindi\"],\"visibility\":\"Default\",\"author\":\"classmate5\",\"mediaType\":\"content\",\"osId\":\"org.ekstep.quiz.app\",\"languageCode\":[\"en\"],\"lastPublishedBy\":\"1cf88ea3-083d-4fdf-84be-3628e63ce7f0\",\"version\":2,\"se_subjects\":[\"Hindi\"],\"license\":\"CC BY 4.0\",\"prevState\":\"Review\",\"size\":13992641,\"lastPublishedOn\":\"2022-01-05T12:05:03.920+0000\",\"name\":\"content_262\",\"topic\":[\"मेरे बचपन के दिन\"],\"attributions\":[\"kayal\"],\"targetBoardIds\":[\"ekstep_ncert_k-12_board_cbse\"],\"status\":\"Live\",\"topicsIds\":[\"ekstep_ncert_k-12_topic_8696da1edbd3ce327d2a0822f75bb44c7e4fecf8\"],\"code\":\"837d2b38-dd5b-10ff-f210-c93bd19adcb3\",\"interceptionPoints\":{},\"credentials\":{\"enabled\":\"No\"},\"prevStatus\":\"Draft\",\"description\":\"MP4\",\"posterImage\":\"https://stagingdock.blob.core.windows.net/sunbird-content-dock/content/do_2134462034258575361402/artifact/rhinocerous.jpg\",\"idealScreenSize\":\"normal\",\"createdOn\":\"2022-01-05T12:04:57.124+0000\",\"targetSubjectIds\":[\"ekstep_ncert_k-12_subject_hindi\"],\"processId\":\"be4d7bf6-364c-42e7-bc0d-adea66117458\",\"contentDisposition\":\"inline\",\"lastUpdatedOn\":\"2022-01-05T12:05:05.269+0000\",\"collectionId\":\"do_2134460300682690561125\",\"dialcodeRequired\":\"No\",\"lastStatusChangedOn\":\"2022-01-05T12:05:05.269+0000\",\"creator\":\"cbsestaging26\",\"os\":[\"All\"],\"se_FWIds\":[\"ekstep_ncert_k-12\"],\"targetFWIds\":[\"ekstep_ncert_k-12\"],\"pkgVersion\":1,\"versionKey\":\"1641384302775\",\"idealScreenDensity\":\"hdpi\",\"framework\":\"ekstep_ncert_k-12\",\"lastSubmittedOn\":\"2022-01-05T12:05:02.768+0000\",\"createdBy\":\"530b19ea-dc8d-4cc7-a4b5-0c0214c8113a\",\"se_topics\":[\"मेरे बचपन के दिन\"],\"compatibilityLevel\":1,\"programId\":\"8f514bc0-6de9-11ec-9e9f-9f0c75510617\",\"createdFor\":[\"01329314824202649627\"]},\"repository\":\"https://dockstaging.sunbirded.org/api/content/v1/read/do_2134462034258575361402\",\"collection\":[{\"identifier\":\"do_21344602761084928012178\",\"unitId\":\"do_21344602911420416012179\"}],\"objectType\":\"Content\",\"stage\":\"\"}}" val event = new Event(JSONUtil.deserialize[java.util.Map[String, Any]](sunbirdEvent),0,1) when(mockHttpUtil.post(contains("/v3/search"), anyString, any())).thenReturn(HTTPResponse(200, ScalaJsonUtil.serialize(Map.empty[String, AnyRef]))) when(mockHttpUtil.post(contains("/content/v4/create"), anyString, any())).thenReturn(HTTPResponse(200, createResponse)) @@ -93,14 +93,14 @@ class ContentAutoCreatorSpec extends FlatSpec with Matchers with MockitoSugar { when(mockHttpUtil.patch(contains("/content/v4/update"), anyString, any())).thenReturn(HTTPResponse(200, createResponse)) when(mockHttpUtil.postFilePath(contains("/content/v4/upload"), anyString, anyString, any())).thenReturn(HTTPResponse(200, uploadResponse)) when(mockHttpUtil.post(contains("/content/v4/review"), anyString, any())).thenReturn(HTTPResponse(200, reviewResponse)) - when(mockHttpUtil.post(contains("/content/v3/publish"), anyString, any())).thenReturn(HTTPResponse(200, publishResponse)) + when(mockHttpUtil.post(contains("/content/v4/publish"), anyString, any())).thenReturn(HTTPResponse(200, publishResponse)) when(mockHttpUtil.downloadFile(contains(".jpg"),anyString())).thenReturn(httpUtil.downloadFile(appIconUrl, downloadPath)) when(mockHttpUtil.downloadFile(endsWith("mp4"),anyString())).thenReturn(httpUtil.downloadFile(artifactUrl, downloadPath)) val isContentPublished = new TestContentAutoCreator().process(jobConfig, event, mockHttpUtil, mockNeo4JUtil, cloudUtil) assert(isContentPublished) } - "process" should "update and publish content when content is already created" in { + ignore should "update and publish content when content is already created" in { val searchResponse = """{"responseCode": "OK", "result": { "count": 1, "content": [{"identifier": "do_2134462034258575361402", "origin": "do_2134462034258575361402", "channel": "01329314824202649627", "originData": "{\"identifier\":\"do_2134462034258575361402\",\"repository\":\"https://dockstaging.sunbirded.org/api/content/v1/read/do_2134462034258575361402\"}", "mimeType": "video/mp4", "objectType": "Content", "status": "Draft" }]}}""" val contentResponse = """{"responseCode":"OK","result":{"content":{"identifier":"do_2134462034258575361402","ownershipType":["createdBy"],"unitIdentifiers":["do_2134460300692275201128"],"copyright":"Test axis,2076","organisationId":"da0d83d6-0692-4d94-95ae-7499d5e0a5bd","keywords":["Nadiya"],"subject":["Hindi"],"targetMediumIds":["ekstep_ncert_k-12_medium_english"],"channel":"01329314824202649627","language":["English"],"source":"https://dockstaging.sunbirded.org/api/content/v1/read/do_2134462034258575361402","mimeType":"video/mp4","targetGradeLevelIds":["ekstep_ncert_k-12_gradelevel_class2"],"objectType":"Content","appIcon":"https://stagingdock.blob.core.windows.net/sunbird-content-dock/content/do_2134462034258575361402/artifact/rhinocerous.thumb.jpg","primaryCategory":"Teacher Resource","appId":"staging.dock.portal","contentEncoding":"identity","artifactUrl":"https://stagingdock.blob.core.windows.net/sunbird-content-dock/content/do_2134462034258575361402/artifact/mp4_219.mp4","contentType":"MarkingSchemeRubric","trackable":{"enabled":"No","autoBatch":"No"},"identifier":"do_2134462034258575361402","audience":["Student"],"subjectIds":["ekstep_ncert_k-12_subject_hindi"],"visibility":"Default","author":"classmate5","mediaType":"content","osId":"org.ekstep.quiz.app","languageCode":["en"],"lastPublishedBy":"1cf88ea3-083d-4fdf-84be-3628e63ce7f0","version":2,"se_subjects":["Hindi"],"license":"CC BY 4.0","prevState":"Review","size":13992641,"lastPublishedOn":"2022-01-05T12:05:03.920+0000","name":"content_262","topic":["मेरे बचपन के दिन"],"attributions":["kayal"],"targetBoardIds":["ekstep_ncert_k-12_board_cbse"],"status":"Live","topicsIds":["ekstep_ncert_k-12_topic_8696da1edbd3ce327d2a0822f75bb44c7e4fecf8"],"code":"837d2b38-dd5b-10ff-f210-c93bd19adcb3","interceptionPoints":{},"credentials":{"enabled":"No"},"prevStatus":"Draft","description":"MP4","posterImage":"https://stagingdock.blob.core.windows.net/sunbird-content-dock/content/do_2134462034258575361402/artifact/rhinocerous.jpg","idealScreenSize":"normal","createdOn":"2022-01-05T12:04:57.124+0000","targetSubjectIds":["ekstep_ncert_k-12_subject_hindi"],"processId":"be4d7bf6-364c-42e7-bc0d-adea66117458","contentDisposition":"inline","lastUpdatedOn":"2022-01-05T12:05:05.269+0000","collectionId":"do_2134460300682690561125","dialcodeRequired":"No","lastStatusChangedOn":"2022-01-05T12:05:05.269+0000","creator":"cbsestaging26","os":["All"],"se_FWIds":["ekstep_ncert_k-12"],"targetFWIds":["ekstep_ncert_k-12"],"pkgVersion":1,"versionKey":"1641384302775","idealScreenDensity":"hdpi","framework":"ekstep_ncert_k-12","lastSubmittedOn":"2022-01-05T12:05:02.768+0000","createdBy":"530b19ea-dc8d-4cc7-a4b5-0c0214c8113a","se_topics":["मेरे बचपन के दिन"],"compatibilityLevel":1,"programId":"8f514bc0-6de9-11ec-9e9f-9f0c75510617","createdFor":["01329314824202649627"],"status":"Draft"}}}""" val createResponse = """{"responseCode":"OK","result":{"identifier":"do_2134462034258575361402", "versionKey":"1587624624051"}}""" @@ -120,14 +120,14 @@ class ContentAutoCreatorSpec extends FlatSpec with Matchers with MockitoSugar { when(mockHttpUtil.patch(contains("/content/v4/update"), anyString, any())).thenReturn(HTTPResponse(200, createResponse)) when(mockHttpUtil.postFilePath(contains("/content/v4/upload"), anyString, anyString, any())).thenReturn(HTTPResponse(200, uploadResponse)) when(mockHttpUtil.post(contains("/content/v4/review"), anyString, any())).thenReturn(HTTPResponse(200, reviewResponse)) - when(mockHttpUtil.post(contains("/content/v3/publish"), anyString, any())).thenReturn(HTTPResponse(200, publishResponse)) + when(mockHttpUtil.post(contains("/content/v4/publish"), anyString, any())).thenReturn(HTTPResponse(200, publishResponse)) when(mockHttpUtil.downloadFile(contains(".jpg"),anyString())).thenReturn(httpUtil.downloadFile(appIconUrl, downloadPath)) when(mockHttpUtil.downloadFile(endsWith("mp4"),anyString())).thenReturn(httpUtil.downloadFile(artifactUrl, downloadPath)) val isContentPublished = new TestContentAutoCreator().process(jobConfig, event, mockHttpUtil, mockNeo4JUtil, cloudUtil) assert(isContentPublished) } - "process" should "review and publish content when content is already updated with artifactUrl" in { + ignore should "review and publish content when content is already updated with artifactUrl" in { val searchResponse = """{"responseCode": "OK", "result": { "count": 1, "content": [{"identifier": "do_2134462034258575361402", "origin": "do_2134462034258575361402", "channel": "01329314824202649627", "artifactUrl": "https://sunbirdstagingpublic.blob.core.windows.net/sunbird-content-staging/content/do_2134462034258575361402/artifact/mp4_219.mp4", "originData": "{\"identifier\":\"do_2134462034258575361402\",\"repository\":\"https://dockstaging.sunbirded.org/api/content/v1/read/do_2134462034258575361402\"}", "mimeType": "video/mp4", "objectType": "Content", "status": "Draft" }]}}""" val contentResponse = """{"responseCode":"OK","result":{"content":{"identifier":"do_2134462034258575361402","ownershipType":["createdBy"],"unitIdentifiers":["do_2134460300692275201128"],"copyright":"Test axis,2076","organisationId":"da0d83d6-0692-4d94-95ae-7499d5e0a5bd","keywords":["Nadiya"],"subject":["Hindi"],"targetMediumIds":["ekstep_ncert_k-12_medium_english"],"channel":"01329314824202649627","language":["English"],"source":"https://dockstaging.sunbirded.org/api/content/v1/read/do_2134462034258575361402","mimeType":"video/mp4","targetGradeLevelIds":["ekstep_ncert_k-12_gradelevel_class2"],"objectType":"Content","appIcon":"https://stagingdock.blob.core.windows.net/sunbird-content-dock/content/do_2134462034258575361402/artifact/rhinocerous.thumb.jpg","primaryCategory":"Teacher Resource","appId":"staging.dock.portal","contentEncoding":"identity","artifactUrl":"https://stagingdock.blob.core.windows.net/sunbird-content-dock/content/do_2134462034258575361402/artifact/mp4_219.mp4","contentType":"MarkingSchemeRubric","trackable":{"enabled":"No","autoBatch":"No"},"identifier":"do_2134462034258575361402","audience":["Student"],"subjectIds":["ekstep_ncert_k-12_subject_hindi"],"visibility":"Default","author":"classmate5","mediaType":"content","osId":"org.ekstep.quiz.app","languageCode":["en"],"lastPublishedBy":"1cf88ea3-083d-4fdf-84be-3628e63ce7f0","version":2,"se_subjects":["Hindi"],"license":"CC BY 4.0","prevState":"Review","size":13992641,"lastPublishedOn":"2022-01-05T12:05:03.920+0000","name":"content_262","topic":["मेरे बचपन के दिन"],"attributions":["kayal"],"targetBoardIds":["ekstep_ncert_k-12_board_cbse"],"status":"Live","topicsIds":["ekstep_ncert_k-12_topic_8696da1edbd3ce327d2a0822f75bb44c7e4fecf8"],"code":"837d2b38-dd5b-10ff-f210-c93bd19adcb3","interceptionPoints":{},"credentials":{"enabled":"No"},"prevStatus":"Draft","description":"MP4","posterImage":"https://stagingdock.blob.core.windows.net/sunbird-content-dock/content/do_2134462034258575361402/artifact/rhinocerous.jpg","idealScreenSize":"normal","createdOn":"2022-01-05T12:04:57.124+0000","targetSubjectIds":["ekstep_ncert_k-12_subject_hindi"],"processId":"be4d7bf6-364c-42e7-bc0d-adea66117458","contentDisposition":"inline","lastUpdatedOn":"2022-01-05T12:05:05.269+0000","collectionId":"do_2134460300682690561125","dialcodeRequired":"No","lastStatusChangedOn":"2022-01-05T12:05:05.269+0000","creator":"cbsestaging26","os":["All"],"se_FWIds":["ekstep_ncert_k-12"],"targetFWIds":["ekstep_ncert_k-12"],"pkgVersion":1,"versionKey":"1641384302775","idealScreenDensity":"hdpi","framework":"ekstep_ncert_k-12","lastSubmittedOn":"2022-01-05T12:05:02.768+0000","createdBy":"530b19ea-dc8d-4cc7-a4b5-0c0214c8113a","se_topics":["मेरे बचपन के दिन"],"compatibilityLevel":1,"programId":"8f514bc0-6de9-11ec-9e9f-9f0c75510617","createdFor":["01329314824202649627"],"status":"Draft"}}}""" val reviewResponse = """{"responseCode":"OK","result":{"node_id":"do_2134462034258575361402"}}""" @@ -143,7 +143,7 @@ class ContentAutoCreatorSpec extends FlatSpec with Matchers with MockitoSugar { when(mockHttpUtil.post(contains("/v3/search"), anyString, any())).thenReturn(HTTPResponse(200, searchResponse)) when(mockHttpUtil.get(anyString(), any())).thenReturn(HTTPResponse(200, contentResponse)) when(mockHttpUtil.post(contains("/content/v4/review"), anyString, any())).thenReturn(HTTPResponse(200, reviewResponse)) - when(mockHttpUtil.post(contains("/content/v3/publish"), anyString, any())).thenReturn(HTTPResponse(200, publishResponse)) + when(mockHttpUtil.post(contains("/content/v4/publish"), anyString, any())).thenReturn(HTTPResponse(200, publishResponse)) when(mockHttpUtil.downloadFile(contains(".jpg"),anyString())).thenReturn(httpUtil.downloadFile(appIconUrl, downloadPath)) when(mockHttpUtil.downloadFile(endsWith("mp4"),anyString())).thenReturn(httpUtil.downloadFile(artifactUrl, downloadPath)) val isContentPublished = new TestContentAutoCreator().process(jobConfig, event, mockHttpUtil, mockNeo4JUtil, cloudUtil) diff --git a/credential-generator/certificate-processor/certificates/8e57723e-4541-11eb-b378-0242ac130002.png b/credential-generator/certificate-processor/certificates/8e57723e-4541-11eb-b378-0242ac130002.png deleted file mode 100644 index 7d761dfcc25c69ce241714ee81d03bced4539b5b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 808 zcmV+@1K0eCP)EGoy z2f=u!qvAIe3^1?@f$lwum=Pco18*tNn6-vD_RtVl83%dyxVeBsg~r_8kk+?t*hO zn+DSYzS6K{PJ02wz>MQfW2n-Llr$F54vUA!t-d1?n^d$FXh0x_X{eWY1@;jc(^lXW zS?VDeUZBkH0}}NVXa!?%$r``;4zN(03c|t%qcypJcV*Raka+Je;8|mdSggBVfoxg} z!jLfoRp|^LsOP<{fM;xOBTJnIDzXn~F952xDj=|agjODl(2A<<#R4DL5RkXflr5Z5#7mlz^yN+^G`C0+Zf%R9740eW^-%#LzU}K}2(Y_~TV}a7JdIaOK zW2oj8fSk~q3N&7aH964tGNG|RW7W1=8Ay&yy0#Tm@yfD>H+}-NehcFa7~sTeB3FgD z)^A}PZ;lFAffel2fnMV3*415DydOv|8{|gomj2BXW4`4n8v_JS|1#b+aiES# - - - credential-generator - org.sunbird - 1.0 - - 4.0.0 - - certificate-processor - - - junit - junit - 4.13.1 - test - - - org.sunbird - cloud-store-sdk_${scala.version} - 1.2.5 - - - org.slf4j - slf4j-log4j12 - - - com.fasterxml.jackson.core - jackson-databind - - - - - com.google.zxing - core - 3.3.3 - - - com.google.zxing - javase - 3.3.3 - - - org.scalatest - scalatest_${scala.version} - 3.0.6 - test - - - com.twitter - storehaus-cache_${scala.version} - 0.15.0 - - - org.mockito - mockito-core - 3.3.3 - test - - - com.konghq - unirest-java - 3.10.00 - - - org.apache.commons - commons-text - 1.8 - - - com.fasterxml.jackson.module - jackson-module-scala_${scala.version} - 2.10.0 - compile - - - - - UTF-8 - 1.4.0 - - - src/main/scala - src/test/scala - - - - org.apache.maven.plugins - maven-compiler-plugin - 3.8.1 - - 11 - - - - maven-surefire-plugin - 2.20 - - true - - - - - org.scalatest - scalatest-maven-plugin - 1.0 - - ${project.build.directory}/surefire-reports - . - certificate-processor-testsuite.txt - - - - test - - test - - - - - - - org.scoverage - scoverage-maven-plugin - ${scoverage.plugin.version} - - ${scala.version} - true - true - - - - - net.alchim31.maven - scala-maven-plugin - 4.4.0 - - 11 - 11 - ${scala.maj.version} - false - - - - scala-compile-first - process-resources - - add-source - compile - - - - scala-test-compile - process-test-resources - - testCompile - - - - - - - \ No newline at end of file diff --git a/credential-generator/certificate-processor/src/main/resources/Verdana.ttf b/credential-generator/certificate-processor/src/main/resources/Verdana.ttf deleted file mode 100755 index 18ef6e8f1fc1a57750e17b2c611454d010f3ee51..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 129364 zcmbTe2VfIN_CG!|TeRw3mSwA1mb;O$By59?*0|e%ZNSC`gl(|Fm}bBbz$7LKEkFo? z1W0eBkV*n65OU#2j`TtbDWs5k$z4ctNiGG@`uApK2+7^|`}_U>0zIwPtDTuQZ(jMl zH;XVr$OME%p0SfARwsh+=2r+$dRdaLsxR$b6KBf?A_vL2&wSx{58=ho9vaaN zC(NVNFCe^xh3~1|^ZS->8keVp?-|es&D^fV3zj~8$wY)#g%Hy0n743d$ELeqzZu~T z@OyT{{Ep?liae5n@RgI`_nrkE^Sj=^^qNBW4&&qI_AXq~XMQIQ`m*gLLbmsN7kBlV z#KQrE_Z$L#z&TQeadL8=PN&vk6~;Op)?=(b|DtDfYE8b*km$4O^nEH_ZmLeJ)#~zd zocY6&jmcPk!3nK4S*zZUb^DEKgI1emG^$kwy~>Dn80Y63d^)WYenx*u$$ogL4fG|q z1Pwx?@muMFTDeUF{Yua24C*``JxWi9J85xp{6griG5#C*d%iE<=j($P+}-JP?vLM$ zQq?>8#$=<;fQ>3>{oHw-wlCjk&>3`E=$|%Uo2WJTv_5U_{@nar49rk#4Z18{R=zO@ z{yEi7HH_70(3-VrCcDvqwOFMx>Ts&cl!$R6+=1ST-oOU~r2q74y}ECI{GW;rD0hGS zuJpe2uJoSH8mE1j!Ki}Y2X$Hl9%i8b;QJ6WhCy%hb;$;sCVhXt4*QbzeL7XDE>El1 zx^kdDmOi7_q~5Pq>$Pg_eyz%^!Uh9YYYYb9uG-*J8MOI*eJYI(>x{;H9q_;h6Qa|Z zQuS~PLypm$oSR}ax^saG25qVeYqfbMm=o-TnbGQvssw$y)|js`YIItaRt649Btrq@1h=Nj?7>HxYS| zh&*Qq?~+>4i-H2TbJ z+JLsBcAy<-#^68E44KYEGvT-sbpq`|T|j4{*@OQ;vr#wDZkf(ObKtlK^#GlV<_><1 z=ArpO=gV{fS^&oj(L$iTsCV!yv%-5&=NTALw!J(qNRg>N6XMMpckR#K$pvO z1zG{eE78iqFHt}02f7Mf4D@1|UV<)x;{h}<_yt;xRs+2hT?X_rbUDz=Wx58f8T=fr zMQeesL+gO9M;m}{km(iZiows&Mzj&=CbS9YW^^UcE72C9TY!Fwu0mG<-HNsXy&7E& zbQ{_ZbUV;b&^729px2^nf$l&%fbK-QfbK%q4W2~T$@F@3103HV(;Lx^aC{TGdGKR& zvrKP6x5DwQGQADm2FJIfI|e^Occ9%scgyrnbQc`oCDXgn-Gd*Zd(a-Bdt`bqx(|-; zlj;5F{=pB>1L#4Z52A;FK7{rH-7C|F(Zho$&?D#(p!?8%p!?CIKp#bq0euYU`{;4> zIM6510iXxaL7)fGlR%#Y`W`xj4gq}%Jq7eH=xLx&qr*TCqrVQmi~cIpBj^YmKZBkH z`Yiez(7yqF2OUL6fj)YCcRvE#JF z8#isfa?4d)uim!(nrnCL+;!dcH{5vB&9~fo+wFJkzVoiT@7Z(jea!t2JowPwhacIu z|Ix=Df8xNwCl5XKm!}W^^~f{N{_W^<&%f~EOE16j>T9o~V{g3q*4xM5dH22dPkiv< zM;{}Wy$puD3^X(c8vYVIfVbg6@*H`Ae8^nFY-Bbww=u7>8n&LD#XIwvnCAZgs78 z{opZs5C zJs+<)*?qG3qa7dJ{?R6cKDrCueIG6QsO_V`M|mf1KC$EdZ%-^a5jFe;A?`1-z|oih zkzqsc5%4LJS@hiRFY+LHgdG0;d;Ey(|MQul^S8o1;QilTWV3vh0gbr^H2opa!(W1~ z-vS!_CeV<(!FSmRf_^z@Y*>Y$+jpW3;A(vUEAwV_FRa5KU=`ku?gyQ86g1}yG!wLa zCupTE&{Z#h76Q$LUV(K#3mv1{2=wY~Sld^F?s)^WP&a6Sf1)eE3eEu=I3FzF0?_6Q z!PfPHrCS2Ftq-K{BJ>s5w3TQD*s+Vjiam~Q1Iu*@*sV)Je|&=u;Bs7n31%>hIl9(y zC9cBNxQ4EE%wq)>&>*hGb$BcuhsWc3tb{N_jWxIdH&V@kC*X;A5^loHcrtFmt#}Ha zil^Zyo{rmaJMI9D@-{BSMR*3Di92x@o&~NhKfz{f!EZcs^c$7vf%=fbGXwA>C8+))9r{JY{8NLWF z$1Ctk+>cjbA5O)7oQ5yPbp5Tym*UINL7a{=a3;>8Iu>7!j)Nu!eGPj0BhcX=;+ycz z_!hho-->U;x8vRTPJ9=>8{dPo@g96Hz7OAzbMOQBLHrQjiyy|1;C*;MeiT23AIDGN zTzmi@#82Ww_$mAs{4_p{|B8>`VfY#REDqp2{5O0QKZl>kFW?vPOZa8{3VxN0#INDk z@o=1vkKrJG1HXyi!f)f__#ON%eh)p29wCJ|L@gzH25+a<6KvaMVDZ+Yf8%Sx(w{;{ z(4F`Od?Oh}ib)9>O(G!mrDP26;w{|w+%a|)8)ZHOX+(p6AN)jGE_F(+%r(dj`e6si z;Q^4oS7FV{FOcFR^Z+F15wNW<#m~kmNX#|h3%m@x`tIC$@HcJ-4`A=P@AMO-n4aAY zYv4iX??LnooZA3$y9?b3Uc&ho$m)9Va&7>reG@z5r_nRSg5y2-8fnNenB^_t$LD}Q zI}K#|5?Ei4py%;eI9CGa8sO|=kne49_5fJ+|9HVw&ER$Rz}k2iuJ|jx0Q~k6@LxOO zES!t~1Xj&TSWCB|yTRXB2*>N8zt{iA?FP}M#7n%evc7=3y@;1V+FlnuPJ*k@mJ7y&t7p; zI~@4Ry}BD1GD(6nLp`8>>uv|?D~AsJ z*YkhqVx_OLedESTpQmzT`^JtZ23ODUc?`ad57*Ug?5$|`>_rVNdoi3pxG8CG<>uDC zhW2he5=KTlQ8}R{lH@f)J9QI%b&XS6JQW+;hlW0Mrch=Cu#|!Lk$@zFje%nV+7&4f zI2#OnSRUsIi|9M>%7?GWWO$y}=r#I*V7Th^YO3Ji6Ttzfb?gJez?{)vr;7;;Lx`e@ z6ZA@qqHH!3)pKr+aDnLOvCpF@aw-^z49~-ik1_kgLBcrhO+Nne%iOUuFR%rt18+jV z?qQBGEJy;qUz%toJkKbwUMVUGlLKx5Mr@8Ea4HxLoPygG2Lr|N+xg&@44b_s`yP1n zme^CIjJ~0B;L9TJ1?XWCNYumPBGKEd?BjZs{pNmKe?q@QP$^wbo8F3NTd~!p(HqPb zwM8MglM|2~ciXYu#S!e_6Zsj5mKnWjtY(aAVk=tUdEH>TsjBqD9Mt- z(znyGiPrOPWI8Yo#^7?;E6@>;^sq^h6M%=0oC@7YmozdJ~s< zdUIBe;B_{fon$j*`?O(gv8^~YoL#1^wT(?J%bp@k^h^v+3C|U}JYAVHvwQM7M|SG{^bMbfRTadDfoz@KrnbJ0Bi*Ao(cv7#-gG?_VB!D z6kB+O(Ptcn)5Ad*PJsXPco+_cMidx>M)+>^!3lcc!hV3TtQH#%rKKB3V|K$2&n{c@ zRg=LyV_0ZYeMY(SjxApwTh{efwXW`twXRCXwRgncn!9ZtK}**xzZ9>!;T5yA11$3J z^rO145oQTLEa-*IJ6BxuvJo4kA4rb#*msgW~so*TcyHwj(fUP~?0CnriXG&8`j zy>{?3LBX9uYCvm}03ivY8^y&+fmbQ*yh9(h3kfQ_J;9MFq^UCPX^u*5wO$N2Yg_c) zI`azmGXIL~Wy4v2V}Pk=2*bD=l|;|zgzQF7qL@e$6NfjNRrMNCM>M)&S;&TMwu~%| zj@KJR12H6L>8a0`l4alnL0FtoV6CYLm`RU-!F0h!Pemf}&nT=l%3EJb8gMe0Z}VfH z^1|=HLthFH%nas_xbT?id+L$L4*unlM-P&HXMehr(U7(D3q5)I$=1$UEiJP;TiNaB zk3uKkfB)l??|*RkbO3b2%W(7|oWW1rzhT1z53FA=B@7*a)GitPgg0~F1NZEJ)O;c? zRO%D;#hN^8o->>~x}@Gw<*YAXk*x0Vw@3Q9WkSDxnRBJTcXV$=$q9TGUtkezPD_@h zz#7TaWI59c@`OBWO{t|mxy)JV8S5?0EiG;oTD4RCbGTW;Tz$LK717w79$z8@ z#z7oqN+@9NP1NQnM{~X~u|<3;myn|!%>RbkOOu)|@t zrY)&7{4BE=*lYwTHUjfOyidU)0;Wf(U}p#5B>a~JJ9?h238TT^(_w`yyp-Qo5PIxO z2ZS^wl?wW3To9M|(HN$R{uQpqaCSsmn6WQvnbuc-`HZe3Qi^-DJ2~~fo1%~7x%tI- z<kcpI2#51;>v-U}-RW629WMcBoM86uw)y?7b$#`|^)ZCs&=1c_ zUr@1a-&FEod;9nErL8BoEc)?p)7A|SOmtUoTUfRtKd<UJ~7@MW^5w~8u47_8?o%3);19t_0QI=(D>!;m_MUpy*xGJ5!h6;+D`N~}~Q;zUIT zlfl*FYNnd2SG3_a#X`JLF@Tqo<@|ucq`+9SlEngT59!_P2Akn0%I{(jZEA?3*q26( z22(-M3e4e&)ncOBg3*8R@Uzc8{6*vTNO4`w=;G_gNu4j@li-@>;*&3_YMvhGm)_ld zpY-|a{^u$X+eNrOuKmnT1I-A zUh^`G)r48iQiM$rh$UII8*NL7%fs*eGZ2L@Oef704iQjcpwVn5atb;(?uJpLYidg( z+Z&;?q*!`W^1P&~c&>jnPQGt9&gdU_x<>VqJRaDlSu~AeS3Ir;- zEpRZEOywr2C+X&rxm>5ZQ`b-Wxh3i)It}LaDz#29a)j6L^=tzG*DAdRdW`jEx0xWL zRy1IPpwQIATo`l&&717Z@Il0_TGu(qI6CAk8>CJBk&4v(L<> z=i&m62{Qf!j$I^5JNQdEX=f?ESh~Cv_wbi0@LW+^!(6oBZD|+oKEB|!*A~2uXG_<; zwcr)X{Wnk&caV*N4k!|R%>9_1B;2pkE3iV;;ykUWCFnlYY5>r)xcd|$a2n?S^BBCN zU_t0%F&JU&1%m;$70iQ|I~e=;uGq)KdlxvW#q{7-;*EVQ&+`@%!A{USVKL2tvv4*E zqasp)s!0=SC7s~Z{tdJnVVVKsXNiLl0z(cU!%z8CqN7UeGn0t)<+ca0Gq#9aMSFi8 z8O|ulmvB#yXu)Ij1QR_6z%d8lE(cB)q5cAN^c3x6*vlwmTS+e&{wV!z49GWfe{7G>a&J9Z%#eD<+>}T zzSX6wI<@c%%w}h$ aH`Ao)pY|8%DgC_?1E*HnV;T!qkQ%&2)j9y+d=Wml>^ml{2 ztb);(pliimwVDe!)Yd?zIxUb{T&xaThNpxAwdx8>SxQ-;8Mkt+>ZU-CdRCyPc)5C6 zpfB8C>_Y+h!VYh(edVQ!7+ zBkl;%TtC|IWz_JcF`>vn{YENuFye?wc6b6Y&{A~tF4>e)JDMi@k05bzR)8dC<1l>v zT`J?8p>j!?V|4)}m^Zq%tnQ^1tG*wnZ~Ag>WJ}Jl++c3*>e?xlyB-~unLT54+wnHa zu=#hDRo6arQQj)@O7>;5XWbjAEF0xJR#=;rnbXtQFvsOixNBhLh(?FQQa<{aZ&XH3 z-iE2G4%l@9q!tNenGm}Vq8b(C`?|$aR%_9wYD3y`ZLgNsXsdZ%q2U!uZ4Kr)M!_&b zL{+aMD!tyVCkhSgQ7N^2Jr6oj&%60Z+`p!J8XRii0`>fWD}mxzzU+A5^@bRGm|9C& zN#dX}=rv+?--m}|1?1QhA4)T0PvbkJD8A!Mruyt+a%-%eYNsb*rq@Crvrw-%UL`n$ zY@tL58%u0;Lb-8@Fe$S~SSe_nPDc$@2cVMt-daEJB6^h@`l`~^@+v@b>z&wX@gk2? ziEhc!=eTo-@`mjAJQmS;1W)z%c??8jRMW~4z<-}XD$hm;sle9df6 z{B*o-!UsK7YePA?;XvrxX)Sk*VphjWv!{4hJT|tW1;5w*m(t3*VCtKpnvDGH<@Muw zJZWybh73L=^|5SbsNlhK+VF1h=2K7s>J-a)txa3x4-O9&)cD5?FDq!n&DsV}gSX4O zbhysJWY##%=7d@&qbFfdjgFjv$>&8TCAzUlU*s+#0k3jbp{yalG}5ucY*0mMDuPkc zRV$AaQ9(^3;gG3dL@FH<4Q_lXNO5k2ivOxa@0Q+vZ^8bG=4ey21=|jcYRFV2FBO!}m}Y$7)O^(fB2XwiU5`?Nku z`tgfk*cUE2cx7~DuE&y*Wy^Hm{CGGg^I<9l*Mp>93sS(NdNCh>Pm&7IXdP)GD?l(I zvF!nK37lhUIfg*!emO&VZS?M(oYa4q1aKq4ZF&+*+Dao?a1buue^! zk~GCVDWxsDHK%R(q~N52c43BYhG|A(yRSWcnQob>KWClExhL(e07?y&zI*9Jo|WL#oj2r`S}mhCtELHz64v|{urw+S-JU| zQ+KYugNtDpH)34(=^gNW=m|zw1tJXw3OlZ^sL{z1N6(H!Ovu?9+%?^flXvH**myd0nR1VAsdS3$l?`u0F*TB^#~Q~KcB?diMx1cix!sw^m|?8S{*Eq<8TuwFaaYE59zxOAVDAy8LtO zDl3~Po!NcEp-^u6{yW;1=en&~8P-g|xu$@K)&MF=aVZ!0z0HKj&&a(P;t0e)nx zc*utVEg1wo86-8|!HW^G^UNLKe-ZShw1sRNy2n_tK*gze2j}3kxNJVm7V#BqHQ&Ux z@||oS`!|JNZh}`d3p^iz1Q%ghD2+(lDyh#T>*b)Cx|Yyl4fQT*qh}AF^Dree zj$6&X1S@d5XjsBwj-3Qh0Z)x_4tiqnQ!!0tFJN4hxS`~NFm&aK0?rdefv^yjArUea z)Ts1dI1#AQS=MJ~xs2_*1Rf)OPXD(qcJG+?KGLGO?1n-xYN=sq< z|HeJsPUb9m(J}xGo8Ji zz8T%Eu|u~~Pfi1)+#-*%T{Ns>FvCvbIG#*`bWskbqs)K-a&(lO8xjeGgBmYzq&RoK5IlGv>jEs*B?#G+(rv0%&5EMY;5pxQB;$-AObHpm@ za-~|Bu)UttGxc1#eHu5(K8KxS=(P9Q`;s+6FGNth!)3FXLJlIhn_Pk?+wJDv18`n% zRVH{~LQq0n6;g`_nuXdtx`ZfX5S2~dkkM0EePG-E?7Z190Itlb%A&$;6ZVzv%o{0Q zw_@I33ssfxw7>J4wDjed*(Ecs8&%X9_yiC4x8#NvHZNLO?t3{Y``GDEDK5Kr(4`m; zBN~J1(Q)y1p6FRMr&8*ZbjkXNUbGQ6o6NbB+{u;{OS(JaF7=ZfHiru&1(H)!Jn4Z* zptMRXuWqh(syHsawPt~`OFPHW?d?jR6`57qXX{VuP46q}8>KgKM!{4yL1z)INd*>` z9XCPumV z00J9JJwgBsKzm<;uco*3mLIdSr?#|6XYagK z`lYRXes|kUth%ZB@kt%`v>ud>EMBsDU0)v_{phne)YG#lc2%Ua=+bR{{blRO&P~$U zxqZc=^!aC4=k?~DeXQ=2*4+h7l>*$(ovv0BL2No~EW1cyHBTJXA zTh-eu{Y?@H?@Zi(*By6{rm_jO1>AJ7#|rQrcZqXg?bY%X3Zhi11hO8Mga!VhR59xz zP`1DVTE#C>5GDkmn5tLBsFW(6!7D+B2ml3uhskgnsPifUxtIdEpg#C5TA+bHskm%z z1UHUr;<~tgP65aQxNHz?z={Uz2bg$q1o$oowNX$XQ5pKC9|9P56!rQT0KBo+4BBM+ zo7-Zmw!J`HSXd>Ukxt`Vqz>-b+2!O!%n#H3Jd7tB#$$zk1knUB$Z4#abWIa!whkon zCSy*>d|t3fvL2^6GX>pb z#1o=VA4GVvzc}RRQ@ohQ18|mx2XPxMUqruh*^{a!y>LV9dwk%I+iNG(&YQCHL1|xV zMqu5{Zy+_q}23I&<9Hfzu+plZ@|0m5NkQCRq_C0Cn+sfzjcJQ%raGI zQMKr%8m4BnGaZ&*vP|D=G23j8kcnhvrG=GKBS3)aTZ_18xyQX*L1o(?thC-{&kcs77*b z4(U3LdlE2t@z{=gbGUAU0^{UzzKsS%i( zNI{$Q?D2=bugFh7b9q)#)mPNNg&wjuK@Zhvl;~525pUojYlXQmVN_u(ut)${W(ky< zVqR&%^E5-URveA{3m|8UWC%eHN@!r>Wp6s0&Aff~UyPoh&b2iFZ7ESm zbU_P5fx`l$5D){YCh(d&q2LxQ8?ZnXZw7ED>Niutmd&T!2moE|Kuk~8$5tF=9_PH$ zw7sz)+?C4IXWZ=|Q>kFJY7}6Tc%73s>%6+Kt_GLt8g*TKmwKkIPuG{MPYH`Y?DJ_D zgCQZLAu-`-f%&G;KVmK&1-I*3jzrQ?E}}qz2^8 zXXB|Esz#BKo`8%EHTbu$ls;bnp!CtK*}YIxFmC`WuQR!q6;(X6@U-+H*loW3>1t^a znYf^EVtacB_B{hRnVU*#{%IfYaA!(SOW#T#OHZe{@O+5UL;$#Vv%4--a$K_G&_ zsURTX1SPScZy?9wmQ2O5Tg5 zbH`#&kZNe>3Q)Lj%k2yk6P0W}�JuENJJe1oae^h2h8)W}|VtooS;Z$Qk@6`3KzDit@zd9ss(CDQIOB3U!E4 zn$0E)x+p=PfcY*Pjg)C968E=32!=8^7eX79!{i^|y^we72zBQ2$h7&6B)#-Eg7M|g zzGc)L(7CeGGy29dT{qK~0dnP*%451)T$-qC#?!zEC^M9W%35W&a-;HHCHSt8RKecoHMmAup`M1j)Qiyy!2luO1}NbmcpoeqLAj5p!L+PVVSxHloq|ct9*qiL9z=_H<>;4p_x{!^&9HPL0W;#hDn>bYRd3wISS6 zX)w0$8knj#zmLr(J8za0U`u`hRJ9x$p=)^=jEzGc(MT8;)-Pno0ZL|JXHA&R3) zh0YM2d3M>^2}j9S+_BRipOdSf;rAM}LTqhT;X+c#jZiHl?aV^1T{Qr({{YvkQa34^ zRa4YVC)39)1?H&;qeQHM4YB3yB({g;SyrJ|GFUF{0~rBv)ai9@U4xF{%!+y1q4fkX z);V#cMwRO6#VEQM#1i6S3e+gEm;3psG-H|6bpTRDS3>cQnPblGWagZX!JwaEO3w8e zbQ|cDx#D<@!mS7?Din>14#gscVi^wwMuc}`D<9&^`HB2o+|Cc+US17(0G>?2O+3{> z0{8^1z+<8?qvfE2L^UC1mg*p?h9PJ_*HPNZ#fu<-00S>~5tzso{yR4M01lEh2e{B_ zS_~kvhe7j0W{-OxaKZ$@m7B#eEMlQmwFyep`9w{kX)>P7O;oh0TeL04HuJ;;!!qRx zD@h7ji8rKF*{#eH0GTlbmLh(}Wz~1fNHbjvvQeOzb`;x8*0^E#gC}P*8z3^L>L-|w zIM>vUnXS{#+;)>RIAuyl`?MC!U4QFfm2~!#&!iX@KKckNxU^2`qbHt_KJ4h4)je}2 z_8fQ&duPw+o)hc9DR>mPU>`{*z|t2`|BK4l4&b)|>Jgg7l984YXPu?a*`S-G@6sz0 zLr7sz5QRc%52+ZXp!d4H#AuB#gkIRrv(T&eM!Y0ZVNuTW3@Q02wREzQk2_Q}|E;6G z2?&Agv;4{4u{$b8k9*_Rqet;I>kd{oMPD6Jkk>!`*}IqT0KbIQ&)hS5+_)I%t=zn# zd)JR!oa#=BJ&+y9>!GtEUBvwgv*L$({B>dnoe|c~I_Zp9?KbB$b*r}3I1T1wa^hrX zpU2ST?s6}6_T{pEzcx2wbIX67 zYi`%jT(1--2~c#G3#EBQ&I*0CrP?_KH|tw13-KJ%uIf@>f|sg|R3#daLE#9A5Ev8A z9+GVm6$8=B7_Hz&Xtf3p=&D4;9hLhMy+bdRW%cE*Gx?OF4lI5Wksl*FAX8*DKvn_`;y6*uqY&hv+I0{YfsE0wITalqU~>&TnJi9F2Vg zzxJN~nDg$XcY6f1WG38=ddHbQlh0nllqknCW0e<~FG>)SwTu-wmSjQ8z(aO}hs@|L znz`rvW2yUI;Lp*d}br`>|C75qs`(hD2D}MW;q2vnO_I#1`G6^ zQ?QfSbu7bL0F4r$hL(cDO8_w95l5B+fCG4N1*(OLGQe`cB^@~wV)AGK;KUgItC?&j z%rr4wOh3ajoeCOaf`*TR!J!I~stKy*<@k~k>JETW#6cQW#y*vPjeYtS)QY|d<{Ju4 zPJha|px@KL;$I0I)u1QEo2!`F%nF9lVm07zgyRH_Isqp#b}ms!RA(}oLZ*5YDPr>3 zkWj1)sz$2oNI6?Bj8&GY>eNkm3Ta}eaFZ3S$|hA8?jc=l57(pYqMiV|L|CO5SSuJxWlN~x6b5%#m!=m zpK-CDLO&p-%TaGYjgE*rm*SNQqGCCfgSD!1Sf8puIgTBpYGK;g7H+b#K{Z+3&CF-J zx!KBg)ogV?yF`_srlV0>1O@nNpuso`q@p-hfmLdr2o&LmIEdIt8W};Vpk_`;6f%V( zp<3XGpilwkz!NR9!FI6`u-U2sAj?@=K?&7&nS2pn&A0J9KMVZjDA zq3=)%$$kkO5YQ?ynuBxL3`JNWVv!XU4T^5ISD~`;i9$M`AvE)?LKoj72m&256biE( zL0Wi2l_F9wEI4o=Gjz<5(5hDrs90Fwiue*AkgFnpI#pz#b&>J}#zmEOKR$t=vFYTo zU&ekq8vEe{-idDjukBE530cmx#%2*ntYE0-MVtV=RiajL1Vn4GZ5kjTVEX)#? zLErX)v#n4tpl1jXXg~_Hpir=}8}#BkX*u}?7PSH;i#l!s!TbbRoCtyENO89F$T$SI zG{7al#C|1$bQhi?Emz!l)o+*4kyJu;!eO`v1DY&@!9)es>RGrDs^(jv&V4z)0co2^if6J==G#XY2$8W@yjcz%?9z^3k|6 z2%LxU$x+b0*ssv5yP$W=ui`Jc3hr=_+{?WI9OhA#7#an)ufr8g8*X40;&ujnVMaBF z^n;E9K$&qf5vC0)1vm!&gQBkoj|nnZQDAEv%Z@uN@ukVVaa1ajM*fYAyM)|Nmc%wO z>A;7o%VpB$7<%FGY%y)i8$0_hnz{QT zV~Huj{rn9BH|6Gf1gAL3o?uA#Kdnzq4-DHjPjZ4Tw3rh*n%X-ctxlkx{S$U8$cz~P zsE2Ziyh<%tlF(KF`Xebsm!x!P zHb8v^j5!JwXRyGbh!Q{;Fh>^{TLq0tX_D3yPxvXIAQfB_c58iE)xERYwp3{L9;$z2 z;n6RStl2){p6Z4rH8(y?3O1h{TVJ0G2%aVOR_R3Pb?M9JU#lvNtxipT1*CP(;NO`a z*^7`DO%_Y_zIq?Y#wofiTdI8|4(mqR!uA?muPWEo+e+=NxJfq$cj;C_U7y)tutYSh z*XxKdNv z(tco^s`>HBw;p(OchRyyX0=sSnVVl#UH!p!Y{G>j(vOu@zw`Qw@44*OfDw2<4|rcT z#QQoiSnSA4F7(tp#w1sJTKI0h*Pt|EV&v?lIw ztY(Pm5Us~W%$Gupa%Cj#w=u|Eh}N@ZhZ^_Je*T}073Gh1v}~w5bZG4Is+;$2*wL_i zY2|nv!p1EhjUU_KPsg919we8hI6ipsx!03Cp!BT{>LscI%T%tgKqCi0G9XO`-!`1p%EKLORBclT2&Rg>*7v z1LPcZ2ZiA0y)s&?=<~$mQAxSx z+js2Try)tRuN$71H#O&#j1droSb_Uvq+fr`y5{yr)_~lA-Dga^3r0~V<_eY~%TlVq z*oYOWb)%sMP%9vsp;PLWIwYtadLz<_T0?>EibTWD+4=c21{s3)B9Ll{$7VD!DR<6^ zsoIC)k#I0xIGn(kRPN5S74z{#Y2TrNfum1Gy0WUex=&%A z)XJ6tcYP3FED*<+m`7*j=ZwrNSJs)wW{t_I%bSX$+!R|6p2zjrR&l)^V+v>TS~El! z3t*VYXorcDKFVP7G>?b_0}<#*-N|7^H)drQdkI1_b6I zu61lyw(XF~o;ba3+&$wC9cno=^U=SMRpZvDWo3>Xb@nfyUtX!1@IJtO1on<0d^vXl z?2H2NMi(w8qFP%3Ib`sNX^wImYG)m|eE=et4Fb4ku-ilSOC#A7b*7m|XcS-m)?1oG zhdBE)r~QxxCxb_%R$`U!nuuz|VJa>ewpyIKwQY1F2hVg6725g)oG_q*%EWe?1GTFX zdZFxhTM~5u^PwFm#RCTl#1B?GZ4G1&nv;vvFPOS81yhL?I`I5G8JFj%lG}2tTWmJ% z*Z5A_AJwzZ8#MdW$r%|LiyE0VGS2x|;QB+rbv3$JY|J3<;uA_niJ{00JBeGHiv!v` z0JKDHsp0A%$xu)wgGJSjs}p%)!G1lP*arAUAkmW_JEY9^J*G^d&~%RS|3 zk7p?!^JBl;nJgrX$w*4 zFa{dG0;WxCUto>F1QmlULOGe7t^SihLhT&L?>wj5EkU{(Zvy$RH{ohxT+zD7n)K+}LE`s*efNG8wb4KYxhLB~Htxi{ND7Vzv z1bsxwS|f}~1Fl4(KGB^RNnDu7nv(dn_PAL)@2^6k?C%)m`FbJhJuH&G`sXR>Yn*WE zpV)rn-ksO&xp&u&`^Yc=s-DHguvyfIOQdI{@7{a!&G+7T?;YsX0jZPS3caHK&Nwlg zPl~Mho-$Hn9ph;RU*{5GRkG@wufu__qf~4DKl?gV>&U$Nv#&G0YRtaQ$ye4KI#l;$ z&x@ZN*?3js?mBRFZrV$VH+(X_wlOV3%He)n8flVVlfHTJ`O2c$`c%hTR4dJcepA~( z`whOrD5eCwg^`}IOr4`LxzFnNv1ZgJvJUyz-8Fuh>3eH|zv()YU+yLAUk8x|)WJ^t`iAljXkT5050Hz{(c2uDl)sRhXKVL#2DciGtYZ$@w!I9=n`a*4gg-}o!E(0uT`pA-|t+tIW28o z#--uy{Y4`SADG+uO1Y}~)tR%mWM@qeU7CG)RTUm{-Ls>7Z=J1u`BJm^qmVT zR^|l?hWlO!*VK=%@cC>SyUJBl3%nXV_zx1}ZiXFM)nbl@b3lm!qc$i;YtqYU`=ehH0<}X}Pq&rOObp05m`O-r$|@LwF7V|40Qeb$?Bqjj z56;>9>>-0Asd+-p19gX1)iu2HIwT?2G_AWCeH_Ibzxl%}zn{0OLE zt7L0@YEcyfs1gx|AU;5S0NsNkw8Nqc_M&K@ zVyXvr4zuV=3h^8fX}valhyOd z47O9~QFf~4s#oBP$zrxo=vOXLtyH@-bXW=p0Cq@exB#t#!O(K*0MDQ;@Fc8=ZlR7r z{N<3ZgDCJkPA41rI81jAq%(uMF!Lj%4;VHpGV%qm(78v8WE0GXP2Q<%&Ax zI2B~P7Vv4d!P4SC4T)SeSa+cbsCrF5pJfWV5!o{Rl5R?v;S|txsc5E02%dvoQ zr~!>*SF%e1oa9(I1~5+xV7Z`XS(QAE$uNyj5{a&)6%Vu|U(~i~0o4O&?=l&=lM&i% z3f{%*%!hK9=Ycyop!4ZH58VYZyZRJZtiPUsO2URycoZ)FK&rtHNE1me$)j6XWA8(Q zC14GxL@EGpE@kC>Wdq+1n@4#iqu>&m1g;X-FfC{@UIF`U6m$+b2cXY2sFEd!f#O4= zA>H6AK_LYz4+Sb2WQnYmWs*ZzLi7Vrw3)47yV#|`kIMu=?k)iZqBMq##f$G(s6x-S^K@2-#c?O42_|N1MgxW0eGVrltvhpyf8^o>vN zx%SX=8!y@X(59yf>;+G4dT291pctSZw&uA;+7GkM8#o7eJ}^c^=q?Mm+n^V~ukF|N39Xx>4qW z+;_Z*JI351XOYK1N=2zOD-#o8x3CqS05nPn*Z)}flZ)%uTGP{FeZkc(wZd^>!)~PkagBrN|+g8IO^IjJ?Lyu<;NXb-colv%}FaL^X0oNxh@L6-CP#y-$uq~^<>+!L?dk+=EnjJB<(o~*$wiFd@p#=sb&V$E@t&`1 z?C1MT#MY>xrY8!fTf>BUz1ZvZ@Ftc+fNs)KAhWSv-=J^TGx|i>G})$H2qB=}2ZdaE zxadC%AARt*|0sM+0o4yWB3}3iES9CQIQ~cg_$Eil|4Rv^>;M7Nhtjs^t4qytW#l!n z)z`d06s>b#-nRXvIjuR1o0}IeX=+|P0+5K9Tqrq^bIYu&jvTqF^NuVWXkOCS)VyTL znSYU&Vquv0B~q(gLs$ws(H5#aOm?!zn(bGoWQRyl9m*~vW$KFTsJq46;-8W}J<>ti zv!RkOtvB1IRwt=p=hg5~CUNl2($bQU6P|}?$=7K0`h+ZM7d^l$rPEioCOCUdZsiBd?VFSbU}IfUE4HkwKYxnGKn}nP9#$Q}yy~qhYwOoD-IRQ8=`= zX2gF%45^+}F||FF#VFOKb;ef~{6fwH@SCA0fCIvw$SXfX&|88=6gbn)_2bXilzkPy zCS2h3{ZH2%KN-KKF!ZPUa3{{UwFRzmi8e-G)LTRdDl!y#io_xo6~%uOg|&aahZE2L z>YY#HcPycybR#$P3YGajJe~{YWTN%TlEI zvvU1`QMgDE_K(U91?q6USM=BCih))<(LWE*@~^~8{Z_S*B#`vt>o!y^tQ zY&k4ZYBd^57SB68Y3Rz4!;(B}i$J~sJEzw8zsvTJaQ{z|{eMB&z2lq=_xLt{9|&|y ze)kN97@Ud!3+AqOE6qj8RE%#`ImWlxhFJ*vKcMiyZ5h*+<)Jz*53tdg%0r6RWIxC4 zKVMTebPa6W{U5J6{#*P$sU8#h!+m6Vh_|%`uCa((CePpQ2YK)hT`Eh#tq}HC%Kds< zmh3B~%MraJxN_Y2bp=nmMEL3pt}2su{`o3EAK{roq!@B%Jk-ILh^Z@aAG4C{<5`GD zV9z8x9YTNzM}v`Oo5H~kKoRc-HIxwK$3BO29_XqFKzBpl9`>oh$&)coQoQr3E!!XY z%nUduJegPeH}3zf4;(wNUUVyW?huBmhMm&s1S`th!7YZhE?~z1h$idk1s`fa3u{Re9)u>oZ zQ>fJNaA2Ohm*)kjbdfhIqnU)I1)dN~dxUcPWRrNQq3g-Z%ps`7qWEHlXz$T?Sy$?pT3PEP zhS5(_5)HU}rYxRnB#I=`4T{jl1y6K=Lc#MM26O1EJ$t_TYR|oYzjNKJS?jKtIcps| z?VhjxzUQ8=zPe}DrcJZDuhggeDO2!7<2>AH zT!EJv;ZZd{ozrF4M$GkQVm50Ns$j3Zp5r{pdC6ijlWea_<{0_80A}R%1gNX+)adOj zJOqe7oJM~5@B8AEnaqjc|}{M4T)eeO>jUZGXKX$bZU{^2Pd`k~cA>7D-NbzhUg$+)gU1=N<~tcEGIjY~rMii_skWKAC1i5kTVn00WDY6O}n@W5t#z0d6FBX6ap=pl_E>Ck0V>q18nU_{s;O>snd zqb=Q^L!+2;EQ^=?Q8ZD0;6WU91n=^^8%ZkyVWVmHvY8z%V@j)!teSlHG*$indFyUZ z&&}LA;rN7!i)VFB63fKrR!_cXs%qp@D+cytXJ@aO`uvpEUy@y3XY%-*^k}7Nw9TI8 zb|y9Eq_vk>#*o&Y+Y1!KuAVY(V}9X?B=-@2e#hj((!>P6CDhn>>5M%^io9*DjhBb= zMM>Wg_Xzn(bhu0BW*(g zb-{+7_7Ig-Dwu65Ir6UKI8p=K-7W|#Kr$e*@WGQjelI|Qf)g0RIS^W2Tkz0;E2V#( z$XW7#czf@_sH(IN_}1yYXOd~@Gm~CPA-Qw{frNx!LzPab(tLvja1ju(feqJ!hz-TM z%ZjTmYu8;B%j)VX?pjt|-&H?Xn928h&Ycuk+}Hj7_<&?G;m*0|oO{l5p7MKsPqra8 zcfhFSk*v_`CvLvM6HK2xux5IGPT{eUO8wE9kA1Rxs_a-)g?o=cp~M=xRQA z_jMOuoHaNU$O`0TUpDLXZ>0g7A8V+syZ`#mhr;?CaNcqUx{I{4fw5*%9NFvlDH}JN zH`_a%o!*W9HF>M6v}V7Q74$o@T=@l-EL*0lz!ULhXEml(Wi_RZ$ZYd1vCOm0b1m^K zPILRyV!nJ|G?tT-?<-A@aRFPPB_R5^0KW&A9n*aQ;N2YeY4ZJtOG|@k3Vv5|IOuK- z1ob#FTJ-{ZoV4qRc8yYBzN}%GE}Fv`E6ZoV^Fb;uxx2_|0M{MD<_w&G?*Lu*{;Wl| z5GJxOiLSaQgHDaEYiB3rM^QS>@x`02uCfSsH0e_po3tEN0k3IE4@{!x_7gVR$Rvs=FKvCD09?B^ur&6>n;%-fk{M#gBdCn-N zZ@rUDei>pp{0~zrGgCN~C*LR5XkN>I#&up}(>8r|{P@RbgipFY5t>B1uqZi@e*u(9PbDwm z|7UID5n=RN$VVX{d&FmYkaHvWi697GheRXsCy;aG#T5}Wz4oYPXL1K&=lH3zPkb_^ z&oidVp?n75deLl?G{veyp+IJAb7$b#y<(qHO>!^ zPWBN_f`k%j_la8?UI|Ztkm4EMn!l=fyW~>C za7a_Kvs7XNp3)bxVnpq-Gp&hl-*{DMlg$aI@FjhnY5mjJLP6qW?uE_B6Im&mhH6q`IZBhy*z)Jn(wsk>9*aM9h<)*xuRyzl;sZ( zR)*Epiqq?B6ASCFnzH<{h9QH6Pu29N+Uiq$?mWKWSv3Qyy88L#xAT&3@wG*TW5zar zay8J4{Nq=g8hvI+VNq*y+h1sYf63jforO5Q7dX^PZj}O5tFF|Yn^qjn$}Om?7@9G< zVw^BhJIgT3GSxZGGsQQdWUjExFxRsvYiahpf_a6$$OGjMh#H4gua7;D{(#%M$+xCN z$cu3$J`=aUIv6wUuOeMQ-a}!7B)WKvE!z$3zu)M$;=T=Th>j! zcvP_5S^sPNR=ED_VWF$@r83)yuqqpz}Zy~C?Tar_;f0OV}Zjo0q z56NuB)1NjZmyZfydJ`y6_K}ipB>oV5(~s973t2JoB_{CE$&wz3eva$M9-;~#VqYNk zkQx2yL-Ytyfjop|`>F&Y2;35p2*^4<-pqgeSn@jj_1_A~0Qt1X*t1}+S8TpN9}Jl_4`QM{V* zs+8l0;rs+^rxiJ$x-uhjEUlcZH3QXeb%fx*HgMBr9wxKXJl$ll>W(94ndJB>5Ym;| zPO;o-5&W=53VaWa4KQo*<4211`G|SGnjb63CyiG98gj;$LsYPgd4r~I!o zzshSHQZFi4eb;t>C~bfpnhEdt0%fFIaHo5UqEdExsi87`kYPyrM8gFC#87K^Vr-s& zL2zDdZqeHG&LWppliO%Af$<{E-53xG8Z`#LH7n|HAzh4%y7Z({KzAj-EQw60S_DN` zVUj790HU{L&}P*dfSl~3c@xhT3DSnKZ4+Kx`P4so^9xfCv83ysxJ7ZfhrJn@y}v&B`9 z9=>Y-(@#cPX%+ptdx$&*tH=Z!d@&xnZkX{3@g4DXBaTMi7#GX1KxT*w3>`+KbBH>< z?kGapqS0WabdU4=Mw|@xJbH7zOj7F5;92-!}_)n9H0Hu3CtGgitGqQ${-1G+dOPdPnD_+?^|wCv1Q;qt@+ z@!{Vb7j6EK!D10ZUNPRt{|0exUMHffpGtZq!969lvyj0HmF+)B@U-#oqD497yA64`ZT> z+I5*(ti^*-;Hlsw2Pr=%*7u@Zl z#uOt>(h=jxjR+?NZF};2s>i;1;1eTApX2k?x4(V<%P*h*?YEC^U%zqNwvFpShVupD zsrpYo;SI?%U)+1|{f7_VcMq-MCGf07(1Sc~jxtoQ*PH!%KZu|U%oSpZTwY&Q97(DqXdUI9jP^yH2r<&SGXLm<5k;ENCE-Ne^@WV$R{ZLz6+%+_D2mkP) zcTo9*?C4eR974T~t$9)Sv9=NVSq*%GZQK*eWyUmPAR{eT3Y)8>GV_3e6@vy#gUzD`4j$BIZmXU!daikP z_59IJPhfOxT+y7OP3He`ZVq0Qy}D@ifX$|y z0LSdd`=1lQ~99ILHGN~v?FpAvK)&P2mxx?=al z6>knUjeT?3!7rQL)5{vKS$*B>OXttxvu`Vk=6BY&9Gz;eer5iF*K3BoaM3%bAC+^$ znaf(XKAd)y%f0BSwwBpz*3JE`P`v84iju#vAhg%;GhX! zuRWH1+?qRMTHC^I?sUV@1vz;I*X>L$sl05osCT=jwa=P95zlHy_fjB?-+?Y20it`o zGLh3+U52!1u7uQ%9D#k)yXa+Lpe#NnFgUJ_PdD8BxH7jmx9-lmxGq?K2!<`3Ts1m64O>}#s{`0RaEiP@pxJCF`5yFY@3PPHk*@G3Sl;#+4hD_ASnx<<>2 zS+OYAA)e`Sn{|ae-!k97QQoMxIl4gXXwy0XloP|@j0&wmh>M~v)Jc4ghA`rFX>Q5D zBnk9}6croEb;cyt!^B2$x!#3E!m+%6?pk%Z@Z3u;ec%48&bR&rOX}vKN9%7Xsv5d~ z!v(*qF-XbRf1DJ||HD0Vu4`zRAN@=6)yVAp;>}}Mt{9v{x^EV~^{cp{)pF=5tGIy@ zHF0@{RR{}|7zy_Y4oMYAL5>ITY?t%@dg&#@hyKHKk7B7B^6QUWjeJD-5z)ygonOLu zD@Lej`4X+76?i5&DT5IMWnKezc;(={1trgsdrrSB{kaD0*l1^4_ae>nnls3n9i?>`Sgpw$yO#I6gXI|7yN515H-PJM&Ju!}0%or^o zUq!k+Q=YXb63wYJg9X_#$T}r+fp&q(LSB_{AQ(YS3R=yAJ{%5Y>LrxEH0pxGBawUu z4)CUY0W4WfD4y%+A7@}%bqPAjv>`Yb;9DU70y7`gEGH{vIf=cXsh+$cs&oRsaw_N` zbB4gG=!XBviUHGm`vrmN%AkGIqpcN&$jpMSi?c3x=ewAHdPBqhNwqB%a@63z@Ybhy zBt6@XR8+>a`iCv9EqC#s?>MpPPO{6&MVtsTvKMt%PScl@MqzR1MI*M$f8^_3kKQt7=YT;A3r3I6 zxY2v`z{uTo1D6ywPjTLocK_`o_tfNVDH=P`b+hxaTgL3D&DvZwdVIRn`t*s>g%#Bu zn;(64R8dLYvWpUD9)D(ZVW@fe=BJ(;Rg}>(cOzz^3_9K|Y_}8BZ0Sg0geXjv7bDLa ziXJCw{6Kcr2vy<|VV<}N`~n*6B9Q|-mkh+GW;{=-FDbk@eOXWmYY_h=EJKw*13=$f ze!NBieG{LTY(+tjKm7QXwCVI#(nAh%w*eHp4wkYRdD!6CTx}90#bm2C(l-q-W=HV~ zEAJ^K9Y2T=Nm>KcahbLPhjob^hrL4k3uF#cJV^)IWpz?=rvVCS=yNRhhR(I z$j?uY<)`=tmDtI+G6evLy1PJ8{U~*P(~#pxPz1pVYzr{e$vPRWumfmK;XSEb#~`zmaeW>DCiE+3#aMp zjIwd>BeT|AymHyJ*Y;Kon#K=*^)0@>qkO2!F(*kI!c~KRxaXd)hSuccSpb8fT?frC z8`70nl$VmuS0+ zcA8gZrt|f2M=&m0^9M$x;>Prps#n?QHUu4UU|&G9U^3;PMwySh5)Mwj4B43~T`*jB zs(zO!Bi{n1f^<8k5UUe%ZeDE4kEdqGswreWIkHyh9KA}ucHNox>MJUCwJm#jxM|Mu zMTb5eHS73%2{kPt=cki7-`oevsrNu1+ zUXE34T)1LAOQ&-4H5*nfzBu{q*RdOKykqZ0Z@u~9OK-gOCbrFfpr;47S`1D&j^;?O4>%?zaZ~}EbkVgXh|G^$W2!bpu z)I{P20*AO1=*zP$`OQDRrf=dW$Tj?ffC#g|U5C3LPu;!s2~I-ODBoQz`*3@W4>w_b z2veu%R|5S?y)P`3*EY^6VPTKQM9JtfBmDNzm~nu)r5rSV|JH>p0^i6{fW%p7Tl6aa=Buyx#A%=OX;S>^D>2DC6Y*OUao&liac}o? z@`v(xcHc=#-AHMKX0kL%vk>t1E~!(4RM1Yaipd;CB9@39;vRU59^!-&p#$M1Nq8ts z%qwU|;Nh((_c5i6EB2=)XM8oIC#0a)${!{c5c3HCo8*w>5dJs9?nJF{LO7AA<=;_;#&-iIdNe!f1B!isI8t25&F2>pg)`1`MtIRk51aABi=$b= zDV5;|w}9!e0r2v7szgX;Sk_tUZf0*s!#RN$jCg*&<@(Z6Y%{zb5=%yv~#q-NQ_j}U3e_#1|&XjmXQ~b;2 zUwG4e-p`kPncH4cI63?7rq1P~N39?>u3vHY3Li>Kp@|eI`c*i=dlV4GM2d&VfnysJ z-KSxYa?z_=2p{%n68G{CPvMXLKKZxg-_YvQX!X;cRxbkCAGJzK18Y@P6wcFXN+A$F z?a>5q{x|;~ye?_KPco(rppB(=c+Zfr%uQ3Ojka1_hkuPULP29Vtm=DM2zm(8Sqi%9CG6oKNCnD4 zP9U|v_L}`z;$L7QcG3~U zU}2$uzvebG{GpSZq|_LxI|tB{>uN zFPGrE`M-;Ptbw1XVZ=lr)iCPj)3N)%PyU_15^8zQ>ua=JG4jg zvrMcjNnG>k>=+TKEu!5(TUu`1Po80svu{9weIOOVlqqP>IzJ)7&pJSR53x}1A*SA$ z-CawS|BP45{VV@9a-+Eu%ATI;f4X{YLr?v`w0ciNPYuBQ`kV%;1E^L3^!8K%+%RNa zljh$l0S<|e8QndZpJxKJs_-VuMk!#e?do>)UM2PBnGi@_*YB#6-Fd02Y^J`ekZdP> z#CoQ-wYxhTSF!UN)DNw;4qK-UCydREkFfI^^LOt3hjcfb=l;yXV)s`qtXLhn%>Z<%6JM27H4dD)-r!AHz!|pghofmF-s$b!dnjAxSJol=TAaPT3 zc`{QeH0Qgj=Z-uV>S>EfWiWIg4y7`5$p1G(2d?G$D%mdng=H#$0ey-*QG+U!nvL>m zu#5|0D-x|cWuz>0K<2e_9b6|`S6z0bo-l_3+mERvg})ql;_(|#eT?h`uAAdG(OL1O z9FPh$?eHO?eqN0Lu5V$OMM^WL>D#Qf5Xyvl+H7WOJ}sX!<#T3oei8_m+(P64rDN-- zy+Kle@v;?F)1$?M@+vB%OLe<1D4P*0Zi8l65IqxH$v zVv=chE)Wj2+n_b4v^v}jBY`q3CKG<)uW~gMK^~OG7D>SDn7JY)Fkd&{v{tv)B$?IR zUs0nm92U(nPMZqadFZnB>o3DS zgox}DzGGM~%Ok>2o^u18r`m6DKM3Ew@dh*nbP|;*^x|jQ2jRi+a-~YhI<8gUq3@(b zIQKZw1P5CQ9+3DN=A3gxnbkD|HGx=F!1wVJWsh_r!_H~B!H*2%h9LosEKJa~ zG7B>eKWKZQbTWT{#tS0@h%{1iF#Y6H#QMRI8AL&~^`r}v$t&(YLf=%79Ru27TnCb~ z#LdXkVdJt$K`AcPA>G{NvSC~t9xe;)^^QwjiWJc)?;(o==4o@q=-3r^n>9CTH0@Kf z$Juqs4HnZaSLQbO)9Zt6W7A1%;>)np-tXCI3=&fD-{s6yHDf@hNOJjAbjo344vh9T0foq%Bz8X0Qpy>WrfeQw(veI2Q;cms$|*g3d^81NZ~U4T9JMxuWqH5fs!`Qo;kG?jR)Ja;EK9 zzdFIsNZt-kOYgqhuND1?^?ds6$xoA7_5MN?tX(5=p0b(%F<2x66Tll#%>{HzNKO$! z&0yZkL3af|D=_2V(v8c&yo^`^ZVLaO3NWKw3-!L$Le(S3?z`{UBjU!Z5?-NW*MS|P zFR}6($WF!$%p`0T2JeM`Z%j&dR+RdCJ#dVdgk|x7qX8D<2jJ+ZET!rSgsD;)IyiTb zkwqDm15+)PrCKr@1WnIXbcTJ*TEb7%(lu&JW>lMI*I@PbWr}MN^YB=y;xrFO1h}VU zBPWIU7U*|OpJ5|r{#S~3V}|iX+2i-$FMKZsf0!o*v8)N@C?CTUMMfLYuH^$fBRva* zg?1@DsH+Ffo4>*&hSWk8fq>SDS`~TLJSDFqPt>Oe0T9$Kg}0C9m`X7MhC*ysJ{YrQ^C{nu~Y?%XP>1S7Q*D78WzX+Mak=romIy|ICZ5kvtKET zWb>7Y6KXY!6hjg0nh;PomnqZI%u#b0UlFc}R}~K9o6SSRqvAsgr^d$@u8`LR*F1$ary!mq#sr@ zLy>!HSWtz=jcr5}Q7p=m!sIZXqZA+qnH(}%*Ljr@nbZdPma26_U<9$ifw5_{99%(yE=JeVn^NG*M+Nth4BaWJ$!!wo%hUd!PcYd zx7gKvDzzR#5YxMAnJwR@S8uY3Qgw_w~dc+6sK*V;s(ohp>3Qe$Yu_66E z9bE7{7El-kbxm5->S6mPkozw5G89g$I zh-&hG3kxa_Uo>(_RxmB`a z=iU?Q5dkGHEm+gI@vI)1%$I<}1`b(niGJCv@}fD!+8plBrYfEisRHVA8|Cp1r#I#=(eRSKZXG zJbJ+exVt_aayPfV;4hnYr&#+#;xmgweXU8Gx+ zWymA96kHzF1#x$q)x%poAxNZIo>x8H9*rjw3AA}MIaH>wO=I=P@;#aMSf(exK+?06 zGnU6h&OJchpekV`lr4nOK~W=l+(#wfpNpWkEG$J~df-oLMw)nR@PL|Yrp&x^lqLGi zjCotvbzZrpbIII=W1cCnjJj{?1HWB9W!9x)>FAZ2S&?AI(d@brqbJnw8Z&g@h=%Nk z(z7mKJY|(%XJ0`w2Y@5(?N|p@Lfu{t@t9u=C@8^u5oM4P&!A$7Td?Q^cuhMj0w99M zX~resg^_01EAfwOq%3-BVbFP6Xb>+$3%N=f(#U})1j#s2jEn!)3N=IR>_j^o;JL^Z z1|H;qefqU<6?&o-8pLC0r%K5cg(|dDWveQw>Z%g?Qm(Y3bad&g(j}#vO2L}M=^Kgf zUhNvbvv%=xAxot-kcD<`vQloc>ce5)HzR#5#Ud^3kUreN?R#8!JPs%Jw)I98ZN)IQBx5ZFaQxTAwsnlh|Hs1CG*f%H@Zvn z-CSvPKre;4HBHv0&L$zec%;hc#)>Jc_yDGYs}VXn$4OD_t-bIcC_Mg)k_-h48$|Kw z4UG1nZ2-|8xN_Y8O_7GIYP+W(?dAs=oyXY8z9>>`{_nC4;Ld|>McjF#xWmfDx-bG- zK&h6c3Er~oaJ;S}Tw6CJJi2ab#`MCr>h|GnO-nMC6waxhGki|d=8VnxYpORit{Hp< ze|h%S((R1_ugK*TE1F9G7F0*78Q4%M1%sIlj7g$B#UwF6NiWVx=ZXjTHBx}v-E3_R zH8(cTYVK;51KURb=b7)})QeM6>`(Jc^irw(Jf^*_6~fts6YhNP&*$D-a(m2KEL)-p^QS|c2j+3S$0GIXnB-r zZ04}+mV5#{bvn%8%kq1@HXrYlwBbQENz~;IMil|>a^B;0W=MWNzc&_&HO7Rjbl$Kv zsv;YiBFAw3q!*JxP($_?%OhhcaE!gLA4ernpg8W`Y|MSjPkh-nbm*fU zQ?@lHujQ8Y?)-4Y`w^`8s$wZN`gwTGYI~HU;zFT5wwr+H`q2J z+Xl`8u)SEryu}*ONKm1&Z8f&RwyCzowly}b&9*#@|1+55<|^|T^91vJ^JcTwJTJtU zpuT0n!x`lCVP!B0J4p7=Pu9Zh^Jl0vG5M@Mw}xKO#F=9wsM*j{BvS~^)@W59N8&0f zm`R?FoA)S--TZi}ocUvMdSyC!Jj7B~LKE}0FgZ&uf^@6JiL+l>Y_!X9zrz!c$wM<| z<&0P6dzR+RQ8qa?1-f!N8?7cOU8<3%couo&+S)42iNRJsup~`M6VlT1ZQ0r3#(Zhm zAe*6t$V|&IPhb(&q}A#D#W~L6t8jdTni`wL!RgMeBU3n1RviEqusSKgJJTUTc^PzY ztB835mG&Yk&%&PnRJj0qzj@;O!!P9`)fXiF9|Z&gJ=2)1=q)9X@*34S|8q@&gdx3* zd6kgM+*e!xYaP~}{1B5rc}$Xh&>KsL1a+#E!Y+es(d02qf*07ERTSrXN*RTt8c+)G zZWEMZpmt!eB3zd}M%xk`79O2F)v(ym=|bT((SvGj`iwzfDAQ#Q*29q`czhbaAcb-p zbA?d4WZH^~i~vuoHF0Sb_Q)4#@#GgJlmwDL8N?qMalN_k-g7APHF-aF?Mo(pk~d%D zN^1Fk{p;`lfj=KJlJcJj9`0||Aa2VEbjY-Oo3Z;RP2evCM&P$17Iez~CwH}~nNY$g78TXo?6;Wu z6hf3+?Auz>%WRbxHHqK%72{0yE(zv6%q|b~q>CO&Z7S@h#TWrq1SkL|bCWXB9P|gl zkZO(xE2LV~1*j_?bygQKYsZ>T*KtKh(=l-WM z0;6smCy1&fVRNC8et=u$yhQ~? z?H40Ems#qAgevK?`msil*JK^rM?`nw<#r{XO0}dzx#FJF9`=qfj$PG~x@tGBicsAx zs}z!oC9166B^`hVl!F6M{Tx|Kc#7-}*n>zG;)W|8tqF|xphZQ;kwvBIMm~cL1cCg4Jdbt?-4z_} z;&`_6;af+=OS&uew1yI%>1O0x7wBg24}(Dle9%lOvaO5&0QNC)?~CJc zr!&O(u5L-(AvHqmG#X)*64XvCMj8}miw#gysKumkG)g41tF;`N!;Y;rQbp*%Qb$C0 zNR8;G&px98f#lSf8qJ*4XbKfSLRSb`ftX%fxB|qiFziy~^i(qdE1B_h(gIFV)T3D* zH1+%!fBT!pb^2RgPgXEV&;jh3ma5M*6(MBGr*%;kULofRk zjj?}QAHcIu(My5L3iq`Icfwh1ebC*JYRls^^zNo}z4^O1S8Be>%lx69ex2*h-<9&g zS5<1t*fnIfBWsRo;1Roakx84OEzs6$B_NdAb!OgI3C3uT&S-STWNRQqbS2z0nRof7 zwE#K9Dhr%SX$s}!>cXlgc^*N6DSS)kgtS?*`KBh`s2D{f4xo%x<43c;hc$oQtKH(cxo6_Y5Tn6Afj$^ zSNuD3xGBRXNc{!}oA^9T;-Xe`6!?0F>MgcCb{=MN>@WP0zGAD7d6&2ky1z<2Wus0d zVWqXMIX6EfyyEDEm4*buaYLP4TV5BB3u zk90+ZjErE=1_JenN}$fQXuE)@xf~U6FjH)*z6bjiIbhY(8)*obKfEX3;(sDjhs1OK z1flvx5QFC@RoAp7?flmm|Jm_=5BI0JtvE3};6Fl{9wpKm*%FZ>0AlnIXjXFe8S1`e zEOLy0ax&Z9aH^`9jX;VSj_vNV+xK$sGx^1^9oZ?U0K1M_yO`~7wYB%5$+4>fDdIYI z)wyx8T?iSd=meuV74yQF&W?+bL|K^H-~F#*Z86`}KqgV%WQEAw8ik9Xcdz6 zT?h}7tcZK0Vr*(1*KuvqKHHrNXIS zBJUz)f&HAKz#?qzpHmq4-o!3p_0O&j%xjKf?2Eepp~;s2l42W~tW;(D3d-C$ns}Ms zt=D-(T`ZVWHlJH;m4a@r%x&egaW@k9v@WYoS2KY4K|s<`1Gg_m^ekcbo+W-1L?Liy zvn(c+AAo#AkTvcsaH1hqhr;D7PYon$NaIlhLwRbtX`k_g|IuVmX_@ftj^Azjep1Vn ziOu8wdF}76`MzcJq*s)>+TDw1-cUAZaANo3-Ls3jf^sPE*Sa@!&ymYTKx{ zcV7Nd^SGJPjW>#O#x}jyHTPi6fC}NvHUWN(`;+&I8~Lj0>cg`;AFVB{hm2vhz@SG{ zL}?Ov`(xpA>wy_;@kx?D+8SK>oZ@?s5oeefV7iD4WcA`Y|r-NgN+Y_@gr_qX-WVL3YLJa&ybgB@^ z2?$|oYM`u`z z7)!i5^v^S{Tle(x<%6#p+&JOzP5TcDdC7kyUjRFYX4mhOk!|+I?IVxgVko+4{N<?Yfx$*#t3Kv$cxTRrUEDBcaocRM5d^*Ccl*GdM zd0jI*_T*}Pmrb9tHZu~AWV~8F0NnXQ3I-0fTEv5TyM0t$;T*5{ln&e`|KNlYyDqVFAne@CO>Q#`t%(){PK>wZrXPUdAEN< zIE2avIr%z|{?4YSyWTqa;mdz`_bnU(z~*pxzEpc*fRY7xc%w#EIB@X8v=IFAt>O{n zfKv3Dg?&2ps&JybYz81@3S3W-WAUrx(qqY`;wka*Gb6>v@5GEOOwN-(mo{QPvXRm3 zsdTTg3K1JtM5MtlS>;UeLO}K(@BW8^ih6+DYxJ^dt!raYu+uE%0K99rXL=>SESrMq zwdn1Yxz^lJZcA=Qu4K*SUC3vKLxho$vp5mre3&*U9gSbcQJjlj)V+fwD`2zP@^Ms` zyEOtfiy$g6~9|EZrG@2w;kXeJM43l-{19{ziPx!1wQ#^^7P|>I`fd>Q|MTz1UdzkK<~$y zpha}gDzw6K(zMEE2>=QiF;^`1ay(ttU;_CD92q#4Sb0z?5&FXVrJobZBDaU*X|S$E{t)U&t{BKp5sxyq1s#=2Y)GZPS93Fco-8I zhf#XoYF}>g#Jv``XN)pv*Pwgu8GYOM8x9IF!99F&c6Qc~vcxd)o`ns=p123=;koWl zbT42$W1-i4q(qE9X-pbl$=mj{wWEyWLZ&OcnL z6C0^^U1Q^rhPswYzOt?|h;>wv4Ma%6q>9!kAC1N*RuqU@T0Hf*QmeI~uu(W(+cK&} zsICb4;gKoVXomX-1RDJKYlr)KmUR~2;>?^?-bE~blVZ+J43k(&A&`+LLUTY7avZ>z z2UQ$_*0q?D#*5?Z31a1u*uv7oN5~Bt?;k(wF>2HVTa{@eCB_*09TVLuvV-9|yyb`l zMAYPyK0ZKv8viKqz6cK?g&fko@X?cte>G6=9Z^_0d)R^vLng%LRR7O%tKUd8 z2gbx1+%J@TLXVXBe3&yea{l#B=>c-yxfK}CtfCBsesvgA)#JoQmNme0Z;A_q?U)gf z%YzF5y?33Ro`9-JJvnd8=@gH%Jhkn~lb1h@g4fpypezxn70Jh1(Q-dGdymk$$b3L$ z>jxVFnA5P^VBl~*A_IVNR8rocdP8KaIR$o8;o`nqv)fCQ`JmGRKS0(aWtP66m_`If zh9(B4gcb&65JG;ac(7r4+PUN1vcj&A4K*1*Z;Vn|+*KC>|G%Dp4UCtsIwS2QQ8eHXss5uy55Z3k(%mp|o9RP;7$mAov-I$_(N$BSIvwCvucY)f=QBgjL_( zV;Te=g#IT%zJoHE#!_*B`$a!lR$(yqp5d_SJ8{9N@mxpG;%_2Did9AY_dXKa%>w}wCbZlOK zXCU1_bW!!nn(0egewb5iHXG8c)-(+m(GGsW|Nf)>mn@r}{D1zMoINET8FgDhd{x(9 z<_Ph-w^Rp9>?X6x;$9AuS>RxpN`u8C?EK78+}anhd|wpR7f{n84-y~}mBE7^IJ}Nd z9HCB-N$nMnoM~+qee@#EJM$^{a#ZU!auFbRj@!fCijI;0mwm%nh{KF?6TdO{ATK=jC@&j<71B|lRU(sz;*J6g$3fRXw}QjSBx54R&KS%=Jits?>~;>4ZSLE{KD}fLM<<% zx(j~R+kmkW=?c7|SH+26xL*WqFs=yeO8|YB^y>VQP*6umKVg)to()bln4Mn6WQ0|L zV~t<3@an4w7c%-$ulE31@&; z(`1B?D&AW$1T~6!YMQ(arH8IyHBHzR=VAd)a(BbuVSq2AIBs>Tt(=PlIQcEd*^$ky zTWox4Tkmo$skYAT5!H54+jH(L6&ir z`I`C0xt2ASH5t|dI-`a_Whg7FYA-V9i`yHWmHNzfQ@niCR7$=&J z<0F$Rh4!g$%;d#>j^rioxOG;?Y(DE5em;Nmz@{&JWBs9M@_6#AyJaPmf<2A}ICr;JGW|D!2p6itMtAGNC*SE#+ra?=G%rtY|?6!;13q;CU)}mzJ`} zR$k8e&MN9%6{>2iVnw|x$&_PG5CQ@+FBntG=-7t7P5ySWn~E6$T>QT;@(t%QFAe$G zg}y6}{*$-=>}ucgZkF?cJ%HUN*Rh=&XDj<8w=T7^Z6?HR*gk>!C(UBlj80uc(bTZw z0X@pQ;`*T9#cPIlb4&Ec_+CWwVHg2Oy zuH?Knui!P>yvPkPftG}GSly!N64s+Av69xBhD;Ts#UpB5+ND&OnEVXnPNi4!Gh!*P zB(oHn0o*Py_j(8#nOVX>N}!39KR$FKk#^$Hp%cR2P8>?^O{7T^PTwQElgN`M?EDA* zBoDpx5(b7|QY_B}Oi!y)A_hExwE@xVvW&Ca9X^ynPnR5^an!l3q>G0`;f}E2a(hLi z-)=Quj^uE}61)V7hYr`Ho0G}RvMbI}%@JTB!wJ|>W;>l#*7@MUV^oIq+%oCXw}4A+ zOMb~pr3;Apb7HynIh>_A+_ERQ^zPS`MtCPJCHQ7C0g#*#LO3WbBQ3KHSO%#~BN{|d z3gzTmO(7tVeEDgnyk!QRajL-u7TBq2;xbAP`4+Xb@%2|I3Xl*K4#B^aQArYmaF$V? zfxUrYu&21km}~$ZS+xU$pdzTMMjh9OpLs@Te&(5(+KU!cwC5Jqhf4~D%f@K_iuvRM|voUa=r0A{HlI=cwSBMYol{mkh6(Was3RRQ{e?rCYC{zvQ z3ko%9kMOhLiIh9lFdm6EjXR4ek15NS!oe?G(3)Qx3>SK{BApAz7MF!11)l7PG=u-( z;!FJDtjuA9F1{o^l$FW6X|vGZZj?ne0zE%Qaa+pF=Dt}kwl)G%0utV*kS_;kn$_NK z>Mi0j+gVc&gT8O(DfHcMz9D$~W;T}S5N=IPqQ-Cod2-N;s+6@=rUZm`9{EOc7uSiR zM*@|3+KbvxLT?-&)ALspnIfM(CI>qR%~OnBs7jm=YLBDHQ}qqtZzF4V3tIj_`#Q## z2Ii>@lf&UT+<#%EMNJCSGLtjvP={%)c9_!p4bvem^PfFT^|ACWv0|j@oi$c9OR=13 z@|As~He7wv{_AfUHl}H0&-$r5^y;gJwyj#e1O0?uC6s6uBJa(Pn6Dd<9YZT<^<=9# z$|(eA&_;dRayY~(n zHfU%b&<+c4cA&_-CM~L(%z_`DuthCOVFp9-rnaDohlTHKPdEE~wpcJ| z6f*nGk+=+aqMkj-+&6Sc!)=*jnZWi#9D!0T*zW6(B2q0KsJZ-_FVubav_?O8a%7NG zpFJ*f`wjZkWbMm{=8kK18gDTeZ{3+mGJ1D%mN1;(*~!<)y+Kri_5x=^N>k6{zyO)aPKW#1 z>8+H8LeHmKO@kKn&mGFYw7`%tt8nCGkH`EOf9F9pF*^O(=WM2<#*F;@{FURx%Wio% zl_!l6z6||vEz`G)FhURa9`sVu$o-VL&ramu!W;E~#s&R%ESrP(x;H}uCVQd8ulRva zHOU61yJ9W=7N^<+Sd~WZB4u2@@HYP^B!E(FFg_rJ%!Pchxx}a##~T+38>wuR2r>~P zr_(?sO>O&#(8l3>nkoo#kKq!hp z{2O9JIG)s>5I#+Qd={obL!hD~j^aK5L$Cool9N;T&!8FjsDht?dStaa+l^)c$TOYA zg6t#@-{t51djXO{9{J^ISTmr-#PN@QfY79;KXmBWk^TH{z2Td7A6v@H_ixRam#_DY z&7axEYg|8aj_T~&?lqZiHF{enxyxzwV{YUDY;6!K32OsZlvApspdsnE}a#^;laF)nrdClsz>YP)%@K)ZeggEVN?Mm%0ft1l2k?~ih z9j+a(ov&T1J;EQ+8kom=92`39G`tn;c_H9Hb3yl8p&8B()lB5uQ4MI=k8dOK0 zprI06^z^in4q6B=>3EyX0DP9#X;|;G`V=3Eq)JP@^boO{)QBP`t-PEH$fr=b{Hm2m zkBiS(2}~Smc?59lkHL|)B={%za^bQkUU@igT#nIG8|=DfhurYvOK?-&Ds}wmJN^fq z=8%lliyBbsyl}b5KZ}cQQ<}$(+I2lXShEgE677C-ARzd?SLKKDJMsm%W6hW6v9%8U zu15+m_bViT1Tt;%byHFnt#G%h)ScDAs_H87c84!*=IAkp#vD7ww7Zuc&KsL+^wj&C zXL~(46NS-Zw&%v8&2?v>ci=R4XxJw&-hx+3IR5@0?Ow2~tf&b_K0N zKnl?p!j54bB6EQ(@a(W`#ub?5bY!xug51gDCMgx1*#@a0MriAp^j4?SJH%j>GxWOL zT%kSFoRbrZfqcp7g!8mBiY!yV{_^}D(E@A^+eLH^7K5gmG3nz}nZYzo3R3jWd5T z;?YiWqu7k`{g4Ml@&r5 zyd|eANAS>T8ie!P2Hj zEo2(ql6-Gl`{cRt{G4=#)MQ8}#+f;Q?Tfd#*{SCfC*~BkUe(H*ROM4eIlTo}CY`C- z1es!2sIAcMQd@bOo11DS9675Mw(8YZ-o^u^R?_@uwZgO$wUu{zTVXh^etjWyrrOFo zsa6<#i|w63ppQ88fDxh&C??h3A}vcg%RRN>j0mQ*s~inM=<{gL=0VuzGyC4WyBkI7wU;~Sb3oSN6=IFpH>tCTqWeoitNM{83rC|+{Q^KhFDwr~$ z!e&zvsx`YfTgc4JCYM#VGitRKTU)GGS`S!dE10PfPf+w*^n3KO-kQg2z&Mc&I0x_` zwcKECDz}(h1B?W>9Ml@TL5_owZj3xZo-c2Ljyx}yrCoiWO1olo2*q*e#X!c?7nc*0 zBYY~%@v1^gp%{aLoH)qwLMk!`rvyvAVlX)t$=H4fSk%RU%w`?<(lXDA4+R} zxq9`jH6^hTH8mrf1`HT^5&VS-A@I*@ucZ>K@}$swW!auR%ar+{XRAh!s;(K`d}e|0 zVxm$$ll%(2<9JFe+f7>zje?K1qHd$uHjb>7lse#toVez0?xINk-KV~bzEp%<2Vu7$Xi>X zb5V*7%ibcQDK?1=LxcIHECe-HivM9}5LRtpRu@5PQt&&-Ff|lZeO4+Lgd3&Ot7Mv~ zS%-=)_D|&^+)YpXh+w}`;MZp-brnUU%NlDJ#Y$V9hO9-U894`5ufC-y>!R(y?oH_$ z(({zA#q&8Qt!vOkug~r>ngiP7dY3NPUPOq`@904R-N=hE#zqC|HRhh4Vmu_gDn1!UmHrx2CRL#H5SJO&Fh^(qvc_I@aD* z>NN~E3>!l%K`^1CR^)2ytg1ePyR$kFti@mAYUDqB#OzS9q(~_exT0cHkLJTq6&0Zg zQeEU+prM^8$YLQ_a#X5Y@;yO`6PmvMT2%PJd1U1fesxM#+I~`24&k0j$;!TK&Xu{l zdS$NF_ER!<7k50>PTw`G9TovnW%q8Lybj?91pt(uQXE*@RwoF=oU=eE>trp@sGPx0 zdnVCYTu7DZyIJeO)F*AdpTvjH`rNrvehAB`fS-f8Vf6SMXCMj~_&Lra?}q?j?^bMl z*jQrhFm{3p*GOD2sul`X<5OTYcH&u-J;90HryfIVjD7g=?)#5Y%Y<{#8^G_(b+KoJ zZH9fusks~vl?9~B?X)@Oy~ah%Sk3!@H`E|j4kM;_}&Q;SJ5xm>;`6K@lQ ze5!2?SgMefCuD@evPl=26<87I3Wx!N#;MT-oaZTX4N(li05Bj-Ox44hV%%UrJhj+8 zSguGEkbj=O)Ea$#1g_+=cSj&F+^p>N1#>Jn-*Lz7{VQKDODB32PF`4z`4lAQS503{cnt1{qQ4ObJcQEIl- zbl1o{7(b?g>jT9l{*CK{k$uqK*uh$U7cDp5!2tzyzLv|V;FXRfR z6>cj0k1g*<%PUY%ph{2+AGG_0ynZ z$vA$I7)W!V{!{5mNH+t+9{(eEIrdY*6L4aFs(7&}MM92ogdAf98aXqZ`Z(xlTN3CN zfCp5mIAKx)Ty>&cdQ~Y{ z>bCG(pnAfoW|DmD6I6+zQE_Wz!vlI0g60HmTm$J-tue#_(E|4~tWcwdD>SC~hhSl* zJoMy*O{E#&24r7SjRql6x}}s)96z48>-cf?hRK~&0T*il+M7I+S#a6(r1+Z?-*rPr z1rEW&_-I-zsWpjf@kibRE36vG`hTXb{t8!z6&G(6t#+GT!4nOEH4AVDo(J#`{~%dt z&Eb!xhSRUonY*b40HInVB155{73j%d_$cfDu@ zXHPL8xFH0SxR9hsov8J9Ztpd3(cSr9bC31=d<@2*pP(x!tiW7PPlK&U63@+$h9adO^0fXN9017T@wWtg)^TwEg;(i8$BypA)1FF06 z-fBfUX&1_U>`kS7BDOXo!Q8_*!-xuwURc1ZZ}G5LV7~=K(LZUx_yPQpd+teE@3}{s zAr#ZH?OjF8w!td$z%pQ7S*#TBLEwE39z6W-c)tyED8d|{tkgH1Qy#_*1Y8d4;tYc$ zX|ym@oFYu@Ef-_?+2vxQJRd=^m~H1P7K0Ut$i8$l>r+1KBg?vC?~!$-{=Zzu-s5@T zJ!1@F?-5(Z`60+V}y*M`@qB?S%dyjkQ853 zv|2iT`lvLf;!L{uNd^6ctj>h)$e9a|pm(Xzh5hqlXE+w0Iahqg`J7s?2r z;ZVhMym6j>o?(IUYW>xQ>x_5kZ#Ud!G;4Gkqetg4=8Ji{3PXi)h+&9via5b|fpHzm ze(sm{>mHXL*L@*

vbuJ(?4(CT){msbmkm>S)GFa--Gf7XK85dD1Sb@+pWqBe+Gu z)-H*+>W}DOL%fMTl$92&w!y&e(67SpKGbXUK=>N|oy7VnvU&ECU{g7-uJU-sSvUaqRj_pej)e5y{(r{?M2s=D)>+$8r_ zax(`q0U-<_VTuev0vH4ogCjUFI$)y+qB7VhAgByck%l1c)M|^*32ob{ReW|g&wePk zc;B^8-6SBi{oa50eEy%$|D1d4-gD}lwfA28x7S{K?X}m2hZcI4D4yPr1YZeHtG3hH z?RuhKuxH)-((I?R$hX#aK4r>Od=78K;jP50(Rifl$)-K=$Q9mOz4v+@rTXUj-Z~0w zy{6Y^>+~ryYTfa;7;@D*IiPMC1ei9HA*y9$!?iRZoB!#? zCvN)LhK*YeE*YP=>9i+K{qV%1$%Bq-ZvNifs@49^7qu0EwJTOV{bBpu{O#vm(?2le zas2#+oOfVwU-3lk$Y;FnwQqRWvOI(TNZ~s+Frsx z!}j5L+un{l|H_V%-F1)GHE(S?ySq!OCjAQgOQNe#dH>}tCQI{=E`5Ffz{L^ndQCsww z7S~Sc+}8QsW1X*@_(tj&8Th{)Rb1gjjr=TY+h5%|gml!3LKzin$WHdTyqQt4njI;7 z8H0&_pjN7>Cy-t81Rh8-kJnzldHLSu4%_k>BeO7Smxz}t#(;LvdNyy_@`w!3wO!xJ z{r9R-_;>jat)_Dg7?#zt9%nb&1Zm#);?JBXt29M5OllutURg(5W07&C=t+?=G*H^1 zq<6KN?VPf`zP%|~LizOQ>$7t`b5q-TE^WF8OCfzqdD^+;yJVs?v$L#~%Z|R8pf=HF z1y(R&5Sggd$Qnk;n_<-$8_H{=9@Bdj+bu~z5eY$yI2CJ-Wxr8Y9cc^Fv{^PH{={lS z=LNSMiRZ{7!i7+x6XIp0yt-xD4ePYE@3Zsj&cAVSKdK%0`_BLDZ*^*}Z~wH@H`w>CllFh19yOo5 z@r3h>rR1&oLglpM_P%bSzklD%3%B(YZ>IeyyDC-;ukGyix_yLAWphE3`yRJR7vgdR z{dANq!nd7ZJKMGoVefJ!Ty@tf*EZK~*F~5K(aBBMbm>4*ExWYnLwCwr2N+#z~E1@0ty7 zdi|=!NB#O2@6l|p-~07bd>^@T<2NqZ*ZK9em9>qY4e6w7oKmW`z^cWhS^=0yCN4@i z7G`d>V91O4@cMKyy(?|a*`^lI22U5JYq2!F(sZqJ=gXJK&GpE|?LOMOGpOBUzHe%tunBtR$ z=g>HwT5ug6?aye=eS@{>Z-4NE-=3}y4D>Ai&W$&IXK{V-3(k>JbI+ztdzzIYr~ShD zYX)nLy_+`eZPW%GwlB^fd*wUddF8S5U;MP@Ip!_zde>Wy?fmK3YoDH6y?F8JsqbF< z(QE!?X4MQo|04Y+^Aoby0R1YmhZ_1a{p_92o!B+L(_Vrv4N;+b(yP+j(!0~1oPAou z&%}PCcDw!d^pkck%Hu(dNsOLMUhsLf=UMxK;|ZgS2VeYsyB$eS;faz?Ia2!>JEy%U z$6An(k+VS;j%}KmYNaMqSEN3kawSsr)az64OZ`jg`IOt2N)u&U*VTf8m=BjAo2i>c zbJlbyx0Q_AM;<0)KV}og3uK9$OmoOuI2lV@-Yb0VuUxFw_{YD9JJLCfop?JU* zS>f{8y#AnOkD0a`QpMD+)O9IG$Y=VmN-AFSA^t&Psu!;g5%ajRDT913(rwI=-h`KT zp84?lD>rVqJ5;Ym6r*3zLnNjWtWR4QyL_cIwiUo=K0*j*bRvTnG^Qdp`sYlT0s0wG{yM0>TmFJ+)(IrvL2)hbJ8^rRIJoV;Or@ICK& zXm-V_(c%7g9(DN#jz96|jzy@g=M4``E*~4Xc=IPa-(PVyh~Chi{hJeWD_>JCR(2hC zCt~%Il~Wrh7caeW`^L9TjI4?Ua-TTsm@B8{uDea!ybefq;exlJ^KWEAA3o{wd zP`$cSIILm_;XRa;7(!jG;C*Vnc)_-!-9Me0D6CJ-6}A!n!;zTLojKL-8?1a@&(qi8 zAbozxo{=uU*mayyVVBf6N;FKNP_uUcB2mV%m!4|N2Ti1X)51l z>nl;)EvvRI*J0Kcfna5CW7_;LYgd*>9Jv~Ap&$IyDKqzNJ9_EB$@>m&Sa*yzr8$nh zbuM_y%`0#J!s5}Bur3TM zy1Ems?d0cv_LDyzh?k1zp1kd%LOyXoyYUA<{N6KcwYPuAC4+r^{r&xKIMM!QQXi&; zT}A2v+imT2EqmR&*go#Xdf~m)?upRQ1AdR`b|J!e0ttq^as|A8alm!D-0PjI+~+vY zbYI4X9$shAjX$AF6d>BxPumecGY)L`64Knx68}4g`W}}au!ah$+bx?(l(iOz(x;Z@&doB7+msvYQT`mVM~JLapM zO`S9B=gz+$+r@v(-)Y}IAEkWvvikp@DPPrgZhKAE-ILyzwx^dZ_N`+w+#2e+yupe& z%W7YHHEIsC0g>Gr^_h|Dkf8R>^<8zlHR99sdXZtDgE7#N<32V0OM#9U`TbW0ey)Gs zwQ1{DE_&pmCy&{@^T3AjMemq}QDlW|wkzl=3aJUD_VNHDq*UapmZ7#p6cLa-OB1ZJs@K zxq10iu(71!bGVbPa%gzQ?VoN3!&7OIe>MJ@%-XWqm~Fc0^Eb_O?7GISjYk`fMq_rS zh+sc|c0rA`M!9O3t1KsYnn;Nc4RfX4<76R!EG=ds#)0Ju#Qclu54xT~qT}0piQR0t zqeEj;H(#{twz|9beY-FC7%J+8*Zus;TOtK>$C|M<@nqipurHTLFP>g~N;Z?c`JF$% z?n3Q9c71un<`YhzTY1`!qmKI0sh`vmcSQGezV`VQ7tBtr%S4TS-M)7I!DxRpmRz%V z>ayjZ)5gz>-qHCj63~U%8(`)xX?wl05UBPiGkKaCKiRHf{FESG!f<|bepeogc0R2} zQP?mSP(H;{t+6C>ujGx7RxisXJXxv zq_XoJv0m;K_wXXNE*22C2YW`{c51tRX6@pdJvm-1x(nt+*5R9Qr-wYV9tn-bX6mUE z@P|*RelL!j1d-SAcOb@?md_VcaWid2^kk$WjnoQhga#$f-qQ6Qs%I-&y){UqrmM=B zB|RSdi-?O6E;Q?ozvo@A-Eq<-LyN{oCPv@?*3NGacD6dt>IAi8KKP~Mj#{UEZO?tv ztA_83R2t`R+;CB~5}U7JWMdcM+KMHgrc6@$cX^(rPJSyKBL3clb}F?J(T2Tk?=J6N z{9U|kfW6>lR|E<>BV;7Zu zvL-6~g5HR_4nq#l&bbz!{=4h%>iquHQ_nv8w4Iu9*L(iBGqUmdtIzxN+SS{2$M_46 zZC|tg@xAZ*kIj+gy%T4he!5or{5@K6_i1~^dwUlqxs~_-#3F(RzWq1Ka)t*!eZBGGy!SSc6-Kt~zJE1N=nuYl_x-T3Ad(}G z7l~(lYATtYm_S(cPt?5Q$tiCLO_lR=hDnKWXP#?%7vJl`3en7Zz* z#GujNe(mI<@oyft=GaF+IRE?kKRDQ$sGiTS8yY@yWnyVC>@VoGc5=;J*cEU(uGhAH z@KFKfQyVU<)+6&@HS4`+AGhb;sTGS{R-M>TsgwMJX54VhMx4+7HOWkuIl5vFez96U z6F2t|QcRgo6lY}4TEctK#V_cbugVyuWS0@FR@?JD@G2}g^WZ`IhYr4aN$t3r? zph!(sEG1c7^OP#jy17wXcx`HXYFGF)eQ$WL?(u5oL?EDf9K>d0aYdX({yW|K8T&fi z9$VZY-tx2>Swg9#i86g#Vnm$uWR;ef7P{-?kQvJVY{A*ueoAz2yYe(`zdFFn^{&}rB{)!yE8EYenIj>8t3G1KuB!|qfvW3tbwEtyK2n#<-1 z1oR0{Ru@y)K4MXSQZHx)Hl1yl*^0TByVqA`ZY&NsX%eYaW-3P zb4zQr`8QRPk8WUB2{a|L7E=rg1S{E8;}BsZjY5X3GCq|doSHNl8Md(_G3~(WVHaA! zj5e!@KIE`&FYHG5%lW^r?3f%sb)a!!B&@F{UDQgt&Q8OI@ z+h{xIX4S^H5APG?r=5kIm#}tZFH1S%_C3L_k}B8*#bZiSwIaO=PH|}dc&8?dHaoRP zSeNlbSN%^zoqI&8y=4B)YFDm)5GZ>a&9v*MM>V#?V)4R+*O6sUEWbaN*d{6vn;5la z6C>7>vW1Z}q~{JTgnmiaC8Fd|KV_L5<(3jQu6|2g{M_-27yd~*S&4*iTCM44T1E*J z?dz6Q_!b?NG?ELC5EznwnF~5RU-*ADj-lP=hvOfi{2^P+who^W!!*`M9g%UrBN+3H zGt(4_`Td@7JQ&nsB^Y5##9|(gGv;ETs`kf1l34yCGF?w=l<6mwXf^*dHfiv8jN<#T zJbdK9pMQSflDB-wapBdSPj$lK_g&n%M?0?bw|4Dm`+eF?fBK1Y9~)WS^Chd4hi4#3 zwS=eIo#b-L_GEj|HOVhSUAYuGD#YN7UOXb zlTN>BpU9;*=JWY`i>9N$GCJk)2g73%o_>3LV8ZFMC(5pVB2)NFQ#<{er_T59nsWE| zYg6{#u?e^Bi3$^JiG?YE<`otouKKLu`j5SRvE)p}8$^eI!qm&-eZsZ3Tqs^+HLy_L3bMoBzZ}ZQc z{rI`_&;8QAW&2r-PjlI0mM&d$$7hxW3VQ#w+a5pp`avV_Tl(2gtXaBZLm^*}wJx8r z-!*@{`t3}A`0QoJ=d$Vh_J69dy>|9)ul?rvQ{AWU?LRJa_Z9c0a@mun-+pDUw8bqi z{=)fo^qsbCe>;AxwtdNKw6mA&59|-_9oakjx<$TG(R*a&aG=D;%(DD=spK2qJFE>4 zH`${l651cK6Nz@*=U-O9+LMxx>YnLBVcE1Jm?Grmijk2Zm=S}%OqD*XG=7AW8YU|h z$g=kA&@AKe%cYVPWa`kyMxrMwgNQPmh^Az{hUMRx9G*RC_2yr`{k?x)ub=+>1#9md zpBy2O`Sm9}b^oWnJux_N{!ts>wq*E>RA|HheA655+pux#$1Weg*#7m_n|Gi6$)zh- zP8FV6vZ2)+IDgfqGmC}nUHdPY+?q}&3i0T8=!d1r-u~gMPTYTQR`B(Lx!C&^`!DUk^8C>g!lwS~c8%CJ_6Td+J*;kG z_YDU>L4uSZyC75}8pHWF6$#{M#UDRZ-b7mNC$CS!0LSys7K zi|oo}=q?fTXym zeW_0?YE7-TG_6gQHfU=~Cu?WgUQ@c*_PSE2Si;I4ln<!8M^&tGn}=wA1e9{w6ld;i}}9P__-(A@0dRUwdxGb$fS;&$!;CE{T=sZ^5*s>Z+oLmA&QRe zN`uF;ZJDh0zx#fKp_)}UQLh~nC5Og1WkMH^g6g$Z%@He=44P%vod7Y0_G}ql|C)~M zN*|p6<$tONf9OBeb{G1$mwRR$>1*=;nr{TXkcyY1MnFAk1jLZ#upa=NRazdLHEmb4 zm;M)ZJe%V`)#GxTb6W1z^f-*f{|9QD^FLSAUR^a~zUu#v^s6HMf`ym+c!;LTK9g81 zEloAxl|2_+aj(}F3$d0Q_p*@eN+6o5vUP~2YOQcv*pmRE&5|LP{m+b>etht1c4+<6 zk6qP2{kWrnEoR}JjFgF)u%um^h^~qr8{Ol3UDCxCKoc&PEgdApmo(X|p4CF};5kG{ zkQOT=UTa9VFbJ}GO{`@kcF0(O`Sv#l-~8hc#bY64(rNcu*je`9?azmfyk#*Gp+hn`sM=|$0$2DKvcF-GlQObiZS14Ev&8o12 zR_N5{o+Ta#y15GOdqjy*2R#=*Pf~;O)78z<&%4^c*Fh?>!m&qEVad@r>&?;seevMo zw4#w>bQbT$y%;4$xLRo;vXHl;fsUzo5R_L9S}itv);#~_Zla3QG9*yH9S40RhwC{~ z4kIO3xAUIyY!;(KMh4f6t+U?vrsV!3ht@1-#O}bDK%)J3jSVvSDpvjlXRlYYGfXVl z5?u9H9!Pg~+jqVCC|VrFx>f;))QkK5D&Iw{&&RcsD?_+BWHPeGYCKtSl*zqi4Y*%& zEJl?C6N9>5j+ZW}>o^+OGuTu1upU#{1`scOnB~9_1z8x@yh=e9>;a;EIFEVdq8zdY zTy*f@E7?A<2FM_1{t3#UY#c5u1k^&&wzM5NT#!Sh=o);=dbwS;a!xFM%c>!{UdZ#S znKBl#{8zL43TB#aRt2Z0VhL<2OW@wNUo3%VxG#1)dqf|e!FCl8J7AhxX!zJ^YP?}- z!9KSm$()PBtfk_Pi(X<1tfgydE4Yp_1LEx^LPJ&>{DnQR8~WYt6chO20lMt0c4!{o z+TGLXJayMuXP>^i^WQ&qb?5lVmjAf=4PRTicHIl#aa6-6uUYfJ?sxv-_~?$#+wazN zSO2cg&mX(H^OM({bM9pQhs%#Xe#7RcuK3jJ?^(Qf>gc&2^-k? zwO3pc$EVlrOC-(Qw&V%9OOqGpyutBUAr^P!9OFrLeD#gzSg)ONC(qK-3~TzvRsop(Q`t|rM8P7MP*`QTvie4 z(eXBW**a{-xbCpIG6|p4?eIBw;)!jDJXX-$Rdy&@JhVefBU<1>DV6Rw|7f2K0EtB) zgFh>-{aD!%J#qqf%=yeg;+W0-UVG*PpV+x^%@;4yesi$%?QLiE4UX5F18>=8-#mY> z{k3D?z5l{bOpP|?YxdD(zPNX8^Eu7_0{xX*o#(s+>n%MOC43#8CvSA;@!ng^xFi`Y zPB@n}hZnb<8=CFKXBzvAi^lw8A|Lt~rd8a1%Wd^Y-Ck!`wB>2r)U4ZI8A+#ujjZFk zCB-GPOY9@r;5(+RK1a$XGuW2sle$D*v<7WzqE?L8%G7ENXvN#d`cZxmF||I9JBL`t0q^xxqZbqhbJby8}2{zluzrQx#9R**V-M^ z6N7tZZv3RK>tvhO`R{n<#AKJ6y?um6LL_I#|r-&U9m>HS)+oLDf&2%ac zeST^%Gnf1He``+;`~VF{B9tA&+*SgQoH6D%f_{f9ZaNb2i>t1n%;*m>t!qU4mu?b^ zoF~7`>l#?*@g}oO|L7j}zm#QIw(HpWN-Ol6o~ssiz3Q{LLZ5H35T7}_lU*L%5Z^Vm(tRc;YQ4A!K10$w!{<9KBL;R87968@aG>^5KL zITa^HY)Hqnb4$ck3B%Eh--Qin#u0f+EJ$n-rah%5h71X`V@nJrW)t{)U`%?-GA3Db zLWitL%9bQCl>XA1gq_}b&pj_Zc+aK#F0y~|%FY)$_k^xErt_Spcm7qo`^(z%n&X8z zNAb^{)7~Lt3KfM?`6EzVN;XcfIZDn)Z2zrou-C!B`tbUJoxz<$=LgRp@`@Z6PgaQ| z-ZuHC_Q1fvQ0jrhh9#3XEyL0&VpuXUEY)%;N3*zeL<~zq1D>Wm+M30%WaQ~6Y=i#t z$SM9UBN&!O_WSpac$-aa#9kX1a@(FP67}6MlyM2?qbGAN*Vt$TudYpF=Z{(1&Huh} z>F}TuBa>xb`s=nO_vk-&ZaV4VEuD9LU%O{yLovJl*ojqd{J?-O>o32i{c!v2av&a~%a=NaguO@6c)a2Wx+FV$Pwq>^oy)mLW(TetxOKoWFwmSC zVOD=wLI;Is*)VXhnw=WVO)YWy;`oJH`zl<)ifg;Cje)|4UyXE}st1ukdSYWZWJ|(K zsV3A8yTKjNdv=@KVaL#}EvdUt_F+SkfxlCJp%3B(}wf@0xv{%Ox>AC5pYpL+t z+@a{q%AsY^XmD!$o7qaPQrWj{``h!L%sZ_X>D&!MeYTbD0kPQFLv%Weu(R10;eXP# z61nga$7}J1K#_%-c8AAwUd6_4&n}i=I9(f&D)SLW7U%AV=Rg0+hwXFsI@Y}KkaNci zPdJv)f<+4Mb|;jLNcKwlixj&yq0JbCEDIHBr=BYMhr|T*lr0j8lp+o+K~9w+d`Wg? ztPF`Ahz#MwC03v#+!(}BRM~()>k%1zsTJsOcF%R)ch_C_x&Br=Y5NH$YKJqgJ@~%c zZ-3v}r<{DQwCpcaj=SKS$qLb%S*3i+@J=}cTCp2n zYVNtfGWXzmz5}J<5gs)G#y zhD&zKuCwD=YDC6mFSF|`^vc%U=co#TZFSgX=ga}3(zzX;l9^sosT?yBUGDdLiN7#r zjLrC+9tYU`(lIy9-0|d!nWKw^g8k$6aBTGV|Frd=c6YJ-eR3LPXN=f{Z#uBbvEI4Y zvES(qv==hHjDOx>e}n5%0*AE_xh}sQrAmS>IK^Qcsp52c0zQ}K@%aNpoupJ@JzNUg zU39r23Zyhh zYw2liB0Qa(NUzg2`B#QFC6}|+&2izgwKKz)X%~fK%2C!?E}I1P(QU;0%KF~UyqBl~ zOAif8q-#E6$C{VBuvxBRQ)7<#i$1aSvahXMw*_rnJN1!`fgPVc?vBrY?#?CqhI_U| z0;?yRYu2v)(R;OkHapq-tu-ru`s9;8Dx_l*A!>uLTmMh(+XBbBKKW?LtkOptJil{> zdjr0p5*c`z?Xq@cTSd!j*+LL4zjtD`j6>5O+U>ADm3A%}oYmb-d``K2f#BlWv32R`BM%S0arWTPzI@eHQ|s}Wn7;Z$^B4DS9=YU11u<^_9zVwWdnP=+y`=7M`i;?-pxBu>^dBGj4Tbx%Y zkJ^(I?jAxPd`7jP?sw2Ac1EP<}u7<;i+rcrUH~}(wv^f%~;%m z)R+kYCGLlZCUr5DIfq_&*8zLafh!40_`)i|eMcOml&!2tdP%GX^`JZF?sxACJMF&c zES56j{@CKupwhEx&2QR#5%dK*BI%P}*;w{+OIh*2Nw=T&z!!<&W1e#Cs@s+x&_0!` z+;@lefzIEz&#c#avW7ix$qlzesR$71PpnriDYe&U+Wy{ax+vk_U*WpdWfx1CPCzr+ zpu^Jdc63c;7zh8ta^+t4r@aTXPs`6jksXgnTAkhSw1=vcfUJ87UaKl z>@#)9KL@*+HL2&cR~xN!&$##QGhW+h)*oBGXvGIE?i|;7(C&QhP48YcJ*^!-c62)9 z@4PLVHrGsVd0TU|#V$>qqs_6tcUU7n*KI1_Vlz9joh-SkonLW(R$_*IJhU;w%XU+^|l0aq)hO-^)#nd%Hq2|asoL4Cch72`18H2}; zvtOw!2dsiT!kU1;EDIfpDvJHSKYvU{_M*E@bj!1%9eb6c#8grAYq3uZiRp~IFPP4b z%=cfsf-WH0ku`CvI7uhCdrs%u{*B5`7ZL{LJsOh+_T9pt?xuMu>Y*Wk;%f-5b07mLE z2$1zhLwMFF+t+n|(|K?>`;DP>k3M|kM{Zd*_l?Z-N~sZ6 z(I17syI9J$wI`Yp{CQ9A?etcg;f1*nuB+ZvU}%$@TBpylm-*Pevj>d-O`y z9;9mZbCzq5bXI9!Z+zk%C%mz@Cp-Ud+9m#Eazp>rO8n9fSpZZd9Wi($ZPz@{COYkq zV^|!o?Y4S}?I_sW7~d^o>2%I>%8t+z_<62$!*rSyQYqa?>1Bz!?-}Fc2KkQRJvBD|zOQ&D8=huFAiGPFNo?U3qNomhSEq}T_PE*#T zJ!h7s(~cl|PV;35r=8J#%k53X(^h0)-J@aU<0QCAKoSLB|4}0 z!&*+8V^06ZTi$T_w~jh$D{eKRvmRL$Y(IA9wNGa3OXt7rfBfTnzAzIUE}?d=`_T<7 zre8H#`s(P$4}bK=&wOR&sNhA#iT8=-6~deLVbTU8jxK^ zXYS{eOkQTY@FCkUv+j&=ET>lfK}JvAGG!Of)|m+xo~^Sr-{D?ILT6%C5-->6J8`GJ zc2>u~^{LXt@Pz%ahbv)%m4~b9bYwVps0+o9X2CT^*%K!Z_n`}eJj=rml)Yq}*X@_w zqet{fzgp;C|H=Tc+x3>F4)?upwz}R|85wr_W7U6kdDUy%!G?VWxKqAGg777CCA7xF zj48sHazQv_b~(eYxkJrOO-DXsnuL9|MCJ|zzcee0GuC)ewX#?1kj1EJO%_@m^Gg_T zNWqx@cgAv|ZrA(@{jxnW{|D*pwEMe76Ip^R{r|+i5MW-}PE>n0toHu=A8isB+?HoM z09!uij#{4Z_HKm81S8gj?F;RrobGY=O7~`BH}54#hQs5^I8v_F+B(N}+wt0^4xim4 z%bZ;%;a=9+Ry*xpov`m=V{?GjK6X|w1`|(tWd{JS{*>D2AjXe>Rs})xf3S=LPhTf8V$y!BmdbMKxsKDZf%uOmt2!1#!Y9dR0 zV&d9rg_!SMaBbZ`|GKCCp}Shz`x3*GbH|@N+P9s(vU2(e)05K3)hJ%#5%v1rt`as8 z?~KjmwLdk>4p{ry1IyuJFtA`jU|fHM6RWIm8)Hd(cf=#!lU)-+=WUt&o2y53I zba)@OVuENl&9gA1I|tzU1{B9EiYCSvD9&PSaXqzHHq(>kp_*yGQrROQOs4;W^1cK1 zV?S+83}_e3|3)Z()xOj-VgC`DHxb^BWv<<&4e>Urvdkw;lsyyAf53f?ed(-1~_V9`c3Uu}ml#yDYP>=&m0# zTiH~xS1QG}5uYk<85ZxgjLc|_a-G={!{3YL92(J6Y!#h_Se0tUpL>dc;x-jg9F8WL zsOXi5;%sQ5I7j!~bzl=0SK0lD63YnvS>?l~K4j4^+Kb|?79GRB%%~kD22q0d?;#~k zqi=G)_&??^e1KTvn;&pI{Dsea?qS(5?=#!ae9iI4zvj&Cozt`jJ8NCzD)4yQ`Do|# zpZ?$nKaqX%?!M}(PkrjjE9Zaxk#0@>-(fEznn1=rVZYY(T_O^c2}iXV6PrT|vzJcG z!I$D(&qy5G1Q4Q(qLg5_9J62hiEr(Glkc6pd)K~xx<}3S-|fpBw@_<&+uTF8C}w@x z(N@<3Tj6vL?B|FR?{W zYI?Fr1k9buZzf+%x|66YTaxa&OhasO>t?HwtVCOtWTT&bo^3vS<$wgcJcxr2MZj-@ z&!HXN89#;_M_6;ZlMX5df@pDnSxDSf9TzPfo_NEit=BG()^FN$>}$_B={=VpzjN#H zEB>Y)ZGT|X2S0i0(VO=g_Vn0~>#U8Bgpa*= zd}^uV^i`L&`o?nRE%C`zE-Fp$J_Q;fs(UiiPa&Ewe8w>(bj0d`M$mMOB2I;}f0ML)sta zpS6F+wef{tIm~w>7oV_a9oM^lul8Y|LI+x*2lPZSP)xM!E$3qUV&}TP?R|b(7}^t= zj5vpUJxQk{za?Uy*x~?=u~IA4Z23a5q|>b?Zb!S@gDP06bsu71QSk``ik5tuvUV3o zOdXzswxpG~pU-I49CYtO@NUQTU;WS>UpnQ4t*alp?){(Ly!F(RzIgnm)jNgCbbI#RQo;7=U)!BQlcE;G+*~y+|aepyT z-_*HubVdsS05cSWd{|WIh)Tx#MyBy&f--pRmij zJZ_)Y=kamZ%U@=ad4fRCakeu9N!{tR+vQsa$@3g4O_&rsJWZH$pO5s8pw1+U z(D9NTPI*Ss@k)YTPBJZjIOTf^;c(-=;C1`lEUu(%@+sG3(nSy~RV2w$fCn5dW=7y% zmx4;bqNP4D19nedvyvf>mQdtZB`N7p5Z(zGkkVlqsYQ?I#M$4bqOOq z5{3(g_o5^(HG!D9bn1k*ZnrI;PMKNs5LL@p;c{?w(#FeAxTeYSvZoy zk{5YHRSVzn7f1m@+`r|n2OLSRRc7Q_w|YgsQN#sDa?w4wC)Mir`$@#_^9%5Joe$FY zd#G3`HQ&JjR>1oS0mOW+RERLp<8-)m9+$$s8je^C&jV5n9+7llFgS8iG31nk1mppQ zBb7zC=kzY%NU|ac2*kmY;3z17a0fu0l$9Fx(qiP4tXk&+is(OhT@_exVo_aq2zJR= ze%gmC087<1IVF>R!I4xwz*i&$N79Cc{TzZLvLj#dk2Du)ag@kXz9t1~GsTNf(-W7SI(o!FmAh2=?>!}(^hguVv0~inu zyek~#uyADE5NIf{ss}`VC>%)><-ri_b-IH{D-l>y*{WnxYB`0(Asq2Nloyh*sLGX` z0f_NcfWOE%IRz3PFPUUf4F4dH5LUDsp61oAJd$q|jzlV}+E+y;5Bzj-B$<*53I&BF z9Kb375upJ)Ri*g6fgnin3DjT-ukhD80Hu_JW=)m^XCRcJs$W$w0vSIE>ku3TjUYI3 z0hN#s9C;OvBq{4q?Hh=ZXDIr3fPTmC1z1#Sh?0>_5g%lc=Id2Bl1mz`C9f#CpiufF zxvw7NIS%VmeM;HLaWE(e1^qz_3nG^&03~!ulDy9i=H|RXX%`fp`XfUm>~I^3j2K-)EC?Kcc3N~=4QuzFC&}9|KM{!|< z@Is)*E@oth@`=u&ipgIAi+Y?uCFE3a6e3Y6p41t&D3t*TK!Papg~EWy2j`La$Oe9e zQ5+zO_6E+>liY(cIO<9_cMx5|Is`|dc!;QQ0tRp-4NlYmMaH5fGSw3zQS!l;l1YUl zZu>&~O7reMP8pBDk>m;1bBcI5j3f2%E8@r-0waPWq03`5O>!#PrZ0vH1aXTChrA($ zBMOhWki5Fsla&><1}uUVtKXCW*?2 zRNY}5ac$vHuSJBjLIj?g8fwl`97D^osbp=&s;9vCA}l4d)a`HI+9#c2?Baa2M;1{ zBd&Q%zLyR~*vE1fQF{Gc`rVvL(8#rtB$6+Lz0isT!I4)-X7SS{rJx~{q<;LqLpb7l zC|gt&UDa19syqF=)Tp#cfS(11S_4ulwsLsH}w$bknVf+M!~iUd$;{6TP}bUgCSVRZsJDZ{%UAOq154pOLq z!VwKkE(8h+M+6C%TnLVoyrQc3o+1SypmHZ43P&mdxuQr`8Fdm0>YTwKOyd>!PZ$ob zn`-%FP!hIx4}~LD_ar4freP@X?J|Dhl75PPWWlLcET}WMGnCXg;O1n{3{0yO@47@g-v+%lO}h9q(Z@v zdTy@eeJF*ZxVj3C$W}lGl01k`pOzNJmC7$rlJ2d0_z(mruR*B?v@7x?=}>D{<@36{ z3sB{G8Uy{ET)<*C2}l9dweX$JMlvcT_Qk@1pxYO9A(0gYAZaBiFI%UmR_YXC=yO?A zg|5(8cqob>as~Vm9mPEwl9N4OsCF_L5gY}A2;d;0Y+xdy2@a4q3IhW02!=xz|8>to{Jo*@lXI+sj4b0A|fO>kb|6AI8u4#Q4VCS{6?c7GYl&c>D&p1MTUiG zDITu5;5QgDqJgMrLSd+jYQ}>M>P?uV`Ychh1V`NN9xSP#VS0rT{(x#sk`5|ES8xOf zj!V^W2lgo(A#ynx*K%4og19^_eSkK&arm;U7AmRrKs__d6^|BBMC`DZj`Y5~4 z(xRzdi>hQ;&M4&oV$lf7FtM39xu)=BGD_!7o$Ee56mba?QB8tDdO&Ib?gjNQnhsJC z{=){KBdC&^2m?FHDrJxgiV@)h>><=cfhe^Y3{i)CM{fq6Kv^Wna0~XJuY{qbdIE~b zpoWK-l?eNN1z6E<-NqOp%y!8!ja$`xx}YG!R|vIe+-$$51Cb@Bni>)tn&yQ@q)sU zd|^=)nsOSH!xan|QF3JHa&nEVA(Mu}5e+F4795d>FgSt{WP{QMBU0&Tv?MAxlEf(v zKe{d#QvV^Tpmi0@>`Eu4yBz+Hxz`8&R8T!4g)bm5Bg#eIZ;EUy>Ore(tx9Wq!eAd$PvUX zjDVMvHWbx?6;hFs!3LT!I;7kPM06=X*+6ok6DJaIQbZ8AF#usGOdTo`bZKEw77a0M z=WR4#q%4IH;e{eY4Tcbm&_R2qCD8*gmlYKZ6Y?r4B1{g!QM^E~XG~g=P*Mj+A%!E7 z>>dh7fjBioGl=M3nqm}YMgX-Qj>8AR5hWBDc!DTbK{x0Ee>Cc#0E~9Z!{HE{`bgf? zgOr&=mz)=C_ z2+zDc8Uaee_U=Ii`ojT1I|t|s1SB1XTioUhhE$c(bqEwK9Er9|D`d_{#>7E%VI_pg zjNmBb=g~k4_Id(IH#(yN1*C+4lv+-Cw=glRGuDhk1uz1&Xhd)n5>BGE0v+0&;GI%(PZUjr4>(FD74{H8 z1Uv}R&UETfpiVvNVNhm-8Meb6&=`?^570piP$H;QzlY8Nb~#UJCMx`05bMU;qL~hYt%!p+q78Xye{w3bx3}w4U`>G^`oMf z&}>91Nqq|u+NP9LN*qWV7*K*KqBZdYHxwx$Aun6!0*>gHsU){WRp<)U1so-!BpMFI za{pez=~|H7hz<_MouU4bK{SICG#CtyI3t^^##L{N}o zB6B2;lI5fg$oh?NIs%TutY7si+XcyTC}kp;q-LUFDuI3yK`fQ3OAbIbI0DT|WumRP zu@MTo#6Cj~$P!In)Sa+oTpfZLg(DHPFhdSxt^6jF0MvIda!iwR`^#iSa;tEUNRsB;J%VDEL6NmsFTHLunG|(asvaF8tg2KL zT%G_$jHCb+>b~HJ?~zJ!EuB>`Y9JyJ$z+%Cbclnr2O+3HA*3Jxm!M>w%5b2J2U1C_ zFcPYYE>z`8&Lkum@F&2JKWWGr4w3r^nM^X6Kz7iPv;DR(QIDea#-IWbzKI!>o{5Qo)Tzc>f&)2&JeQLuaUj zx)O@POd}$VA_X6)xfmrB7$`d zm*yExr2uR!X{1mMxD$;@UlvP-Lxl0wV=w|rMA*_((k@UBL4e>$$H-WXAgPtb0Sv+< zVSD!oxur7^v~!3C7EGm5X!Cxn1LAe5PEl}yB8?kmCWR4vor?eh(QTv;x#1+DjZdA##Q6Mj`PMdX4`>fmF7usVE$! z>uH6fxNb(#)bu!#MA^WsTCkLfbcCeIp5YHfLvffHV^tJ6pk$(MNJ{|*sJMcol)Q-@ zLi8sk?@@Osdd#Xw0eTllRtoYNvVPNP-i@bXX;cI5(8GbFcnZrL*IejOikWFWEjXfC zh`5lvV$`c2!Vwq*N-xC`{R|C-T7XmoK-VF>vv4G!iqZI~HYJth zT6(2$jC>)NpcTp`FvNvSukJ%^bp;Is;1aJ=g`5jGqF)vqSyY9tP?ak=({WvJluE_) zKsqibkPx}cp>TviP8dmW1eOi#192k)FKIQXinMsa5lS<>S2&UzRK1ixt}_w?M}e#f z^Jx0gT4+H=)SwQLR>Dvtn#shHk!VJs2^wS6h3HQlbSCha4i-8g>QZz7aFi55!L#X{ z>iyuS$fImC%g)GwU^1GIGvJCQ#fC6sZMC!$2@6M0=L9$cGpVrGda4Idtncn>C}+WHymSq;n^p z6d9B>G3#@kjPspgB=Xs477g1&w@yWqV|rUpunBXN?jy<%Fl1D*aOfU`b`?bFA~l$x z>)C7;H6$Rn;dIn*&?-eSq3_i_x>u+DBLPtGV?nr$iB7KNSv`@A3S^+A7ukZ`ujhSK zR0Il&6a?UsQgY-KF&W4y12HIDRE4fkl`A>3kn|dnteJ=evq?Fl5po|RlUbBN>YRoX z&v>YUXe=IJiF@3Dmy|Y^PGWKkM|nB|8yM7*IaL$MB;1WAqaXlFO0ZCXlRzg%V>F}j zcq~pG#-(#h#uBkyE|D=$>;N^AtAYq z%r;b&Ch1RsBL0ywbmi-mL4OkD`+yNh7zsn3jik~BB@L&@ZY5(R{E?#1Ktc2a2q6Ul zxTKUE`4pRyI{6;T7F9(TszOuFeAaby>Ore8u4T%VkS*QGDS?H zkEfHdR1#Pi@l2Z1BNZtb|M)-_{Fa(Xr=|R9BNb8dw^T|~0T75JjhvB8#uL<`B9Ugb zR-ur}#u>Jgbu{;s4qk5rSBY`qV;e@R)8FVU6xOfx3cEO|v28zC!-XQ50c1C)gA-NW!9%%XaRT*l%QCK@f3O7ToQB)8#a zJP3wjnM8(s$EXx~9O+$<_F+zji}6s31}x9Q)3}%=pcSp>;;Eof@y7yzSO5w_3Ig$1 z2HF|bK`r12c}i#Nv#2V%P!*bTmLTa*#)`#EOfQ*o#$)6@MJ7ugijYj_qPcXQ!8>_K zhmy%`I+01oC~Y!l0;@=zr;#*qlt?lc<3@}XZsf#_X8;XDzgpF#0;mO=bTOVzC)3oS zB4H-esd71!PbSML-o`j*$*Ux<$D@&0c0pdz_t2+W4#81*v}|J- zr)LwjG&o9R1xGUGv<}q?7-ecElZcs_ke)JfFw;z>;^Y86NH;-Gf-sK<5d(p^uf!Fj zf>0t3%n^x+tcVDSZm$l(3ZQ+Rdke^XOJE_r_45M5wC(J}hZl|KW zF4dXg4%h`nenSUT2{WDwb1EFA5smU}%*-aF$1}-tPd=Uw8npo7&*MTtNI@VGH=$%g zAd@(ZBaz=ei>jgvRiP^j*Z`c!oZO&KfDoXy(1J3lG4GSq zqT^XdG+HPW;&nDT$fpwdTqJ5Hin)9;Z)Q^2Og)pz0LTIeXPl9wjQ&v4Ut=qT6k2O4 zSx6#+qp1RK*GZE**@9rNP)){lt_xYd zV}q_nEnOD_1f6~=gtX;#3_6R*3aN6?9RNdrH~;)e_>rHnXr9p^@xu$St6s0udqy${ zOgNnmWs;dB9g1w>knkr0iD)jBOXX3_OrBwQsAp3!93qK)fyzkH7p3BTrBpVQXas5S zNhl~%5J)BImpO%m8t|b#Ax|-B_${hJSEve2IqQW~GLT8us`+HJUXU}DB==b|S*P<( zXL6-@Ial!u6EnGpnJFTja!E>?DHRyB$5PmblCXj0(R6vLCJF_(n<}J0Kn7i=*(^{2 z2K1vXGRjEtZQEgd-^*Vs*&MKN*!w ziqBNmY_*!%Y^7ChktTPFl>!2&(#WKuTvrG{0HyLkGuw*MnN%pej!2AIi^ZUoefdlW!F@7TX4}+^Nnui{R;!gQn}*z`R2laeAs%+yHTEb6I&a7NcNLV>S^KsHl0Zh$&3wIL zkezHW%fLQs24N;5&rD=9)e79rRFLP~7E7gS-mF!PM9J)})N-{_F;^)LlyY=LyqeF^oU@36 zU?dwH;0l><6e$Sgvo&bf@>R|S93>(phN3}>s?ZgxawX?LPc9oS<_7y~ zxx_$^oVhHyFO$hZKV>LZ8<}Rc6{HICrK(XX^;QbCDml)T8a;+Eo~N_0fu@tsBKPU; zdg^dDU(eUfQW2GXY^vDShHI@N!Oh@LGM`0KQ;3q( zAze;ywvcNy^K~=Ro1-OV(oCGRTC~`_nH-#?cGMv_8a-;%hU%YcmL_X-Or<6R6{X{e zo>wZRQq5*FJ6fq!drSG=o`1QYtk^dq+u=JGEvN{x^pU**MqD z8sAkay%Qs)QR(#&)d8Bp;G8A^_B`ODbzV>|MgODft7zeQ$#hDYax>BZ5Am~rco+pr~8X_ zBR3f-7)AjKLP`jf3cW=+drCbebVGHfqSXSFEviCSsLGX`BaqZ9h0)>OLTaQbXR#2X z@MLl{KmjT}{n`GW0bQ6_=}A;8E%Me=s1%Ep{$`?BG)sD^SV)zMbpEA6rob?z&}_op zQnS>^RV!#%i>8{@Vh^n=M{>oUo@%|-1Bq&}*sS!_#>aaHE7kETs}a-1LIZhKKwcFn zVy4i7WPg9Dk?b&{}qiA;ZGx|iUpm3|p*kYxAB zWcvI2^ON;@PpeXFHB*^-VX)Cx?(3H*XO8P{lOz)9PR7(A& zQmz6?-mWzI8dlZjqR^;|udX_d>Ra)rX=^OKX4 zbRVgDu7{J?r8-;HmO!zf7b1mBqtdAKm3vY>JR>VLck$P!zysw;@=$ErxBKN&y za+1!wQg06Bhnpk1FtOf5ha7BF`G@#^F zwXawh0MQv5gB}|hp~Y5;naNpESmY2KO>dvJp`T}m>N72(j@5@`v>`g4b(rSR&`@c* z(P$3T%LDysvr!u98>|jC8nsqqaii8?cr;Y6SJ}k48n4y$ctu~tRV`CU)hk1lN}&!( z-mdozwyNqV$fw|_t}azFMLtXQ+Vu3|dVOf|;52D+r!~}s|3g#NQi|)L7T+}*gENck z)7fk!GBi}FCF-VGtJM>c?Dj^zf%eQncYtOA+ZT?q-Y7H*=0LSpsn&*uP@blz*=D?! zZXht51zvAd8&x`#dbM7qaYswpzItDMkp6_TP(w9s)mwETQXU+xRSWg}V7*qF8>_Vv zrI}bYf!JzPg_IDeR|jfx_SgIC7!cKI#v3#>-J+`KLRDzWxp=5ni8iW>rv|I$;vqR} zl?a6=lhZ-U(C8m6j`dII!o)^@y3rWv>ly5?_SR~Rv7vOWRv=7Rt&*+R0AIb5uQE)j z3=hHG`cQqa+-#)N=JNJXqt;LBD)-eF)%yDzeIxylXx3`OjsE7+Wdjqv&81Dcx@@gF zSg6-4wFa-(ig|jRdUb3JI;D|nwVtIh=<$gOT5P?XonBdomvRV>=6287NUAv1w|2xR z6#Ayp42@{h-J?*LnwqN54Gs>Cw|d7%^M%3sqS1-Q#L!@KWN^h`bC9lS3JE}SZlqf+ zBh@gLx0-aJRo1{yHE24JZGA(+qCXcpSeKk_6_qFEcmiP5d%}vaa zCU-`rhWhIDsdlq&ay>P|cY}iyYghEm73uY-rW(z3Ujcoz4;$+4!4~k5#xXH5F)}h@ zEEJ%xug3uQ)xvnQ*=V+=rqG|}=H~jwTlqnP%nem}eXudu80Su-)uPUyDz zB!A0qX5PH{o%5S}-n=G@-?P36wcm>7=FN4`{btnpCW_eX@j$!Ho@Q5r)q?(cY`e$O zWJdvOus3%!+3gk^^3cB7+SKH+I9j7!&alNAwQg?OEH!U-)l+U-w=`{O-n4PEck`B} zKwyij;R^5O&CSvk^($6f778_kQiIgly9K(WKeD4(E+$-l`36&{b&qGI$L)!x}&_s+Uaq7K^p|t#yt@Cw>5Tm>p)n&2KeZ&CRQtS2vryR_ zQm_(U!b&*lw>@NOxy)hS-W{-*wuk6vX<6CY>IRds<&eVR?by847eQy)Wp#M#oQ^iH zE#Ni7EF2x7I*YZzy2@&8ff~^Gx3)A;L`ygX?OH?DK(ot%e$+sJ$Z7F99FAtMwa4Q1 zIz7lkui0s_gd9HC&Rv11!?nw0wwUX!=3rxMYm3#{YH77@z5*Y!Z8dN0Yz=N{YBQUy z^-YbK3ywyupwwI++mGDge`H7f*Yyh+9W+E-L&4Pz4Xy|dS8G6-R{t6rB9Vx>-{eZ2m&9TAN(BO2sHmq*A&gb+wozx}j=;(kM;`6lHpLLCIi?5}j-Qlp= zoe;)s?(gq+wL49|Eq?p}?z8*s?e2D`&1ti&v8=T;gk51*huvTAulH?nY;31FG4?uZ8d}kll?0ogD>1|?rDpyAtn04Ccm#M?CuELVHUpb zh{@sH;#%W$*l@hf8k@~!Yqp~@v_&FooRHPI)#~?|Obv&IBYsENheJfd&OMHB*dIb3 zhV6cbBjRfd?A_O~!{^@{usdvxPTSTN)aOpW3*TC8X|i?M-1eUBuB}#cm)+)SG;dk6 zX8n#GsPJ9Z#{M{RhyRfs4c|U2V06$N^CvpjHaGiY8!>-FZL{*%+>ByjA8u=F-{yC1 zi#9g5J9b68J>BhXzRtFTZN4@%OEJIS<7@SKB%gnclJbb|Kj~(nD22Fp!-x=%h+wHOaUi&88$2tQ6VCo({ z;2&;oUb{9HbNe>>!Li@Jac%SMZT>dDA7iz~?(S~12x~ZA?l{-9wYIf3Z}a-xULS<9 z+lPmT{o8zvZLRJ2`TQH(JZ+wB!EJtz--GXAu5&a;{ZW6nx4p3)X?&Y}n>ziSenR5x zj`_S+zop0Tvme>*?cC@{Z18T}=miEsMF{+!Z9e)DipDj{uUTq$yB%v3tb~`a5>EOZ zjQQMeX!9K0-|cNa7^9!hz1HvF3MPjyg#c~Q-S$1vfi-Py9SAqJx9y4syQ3bMMceLJ zBW%OJ#_w}C`+aNN?tt6kMP=yT5rcO9F@KMv_IkEW8wVTZV-?Yy1-bh=djf}dvx3{;ar)QlmLA!Td#2Iy&cZI@% zP`IxT!=Pj`*}f~>9C3Ex7jEAa2}XiDJ9f1N+k)P8-VI($tS#2o8|vE9wI$*VH-;N~ z+IrfeuBgk`+ZPTw+U&jUVNYRSsArQmEd}fAgY}Uhp&|rr!Chhc5sKz@%Fk?y1_Iu7 z3Rc2PSP3Wn5`E#ohDb1RRd2|g=%Zga0PefNWO5axh{X1}_s0&dLl(3}W6ja%K&+!T z7VHd%qx<@r!(m6;y0$P*;tsD11lrM&!a?kTzCF-x+n%;wUst5L**ul$i-u#7XcTr6 z565EB9^_#x*cA@plJ8pWQ+9WFzISZ{bh_=n2d zg{LZ33-7PKM|fAo^Fp3A2p=(gNcc3ayAc0zJpU*5bKx%BzYqTZ8a_1lBgEfYv07XW z|2|5CYbUP9aUou~8`ssijv78B9st}=f#Wq?f2h2j&YlzQg;XairiIHZt`VN8c(;(R zY=*x~c*giX;h9P&?(s!3!k&1ZY&`Fb4c@N_&bEW@t+mH;soNRg`148 z;54}J3A2Q=>gU36#T$i)@nxYB{&k@LD_mW;?gtN#Rji)-Gk6~`J||2V|16v_ZV^)O z)5fQT!_Z5F!WGMfGt4eL$i6W5r13Mt7Z85D>KWk-#T&mT9OiIHXE@B^GloVXjAy5i zHUt`b;Rh*fbP9(mRto^|Io7Z>tWTnxB$g-Q4veENbM$#|mT6T(s4-v+$z!8L=pBUK-PT;CH8 z;r=Mu9^(E9*Gu}|0)~?&i8@-`wS1v{SvnO zZq_jOOW5nZDy|2(&XMoz$8%(Zw;|6T!}TPNS&dXMT#%h?7;vuw$E%B5z%%6q<%8xU z%x&*V;Sl`mC`>$P7q>IAHC$vD(2pBk`(0 z&P}kn4A~~x2iXQ}eyQ;%$T#rLWq1K)0k#Rbpc}E?xR);B8+1Yb`_=1q&<#>~A)6&W zb=`U``*|P{~;(?bz*qP>2xr2=NbN@=%4eky1LuQmivNz2q z-H^>ud89I-`tQ{Eq4G&(MDw{0$u3EcSeB&)j`L2AsS~hM^+!ndhTWA7H08NV&OlYK{c3tb4G zfUz}lN&X^LzwoI}ge~pW?2C;fk5oILa!qAZ zr9t+1Kuf>S*Wpp_p#SG#f45eAPB^96BmCQNy-z$TknQ7QE8q*Dg+r(DwX9chw%s0l z5e&O+E34q+jBx1aS>lDa1^23D5%!?90i-89yIiaPD{MlJmqqBE+JTC=aE$Cqt?whS zgNGn1m51v%zf`xA?I4e!|9dNzpq@rON9D}$38k(#zQ*(Bah?~5uh^hGH&*gG>oDq% zGsfj8PXzm@&{}zyaI*3S;VAIoz!t=j?Zc-yvX{zzb8}?F74JtKkMd4=PyA5cSN~R6 zQBCdF+veuD4WP`Ethx@+CUV$63bll*>MMd|{H3s`5~sD&#r5%$u!8hKaPxFQ0*;p=W}D-91}#6=b2fz2aKbmA}IEQ(Oc=`xWIW+eIVh-@|L7sA$#~kAB2_ADVo?ap>o4XF~^0}+vo|?M_?ksnSpG!DD z>k(5BHX+S(aNTpS!u24{Cc(?YzPYCnCVn=7lap}w0rMtdKYu#J;}3whO~Nn_pW^N; zcOT;J!yMbEdH6KKSAeUR;Vy@cuD}tk6!w716~g^Ie2T}arYALdbsNcY@-kWSB>D4!XfV7kEcz-ve&)^_Y&@|2dySy19weA5Mj!xCc%R* zBs2*T9`5E)ecU}U_fEv$%j56kG52#kClPJ|hd+XQio3IL&0IP&m&**yuOj9IcS$K`ulkNgFNOW51-;VXW?3f zT4>k`Dp$d6KpLyi%w4mPM40GWq3ykJeLTG%a#^7yM`no(m413l3fi9MJws#76+-fF8&tojahh z$Kc+}!=!VEKst8_q;n^y=;Rcg9J34Z-h*!>kQKQ&T^De^fH2{7ahxt#?8^ub&wT|h zVRi|G*#(>gb&6w{W0&C8%0I4OLBKc!su zK++$=U5*m%fxHx6Kj($JfxBdN9$>o#ZUgxAAdkqkgC7s@{~oRf)IE>_^@7kfhcXFS z$lZ>T>w!cR9^}~e&h0>WKMxOa_W*Z?xqAqC?Gch3{|VFv9^p<-*X5oat(O7J$t#NUS3*#Tv8t-eGcDpB}sjd^d#I3+}#A-`goc0f&Y6D z?&I!$9zQ(ydBjj&`*`{CAx9_;@#90jP?$=+kC%Esr{(9g{NM+*5~t@5H)gn1TjkjoVWtpdWN!yvboAh(tv z2c%G5tI|z{7()ycgv<2tANH#A8ly%v8pM z&_7|mkEbX72YIOvLMKG!6n{!BMF@O84VUVc5U*Q8oVO54`m2Z`&4jpSLR>Q;l#v$@ zLz)jkGXzBxL%a-zxMo6}R*09x5Z8Z*>pucMe=S6S;Z?Z92uFF@k8*2_BIbV(LnW>Y zT#-vPKo>AjcouFqVhTbJj_41x}w)Py}agW3+4+Q%NiJ_q*_XthT$2^qL;JWV@y zhe5qZxQd7G=V{0$dpL(Z!owWTX}B?P`!ZbOHU{2a7Ip#iJ#f2uxR1N{arY#5sm|F2 zJogB@IqYr@yPL!A;W2x7%pM-o$7S#1viITXZ-hSKAn^3zDTPT7{X9)SPt%Y1|3M7J z4{{qD0uwzPaJPZOgP`~yaJTbt9}j2w)BAXsG=C5j35xm> z!=ShoE^$5#iWDZ!hk^4IxP)_ z_<|AW9s;%Fkel#D9|WpXHdd zhKEO~TgE$ii+m^e{E=`UPkA3wzKk$g)P3Bd?&B79 zAJV)m+|T>2_w&BuDPG5(;(gasyzhF7_gznc%3~ z^`j1X7#GFtg*85m60jMr0((E8KFy&%&0|h;Y^OQ4(>(q($99@yJI%43=GabiY^OQ4 z(<&@sdkXTB>*Fr<)}Df-7U5|gkJgokDgJ35Pveq0^|wu!5{=>$Y{U>W3>rRU_>D1c ze7Erld=K#t6;j2UD$Z1TE5BD|uKIX&YxNPlrg~S+r)z#w>#9xGeiE;${Aux3i@(35 zcgej=zJph4uU`78rQgG5m7A7*Y`L&}$MQ$<`oX7GNGl#)@#3W|m)^3nYUM2}UtDEc z6stA4oZwbi$-{@G>QF1r_>Rq9>y&9y7mhSuJ`_9yu6?Xk;GuM^iDTKDL> zKfK{>Z}=XzNfy@s%ZB8JC+m!LiMs3RzApu&LFu)PpWOJ_jn8lVIXxp59huEkCzjY5id9 zC9OZRP1;_t&DrbiSJ_{5Bpn}e{Lb0woOE9AyvzA9*HYI%y1wlCf%^^agYN6yA9w$| zXET<+hCIhTKl85ehQ06i{=(Pp+wZ&1_gVj){*U+{@qg9-L;r6A#=vC(TObnH7dRZa zHgF>F;lSy@H-loZJ9t&_oxzU`5>F~X6Rc-HY`&8Q# z?VH-KZ~s91H#;gi_H}%_n7|Lw8#?C&|!b8XLwo)7h$?)gg3_j`V| zoo!#Wee-tz_S<6Bv30RqVjqn?8v9!8#n^9mRP0!@qj|^n9nb9Cx$}Wt_Fd2PntJ#2 ze!cfMyO-=9+S9vdWY6@T7xw&5pTF;S{geIg>VL5RI|Jgtfr0eEwFB=M_{hNNfoBJP zF!0KtFt~ECVbC+!GkE3T)ZmSKH}8FR?+^C=_r5**KED65{n`Dq`(GS#3>_Q#+JQ|6 z0teo8;A00qb8ywcD-VA7;F*Kp7>*9#H~i4>)5G5x{@L)K4jnp_J@l4CU%1kIW!sgz zuN=KHf8~u=zI$ZV$eTy*8u{qRBO}j@{OPJ^;}682jDIWsi&0_pveBl|!067=kQkvN zq`rMParoqs`Xlm@myi5DeM|bK$;L_RAKC={o9*n-gN5vRoCBk{flq*ym|c1**E{)o9}t^Yd4s0aNY3y4KKW9 z`CIzma_d{3eQV=e-|^NT-?;zAM{e49ljo+#Z~EHJzMDI4?zwr-%|kc;>Xt*dJao%5 zxAxt7>#ei5zIf{&ZoB!m2X1@*Z5!YAfwz74_TcSPxBvF`-_q;|>l8-u6`v#Me=&^r ztr$%R#&?PJ_|62aQZ`Y()I^YPbSlbG}&( zS0Y|px2i(=yNQw!pT$6$ukf(K$+vm9lE?pqhpRY;zv1C(3X38S(-~|xiI=EhQCKRz zM-4NCKdOce!je;UG^XkFXr)| z=i#M1{45VIM|dgwE)QRd@Cv5zzmlikz`*@@rFxzcrC(94GKT&c-YM2 z*Yh+19F`=Yd1ANt^XVO==?T8FrzymZAu)l*&tSsgR2&<;i&Nh?E^WH*4se zXN8Zd{navpm0(;md$I1~)0}!vfeOh)nkswiKp( zPR+}8?b*NKi58|*G6q(qJm=whqzPRCH9KD#XVx5*e?!Pm4$yh@CVqhdl#in2H+ zj*AmwN<1tc5!2$Nm=Uw$lz3FkiFvUg7R6)YadBFl5w8~CC|)C8D_$qQNxWWsvv`B} z7V)j(jp9w>&EhTMt>SIs+r-<&w~Kd(?+{OjcZ%;6?-Kt`e3$rc@jc>u#rKJKi|-fj z5kDaQz4$@#UhzKhL*j?UkBA=?KPG-${0H$9;wQ!X#RtTH6h9?CD4rBgiL>HE;=|&n z#na*=;-lgj@iFmn@iXFQ#m|YK7oQNnAU-KRB|a^FQT&qlW$_vDE8@t?(SiQg8#BYszWLHrl-U&Zf<-xq%%{!n~Td`bK_@kipni$4~BBK}nT znfM>#&&B@~e<8js{!)BJ{FV4?@i*dc#ovj)7ylr>D*jRYllWiae~bSk{#pEA@ilRd z2~1>+;d4Q(f>p9AR?TWyEnCDEvn6aPTgH~NOV|o_DO<@_vDNG{wuY@`m$P;34QxHz z!0MR9HZl{dXPej+tbsMM&8&$vvn{NJnVE%ISu3+KJ9986zSQPs9_D2}=4SyGWFZ!2 zZLFPju&u0t zm28Av#o}y~C0LTlY>bVw36^4q*%6jzlPtrsY>FLaIhJPyR%FN6aW>6n*wyTf>>740 zyNKo7m0l7IrJUjlGTC&fd=MVDDfj*q!X1>@N0q3~RycJ?y>g zee7=bes&N00Q-CPL3S^@k9~-Jn0&9{+WG?eVcuUeV4t!{)PQ3`yTr~`vLnQdy&1w{*C>J{X6?H`w9Cg`x*NW z_H*{1>=*21_Dl8(`xW~&`wjao`yKl|`vZHG{gM5N{TKUh_CM^;?0?y7Yz|!o(SWa8 z8jScX6F!z>s5an`Xu~4IV#5-{Qo}OCa>FHt6^2U|rE;p<*yuq;E zu)$DgkPI6QCPTepli>ZROF}7ov4(j6Y2P5_0eLs zASXxDwVC4ND7F9NnaX4~jebJal$=Y*nL*)P$U=1ddcTS&>i2^ApCaT+@+E$vHWn%2#ycp!@31gq%zvhbp7- zM6n=O?oyKVYRPH_wNPbTNfGA^E{ZFhkEP}5ns_M<2d{{8+zAc0tU$^Nq+A9G;tD&J zDJ4xxNu%n8V^8U6G8}tlR!N#wl4i@$W%Z<4C23JfT2zu2%aRuLq{;aBxSXp@f<2fH zS!p#{UK;AD%F79|g33`Czg!?YCGn>wY5_3`D;`bfYjg5=iU8zfO(Hur!&P3RcVR0y z2h|$2>I{kqSsLqzR`znMs7__ZQkhg?riKiCGL39a;Q3M6p=44o_gHU~Wl@!6k|@-b@+5hSwHk%2dWm+U zXqeyeI)jIJWMvZL{G?o!&R#8N#^stBP3<*9TBv#qrG(T~ohar|>m+74&%7ccY@BC) z$1$?86dOy|C^mxZUaC|*WF?g2OF3;qi-lN=a*a^fWz6nqDPMZz)Y>ukvJ2c`{h`WRO3pPEyX~^Qqd>qY5P# zCgT_aW-3N=s4Z)dERN!vDHy3FSE&Vg@kB8*9?umg)A3@VGCPh+_eez|4zS7{+>9#I zir241<`wUN;$6)>)Kb|aat1Y&AzGL)$c2f@AK6a*I&eYvCol(%0^)kZK~H9;=0qf|f%*DGAh@d<85Zlc?OrC9NFip0NiK zSNE%OR_C>vt5abup?IB&APMeOp)8TcYO=@Fs=IPBl?0l~4h3_ZdsQ8Zo~q)CUl~!5 zBo!~h6;>5h6Ul0#sPa@+yd4VpwBqehBGcS6?f^w&n#k@_9%mG9my#;Oy_#N_E|0It zYN5(LB_WCfRs4NQ`kdnRE0KBbRS#%-DQJ2bP?8n6S20LNQc)xq?S)3EsD&yIDA=y% zp78*2d^MbkZE;jx$Z)w3H|(BBRm9U%6Y+|qoG!$xWKl$cpJjDV+>w(Y6xwqVZDLxQ$Yb!>Qh;<;1ueSkzrgw`igiHg=d zdz{t+yR<+?4H%-CaRVyps$P|$tjbWY!cdlb)qNTqx{ti0vZ%kr#v=5fsQX0S1mge+ zTp)o56@iP2z=JCOqKbb|!CzDa9#jP`ssisTVO(^)EId>eo+*VZV4~ysTHcr9)?LfH zK0H{}q4cS$;!6K#k<#4rXN#0(mxpWO)F(*Gsd(i+rTRXuc>DO{%Hv9^eOmQjb6g8m z4k-y|xTmy_RDI|2=!>Y$HFeKm>#bg&(^f-$T67lh6qO_I5Kk=Tm8TNUFQPuP8eYu1 z&DsOr{Z^WU<-8DR+RRN2(L0!I>rN3pMZ{$NL0koQHfXXh)<3t<7{V< z?JBbUDOeq+$#$m-gmJ11F_r=AbX%ZG(MW(5N4NkOUy%TlMV2YD=~Pu# z5x|g}$X4Q9_P|SZaw7DLrUEX%r2i6=Wf+QIt{l*!{*fS*>o&69g9uJV%M?Qbu4xb zORHVy&#vRM>-^bu%yu2KUB~RuF*|h34jr>Y*MUQ);m~O~bQ%twhC`>}&}lez8Yr<; zcA+7sPQ$6waOyNldU5JBoH`A)jUy~N4Y$sfTZeG#5N@3-w+`Xax$@||cywMoIxij_ zvq#75(J^~;%pM)HSI6wt<@M?`ygCi9PQ$Cy@aiiNq=`?&g4WCZKr_=E1 zG<-S@pH9Q4)9{qAXf&W-HMeZ7+EZJrM#I*s(Xh2@G;FOJ4O^>5!`7Qf+4R8?5F&Ec}MsnlXr_n4EBv96QNEk@T_zgfzwfBkj|&773op*hg+ zmhuLnbjzCt{dRc-HF#%satgBx`R;+8`vu|AxhL8;%r@EG%V(w8_SxODQ?u94-aC78 z_W9YDW^13A{q-!PBvYThY>m4vA}-rox0m$}jtsKwfcU|I2M5@m{>zMg{i}`reXESy zd-{xBJs#t>9=ov{SC7kQ47S;gp|+4Q+_ur!*|y%ewXM(CfvX)?o6ByrJCa6+%V~5u z2aI^9+W5TlCFif5hDYaKdFbIQy4{b?z4Xw-%j@C4(!S*3nq}^XFYh)Ud+4Tzz}BxH zdWfg^@AkQeYBsqaTG?&9^_EL96*V=@mfiE>-?R1)tX}JGzi0J2cl&!TL+IU?t#jXW z_td?j6=UuD|j6Ti$W!i5pMceBzd6?KjjccQ4D;R6;-KAi6#mdW#m*ARODYmcd z=yWez+FU18ic6k~)h&K9R#*E(tgZ%E#b;u5#?Qv;3}<3>?6Fv#csf=mJQAyW>dEH1 zCqCa?_u2NnpV?UV*qM!WkDT6E_vBMgFZuivUs&>)&wg(4na3VqeEN|`7cc+J^`Bww zXRbfPmYr@p-Fx~?r;W=_TM^76`1#Y{I6Ze7UqA8~7cXHIM$BY0QDD0(Fws7Db^0wyDmI$z--#_c=PP*2q9a+K-a3L$X;~r2z~&^XP+TGx4<58r_9s zizW(%DV&#c{P=Opafc1(qF(`i3sYu{j#I}%rcRVw4D`*kHf)kGIHJ3dsUS}mtVAOqB_`tN zQ5Hgj16^jXNwN}}LJIQ;;Rt4n(%E=YQde=ME`Z|%qb`gEl(3c}XjLFM>zs#7d{PdX zl5#$gOHGkp%7|-FI^HqDJ4RT?vN>sADuc->tQ2DvduKdhk@m^b1ZGXKbUaZUmmvh^ zp!g&YEg)ll0zFpAV{gF`7|4gBOrmn&UX1Gajfy%`$hSaTIdLRgEJ%}>j+L;4O+xLC zWBe>z(Ca0ADq2(|I3mv+$HZE`jD1xbEgJhRntH%B*O&eT;y|!cH0MJHpMR&N(y^Wu z;MhvIHGmeKfTqGsp`|=nfKCde6XK>tqOoq-ZALz>KG3y<`<#UFR2dc?bzI39#pM24 zH7!JrEGQ8w$#8aDdBVIdV~eD6!SNyuW;=`1+6~9DAOVk?mkOfI11Q~N0yIr?)Ke2P zT8czAo7Tb_yYbPqqD|1BfSDf8q3CDEwPe|JQo`6boyt#WQBxDyLN-5Bw31suER^qa;!6{N1dXjI`&Dr9wi zQ+Ps7Pe~ZENRycTMN#I-!39LXU{s=6k9?jd7*A(MWl5gq-2}o9s8mkE^fBhJk6{uV zxmTEwCW{F;RQ@od3lw;;P)L2Qy{zC-RZ2Eo5`;q`Z;wbhIUO=V!MOtFyCkgqpu)jo z(OAf29g82MdkY+;dXs`>AcYysm`=!Y!9*%YoyDuhL>?txg-6&r8Bb+K@|4!XA5CJq zA`XI(Oa>)hX)0A+IhMomW0;Vq3Q#FCl5{MdlSanlN2QSBU2~0e@K9h$t;BCFQ^owm z=7WtRsE<1m2~=!_#ulk@u)}P3``jKJT4A;|9%?!}JsOe3k=^le`B+LmuB0HGo5zY7 zUQuptlHN!W3IpSDkWXeYwStQ6@|;`tFy5iF0z z(|O8$Fs#(OmaNiez@vG_hXhr78_bK_C6K*eOqY?VKJ`?n0- zqLfEPIi0EsN3ZZx_O^a2lFjZL#`66o816-*wX@QU?)43%&r|^pY%2ob<DAU*X|&Ktp<`g3 zLW>zs+SL{Y%^59$(_n=+G0Icx;k-4T%#Y3}%@A50Q}}GWpunrB&1FfEacY%V9?4t$ zCZ(l+V|fLq;2()6M-u52nqt3XO2o}m#Zj{zQ$#MGmyVZ1J?wRvTFT+%40T6Gh})4g zmIu;6k@s^osL8ISHmsay>x42~MC@obIRoYqLRU;mCDH%MPN8MU6wu?5bJTCC(I3r= z=6MMUHNlN$Gx~_w)QeWQfF)VUY!2hQ-8CgZ<*5m=Ws%r?kVTs{ZNexFA}5Z7Oj;9y zA&Sz2PzTUdQI&dq*2WgEfQ%oxn((|XH~hyU`Gha zgG!}jE@PwSTsDhNvkIXW-f*cun?x6_1?Gx+7X^>HW`}V`^M-L;(;Ya9hX#6Nv8I9QJFwv8B9x9Xb1jiHhaX9 zkqcHiGqP=~HJ?0!dZT`NY;-amvNiA_Pc{=eX2+?hga&-SZf{hTt0_=>?PjgdjxmAS zO-D$hXFi+klfXvVUPvfrs-36&yJtl`(}rQixBXU!_*ZG!{S& zO?#^_3(m&`I!hOBNHCSZTIV)5dm*Y-lrqF#V8L~-I=Jbx5ctZzIQwL8uGDd?EMDh+v=&u^n5o1c*)mSPeXnd*X zU+RH3LZjyt|135YAvH!-{f$B^Do;v`o{dWKkzljk{ zl*p!VLuNETg=&Sjw0{wTFP0P35p^x)huYE#w- zS)Om4b)3~8l%+gp^R^H`85a?h z+E?P;DjZyWML?oyakNBO2l4amz_ z$FWLC>nmGq(rB@OZCSMHqIT73EWMB?y65e*$`bbM#|tV8Xm*Jr#&Y9+wO1_VQ&*#t zM{5?vNo`r9YSPpx#J z8CTX^)%56mVof$=QpCsn1C~D2M_PZd3_I3&Fnq?K27^qRB~?@Av62lr`J|%WwVkgU z)n2%QUSF+$oh2>oBCEJdnN_YHn#4!Wq91&&{=4$*qITgm$yHyO?@{fi%#M{;9|BnZ z&5frrW{f!eUf8mBuQ1A^U})M{@xo53DT7uT6=9YHlUd5J6iYN(2``&YGbx$4U>Gxs zg{C9wBW{lIq*<9%!=%LP%&94ewAA@@7S`x&b@)6^hr{k#fQ-7-aU{Tt5?LxPU{2RY zxb|~ZCrK(z$)RZQRiJ4;9YLZ%0qd}iCY*gm@XnM6qn=UDv#HA9m zxv941jmo;xbop3)-fT%HjSu;9nP~#=JIEQE3GDHen^d3{a!Z?20B#LPDJdvTThdtQ zE==HV%NAuWMv|rvrVgpg9_qLfD_csIU=pLY2LGiQ3cV`h(&SU>7`R+SC31x35Y?G5 zsy^~kC#EdM@v@SPSt;m)>b;U)RG>+0SHl>2!C0icU}D0Tu*_i4g6-28Od4thSg#*~ z#k5kXYELvD1k#TewIQ>T;={^ zQ|*e=4uuj`<^JMR#XH^QL~(o3X{K^z#kdQ17n|a?ELIT8>RRRgqEpSq$HuUewWQu{ zYRtu_EXzmCeRxTroe1-Zhn8UsrDpfbAKj#ydPi%owgra5ISmzIcYF&CfmH1>~`)ge1&9&yp>VheMwoGib;_*C&) zIGqnwrf7OooT4r|?P9*XAnl~vi%qjPo!k;t-s&hxj7Uk1{Qp6}KQVz#VDrf zu`s`p=v5*vHr+lM2Yi$S=C>D}CT5Q@U4)TU8DTx{;!~#+7|KR3ee{BxoV@6Ci#Rx_ zy!Pr-A}%)Fe!Ty&jg_=baT1Ta=+qPF?w0jZR>3F5CE~EHhy~N?WQ^ zcBNxGm?_+kB`7*_S}9fPf{nUlbIt`& zDno6unXjz{lzj<~)>bUnDjPoiZn{~pba_7Op!QaLW!~$-8weM8v6Chnwd~Ncx-8e# z{8#f+&oFiOY5BURX1mx%t_4dW9dHb_I^G__s|{%l81xnNlqfuaw|S39v?qgBI*AVL zKEQjd#k@tcEd{@}+)8&UCB+L)*uteZ0QV~Asq>N-wBnJ@i^>M}L272pA1URnQQ5UX zMxy4cxAlrZqYm9zg2vuHuqjbZ?$=*`S9sNE%}d>ZMct&-oAFD%i2~l%%}I7=i-a7u z{RJRpI49@)(k?4DTav2IiF`*2`v($WWH56CL+s37NLsELiC$X;S2ma3=XOgt#Y*0s zZ<6v^yk3}7ad4uPSr@f%0==>Wf?gS-O*^WsQQ1Vy>l9Aa02b^*I7jJpx@j|oUOG`Y zRZwl;!T(h{7bI7Ez5tO%)_S*SSqT~Io(q0pk}BSTAD#FaG`jZK{PsB9DG0dydW*jl9WqrOJ2 z(S=Xg&{ZV*gq;6r{`|dKXYaM)?5Mcri%`D7Y{AAHO_o5}Mjftt^TgPy*9Q9TcFhd5{p|}V zs%`x|F7zy*%vBT22ZI&i86J`yBCqDFqs(Yj&G_xQJx|!Bu*PU7_L`w>K=rvZK84Ne z6LvM-3i*~rmKT1pPb>p|tkzWOn|mIax4Pld=)?@Hag##>8t zNASj1`Kw$UfNx6V{1JeHMBnMd8CN#g=zU7Oq^3$Tro4csy>BO7qhz1f`Lv?W(cYw6 z05VYi{@qz{WpCgufSdO|o(4y4fzDn1Lhcs4pLY#c5H~ZOPsQ>%t7i$<&r3+z+Yr=8 z)+p;3_&a_Jc-7wcL*nvsz_U^ZJnQ|x`Kmp~%YX}&toDTX+kgu==u|pM=YBU(I!lP0 zjPK7B%=+;jXX&DkC%}hl)U&pwy{T-Pzm&^20P~_tMT}S9nhB1LP_@dftfuD4Pw?){ zIlpgXDP`hc*Z{jgMCxEEyH#oP>R(_Znb7!%r1DTQ`Z4G-;rJr#OH~dYqQ0bZ#xxza zja>%?9HWUBw$gn6FVzTnYz$7}coa+|OE^nKs{_$H8q0yWo~dV<;8{!(Q5narpuF;( zvBwdi9KJ&#iFa@(Wr?5niSv^9!9eJ96>%0TPSrtslb018($-{!HQJd>uMr-XM{#J2 zBA0T+3wA={@RfX{grhy^WbBz1sb>tYlS$e+F+@+rN}mCu!$$C;HN+wf-~^gcoQ90| z?tvOIXBrF2kxw{09o00?ejJHIx=Lo{JUyP2!E%Zq=`~&pQRDq~oKXZ})Z=^Td?yH` z^Q&kZ!sARH{CQ~`4nXi<>I1i`B;4s)3 t3)KzWf_Z(5kW%B)&PZ - logger.info("") - } - StringUtils.substringBefore(idStr, ".") - } - - - private def checkDirectoryExists(directory: String): Unit = { - val file = new File(directory) - if (!file.exists) { - logger.info("File directory does not exist." + file.getName) - file.mkdirs - } - } - - def generateQrCode(uuid: String, directory: String, basePath: String): QrCodeModel = { - checkDirectoryExists(directory) - val accessCodeGenerator: AccessCodeGenerator = new AccessCodeGenerator - val accessCode = accessCodeGenerator.generate() - val qrCodeGenerationModel = QRCodeGenerationModel(text = accessCode, fileName = directory + uuid, data = basePath + "/" + uuid) - val qrCodeImageGenerator = new QRCodeImageGenerator() - val qrCodeFile = qrCodeImageGenerator.createQRImages(qrCodeGenerationModel) - QrCodeModel(accessCode, qrCodeFile) - } - -} diff --git a/credential-generator/certificate-processor/src/main/scala/org/sunbird/incredible/HttpUtil.scala b/credential-generator/certificate-processor/src/main/scala/org/sunbird/incredible/HttpUtil.scala deleted file mode 100755 index 15cb5a904..000000000 --- a/credential-generator/certificate-processor/src/main/scala/org/sunbird/incredible/HttpUtil.scala +++ /dev/null @@ -1,19 +0,0 @@ -package org.sunbird.incredible - -import kong.unirest.Unirest - -case class HTTPResponse(status: Int, body: String) - -class HttpUtil extends java.io.Serializable { - - def get(url: String): HTTPResponse = { - val response = Unirest.get(url).header("Content-Type", "application/json").asString() - HTTPResponse(response.getStatus, response.getBody) - } - - def post(url: String, requestBody: String): HTTPResponse = { - val response = Unirest.post(url).header("Content-Type", "application/json").body(requestBody).asString() - HTTPResponse(response.getStatus, response.getBody) - } - -} diff --git a/credential-generator/certificate-processor/src/main/scala/org/sunbird/incredible/JsonKeys.scala b/credential-generator/certificate-processor/src/main/scala/org/sunbird/incredible/JsonKeys.scala deleted file mode 100755 index 6c2a17830..000000000 --- a/credential-generator/certificate-processor/src/main/scala/org/sunbird/incredible/JsonKeys.scala +++ /dev/null @@ -1,123 +0,0 @@ -package org.sunbird.incredible - -object JsonKeys { - val CLAIM: String = "claim" - val SIGNATURE_VALUE: String = "signatureValue" - val KEY_ID: String = "keyId" - val SIGN_CREATOR: String = "SIGN_CREATOR" - val SIGN_URL: String = "SIGN_URL" - val SIGN_VERIFY_URL: String = "SIGN_VERIFY_URL" - val DOMAIN_URL: String = "sunbird_cert_domain_url" - val BADGE_URL: String = "/Badge.json" - val ISSUER_URL: String = "ISSUER_URL" - val EVIDENCE_URL: String = "EVIDENCE_URL" - val CONTEXT: String = "CONTEXT" - val VERIFICATION_TYPE: String = "VERIFICATION_TYPE" - val PUBLIC_KEY_URL: String = "_publicKey.json" - val HOSTED: String = "hosted" - val CONTAINER_NAME: String = "CONTAINER_NAME" - val ROOT_ORG_ID: String = "orgId" - val TAG: String = "tag" - val ID: String = "id" - val ENTITY: String = "entity" - val CLOUD_STORAGE_TYPE: String = "CLOUD_STORAGE_TYPE" - val CLOUD_UPLOAD_RETRY_COUNT: String = "CLOUD_UPLOAD_RETRY_COUNT" - val AZURE_STORAGE_SECRET: String = "AZURE_STORAGE_SECRET" - val AZURE_STORAGE_KEY: String = "AZURE_STORAGE_KEY" - val AZURE: String = "azure" - val AWS: String = "aws" - val AWS_STORAGE_KEY: String = "AWS_STORAGE_KEY" - val AWS_STORAGE_SECRET: String = "AWS_STORAGE_SECRET" - val SLUG: String = "sunbird_cert_slug" - val ACCESS_CODE_LENGTH: String = "ACCESS_CODE_LENGTH" - val PATH: String = "path" - val PREVIEW: String = "preview" - val ASSETS: String = "assets" - val TYPE: String = "type" - val BUCKET_NAME: String = "BUCKET_NAME" - val KEY: String = "key" - val ACCOUNT: String = "account" - val containerName: String = "containerName" - val ENC_SERVICE_URL: String = "sunbird_cert_enc_service_url" - val SIGN: String = "sign" - val VERIFY: String = "verify" - val BASE_PATH: String = "basePath" - val DATA: String = "data" - val MESSAGE: String = "message" - val OK: String = "ok" - val RESPONSE: String = "response" - val SUCCESS: String = "success" - val CERTIFICATE: String = "certificate" - val RECIPIENT_NAME: String = "recipientName" - val COURSE_NAME: String = "courseName" - val NAME: String = "name" - val HTML_TEMPLATE: String = "htmlTemplate" - val ISSUER: String = "issuer" - val URL: String = "url" - val SIGNATORY_LIST: String = "signatoryList" - val DESIGNATION: String = "designation" - val SIGNATORY_IMAGE: String = "image" - val SIGNATORY_EXTENSION: String = "SIGNATORY_EXTENSION" - val RECIPIENT_EMAIl: String = "recipientEmail" - val RECIPIENT_PHONE: String = "recipientPhone" - val RECIPIENT_ID: String = "recipientId" - val VALID_FROM: String = "validFrom" - val EXPIRY: String = "expiry" - val PUBLIC_KEY: String = "publicKey" - val DESCRIPTION: String = "description" - val LOGO: String = "logo" - val ISSUE_DATE: String = "issuedDate" - val CERTIFICATE_NAME: String = "name" - val KEYS: String = "keys" - val JSON_URL: String = "jsonUrl" - val PDF_URL: String = "pdfUrl" - val UNIQUE_ID: String = "id" - - val GET_SIGN_URL: String = "getSignUrl" - val SIGNED_URL: String = "signedUrl" - val ACCESS_CODE: String = "accessCode" - val JSON_DATA: String = "jsonData" - val STORE: String = "store" - val key: String = "key" - val VERIFY_CERT: String = "verifyCert" - val UUID: String = "uuid" - val SIGNATURE: String = "signature" - val VERIFICATION: String = "verification" - val CREATOR: String = "creator" - val SIGNED_BADGE: String = "SignedBadge" - val EXPIRES: String = "expires" - val CRITERIA: String = "criteria" - val NARRATIVE: String = "narrative" - val RESULT: String = "result" - val REQUEST: String = "request" - val QR_IMAGE_URL: String = "qrImageUrl" - val QR_CODE_FILE: String = "qrCodeFile" - val PUBLIC: String = "public" - val PRIVATE: String = "private" - val PUBLIC_AZURE_STORAGE_KEY: String = "PUBLIC_AZURE_STORAGE_KEY" - val PUBLIC_AZURE_STORAGE_SECRET: String = "PUBLIC_AZURE_STORAGE_SECRET" - val PUBLIC_AWS_STORAGE_KEY: String = "PUBLIC_AWS_STORAGE_KEY" - val PUBLIC_AWS_STORAGE_SECRET: String = "PUBLIC_AWS_STORAGE_SECRET" - val PUBLIC_CONTAINER_NAME: String = "PUBLIC_CONTAINER_NAME" - val VALIDATE_TEMPLATE: String = "validateTemplate" - val TEMPLATE_URL: String = "templateUrl" - val QR_CODE_URL: String = "qrCodeUrl" - val VERSION_2: String = "v2" - val VERSION_1: String = "v1" - val VERSION: String = "version" - val GENERATE_CERT_V2: String = "generateCertV2" - val SVG_TEMPLATE: String = "svgTemplate" - val REQ_ID: String = "reqId" - val REQUEST_MESSAGE_ID: String = "msgId" - val X_REQUEST_ID: String = "X-Request-ID" - val CERT_NAME: String = "certificateName" - val CERTIFICATE_DESCIPTION: String = "certificateDescription" - val SIGNATORY_0_IMAGE: String = "signatory0Image" - val SIGNATORY_0_DESIGNATION: String = "signatory0Designation" - val SIGNATORY_1_IMAGE: String = "signatory1Image" - val SIGNATORY_1_DESIGNATION: String = "signatory1Designation" - val QRCODE_IMAGE: String = "qrCodeImage" - val EXPIRY_DATE: String = "expiryDate" - val ISSUER_NAME: String = "issuerName" - val EDATA = "edata" -} diff --git a/credential-generator/certificate-processor/src/main/scala/org/sunbird/incredible/ScalaModuleJsonUtils.scala b/credential-generator/certificate-processor/src/main/scala/org/sunbird/incredible/ScalaModuleJsonUtils.scala deleted file mode 100755 index 3fd11a157..000000000 --- a/credential-generator/certificate-processor/src/main/scala/org/sunbird/incredible/ScalaModuleJsonUtils.scala +++ /dev/null @@ -1,32 +0,0 @@ -package org.sunbird.incredible - - -import java.io.File - -import com.fasterxml.jackson.annotation.JsonInclude.Include -import com.fasterxml.jackson.core.JsonGenerator.Feature -import com.fasterxml.jackson.databind.{DeserializationFeature, ObjectMapper, SerializationFeature} -import com.fasterxml.jackson.module.scala.DefaultScalaModule - -object ScalaModuleJsonUtils { - - @transient val mapper = new ObjectMapper() - mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) - mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false) - mapper.configure(Feature.WRITE_BIGDECIMAL_AS_PLAIN, true) - mapper.setSerializationInclusion(Include.NON_NULL) - mapper.setSerializationInclusion(Include.NON_ABSENT) - mapper.registerModule(DefaultScalaModule) - - - @throws(classOf[Exception]) - def writeToJsonFile(file: File, obj: AnyRef) = { - mapper.writeValue(file, obj) - } - - @throws(classOf[Exception]) - def serialize(obj: AnyRef): String = { - mapper.writeValueAsString(obj) - } - -} diff --git a/credential-generator/certificate-processor/src/main/scala/org/sunbird/incredible/UrlManager.scala b/credential-generator/certificate-processor/src/main/scala/org/sunbird/incredible/UrlManager.scala deleted file mode 100755 index 55306e559..000000000 --- a/credential-generator/certificate-processor/src/main/scala/org/sunbird/incredible/UrlManager.scala +++ /dev/null @@ -1,50 +0,0 @@ -package org.sunbird.incredible - -import java.net.{MalformedURLException, URL} - -import org.apache.commons.lang3.StringUtils -import org.slf4j.{Logger, LoggerFactory} - -object UrlManager { - private val logger: Logger = LoggerFactory.getLogger(getClass.getName) - - def getSharableUrl(url: String, containerName: String): String = { - var uri: String = null - uri = removeQueryParams(url) - uri = fetchFileFromUrl(uri) - removeContainerName(uri, containerName) - } - - def removeQueryParams(url: String): String = if (StringUtils.isNotBlank(url)) url.split("\\?")(0) else url - - private def fetchFileFromUrl(url: String) = try { - val urlPath = new URL(url) - urlPath.getFile - } catch { - case e: Exception => - logger.error("UrlManager:getUriFromUrl:some error occurred in fetch fileName from Url:".concat(url)) - StringUtils.EMPTY - } - - private def removeContainerName(url: String, containerName: String) = { - val containerNameStr = "/".concat(containerName) - logger.info("UrlManager:removeContainerName:container string formed:".concat(containerNameStr)) - url.replace(containerNameStr, "") - } - - /** - * getting substring from url after domainUrl/slug - * for example for the url domainUrl/slug/tagId/uuid.pdf then return tagId/uuid.pdf - * - * @param url - * @return - * @throws MalformedURLException - */ - @throws[MalformedURLException] - def getContainerRelativePath(url: String): String = if (url.startsWith("http")) { - val uri = StringUtils.substringAfter(new URL(url).getPath, "/") - val path = uri.split("/") - StringUtils.join(path, "/", path.length - 2, path.length) - } - else url -} diff --git a/credential-generator/certificate-processor/src/main/scala/org/sunbird/incredible/pojos/Gender.scala b/credential-generator/certificate-processor/src/main/scala/org/sunbird/incredible/pojos/Gender.scala deleted file mode 100755 index 89753bae7..000000000 --- a/credential-generator/certificate-processor/src/main/scala/org/sunbird/incredible/pojos/Gender.scala +++ /dev/null @@ -1,5 +0,0 @@ -package org.sunbird.incredible.pojos - -class Gender extends Enumeration { - val OTHER, MALE, FEMALE = Value -} diff --git a/credential-generator/certificate-processor/src/main/scala/org/sunbird/incredible/pojos/exceptions/InvalidDateFormatException.scala b/credential-generator/certificate-processor/src/main/scala/org/sunbird/incredible/pojos/exceptions/InvalidDateFormatException.scala deleted file mode 100755 index cd9dbf166..000000000 --- a/credential-generator/certificate-processor/src/main/scala/org/sunbird/incredible/pojos/exceptions/InvalidDateFormatException.scala +++ /dev/null @@ -1,6 +0,0 @@ -package org.sunbird.incredible.pojos.exceptions - -class InvalidDateFormatException(msg: String) extends Exception(msg) {} - - -class ServerException(code: String, msg: String) extends Exception(msg) {} diff --git a/credential-generator/certificate-processor/src/main/scala/org/sunbird/incredible/pojos/ob/Models.scala b/credential-generator/certificate-processor/src/main/scala/org/sunbird/incredible/pojos/ob/Models.scala deleted file mode 100755 index 852f1cc84..000000000 --- a/credential-generator/certificate-processor/src/main/scala/org/sunbird/incredible/pojos/ob/Models.scala +++ /dev/null @@ -1,232 +0,0 @@ -package org.sunbird.incredible.pojos.ob - -import com.fasterxml.jackson.annotation.JsonProperty -import org.sunbird.incredible.pojos.Gender - - -class Models extends Serializable {} - -/** - * - * @param context - * @param related Identifies a related version of the entity. - * @param version The version identifier for the present edition of the entity. - * @param endorsement A claim made about this entity. - */ -case class OBBase(@JsonProperty("@context") context: String, related: Array[String], version: String, endorsement: Endorsement) - - -/** - * - * @param id Unique IRI for the Endorsement instance. - * If using hosted verification, this should be the URI where the assertion of endorsement is accessible. - * For signed Assertions, it is recommended to use a UUID in the urn:uuid namespace. - * @param `type` - * @param claim An entity, identified by an id and additional properties that the endorser would like to claim about that entity. - * @param issuer The profile of the Endorsement’s issuer. - * @param issuedOn Timestamp of when the endorsement was published. - * @param context - */ -case class Endorsement(@JsonProperty("@context") context: String, related: Array[String], version: String, id: String, `type`: Array[String] = Array("Endorsement"), claim: String, issuer: Profile, issuedOn: String, verification: VerificationObject) - - -/** - * - * @param id IRI to the awarding body, assessor or training body. - * @param `type` - * @param name Name of the awarding body, assessor or training body. - * @param email - * @param url URL of the awarding body, assessor or training body - * @param description Short description - * @param publicKey URLs to type CryptographicKeys - * @param revocationList List of HTTP URLs of the signed badges that are revoked - * @param telephone A phone number - Part of OpenBadges, not mentioned in inCredible - */ -case class Profile(@JsonProperty("@context") context: String, related: Array[String], version: String, endorsement: Endorsement, id: String, `type`: Array[String], name: String, email: String, url: String, description: String, - publicKey: Array[String], revocationList: Array[String], telephone: String, verification: VerificationObject) - - -/** - * - * @param id HTTP URL or UUID from urn:uuid namespace - * @param `type` Simple string "Assertion" or URLs or IRIs of current context - * @param issuedOn DateTime string compatible with ISO 8601 guideline For example, 2016-12-31T23:59:59+00:00 - * @param recipient - * @param badge - * @param image IRI or document representing an image representing this user’s achievement.This must be a PNG or SVG image. Otherwise, use BadgeClass member. - * @param evidence - * @param expires DateTime string compatible with ISO 8601 guideline,For example, 2016-12-31T23:59:59+00:00 - * @param verification - * @param narrative A narrative that connects multiple pieces of evidence - * @param revoked Defaults to false if Assertion is not referenced from a revokedAssertions list and may be omitted. - * @param revocationReason Optional published reason for revocation, if revoked. - */ -case class Assertion(@JsonProperty("@context") context: String, related: Array[String], version: String, endorsement: Endorsement, - id: String, `type`: Array[String], issuedOn: String, recipient: CompositeIdentityObject, badge: BadgeClass, image: String - , evidence: Evidence, expires: String, verification: VerificationObject, narrative: String - , revoked: Boolean = false, revocationReason: Option[String]) - - -/** - * @param identity Value of the annotation Example: father's or a spouse's name - * @param `type` IRI of the property by which the recipient of a badge is identified. - * @param hashed Whether or not the identity value is hashed. - * @param salt If the recipient is hashed, this should contain the string used to salt the hash. - */ -case class IdentityObject(@JsonProperty("@context") context: String, related: Array[String], version: String, endorsement: Endorsement, identity: String, `type`: Array[String], hashed: String, salt: String) - -/** - * - * @param context - * @param targetName Name of the alignment - * @param targetURL URL linking to the official description of the alignment target - * @param targetDescription Short description of the alignment target - * @param targetFramework Name of the framework the alignment target - * @param targetCode If applicable, a locally unique string identifier that identifies the alignment target within its framework and/or targetUrl - */ -case class AlignmentObject(@JsonProperty("@context") context: String, related: Array[String], version: String, endorsement: Endorsement, targetName: String, targetURL: String, targetDescription: String, targetFramework: String, targetCode: String) - -/** - * - * @param `type` The type of verification method. Supported values for single assertion verification are HostedBadge and SignedBadge (aliases in context are available: hosted and signed) - * @param verificationProperty The @id of the property to be used for verification that an Assertion is within the allowed scope. Only id is - * * supported. Verifiers will consider id the default value if verificationProperty is omitted or if an issuer - * * Profile has no explicit verification instructions, so it may be safely omitted. - * @param startsWith The URI fragment that the verification property must start with. Valid Assertions must have an id within this - * * scope. Multiple values allowed, and Assertions will be considered valid if their id starts with one of these values. - * @param allowedOrigins The host registered name subcomponent of an allowed origin. Any given id URI will be considered valid. - * * Refer to https://tools.ietf.org/html/rfc3986#section-3.2.2 - * * Example: ["example.org", "another.example.org"] - */ -case class VerificationObject(@JsonProperty("@context") context: String, related: Array[String], version: String, endorsement: Endorsement, `type`: Array[String], verificationProperty: String, startsWith: String, allowedOrigins: List[String]) extends Serializable {} - -/** - * - * @param `type` - * @param verificationProperty - * @param startsWith - * @param allowedOrigins - * @param creator The (HTTP) id of the key used to sign the Assertion. If not present, verifiers will check public key(s) declared - * * in the referenced issuer Profile. If a key is declared here, it must be authorized in the issuer Profile as well. - * * creator is expected to be the dereferencable URI of a document that describes a CryptographicKey - */ -case class SignedVerification(@JsonProperty("@context") context: Option[String] = None, related: Option[Array[String]] = None, version: Option[String] = None, endorsement: Option[Endorsement] = None, `type`: Array[String] = Array("SignedBadge"), verificationProperty: Option[String] = None, startsWith: Option[String] = None, allowedOrigins: Option[List[String]] = None, creator: Option[String] = None) - -case class HostedVerification(@JsonProperty("@context") context: String, related: Array[String], version: String, endorsement: Endorsement, `type`: Array[String] = Array("HostedBadge"), verificationProperty: String, startsWith: String, allowedOrigins: List[String]) - -/** - * - * @param id Unique IRI for the badge - HTTP URL or URN - * @param `type` In most cases, this will simply be the string BadgeClass. An array including BadgeClass and other string elements that are either URLs or compact IRIs within the current context are allowed. - * @param name Name of the achievement represented by this class Example: "Carpentry know-how level 1" - * @param description Description of the badge - * @param image HTTP URL to the image of this credential - * @param criteria HTTP URL to the issuer of this credential - Profile - */ -case class BadgeClass(@JsonProperty("@context") context: String, related: Option[Array[String]] = None, endorsement: Option[Endorsement] = None, id: String, `type`: Array[String] = Array("BadgeClass"), name: String, description: String, version: Option[String] = None, image: String, criteria: Criteria, issuer: Issuer, alignmentObject: Option[AlignmentObject] = None) - -/** - * - * @param id IRI - * @param `type` Type of the criteria, defaults to "Criteria" - * @param narrative A narrative of what is needed to earn the badge - */ -case class Criteria(@JsonProperty("@context") context: Option[String] = None, related: Option[Array[String]] = None, endorsement: Option[Endorsement] = None, id: Option[String] = None, `type`: Array[String] = Array("Criteria"), narrative: String) - -case class CryptographicKey(@JsonProperty("@context") context: String, related: Array[String], version: String, endorsement: Endorsement, `type`: Array[String] = Array("CryptographicKey"), id: String, owner: String, publicKeyPem: String) - -/** - * - * @param name Name of the awarding body, assessor or training body. - * @param publicKey URLs to type CryptographicKeys - */ -case class Issuer(@JsonProperty("@context") context: String, related: Option[Array[String]] = None, version: Option[String] = None, endorsement: Option[Endorsement] = None, id: Option[String] = None, `type`: Array[String] = Array("Issuer"), name: String, email: Option[String] = None, url: String, publicKey: Option[Array[String]] = None) - -/** - * - * @param id identifies the evidence from the urn:uuid namespace or a HTTP URL of a webpage which presents the evidence - * @param narrative A narrative that describes the evidence and process of achievement that led to an Assertion - * @param name Descriptive title of the evidence - * @param description Longer description of the evidence - * @param genre Describes the type of evidence, such as Certificate, Painting, Artefact, Medal, Video, Image. - * @param audience Description of the intended audience for a piece of evidence - */ -class Evidence(@JsonProperty("@context") context: String, related: Option[Array[String]] = None, version: Option[String] = None, endorsement: Option[Endorsement] = None, id: String, `type`: Array[String], narrative: Option[String] = None, name: String, description: Option[String] = None, genre: Option[String] = None, audience: Option[String] = None) - -/** - * @param narrative - * @param name - * @param description - * @param genre - * @param audience - * @param subject Subject of the evidence - * @param assessment Assessment conducted which elicited the evidence - * @param assessedBy HTTP URL identifier to a JSON-LD object or an embedded profile of the individual(s) or organisation(s) who assessed the competency - * @param assessedOn DateTime when the assessment was conducted - * @param signature - */ -case class AssessedEvidence(@JsonProperty("@context") context: String, related: Array[String], version: String, endorsement: Endorsement, - id: String, `type`: Array[String] = Array("Evidence", "Extension", "extensions:AssessedEvidence"), narrative: String, - name: String, description: String, genre: String, audience: String, subject: String, assessment: Assessment, assessedBy: String, - assessedOn: String, signature: Signature) - -case class Assessment(@JsonProperty("@context") context: String, related: Array[String], version: String, endorsement: Endorsement, `type`: Array[String] = Array("Extension", "extensions:Assessment"), value: Float) - -/** - * - * @param `type` - * @param components - * @param name - * @param photo A HTTP URL to a printable version of this certificate.The URL points to a base64 encoded data, like data:application/pdf;base64 - * * or data:image/jpeg;base64 - * @param dob Date of birth in YYYY-MM-DD format. - * @param gender - * @param tag Tag URI, if URN is used - * @param urn Uniform resource name Required in the absence of URL - * @param url Uniform resource locator Required in the absence of URN - */ -case class CompositeIdentityObject(@JsonProperty("@context") context: String, related: Option[Array[String]] = None, version: Option[String] = None, endorsement: Option[Endorsement] = None, identity: String, `type`: Array[String] = Array("Extension", "IdentityObject", "extensions:CompositeIdentityObject"), hashed: Boolean, salt: Option[String] = None, - components: Option[List[CompositeIdentityObject]] = None, - name: String, - photo: Option[String] = None, dob: Option[String] = None, gender: Option[Gender] = None, tag: Option[String] = None, urn: Option[String] = None, url: Option[String] = None) - -/** - * - * @param creator Who created the signature? - * @param created Date timestamp of when the signature was created - * @param signatureValue The signature hash of the certificate - */ -case class Signature(`type`: String = "LinkedDataSignature2015", creator: String, created: String, signatureValue: String) - -/** - * An extension to Assertion class - */ -case class CertificateExtension(@JsonProperty("@context") context: String, related: Option[Array[String]] = None, version: Option[String] = None, endorsement: Option[Endorsement] = None, - id: String, issuedOn: String, recipient: CompositeIdentityObject, badge: BadgeClass, image: Option[String] = None - , var evidence: Option[TrainingEvidence] = None, expires: String, verification: Option[VerificationObject] = None, narrative: Option[String] = None - , revoked: Boolean = false, revocationReason: Option[String] = None, `type`: Array[String] = Array("Assertion", "Extension", "extensions:CertificateExtension"), - value: Option[Float] = None, awardedThrough: Option[String] = None, signatory: Array[SignatoryExtension], - var printUri: Option[String] = None, validFrom: String, var signature: Option[Signature] = None) - -/** - * - * @param designation Designation or capacity of the signatory - * @param image HTTP URL or a data URI for an image associated with the signatory - * @param publicKey - * @param name - */ -case class SignatoryExtension(@JsonProperty("@context") context: String, related: Option[Array[String]] = None, version: Option[String] = None, endorsement: Option[Endorsement] = None, identity: String, `type`: Array[String] = Array("Extension", "extensions:SignatoryExtension"), hashed: Option[String] = None, salt: Option[String] = None, designation: String, image: String, publicKey: Option[CryptographicKey] = None, name: String) - - -case class TrainingEvidence(@JsonProperty("@context") context: String, related: Option[Array[String]] = None, - version: Option[String] = None, endorsement: Option[Endorsement] = None, id: String, - `type`: Array[String] = Array("Evidence", "Extension", "extensions:TrainingEvidence"), - narrative: Option[String] = None, name: String, description: Option[String] = None, - genre: Option[String] = None, audience: Option[String] = None, subject: Option[String] = None - , trainedBy: Option[String] = None, duration: Option[Duration] = None, session: Option[String] = None) - -case class Duration(startDate: String, endDate: String) - -case class MarksAssessment(minValue: Float, maxValue: Float, passValue: Float, @JsonProperty("@context") context: String, related: Array[String], version: String, endorsement: Endorsement, `type`: Array[String] = Array("Extension", "extensions:MarksAssessment"), value: Float) - -case class RankAssessment(maxValue: Float, @JsonProperty("@context") context: String, related: Array[String], version: String, endorsement: Endorsement, `type`: Array[String] = Array("Extension", "extensions:RankAssessment"), value: Float) diff --git a/credential-generator/certificate-processor/src/main/scala/org/sunbird/incredible/pojos/valuator/ExpiryDateValuator.scala b/credential-generator/certificate-processor/src/main/scala/org/sunbird/incredible/pojos/valuator/ExpiryDateValuator.scala deleted file mode 100755 index d302236cd..000000000 --- a/credential-generator/certificate-processor/src/main/scala/org/sunbird/incredible/pojos/valuator/ExpiryDateValuator.scala +++ /dev/null @@ -1,68 +0,0 @@ -package org.sunbird.incredible.pojos.valuator - -import java.text.SimpleDateFormat -import java.util.regex.{Matcher, Pattern} -import java.util.{Calendar, Date} - -import org.sunbird.incredible.pojos.exceptions.InvalidDateFormatException - -class ExpiryDateValuator(var issuedDate: String) extends IEvaluator { - - override def evaluates(expiryDate: String): String = { - /** - * regex of the date format yyyy-MM-dd'T'HH:mm:ss'Z' - */ - val pattern: String = "\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}Z" - if (expiryDate.matches(pattern)) { - expiryDate - } else { - if (issuedDate == null) { - throw new InvalidDateFormatException("Issued date is null, please provide valid issued date ") - } else { - - /** - * to split expiry dates of form (2m 2y) - */ - val splitExpiry: Array[String] = expiryDate.split(" ") - val cal: Calendar = Calendar.getInstance - val simpleDateFormat: SimpleDateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'") - val parsedIssuedDate: Date = simpleDateFormat.parse(issuedDate) - cal.setTime(parsedIssuedDate) - for (expiry <- splitExpiry) { - val string: String = checkValid(expiry) - string.toLowerCase() match { - case "d" => cal.add(Calendar.DATE, getDigits(expiry)) - case "m" => cal.add(Calendar.MONTH, getDigits(expiry)) - case "y" => cal.add(Calendar.YEAR, getDigits(expiry)) - case _ => //break - - } - } - simpleDateFormat.format(cal.getTime) - } - } - } - - private def getDigits(string: String): Int = { - val pattern: Pattern = Pattern.compile("^\\d+") - val matcher: Matcher = pattern.matcher(string) - if (matcher.find()) { - java.lang.Integer.parseInt(matcher.group(0)) - } else { - 0 - } - } - - private def checkValid(string: String): String = { - val pattern: Pattern = Pattern.compile("^\\d+[MYDmyd]{1}$") - val matcher: Matcher = pattern.matcher(string) - if (matcher.find()) { - matcher.group(0).substring(matcher.group(0).length - 1) - } else { - throw new InvalidDateFormatException( - "Given expiry date is invalid" + string) - } - } - -} - diff --git a/credential-generator/certificate-processor/src/main/scala/org/sunbird/incredible/pojos/valuator/IEvaluator.scala b/credential-generator/certificate-processor/src/main/scala/org/sunbird/incredible/pojos/valuator/IEvaluator.scala deleted file mode 100755 index d63893df3..000000000 --- a/credential-generator/certificate-processor/src/main/scala/org/sunbird/incredible/pojos/valuator/IEvaluator.scala +++ /dev/null @@ -1,8 +0,0 @@ -package org.sunbird.incredible.pojos.valuator - -import org.sunbird.incredible.pojos.exceptions.InvalidDateFormatException - -trait IEvaluator { - @throws[InvalidDateFormatException] - def evaluates(inputVal: String): String -} diff --git a/credential-generator/certificate-processor/src/main/scala/org/sunbird/incredible/pojos/valuator/IssuedDateValuator.scala b/credential-generator/certificate-processor/src/main/scala/org/sunbird/incredible/pojos/valuator/IssuedDateValuator.scala deleted file mode 100755 index f4f508301..000000000 --- a/credential-generator/certificate-processor/src/main/scala/org/sunbird/incredible/pojos/valuator/IssuedDateValuator.scala +++ /dev/null @@ -1,48 +0,0 @@ -package org.sunbird.incredible.pojos.valuator - -import java.text.{ParseException, SimpleDateFormat} -import java.util.{Calendar, Date} - -import org.apache.commons.lang.StringUtils -import org.sunbird.incredible.pojos.exceptions.InvalidDateFormatException - -import scala.util.control.Breaks - -class IssuedDateValuator extends IEvaluator { - private val dateFormats = List(new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"), new SimpleDateFormat("yyyy-MM-dd")) - private val loop = new Breaks - - @throws[InvalidDateFormatException] - override def evaluates(inputVal: String): String = { - val simpleDateFormat: SimpleDateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'") - val cal: Calendar = Calendar.getInstance - val date: Date = convertToDate(inputVal.asInstanceOf[String]) - cal.setTime(date) - simpleDateFormat.format(cal.getTime) - } - - def convertToDate(input: String): Date = { - var date: Date = null - if (StringUtils.isEmpty(input)) { - throw new InvalidDateFormatException("issued date cannot be null") - } - loop.breakable { - dateFormats.foreach(format => { - try { - format.setLenient(false) - date = format.parse(input) - } catch { - case e: ParseException => - } - if (date != null) { - loop.break - } - }) - } - if (date == null) { - throw new InvalidDateFormatException("issued date is not in valid format") - } else { - date - } - } -} diff --git a/credential-generator/certificate-processor/src/main/scala/org/sunbird/incredible/processor/CertModel.scala b/credential-generator/certificate-processor/src/main/scala/org/sunbird/incredible/processor/CertModel.scala deleted file mode 100755 index bcba10565..000000000 --- a/credential-generator/certificate-processor/src/main/scala/org/sunbird/incredible/processor/CertModel.scala +++ /dev/null @@ -1,8 +0,0 @@ -package org.sunbird.incredible.processor - -import org.sunbird.incredible.pojos.ob.{Criteria, Issuer, SignatoryExtension} - -case class CertModel(courseName: String, recipientName: String, recipientId: Option[String] = None, recipientEmail: Option[String] = None, recipientPhone: Option[String] = None - , certificateName: String, certificateDescription: Option[String] = None, certificateLogo: Option[String] = None, issuedDate: String, issuer: Issuer, - validFrom: Option[String] = None, expiry: Option[String] = None, signatoryList: Array[SignatoryExtension], assessedOn: Option[String] = None, identifier: String, criteria: Criteria, - keyId: String = "", tag: String = "") diff --git a/credential-generator/certificate-processor/src/main/scala/org/sunbird/incredible/processor/qrcode/AccessCodeGenerator.scala b/credential-generator/certificate-processor/src/main/scala/org/sunbird/incredible/processor/qrcode/AccessCodeGenerator.scala deleted file mode 100755 index db1c2ca37..000000000 --- a/credential-generator/certificate-processor/src/main/scala/org/sunbird/incredible/processor/qrcode/AccessCodeGenerator.scala +++ /dev/null @@ -1,59 +0,0 @@ -package org.sunbird.incredible.processor.qrcode - - -import scala.math.BigDecimal -import org.apache.commons.lang3.StringUtils - -import scala.util.matching.Regex - -class AccessCodeGenerator { - private val stripChars: String = "0" - private val largePrimeNumber = BigDecimal(1679979167) - private val regex: Regex = "[A-Z][0-9][A-Z][0-9][A-Z][0-9]".r - private val pattern: java.util.regex.Pattern = regex.pattern - - private val ALPHABET = Array[String]("1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F", "G", "H", "J", "K", "L", "M", "N", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z") - - - def generate(length: Double = 6.0): String = { - val count: Double = 1 - val totalChars: Int = ALPHABET.length - var exponent: BigDecimal = BigDecimal.valueOf(totalChars) - exponent = exponent.pow(length.intValue) - var codesCount: Double = 0 - var lastIndex: Double = 0 - var code: String = null - while (codesCount < count) { - lastIndex = getMaxIndex - val number: BigDecimal = BigDecimal(lastIndex) - val num: BigDecimal = number.*(largePrimeNumber).remainder(exponent) - code = baseN(num, totalChars) - if (code.length == length && isValidCode(code)) - codesCount += 1 - } - code - } - - private def baseN(num: BigDecimal, base: Int): String = { - if (num.doubleValue == 0) return "0" - val div = Math.floor(num.doubleValue / base) - val value = baseN(BigDecimal(div), base) - StringUtils.stripStart(value, stripChars) + ALPHABET(num.remainder(BigDecimal(base)).intValue) - } - - - private def getMaxIndex = System.currentTimeMillis.toDouble - - /** - * This Method will check if dialcode has numeric value at odd indexes. - * - * @param code - * @return Boolean - */ - private def isValidCode(code: String): Boolean = { - val matcher = pattern.matcher(code) - matcher.matches() - } - - -} diff --git a/credential-generator/certificate-processor/src/main/scala/org/sunbird/incredible/processor/qrcode/QRCodeGenerationModel.scala b/credential-generator/certificate-processor/src/main/scala/org/sunbird/incredible/processor/qrcode/QRCodeGenerationModel.scala deleted file mode 100755 index e0a224d46..000000000 --- a/credential-generator/certificate-processor/src/main/scala/org/sunbird/incredible/processor/qrcode/QRCodeGenerationModel.scala +++ /dev/null @@ -1,6 +0,0 @@ -package org.sunbird.incredible.processor.qrcode - -case class QRCodeGenerationModel(data: String, errorCorrectionLevel: String = "H", pixelsPerBlock: Int = 2, qrCodeMargin: Int = 3, - text: String, textFontName: String = "Verdana", textFontSize: Int = 16, textCharacterSpacing: Double = 0.2, - imageBorderSize: Int = 0, colorModel: String = "Grayscale", fileName: String, fileFormat: String = "png", - qrCodeMarginBottom: Int = 1, imageMargin: Int = 1) \ No newline at end of file diff --git a/credential-generator/certificate-processor/src/main/scala/org/sunbird/incredible/processor/qrcode/QRCodeImageGenerator.scala b/credential-generator/certificate-processor/src/main/scala/org/sunbird/incredible/processor/qrcode/QRCodeImageGenerator.scala deleted file mode 100755 index 08fecfb50..000000000 --- a/credential-generator/certificate-processor/src/main/scala/org/sunbird/incredible/processor/qrcode/QRCodeImageGenerator.scala +++ /dev/null @@ -1,230 +0,0 @@ -package org.sunbird.incredible.processor.qrcode - -import java.awt.{Color, Font, FontFormatException, FontMetrics, Graphics2D, RenderingHints} -import java.awt.font.TextAttribute -import java.awt.image.BufferedImage -import java.io.{File, IOException, InputStream} -import java.util - -import com.google.zxing.{BarcodeFormat, EncodeHintType, NotFoundException, WriterException} -import com.google.zxing.client.j2se.BufferedImageLuminanceSource -import com.google.zxing.common.{BitMatrix, HybridBinarizer} -import com.google.zxing.qrcode.QRCodeWriter -import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel -import javax.imageio.ImageIO -import org.apache.commons.lang3.StringUtils -import org.slf4j.{Logger, LoggerFactory} - -class QRCodeImageGenerator { - - private val logger: Logger = LoggerFactory.getLogger(classOf[QRCodeImageGenerator]) - private val qrCodeWriter: QRCodeWriter = new QRCodeWriter - - @throws[WriterException] - @throws[IOException] - @throws[NotFoundException] - @throws[FontFormatException] - def createQRImages(qrGenRequest: QRCodeGenerationModel): File = { - val data = qrGenRequest.data - val text = qrGenRequest.text - val fileName = qrGenRequest.fileName - val errorCorrectionLevel = qrGenRequest.errorCorrectionLevel - val pixelsPerBlock = qrGenRequest.pixelsPerBlock - val qrMargin = qrGenRequest.qrCodeMargin - val fontName = qrGenRequest.textFontName - val fontSize = qrGenRequest.textFontSize - val tracking = qrGenRequest.textCharacterSpacing - val imageFormat = qrGenRequest.fileFormat - val colorModel = qrGenRequest.colorModel - val borderSize = qrGenRequest.imageBorderSize - val qrMarginBottom = qrGenRequest.qrCodeMargin - val imageMargin = qrGenRequest.imageMargin - - var qrImage: BufferedImage = generateBaseImage(data, errorCorrectionLevel, pixelsPerBlock, qrMargin, colorModel) - - if (StringUtils.isNotBlank(text)) { - val textImage: BufferedImage = getTextImage(text, fontName, fontSize, tracking, colorModel) - qrImage = addTextToBaseImage(qrImage, textImage, colorModel, qrMargin, pixelsPerBlock, qrMarginBottom, imageMargin) - } - - if (borderSize > 0) drawBorder(qrImage, borderSize, imageMargin) - val finalImageFile = new File(fileName + "." + imageFormat) - ImageIO.write(qrImage, imageFormat, finalImageFile) - logger.info("qr created") - finalImageFile - } - - @throws[NotFoundException] - private def addTextToBaseImage(qrImage: BufferedImage, textImage: BufferedImage, colorModel: String, qrMargin: Int, pixelsPerBlock: Int, qrMarginBottom: Int, imageMargin: Int): BufferedImage = { - val qrSource: BufferedImageLuminanceSource = new BufferedImageLuminanceSource(qrImage) - val qrBinarizer: HybridBinarizer = new HybridBinarizer(qrSource) - var qrBits: BitMatrix = qrBinarizer.getBlackMatrix - - val textSource: BufferedImageLuminanceSource = new BufferedImageLuminanceSource(textImage) - val textBinarizer: HybridBinarizer = new HybridBinarizer(textSource) - var textBits: BitMatrix = textBinarizer.getBlackMatrix - - if (qrBits.getWidth > textBits.getWidth) { - val tempTextMatrix: BitMatrix = new BitMatrix(qrBits.getWidth, textBits.getHeight) - copyMatrixDataToBiggerMatrix(textBits, tempTextMatrix) - textBits = tempTextMatrix - } - else if (qrBits.getWidth < textBits.getWidth) { - val tempQrMatrix: BitMatrix = new BitMatrix(textBits.getWidth, qrBits.getHeight) - copyMatrixDataToBiggerMatrix(qrBits, tempQrMatrix) - qrBits = tempQrMatrix - } - val mergedMatrix = mergeMatricesOfSameWidth(qrBits, textBits, qrMargin, pixelsPerBlock, qrMarginBottom, imageMargin) - getImage(mergedMatrix, colorModel) - } - - @throws[WriterException] - private def generateBaseImage(data: String, errorCorrectionLevel: String, pixelsPerBlock: Int, qrMargin: Int, colorModel: String) = { - val hintsMap = getHintsMap(errorCorrectionLevel, qrMargin) - val defaultBitMatrix: BitMatrix = getDefaultBitMatrix(data, hintsMap) - val largeBitMatrix: BitMatrix = getBitMatrix(data, defaultBitMatrix.getWidth * pixelsPerBlock, defaultBitMatrix.getHeight * pixelsPerBlock, hintsMap) - val qrImage: BufferedImage = getImage(largeBitMatrix, colorModel) - qrImage - } - - //To remove extra spaces between text and qrcode, margin below qrcode is removed - //Parameter, qrCodeMarginBottom, is introduced to add custom margin(in pixels) between qrcode and text - //Parameter, imageMargin is introduced, to add custom margin(in pixels) outside the black border of the image - private def mergeMatricesOfSameWidth(firstMatrix: BitMatrix, secondMatrix: BitMatrix, qrMargin: Int, pixelsPerBlock: Int, qrMarginBottom: Int, imageMargin: Int) = { - val mergedWidth: Int = firstMatrix.getWidth + (2 * imageMargin) - val mergedHeight: Int = firstMatrix.getHeight + secondMatrix.getHeight + (2 * imageMargin) - val defaultBottomMargin: Int = pixelsPerBlock * qrMargin - val marginToBeRemoved: Int = if (qrMarginBottom > defaultBottomMargin) 0 else defaultBottomMargin - qrMarginBottom - val mergedMatrix: BitMatrix = new BitMatrix(mergedWidth, mergedHeight - marginToBeRemoved) - for (x <- 0 until firstMatrix.getWidth; - y <- 0 until firstMatrix.getHeight - marginToBeRemoved - if firstMatrix.get(x, y)) { - mergedMatrix.set(x + imageMargin, y + imageMargin) - } - for (x <- 0 until secondMatrix.getWidth; - y <- 0 until secondMatrix.getHeight if secondMatrix.get(x, y)) { - mergedMatrix.set( - x + imageMargin, - y + firstMatrix.getHeight - marginToBeRemoved + imageMargin) - } - mergedMatrix - } - - private def copyMatrixDataToBiggerMatrix(fromMatrix: BitMatrix, toMatrix: BitMatrix): Unit = { - val widthDiff: Int = toMatrix.getWidth - fromMatrix.getWidth - val leftMargin: Int = widthDiff / 2 - for (x <- 0 until fromMatrix.getWidth; y <- 0 until fromMatrix.getHeight - if fromMatrix.get(x, y)) { - toMatrix.set(x + leftMargin, y) - } - } - - private def drawBorder(image: BufferedImage, borderSize: Int, imageMargin: Int): Unit = { - image.createGraphics - val graphics = image.getGraphics.asInstanceOf[Graphics2D] - graphics.setColor(Color.BLACK) - var i = 0 - for (i <- 0 until borderSize) { - graphics.drawRect(i + imageMargin, i + imageMargin, image.getWidth - 1 - (2 * i) - (2 * imageMargin), image.getHeight - 1 - (2 * i) - (2 * imageMargin)) - } - graphics.dispose() - } - - private def getImage(bitMatrix: BitMatrix, colorModel: String) = { - val imageWidth = bitMatrix.getWidth - val imageHeight = bitMatrix.getHeight - val image = new BufferedImage(imageWidth, imageHeight, getImageType(colorModel)) - image.createGraphics - val graphics = image.getGraphics.asInstanceOf[Graphics2D] - graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF) - graphics.setColor(Color.WHITE) - graphics.fillRect(0, 0, imageWidth, imageHeight) - graphics.setColor(Color.BLACK) - - for (i <- 0 until imageWidth; j <- 0 until imageHeight - if bitMatrix.get(i, j)) { - graphics.fillRect(i, j, 1, 1) - } - graphics.dispose() - image - } - - @throws[WriterException] - private def getBitMatrix(data: String, width: Int, height: Int, hintsMap: java.util.Map[EncodeHintType, _]) = { - val bitMatrix = qrCodeWriter.encode(data, BarcodeFormat.QR_CODE, width, height, hintsMap) - bitMatrix - } - - @throws[WriterException] - private def getDefaultBitMatrix(data: String, hintsMap: java.util.Map[EncodeHintType, _]) = { - val defaultBitMatrix = qrCodeWriter.encode(data, BarcodeFormat.QR_CODE, 0, 0, hintsMap) - defaultBitMatrix - } - - private def getHintsMap(errorCorrectionLevel: String, qrMargin: Int): java.util.Map[EncodeHintType, Any] = { - val hintsMap: java.util.Map[EncodeHintType, Any] = new util.HashMap[EncodeHintType, Any]() - errorCorrectionLevel match { - case "H" => - hintsMap.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H) - case "Q" => - hintsMap.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.Q) - case "M" => - hintsMap.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.M) - case "L" => - hintsMap.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.L) - case _ => - logger.error("Unknown error correction") - } - hintsMap.put(EncodeHintType.MARGIN, qrMargin) - hintsMap - } - - //Sample = 2A42UH , Verdana, 11, 0.1, Grayscale - @throws[IOException] - @throws[FontFormatException] - private def getTextImage(text: String, fontName: String, fontSize: Int, tracking: Double, colorModel: String) = { - var image: BufferedImage = new BufferedImage(1, 1, getImageType(colorModel)) - //Font basicFont = new Font(fontName, Font.BOLD, fontSize); - val fontFile: String = fontName + ".ttf" - var basicFont: Font = null - var inputStream: InputStream = null - val classLoader: ClassLoader = this.getClass.getClassLoader - try { - inputStream = classLoader.getResourceAsStream(fontFile) - logger.info("input stream value is not null for fontfile " + fontFile + " " + inputStream) - basicFont = Font.createFont(Font.TRUETYPE_FONT, inputStream) - } catch { - case e: Exception => - logger.debug("Exception occurred during font creation " + e) - } - val attributes: java.util.Map[TextAttribute, Any] = new java.util.HashMap[TextAttribute, Any] - attributes.put(TextAttribute.TRACKING, tracking) - attributes.put(TextAttribute.WEIGHT, TextAttribute.WEIGHT_BOLD) - attributes.put(TextAttribute.SIZE, fontSize) - val font = basicFont.deriveFont(attributes) - var graphics2d = image.createGraphics - graphics2d.setFont(font) - var fontmetrics = graphics2d.getFontMetrics - val width = fontmetrics.stringWidth(text) - val height = fontmetrics.getHeight - graphics2d.dispose() - image = new BufferedImage(width, height, getImageType(colorModel)) - graphics2d = image.createGraphics - graphics2d.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY) - graphics2d.setRenderingHint(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY) - graphics2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_OFF) - graphics2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF) - graphics2d.setColor(Color.WHITE) - graphics2d.fillRect(0, 0, image.getWidth, image.getHeight) - graphics2d.setColor(Color.BLACK) - graphics2d.setFont(font) - fontmetrics = graphics2d.getFontMetrics - graphics2d.drawString(text, 0, fontmetrics.getAscent) - graphics2d.dispose() - image - } - - private def getImageType(colorModel: String) = if ("RGB".equalsIgnoreCase(colorModel)) BufferedImage.TYPE_INT_RGB - else BufferedImage.TYPE_BYTE_GRAY - -} diff --git a/credential-generator/certificate-processor/src/main/scala/org/sunbird/incredible/processor/qrcode/QRCodeImageGeneratorParams.scala b/credential-generator/certificate-processor/src/main/scala/org/sunbird/incredible/processor/qrcode/QRCodeImageGeneratorParams.scala deleted file mode 100755 index 6d62250a1..000000000 --- a/credential-generator/certificate-processor/src/main/scala/org/sunbird/incredible/processor/qrcode/QRCodeImageGeneratorParams.scala +++ /dev/null @@ -1,5 +0,0 @@ -package org.sunbird.incredible.processor.qrcode - -class QRCodeImageGeneratorParams extends Enumeration { - val eid, processId, objectId, dialcodes, data, text, id, location, storage, container, path, config, errorCorrectionLevel, pixelsPerBlock, qrCodeMargi, textFontSize, textCharacterSpacing, imageFormat, colourModel, imageBorderSize, qrCodeMarginBottom, BE_QR_IMAGE_GENERATOR, fileName, imageMargin, qr_image_margin_bottom, qr_image_margin = Value -} diff --git a/credential-generator/certificate-processor/src/main/scala/org/sunbird/incredible/processor/signature/Exceptions.scala b/credential-generator/certificate-processor/src/main/scala/org/sunbird/incredible/processor/signature/Exceptions.scala deleted file mode 100755 index c4409806d..000000000 --- a/credential-generator/certificate-processor/src/main/scala/org/sunbird/incredible/processor/signature/Exceptions.scala +++ /dev/null @@ -1,25 +0,0 @@ -package org.sunbird.incredible.processor.signature - - -case class CustomException(msg: String) extends Exception(msg) {} - -@SerialVersionUID(-6315798195661762882L) -object SignatureException extends Exception { - - @SerialVersionUID(6174717850058203376L) - class CreationException(msg: String) - extends CustomException("Unable to create signature: " + msg) - - @SerialVersionUID(4996784337180620650L) - class VerificationException(message: String) - extends CustomException("Unable to verify signature " + message) - - @SerialVersionUID(5384120386096139083L) - class UnreachableException(message: String) - extends CustomException("Unable to reach service: " + message) - - @SerialVersionUID(8311355815972497247L) - class KeyNotFoundException(message: String) - extends CustomException("Unable to get key: " + message) - -} diff --git a/credential-generator/certificate-processor/src/main/scala/org/sunbird/incredible/processor/signature/SignatureHelper.scala b/credential-generator/certificate-processor/src/main/scala/org/sunbird/incredible/processor/signature/SignatureHelper.scala deleted file mode 100755 index faadda703..000000000 --- a/credential-generator/certificate-processor/src/main/scala/org/sunbird/incredible/processor/signature/SignatureHelper.scala +++ /dev/null @@ -1,66 +0,0 @@ -package org.sunbird.incredible.processor.signature - -import java.io.IOException - -import com.fasterxml.jackson.core.`type`.TypeReference -import com.fasterxml.jackson.databind.{JsonNode, ObjectMapper} -import org.apache.http.client.ClientProtocolException -import org.slf4j.{Logger, LoggerFactory} -import org.sunbird.incredible.{HTTPResponse, HttpUtil, JsonKeys} - - -object SignatureHelper { - - var httpUtil = new HttpUtil - private val logger: Logger = LoggerFactory.getLogger(getClass.getName) - lazy private val mapper: ObjectMapper = new ObjectMapper() - - /** - * This method calls signature service for signing the object - * - * @param rootNode - contains input need to be signed - * @return - signed data with key - * @throws SignatureException.UnreachableException - * @throws SignatureException.CreationException - */ - - def generateSignature(rootNode: JsonNode, keyId: String)(implicit encServiceUrl: String): java.util.Map[String, AnyRef] = { - val signReq: Map[String, AnyRef] = Map(JsonKeys.ENTITY -> rootNode) - logger.info("generateSignature:keyID:".concat(keyId)) - val signApiURl: String = encServiceUrl.concat("/" + JsonKeys.SIGN + "/").concat(keyId) - logger.info("generateSignature:enc service url formed:".concat(signApiURl)) - try { - logger.info("generateSignature:SignRequest for enc-service call:".concat(mapper.writeValueAsString(signReq))) - val response: HTTPResponse = httpUtil.post(signApiURl, mapper.writeValueAsString(signReq)) - mapper.readValue(response.body, new TypeReference[java.util.Map[String, AnyRef]]() {}) - } catch { - case e: ClientProtocolException => - logger.error("ClientProtocolException when signing: {}", e.getMessage) - throw new SignatureException.UnreachableException(e.getMessage) - case e: IOException => - logger.error("RestClientException when signing: {}", e.getMessage) - throw new SignatureException.CreationException(e.getMessage) - } - } - - - def verifySignature(rootNode: JsonNode)(implicit encServiceUrl: String): Boolean = { - val verifyEndPoint = encServiceUrl.concat("/" + JsonKeys.VERIFY) - logger.debug("verify method starts with value {}", rootNode) - val signReq: Map[String, AnyRef] = Map(JsonKeys.ENTITY -> rootNode) - var result = false - try { - val response: HTTPResponse = httpUtil.post(verifyEndPoint, mapper.writeValueAsString(signReq)) - result = mapper.readValue(response.body, new TypeReference[Boolean]() {}) - } catch { - case ex: ClientProtocolException => - logger.error("ClientProtocolException when verifying: {}", ex.getMessage) - throw new SignatureException.UnreachableException(ex.getMessage) - case e: Exception => - logger.error("Exception occurred while verifying signature:{} ", e.getMessage) - throw new SignatureException.VerificationException("") - } - result - } - -} diff --git a/credential-generator/certificate-processor/src/main/scala/org/sunbird/incredible/processor/store/StorageService.scala b/credential-generator/certificate-processor/src/main/scala/org/sunbird/incredible/processor/store/StorageService.scala deleted file mode 100755 index cf6ca4dc9..000000000 --- a/credential-generator/certificate-processor/src/main/scala/org/sunbird/incredible/processor/store/StorageService.scala +++ /dev/null @@ -1,51 +0,0 @@ -package org.sunbird.incredible.processor.store - -import java.io.File - -import org.apache.commons.lang3.StringUtils -import org.sunbird.cloud.storage.BaseStorageService -import org.sunbird.cloud.storage.factory.StorageConfig -import org.sunbird.cloud.storage.factory.StorageServiceFactory -import org.sunbird.incredible.pojos.exceptions.ServerException -import org.sunbird.incredible.{JsonKeys, StorageParams, UrlManager} - - -class StorageService(storageParams: StorageParams) extends Serializable { - - var storageService: BaseStorageService = _ - val storageType: String = storageParams.cloudStorageType - - @throws[Exception] - def getService: BaseStorageService = { - if (null == storageService) { - if (StringUtils.equalsIgnoreCase(storageType, JsonKeys.AZURE)) { - val storageKey = storageParams.azureStorageKey - val storageSecret = storageParams.azureStorageSecret - storageService = StorageServiceFactory.getStorageService(StorageConfig(storageType, storageKey, storageSecret)) - } else if (StringUtils.equalsIgnoreCase(storageType, JsonKeys.AWS)) { - val storageKey = storageParams.awsStorageKey.get - val storageSecret = storageParams.awsStorageSecret.get - storageService = StorageServiceFactory.getStorageService(StorageConfig(storageType, storageKey, storageSecret)) - } else throw new ServerException("ERR_INVALID_CLOUD_STORAGE", "Error while initialising cloud storage") - } - storageService - } - - def getContainerName: String = { - if (StringUtils.equalsIgnoreCase(storageType, JsonKeys.AZURE)) - storageParams.azureContainerName - else if (StringUtils.equalsIgnoreCase(storageType, JsonKeys.AWS)) - storageParams.awsContainerName.get - else - throw new ServerException("ERR_INVALID_CLOUD_STORAGE", "Container name not configured.") - } - - def uploadFile(path: String, file: File): String = { - val objectKey = path + file.getName - val containerName = getContainerName - val url = getService.upload(containerName, file.getAbsolutePath, objectKey, Option.apply(false), Option.apply(1), Option.apply(5), Option.empty) - UrlManager.getSharableUrl(url, containerName) - } - - -} diff --git a/credential-generator/certificate-processor/src/main/scala/org/sunbird/incredible/processor/views/SvgGenerator.scala b/credential-generator/certificate-processor/src/main/scala/org/sunbird/incredible/processor/views/SvgGenerator.scala deleted file mode 100755 index 4567c2e8c..000000000 --- a/credential-generator/certificate-processor/src/main/scala/org/sunbird/incredible/processor/views/SvgGenerator.scala +++ /dev/null @@ -1,72 +0,0 @@ -package org.sunbird.incredible.processor.views - -import java.io.{FileNotFoundException, IOException} -import java.util.regex.Matcher - -import com.twitter.storehaus.cache.{Cache, LRUCache} -import org.apache.commons.lang.StringUtils -import org.apache.commons.text.StringSubstitutor -import org.slf4j.{Logger, LoggerFactory} -import org.sunbird.incredible.pojos.ob.CertificateExtension - -import scala.io.{BufferedSource, Source} -import scala.util.matching.Regex - -object EncoderMap { - val encoder: Map[String, String] = Map("<" -> "%3C", ">" -> "%3E", "#" -> "%23", "%" -> "%25", "\"" -> "\'") -} - - -object SvgGenerator { - private val logger: Logger = LoggerFactory.getLogger(getClass.getName) - private var svgTemplatesCache: Cache[String, String] = LRUCache[String, String](15) - - @throws[FileNotFoundException] - def generate(certificateExtension: CertificateExtension, encodedQrCode: String, svgTemplateUrl: String): String = { - var cachedTemplate = svgTemplatesCache.get(svgTemplateUrl).getOrElse("") - if (StringUtils.isEmpty(cachedTemplate)) { - logger.info("{} svg not cached , downloading", svgTemplateUrl) - cachedTemplate = download(svgTemplateUrl) - cachedTemplate = "data:image/svg+xml," + encodeData(cachedTemplate) - cachedTemplate = cachedTemplate.replaceAll("\n", "").replaceAll("\t", "") - svgTemplatesCache = svgTemplatesCache.put(svgTemplateUrl, cachedTemplate)._2 - } else { - logger.info("{} svg is cached, cache hit", svgTemplateUrl) - svgTemplatesCache = svgTemplatesCache.hit(svgTemplateUrl) - } - val svgData = replaceTemplateVars(cachedTemplate, certificateExtension, encodedQrCode) - logger.info("svg template string creation completed") - svgData - } - - - private def replaceTemplateVars(svgContent: String, certificateExtension: CertificateExtension, encodeQrCode: String): String = { - val varResolver = new VarResolver(certificateExtension) - val certData: java.util.Map[String, String] = varResolver.getCertMetaData - certData.put("qrCodeImage", "data:image/png;base64," + encodeQrCode) - val sub = new StringSubstitutor(certData) - val resolvedString = sub.replace(svgContent) - logger.info("replacing temp vars completed") - resolvedString - } - - private def encodeData(data: String): String = { - val stringBuffer: StringBuffer = new StringBuffer - val regex: Regex = "[<>#%\"]".r - val pattern: java.util.regex.Pattern = regex.pattern - val matcher: Matcher = pattern.matcher(data) - while (matcher.find) - matcher.appendReplacement(stringBuffer, EncoderMap.encoder(matcher.group)) - matcher.appendTail(stringBuffer) - stringBuffer.toString - } - - @throws[FileNotFoundException] - private def download(svgTemplate: String): String = { - val svgFileSource: BufferedSource = Source.fromURL(svgTemplate) - val svgString = svgFileSource.mkString - svgFileSource.close() - svgString - } - -} diff --git a/credential-generator/certificate-processor/src/main/scala/org/sunbird/incredible/processor/views/VarResolver.scala b/credential-generator/certificate-processor/src/main/scala/org/sunbird/incredible/processor/views/VarResolver.scala deleted file mode 100755 index bf374e30b..000000000 --- a/credential-generator/certificate-processor/src/main/scala/org/sunbird/incredible/processor/views/VarResolver.scala +++ /dev/null @@ -1,99 +0,0 @@ -package org.sunbird.incredible.processor.views - -import java.io.UnsupportedEncodingException -import java.net.{URI, URISyntaxException, URLEncoder} -import java.text.{DateFormat, ParseException, SimpleDateFormat} -import java.util.{Date, Locale} - -import org.apache.commons.lang.StringUtils -import org.slf4j.{Logger, LoggerFactory} -import org.sunbird.incredible.JsonKeys -import org.sunbird.incredible.pojos.ob.CertificateExtension - - -class VarResolver(certificateExtension: CertificateExtension) { - - private val logger: Logger = LoggerFactory.getLogger(classOf[VarResolver]) - - def getRecipientName: String = certificateExtension.recipient.name - - def getRecipientId: String = certificateExtension.recipient.identity - - def getCourseName: String = if (certificateExtension.evidence.nonEmpty && StringUtils.isNotBlank(certificateExtension.evidence.get.name)) certificateExtension.evidence.get.name - else "" - - def getQrCodeImage: String = try { - val uri = new URI(certificateExtension.id) - val path = uri.getPath - val idStr = path.substring(path.lastIndexOf('/') + 1) - StringUtils.substringBefore(idStr, ".") + ".png" - } catch { - case e: URISyntaxException => - null - } - - def getIssuedDate: String = { - val simpleDateFormat: SimpleDateFormat = new SimpleDateFormat( - "yyyy-MM-dd'T'HH:mm:ss'Z'") - var dateInFormat: String = null - try { - val parsedIssuedDate: Date = simpleDateFormat.parse(certificateExtension.issuedOn) - val format: DateFormat = new SimpleDateFormat("dd MMMM yyy", Locale.getDefault) - dateInFormat = format.format(parsedIssuedDate) - } catch { - case e: ParseException => - logger.info("getIssuedDate: exception occurred while formatting issued date {}", e.getMessage) - } - dateInFormat - } - - def getSignatory0Image: String = if (certificateExtension.signatory.length >= 1) certificateExtension.signatory(0).image - else "" - - def getSignatory0Designation: String = if (certificateExtension.signatory.length >= 1) certificateExtension.signatory(0).designation - else "" - - def getSignatory1Image: String = if (certificateExtension.signatory.length >= 2) certificateExtension.signatory(1).image - else "" - - def getSignatory1Designation: String = if (certificateExtension.signatory.length >= 2) certificateExtension.signatory(1).designation - else "" - - def getCertificateName: String = certificateExtension.badge.name - - def getCertificateDescription: String = certificateExtension.badge.description - - def getExpiryDate: String = certificateExtension.expires - - def getIssuerName: String = certificateExtension.badge.issuer.name - - @throws[UnsupportedEncodingException] - def getCertMetaData: java.util.Map[String, String] = { - - val metaData = new java.util.HashMap[String, String]() { - { - put(JsonKeys.CERT_NAME, urlEncode(getCertificateName)) - put(JsonKeys.CERTIFICATE_DESCIPTION, urlEncode(getCertificateDescription)) - put(JsonKeys.COURSE_NAME, urlEncode(getCourseName)) - put(JsonKeys.ISSUE_DATE, urlEncode(getIssuedDate)) - put(JsonKeys.RECIPIENT_ID, urlEncode(getRecipientId)) - put(JsonKeys.RECIPIENT_NAME, urlEncode(getRecipientName)) - put(JsonKeys.SIGNATORY_0_IMAGE, urlEncode(getSignatory0Image)) - put(JsonKeys.SIGNATORY_0_DESIGNATION, urlEncode(getSignatory0Designation)) - put(JsonKeys.SIGNATORY_1_IMAGE, urlEncode(getSignatory1Image)) - put(JsonKeys.SIGNATORY_1_DESIGNATION, urlEncode(getSignatory1Designation)) - put(JsonKeys.EXPIRY_DATE, urlEncode(getExpiryDate)) - put(JsonKeys.ISSUER_NAME, urlEncode(getIssuerName)) - } - } - metaData - } - - @throws[UnsupportedEncodingException] - private def urlEncode(data: String): String = { - // URLEncoder.encode replace space with "+", but it should %20 - if (StringUtils.isNotEmpty(data)) URLEncoder.encode(data, "UTF-8").replace("+", "%20") - else data - } - -} diff --git a/credential-generator/certificate-processor/src/test/scala/org/sunbird/incredible/BaseTestSpec.scala b/credential-generator/certificate-processor/src/test/scala/org/sunbird/incredible/BaseTestSpec.scala deleted file mode 100755 index aad412a42..000000000 --- a/credential-generator/certificate-processor/src/test/scala/org/sunbird/incredible/BaseTestSpec.scala +++ /dev/null @@ -1,14 +0,0 @@ -package org.sunbird.incredible - - -import org.scalatest.{BeforeAndAfterAll, FlatSpec, Matchers} -import org.scalatestplus.mockito.MockitoSugar - -class BaseTestSpec extends FlatSpec with Matchers with BeforeAndAfterAll with MockitoSugar { - - - implicit val certificateConfig: CertificateConfig = CertificateConfig(basePath = "http://localhost:9000", encryptionServiceUrl = "http://localhost:8013", contextUrl = "context.json", evidenceUrl = JsonKeys.EVIDENCE_URL, issuerUrl = JsonKeys.ISSUER_URL, signatoryExtension = JsonKeys.SIGNATORY_EXTENSION) - - - -} diff --git a/credential-generator/certificate-processor/src/test/scala/org/sunbird/incredible/CertificateGeneratorTest.scala b/credential-generator/certificate-processor/src/test/scala/org/sunbird/incredible/CertificateGeneratorTest.scala deleted file mode 100755 index de19f7b9d..000000000 --- a/credential-generator/certificate-processor/src/test/scala/org/sunbird/incredible/CertificateGeneratorTest.scala +++ /dev/null @@ -1,47 +0,0 @@ -package org.sunbird.incredible - -import java.util -import org.mockito.ArgumentMatchers.{any, endsWith} -import org.mockito.Mockito.when -import org.sunbird.incredible.pojos.ob.{Criteria, Issuer, SignatoryExtension} -import org.sunbird.incredible.processor.CertModel -import org.sunbird.incredible.processor.signature.SignatureHelper - -class CertificateGeneratorTest extends BaseTestSpec { - - - override protected def beforeAll(): Unit = { - - } - - - "check generate certificate" should "should return certificate extension" in { - val map: util.HashMap[String, AnyRef] = new util.HashMap[String, AnyRef]() - val issuerIn: Issuer = Issuer(context = "https://staging.sunbirded.org/certs/v1/context.json", name = "issuer name", url = "url") - val signatory: SignatoryExtension = SignatoryExtension(context = "", identity = "CEO", name = "signatory name", designation = "CEO", image = "https://cdn.pixabay.com/photo/2014/11/09/08/06/signature-523237__340.jpg") - val certModel: CertModel = CertModel(courseName = "java", recipientName = "Test", certificateName = "100PercentCompletionCertificate", issuedDate = "2019-08-21", - issuer = issuerIn, signatoryList = Array(signatory), identifier = "8e57723e-4541-11eb-b378-0242ac130002", criteria = Criteria(narrative = "Course Completion"), tag = "0131685518070087685") - val certificateGenerator = new CertificateGenerator - val certificateExtension = certificateGenerator.getCertificateExtension(certModel) - certificateExtension.evidence.get.name shouldBe "java" - certificateExtension.recipient.name shouldBe "Test" - } - - "check generate certificate with signature" should "should return certificate extension" in { - val mockHttpUtil: HttpUtil = mock[HttpUtil] - SignatureHelper.httpUtil = mockHttpUtil - when(mockHttpUtil.post(endsWith("/sign/256"), any[String])).thenReturn(HTTPResponse(200, """{"signatureValue":"Yul8hVc3+ShzEQU/u1og7f8b0xVf2N4WyMdFgELdz74dpfMWBhZ8snsvit3JKMptyA4JKSywqUoNeAMcEmtgurqAaS7oMwMulPJnvAsx2xDCOxq/UVPZGi63zPsItP2dTahLsEJQjPyQOEoEW5KW3oefRJO066Fr/L/Y5XNg2goDhvYHoHdAkpfr/IFsQqG0hWPzKglKOwd0R+LIuv13MIywBjYg9qY6cWs9BtTSMwXyayBhm6YkLgdb0LiBD/","keyId":"256"}""")) - val issuerIn: Issuer = Issuer(context = "https://staging.sunbirded.org/certs/v1/context.json", name = "issuer name", url = "url") - val signatory: SignatoryExtension = SignatoryExtension(context = "", identity = "CEO", name = "signatory name", designation = "CEO", image = "https://cdn.pixabay.com/photo/2014/11/09/08/06/signature-523237__340.jpg") - val certModel: CertModel = CertModel(courseName = "java", recipientName = "Test", certificateName = "100PercentCompletionCertificate", issuedDate = "2019-08-21", - issuer = issuerIn, signatoryList = Array(signatory), identifier = "8e57723e-4541-11eb-b378-0242ac130002", criteria = Criteria(narrative = "Course Completion"), keyId = "256", tag = "0131685518070087685") - val certificateGenerator = new CertificateGenerator - val certificateExtension = certificateGenerator.getCertificateExtension(certModel) - println(certificateExtension) - certificateExtension.evidence.get.name shouldBe "java" - certificateExtension.signature.get should not be null - certificateExtension.signature.get.signatureValue should equal("Yul8hVc3+ShzEQU/u1og7f8b0xVf2N4WyMdFgELdz74dpfMWBhZ8snsvit3JKMptyA4JKSywqUoNeAMcEmtgurqAaS7oMwMulPJnvAsx2xDCOxq/UVPZGi63zPsItP2dTahLsEJQjPyQOEoEW5KW3oefRJO066Fr/L/Y5XNg2goDhvYHoHdAkpfr/IFsQqG0hWPzKglKOwd0R+LIuv13MIywBjYg9qY6cWs9BtTSMwXyayBhm6YkLgdb0LiBD/") - } - - -} \ No newline at end of file diff --git a/credential-generator/certificate-processor/src/test/scala/org/sunbird/incredible/SvgGeneratorTest.scala b/credential-generator/certificate-processor/src/test/scala/org/sunbird/incredible/SvgGeneratorTest.scala deleted file mode 100755 index 418242673..000000000 --- a/credential-generator/certificate-processor/src/test/scala/org/sunbird/incredible/SvgGeneratorTest.scala +++ /dev/null @@ -1,38 +0,0 @@ -package org.sunbird.incredible - -import java.io.FileNotFoundException -import java.util - -import org.sunbird.incredible.pojos.ob.{Criteria, Issuer, SignatoryExtension} -import org.sunbird.incredible.processor.CertModel -import org.sunbird.incredible.processor.views.SvgGenerator - -class SvgGeneratorTest extends BaseTestSpec { - - - "check generate encoded svg data" should "should return encoded string" in { - val map: util.HashMap[String, AnyRef] = new util.HashMap[String, AnyRef]() - val issuerIn: Issuer = Issuer(context = "https://staging.sunbirded.org/certs/v1/context.json", name = "issuer name", url = "url") - val signatory: SignatoryExtension = SignatoryExtension(context = "", identity = "CEO", name = "signatory name", designation = "CEO", image = "https://cdn.pixabay.com/photo/2014/11/09/08/06/signature-523237__340.jpg") - val certModel: CertModel = CertModel(courseName = "java", recipientName = "Test", certificateName = "100PercentCompletionCertificate", issuedDate = "2019-08-21", - issuer = issuerIn, signatoryList = Array(signatory), identifier = "8e57723e-4541-11eb-b378-0242ac130002", criteria = Criteria(narrative = "Course Completion"), tag = "0131685518070087685") - val certificateGenerator = new CertificateGenerator - val certificateExtension = certificateGenerator.getCertificateExtension(certModel) - val printUri = SvgGenerator.generate(certificateExtension, "encodedQr", "https://sunbirdstagingpublic.blob.core.windows.net/sunbird-content-staging/content/template_svg_03_staging/artifact/cbse.svg") - printUri should not be null - printUri should startWith("data:image/svg+xml,") - } - - "check svg generator" should "should throw exception" in { - val map: util.HashMap[String, AnyRef] = new util.HashMap[String, AnyRef]() - val issuerIn: Issuer = Issuer(context = "https://staging.sunbirded.org/certs/v1/context.json", name = "issuer name", url = "url") - val signatory: SignatoryExtension = SignatoryExtension(context = "", identity = "CEO", name = "signatory name", designation = "CEO", image = "https://cdn.pixabay.com/photo/2014/11/09/08/06/signature-523237__340.jpg") - val certModel: CertModel = CertModel(courseName = "java", recipientName = "Test", certificateName = "100PercentCompletionCertificate", issuedDate = "2019-08-21", - issuer = issuerIn, signatoryList = Array(signatory), identifier = "8e57723e-4541-11eb-b378-0242ac130002", criteria = Criteria(narrative = "Course Completion"), tag = "0131685518070087685") - val certificateGenerator = new CertificateGenerator - val certificateExtension = certificateGenerator.getCertificateExtension(certModel) - intercept[FileNotFoundException] { - val printUri = SvgGenerator.generate(certificateExtension, "encodedQr", "https://sunbirdstagingpublic.blob.core.windows.net/sunbird-content-staging/content/template_svg_03_staging/artifact/cbse.sv") - } - } -} diff --git a/credential-generator/certificate-processor/src/test/scala/org/sunbird/incredible/processor/qrcode/AccessCodeGeneratorTest.scala b/credential-generator/certificate-processor/src/test/scala/org/sunbird/incredible/processor/qrcode/AccessCodeGeneratorTest.scala deleted file mode 100755 index 2c57f9ae4..000000000 --- a/credential-generator/certificate-processor/src/test/scala/org/sunbird/incredible/processor/qrcode/AccessCodeGeneratorTest.scala +++ /dev/null @@ -1,15 +0,0 @@ -package org.sunbird.incredible.processor.qrcode - - -import org.sunbird.incredible.{BaseTestSpec} - -class AccessCodeGeneratorTest extends BaseTestSpec { - - - "check accessCode generator" should "should return accessCode" in { - val accessCodeGenerator: AccessCodeGenerator = new AccessCodeGenerator - val code: String = accessCodeGenerator.generate(6) - code.length should be(6) - } - -} \ No newline at end of file diff --git a/credential-generator/certificate-processor/src/test/scala/org/sunbird/incredible/processor/qrcode/QRCodeImageGeneratorTest.scala b/credential-generator/certificate-processor/src/test/scala/org/sunbird/incredible/processor/qrcode/QRCodeImageGeneratorTest.scala deleted file mode 100755 index 3bc71968e..000000000 --- a/credential-generator/certificate-processor/src/test/scala/org/sunbird/incredible/processor/qrcode/QRCodeImageGeneratorTest.scala +++ /dev/null @@ -1,16 +0,0 @@ -package org.sunbird.incredible.processor.qrcode - -import org.sunbird.incredible.{BaseTestSpec, CertificateGenerator, QrCodeModel} -class QRCodeImageGeneratorTest extends BaseTestSpec { - - - - "check qrCode generator" should "should return qrCode file" in { - val certificateGenerator = new CertificateGenerator() - val qrCodeModel: QrCodeModel = certificateGenerator.generateQrCode("8e57723e-4541-11eb-b378-0242ac130002", "certificates/","http://localhost:9000") - qrCodeModel.accessCode.length should be(6) - qrCodeModel.qrFile.exists() should be(true) - } - - -} diff --git a/credential-generator/certificate-processor/src/test/scala/org/sunbird/incredible/valuator/ExpiryDateValuatorTest.scala b/credential-generator/certificate-processor/src/test/scala/org/sunbird/incredible/valuator/ExpiryDateValuatorTest.scala deleted file mode 100755 index 53c178def..000000000 --- a/credential-generator/certificate-processor/src/test/scala/org/sunbird/incredible/valuator/ExpiryDateValuatorTest.scala +++ /dev/null @@ -1,60 +0,0 @@ -package org.sunbird.incredible.valuator - -import org.junit.Assert.assertEquals -import org.sunbird.incredible.BaseTestSpec -import org.sunbird.incredible.pojos.valuator.ExpiryDateValuator - -class ExpiryDateValuatorTest extends BaseTestSpec { - - private var issuedDate: String = "2019-08-31T12:52:25Z" - - - "check expiry Date Is correct For Months" should "should return valid expiry date for months" in { - val expiryDate = "2m" - val expiryDateValuator = new ExpiryDateValuator(issuedDate) - val expiry = expiryDateValuator.evaluates(expiryDate) - assertEquals("expiry date is valid for months ", "2019-10-31T12:52:25Z", expiry) - } - - "check expiry Date Is correct For years" should "should return valid expiry date for years" in { - val expiryDate = "2Y" - val expiryDateValuator = new ExpiryDateValuator(issuedDate) - val expiry = expiryDateValuator.evaluates(expiryDate) - assertEquals("expiry date is valid for years", "2021-08-31T12:52:25Z", expiry) - } - - "check expiry Date Is correct For days" should "should return valid expiry date for days" in { - val expiryDate = "2d" - val expiryDateValuator = new ExpiryDateValuator(issuedDate) - val expiry = expiryDateValuator.evaluates(expiryDate) - assertEquals("expiry date is valid for days", "2019-09-02T12:52:25Z", expiry) - } - - "check expiry date is correct For days and years" should "should return valid expiry date for both days and years" in { - val expiryDate = "2D 2y" - val expiryDateValuator = new ExpiryDateValuator(issuedDate) - val expiry = expiryDateValuator.evaluates(expiryDate) - assertEquals("expiry date is valid for both days and years", "2021-09-02T12:52:25Z", expiry) - } - - "check expiry date is correct For both months and years" should "should return valid expiry date for both months and years" in { - val expiryDate = "2M 1y" - val expiryDateValuator = new ExpiryDateValuator(issuedDate) - val expiry = expiryDateValuator.evaluates(expiryDate) - assertEquals("expiry date is valid for both months and years", "2020-10-31T12:52:25Z", expiry) - } - - "check expiry date is correct For both both days and months" should "should return valid expiry date for both days and months" in { - val expiryDate = "2d 2m" - val expiryDateValuator = new ExpiryDateValuator(issuedDate) - val expiry = expiryDateValuator.evaluates(expiryDate) - assertEquals("expiry date is valid for both days and months", "2019-11-02T12:52:25Z", expiry) - } - "check expiry date is correct in format" should "return valid expiry date" in { - val expiryDate = "2019-09-02T12:52:25Z" - val expiryDateValuator = new ExpiryDateValuator(issuedDate) - val expiry = expiryDateValuator.evaluates(expiryDate) - assertEquals("expiry date is incorrect format", "2019-09-02T12:52:25Z", expiry) - } - -} diff --git a/credential-generator/certificate-processor/src/test/scala/org/sunbird/incredible/valuator/IssuedDateValuatorTest.scala b/credential-generator/certificate-processor/src/test/scala/org/sunbird/incredible/valuator/IssuedDateValuatorTest.scala deleted file mode 100755 index a4e0a5823..000000000 --- a/credential-generator/certificate-processor/src/test/scala/org/sunbird/incredible/valuator/IssuedDateValuatorTest.scala +++ /dev/null @@ -1,48 +0,0 @@ -package org.sunbird.incredible.valuator - -import java.text.SimpleDateFormat -import java.util.Calendar - -import org.junit.Assert.assertEquals -import org.sunbird.incredible.BaseTestSpec -import org.sunbird.incredible.pojos.exceptions.InvalidDateFormatException -import org.sunbird.incredible.pojos.valuator.IssuedDateValuator - -class IssuedDateValuatorTest extends BaseTestSpec { - - - private val issuedDateValuator = new IssuedDateValuator - private val simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'") - private val cal = Calendar.getInstance - - - "evaluate date in format1" should "should parse date in correct format" in { - val date = issuedDateValuator.convertToDate("2019-01-20") - cal.setTime(date) - assertEquals("2019-01-20T00:00:00Z", simpleDateFormat.format(cal.getTime)) - - } - - "evaluate date in format2" should "should parse date in correct format" in { - val date = issuedDateValuator.convertToDate("2019-02-12T10:11:11Z") - cal.setTime(date) - assertEquals("2019-02-12T10:11:11Z", simpleDateFormat.format(cal.getTime)) - - } - - - "evaluate date which has null value" should "should throw exception" in { - intercept[InvalidDateFormatException] { - issuedDateValuator.convertToDate(null) - } - } - - - "evaluate date For different formats " should "should throw exception" in { - intercept[InvalidDateFormatException] { - issuedDateValuator.convertToDate("2019-02") - } - } - - -} diff --git a/credential-generator/collection-cert-pre-processor/pom.xml b/credential-generator/collection-cert-pre-processor/pom.xml deleted file mode 100644 index 51c36ecac..000000000 --- a/credential-generator/collection-cert-pre-processor/pom.xml +++ /dev/null @@ -1,216 +0,0 @@ - - - - 4.0.0 - - org.sunbird - credential-generator - 1.0 - - collection-cert-pre-processor - 1.0.0 - jar - - - - UTF-8 - 1.4.0 - - - - - org.apache.flink - flink-streaming-scala_${scala.version} - ${flink.version} - provided - - - org.sunbird - jobs-core - 1.0.0 - - - joda-time - joda-time - 2.10.6 - - - org.sunbird - jobs-core - 1.0.0 - test-jar - test - - - org.apache.flink - flink-test-utils_${scala.version} - ${flink.version} - test - - - org.apache.flink - flink-runtime_${scala.version} - ${flink.version} - test - tests - - - org.apache.flink - flink-streaming-java_${scala.version} - ${flink.version} - test - tests - - - org.scalatest - scalatest_${scala.version} - 3.0.6 - test - - - org.mockito - mockito-core - 3.3.3 - test - - - org.cassandraunit - cassandra-unit - 3.11.2.0 - test - - - it.ozimov - embedded-redis - 0.7.1 - test - - - - - src/main/scala - src/test/scala - - - org.apache.maven.plugins - maven-compiler-plugin - 3.8.1 - - 11 - - - - org.apache.maven.plugins - maven-shade-plugin - 3.2.1 - - - - package - - shade - - - - - com.google.code.findbugs:jsr305 - - - - - - *:* - - META-INF/*.SF - META-INF/*.DSA - META-INF/*.RSA - - - - - - org.sunbird.job.collectioncert.task.CollectionCertPreProcessorTask - - - - reference.conf - - - - - - - - - net.alchim31.maven - scala-maven-plugin - 4.4.0 - - 11 - 11 - ${scala.maj.version} - false - - - - scala-compile-first - process-resources - - add-source - compile - - - - scala-test-compile - process-test-resources - - testCompile - - - - - - - maven-surefire-plugin - 2.22.2 - - true - - - - - org.scalatest - scalatest-maven-plugin - 1.0 - - ${project.build.directory}/surefire-reports - . - collection-complete-post-processor-testsuite.txt - - - - test - - test - - - - - - org.scoverage - scoverage-maven-plugin - ${scoverage.plugin.version} - - ${scala.version} - true - true - - - - - diff --git a/credential-generator/collection-cert-pre-processor/src/main/resources/collection-cert-pre-processor.conf b/credential-generator/collection-cert-pre-processor/src/main/resources/collection-cert-pre-processor.conf deleted file mode 100644 index 445c1a593..000000000 --- a/credential-generator/collection-cert-pre-processor/src/main/resources/collection-cert-pre-processor.conf +++ /dev/null @@ -1,38 +0,0 @@ -include "base-config.conf" - -kafka { - input.topic = "sunbirddev.issue.certificate.request" - output.topic = "sunbirddev.generate.certificate.request" - output.failed.topic = "sunbirddev.issue.certificate.failed" - groupId = "collection-cert-pre-processor-group" -} - -task { - consumer.parallelism = 1 - parallelism = 1 - generate_certificate.parallelism = 1 -} - -lms-cassandra { - keyspace = "sunbird_courses" - user_enrolments.table = "user_enrolments" - course_batch.table = "course_batch" - assessment_aggregator.table = "assessment_aggregator" - user_activity_agg.table = "user_activity_agg" -} - -cert_domain_url="https://dev.sunbirded.org" -user_read_api = "/private/user/v1/read" -content_read_api = "/content/v3/read" - -service { - content.basePath = "http://localhost:9000" - learner.basePath = "http://localhost:9000" -} - -redis-meta { - host = localhost - port = 6379 -} -assessment.metrics.supported.contenttype = ["SelfAssess"] -enable.suppress.exception = true diff --git a/credential-generator/collection-cert-pre-processor/src/main/scala/org/sunbird/job/collectioncert/domain/Event.scala b/credential-generator/collection-cert-pre-processor/src/main/scala/org/sunbird/job/collectioncert/domain/Event.scala deleted file mode 100644 index c737d7f33..000000000 --- a/credential-generator/collection-cert-pre-processor/src/main/scala/org/sunbird/job/collectioncert/domain/Event.scala +++ /dev/null @@ -1,29 +0,0 @@ -package org.sunbird.job.collectioncert.domain - -import org.sunbird.job.collectioncert.task.CollectionCertPreProcessorConfig -import org.sunbird.job.domain.reader.JobRequest - -class Event(eventMap: java.util.Map[String, Any], partition: Int, offset: Long) extends JobRequest(eventMap, partition, offset) { - - def action:String = readOrDefault[String]("edata.action", "") - - def batchId: String = readOrDefault[String]("edata.batchId", "") - - def courseId: String = readOrDefault[String]("edata.courseId", "") - - def userId: String = { - val list = readOrDefault[List[String]]("edata.userIds", List[String]()) - if(list.isEmpty) "" else list.head - } - - def reIssue: Boolean = readOrDefault[Boolean]("edata.reIssue", false) - - def eData: Map[String, AnyRef] = readOrDefault[Map[String, AnyRef]]("edata", Map[String, AnyRef]()) - - - def isValid()(config: CollectionCertPreProcessorConfig): Boolean = { - config.issueCertificate.equalsIgnoreCase(action) && !batchId.isEmpty && !courseId.isEmpty && - !userId.isEmpty - } - -} diff --git a/credential-generator/collection-cert-pre-processor/src/main/scala/org/sunbird/job/collectioncert/domain/Models.scala b/credential-generator/collection-cert-pre-processor/src/main/scala/org/sunbird/job/collectioncert/domain/Models.scala deleted file mode 100644 index 41c880ce7..000000000 --- a/credential-generator/collection-cert-pre-processor/src/main/scala/org/sunbird/job/collectioncert/domain/Models.scala +++ /dev/null @@ -1,26 +0,0 @@ -package org.sunbird.job.collectioncert.domain - -import java.util.{Date, UUID} - -case class EnrolledUser(userId: String, oldId: String = null, issuedOn: Date = null, additionalProps: Map[String, Any] = Map[String, Any]()) - -case class AssessedUser(userId: String, additionalProps: Map[String, Any] = Map[String, Any]()) - - -case class ActorObject(id: String = "Certificate Generator", `type`: String = "System") - -case class EventContext(pdata: Map[String, String] = Map("ver" -> "1.0", "id" -> "org.sunbird.learning.platform")) - - -case class EventObject(id: String, `type`: String = "GenerateCertificate") - -case class BEJobRequestEvent(actor: ActorObject= ActorObject(), - eid: String = "BE_JOB_REQUEST", - edata: Map[String, AnyRef], - ets: Long = System.currentTimeMillis(), - context: EventContext = EventContext(), - mid: String = s"LMS.${UUID.randomUUID().toString}", - `object`: EventObject - ) - -case class AssessmentUserAttempt(contentId: String, score: Double, totalScore: Double) \ No newline at end of file diff --git a/credential-generator/collection-cert-pre-processor/src/main/scala/org/sunbird/job/collectioncert/functions/CollectionCertPreProcessorFn.scala b/credential-generator/collection-cert-pre-processor/src/main/scala/org/sunbird/job/collectioncert/functions/CollectionCertPreProcessorFn.scala deleted file mode 100644 index 457c32a9e..000000000 --- a/credential-generator/collection-cert-pre-processor/src/main/scala/org/sunbird/job/collectioncert/functions/CollectionCertPreProcessorFn.scala +++ /dev/null @@ -1,98 +0,0 @@ -package org.sunbird.job.collectioncert.functions - -import com.datastax.driver.core.querybuilder.QueryBuilder -import com.datastax.driver.core.{Row, TypeTokens} -import com.google.common.reflect.TypeToken -import org.apache.flink.api.common.typeinfo.TypeInformation -import org.apache.flink.configuration.Configuration -import org.apache.flink.streaming.api.functions.KeyedProcessFunction -import org.slf4j.LoggerFactory -import org.sunbird.job.cache.{DataCache, RedisConnect} -import org.sunbird.job.collectioncert.domain.Event -import org.sunbird.job.collectioncert.task.CollectionCertPreProcessorConfig -import org.sunbird.job.exception.InvalidEventException -import org.sunbird.job.util.{CassandraUtil, HttpUtil} -import org.sunbird.job.{BaseProcessKeyedFunction, Metrics} - -import scala.collection.JavaConverters._ - -class CollectionCertPreProcessorFn(config: CollectionCertPreProcessorConfig, httpUtil: HttpUtil) - (implicit val stringTypeInfo: TypeInformation[String], - @transient var cassandraUtil: CassandraUtil = null) - extends BaseProcessKeyedFunction[String, Event, String](config) with IssueCertificateHelper { - - private[this] val logger = LoggerFactory.getLogger(classOf[CollectionCertPreProcessorFn]) - private var cache: DataCache = _ - private var contentCache: DataCache = _ - - override def open(parameters: Configuration): Unit = { - super.open(parameters) - cassandraUtil = new CassandraUtil(config.dbHost, config.dbPort) - val redisConnect = new RedisConnect(config) - cache = new DataCache(config, redisConnect, config.collectionCacheStore, List()) - cache.init() - - val metaRedisConn = new RedisConnect(config, Option(config.metaRedisHost), Option(config.metaRedisPort)) - contentCache = new DataCache(config, metaRedisConn, config.contentCacheStore, List()) - contentCache.init() - } - - override def close(): Unit = { - cassandraUtil.close() - cache.close() - super.close() - } - - override def metricsList(): List[String] = { - List(config.totalEventsCount, config.dbReadCount, config.dbUpdateCount, config.failedEventCount, config.skippedEventCount, config.successEventCount, - config.cacheHitCount) - } - - override def processElement(event: Event, - context: KeyedProcessFunction[String, Event, String]#Context, - metrics: Metrics): Unit = { - try { - metrics.incCounter(config.totalEventsCount) - if(event.isValid()(config)) { - val certTemplates = fetchTemplates(event)(metrics).filter(template => template._2.getOrElse("url", "").asInstanceOf[String].contains(".svg")) - if(!certTemplates.isEmpty) { - certTemplates.map(template => { - val certEvent = issueCertificate(event, template._2)(cassandraUtil, cache, contentCache, metrics, config, httpUtil) - Option(certEvent).map(e => { - context.output(config.generateCertificateOutputTag, certEvent) - metrics.incCounter(config.successEventCount)} - ).getOrElse({metrics.incCounter(config.skippedEventCount)}) - }) - } else { - logger.info(s"No certTemplates available for batchId :${event.batchId}") - metrics.incCounter(config.skippedEventCount) - } - } else { - logger.info(s"Invalid request : ${event}") - metrics.incCounter(config.skippedEventCount) - } - } catch { - case ex: Exception => { - metrics.incCounter(config.failedEventCount) - throw new InvalidEventException(ex.getMessage, Map("partition" -> event.partition, "offset" -> event.offset), ex) - } - } - - - } - - def fetchTemplates(event: Event)(implicit metrics: Metrics): Map[String, Map[String, String]] = { - val query = QueryBuilder.select(config.certTemplates).from(config.keyspace, config.courseTable) - .where(QueryBuilder.eq(config.dbCourseId, event.courseId)).and(QueryBuilder.eq(config.dbBatchId, event.batchId)) - - val row: Row = cassandraUtil.findOne(query.toString) - if(null != row && !row.isNull(config.certTemplates)) { - val templates = row.getMap(config.certTemplates, TypeToken.of(classOf[String]), TypeTokens.mapOf(classOf[String], classOf[String])) - templates.asScala.map(template => (template._1 -> template._2.asScala.toMap)).toMap - }else { - Map[String, Map[String, String]]() - } - } - - -} diff --git a/credential-generator/collection-cert-pre-processor/src/main/scala/org/sunbird/job/collectioncert/functions/IssueCertificateHelper.scala b/credential-generator/collection-cert-pre-processor/src/main/scala/org/sunbird/job/collectioncert/functions/IssueCertificateHelper.scala deleted file mode 100644 index 49f66b44c..000000000 --- a/credential-generator/collection-cert-pre-processor/src/main/scala/org/sunbird/job/collectioncert/functions/IssueCertificateHelper.scala +++ /dev/null @@ -1,236 +0,0 @@ -package org.sunbird.job.collectioncert.functions - -import java.text.SimpleDateFormat -import com.datastax.driver.core.querybuilder.QueryBuilder -import com.datastax.driver.core.{Row, TypeTokens} -import org.apache.commons.lang3.StringUtils -import org.slf4j.LoggerFactory -import org.sunbird.job.Metrics -import org.sunbird.job.cache.DataCache -import org.sunbird.job.collectioncert.domain.{AssessedUser, AssessmentUserAttempt, BEJobRequestEvent, EnrolledUser, Event, EventObject} -import org.sunbird.job.collectioncert.task.CollectionCertPreProcessorConfig -import org.sunbird.job.util.{CassandraUtil, HttpUtil, ScalaJsonUtil} - -import scala.collection.JavaConverters._ - -trait IssueCertificateHelper { - private[this] val logger = LoggerFactory.getLogger(classOf[CollectionCertPreProcessorFn]) - - - def issueCertificate(event:Event, template: Map[String, String])(cassandraUtil: CassandraUtil, cache:DataCache, contentCache: DataCache, metrics: Metrics, config: CollectionCertPreProcessorConfig, httpUtil: HttpUtil): String = { - //validCriteria - logger.info("issueCertificate i/p event =>"+event) - val criteria = validateTemplate(template, event.batchId)(config) - //validateEnrolmentCriteria - val certName = template.getOrElse(config.name, "") - val additionalProps: Map[String, List[String]] = ScalaJsonUtil.deserialize[Map[String, List[String]]](template.getOrElse("additionalProps", "{}")) - val enrolledUser: EnrolledUser = validateEnrolmentCriteria(event, criteria.getOrElse(config.enrollment, Map[String, AnyRef]()).asInstanceOf[Map[String, AnyRef]], certName, additionalProps)(metrics, cassandraUtil, config) - //validateAssessmentCriteria - val assessedUser = validateAssessmentCriteria(event, criteria.getOrElse(config.assessment, Map[String, AnyRef]()).asInstanceOf[Map[String, AnyRef]], enrolledUser.userId, additionalProps)(metrics, cassandraUtil, contentCache, config) - //validateUserCriteria - val userDetails = validateUser(assessedUser.userId, criteria.getOrElse(config.user, Map[String, AnyRef]()).asInstanceOf[Map[String, AnyRef]], additionalProps)(metrics, config, httpUtil) - - //generateCertificateEvent - if(userDetails.nonEmpty) { - generateCertificateEvent(event, template, userDetails, enrolledUser, assessedUser, additionalProps, certName)(metrics, config, cache, httpUtil) - } else { - logger.info(s"""User :: ${event.userId} did not match the criteria for batch :: ${event.batchId} and course :: ${event.courseId}""") - null - } - } - - def validateTemplate(template: Map[String, String], batchId: String)(config: CollectionCertPreProcessorConfig):Map[String, AnyRef] = { - val criteria = ScalaJsonUtil.deserialize[Map[String, AnyRef]](template.getOrElse(config.criteria, "{}")) - if(!template.getOrElse("url", "").isEmpty && !criteria.isEmpty && !criteria.keySet.intersect(Set(config.enrollment, config.assessment, config.users)).isEmpty) { - criteria - } else { - throw new Exception(s"Invalid template for batch : ${batchId}") - } - } - - def validateEnrolmentCriteria(event: Event, enrollmentCriteria: Map[String, AnyRef], certName: String, additionalProps: Map[String, List[String]])(metrics:Metrics, cassandraUtil: CassandraUtil, config:CollectionCertPreProcessorConfig): EnrolledUser = { - if(!enrollmentCriteria.isEmpty) { - val query = QueryBuilder.select().from(config.keyspace, config.userEnrolmentsTable) - .where(QueryBuilder.eq(config.dbUserId, event.userId)).and(QueryBuilder.eq(config.dbCourseId, event.courseId)) - .and(QueryBuilder.eq(config.dbBatchId, event.batchId)) - val row = cassandraUtil.findOne(query.toString) - metrics.incCounter(config.dbReadCount) - val enrolmentAdditionProps = additionalProps.getOrElse(config.enrollment, List[String]()) - if(null != row){ - val active:Boolean = row.getBool(config.active) - val issuedCertificates = row.getList(config.issuedCertificates, TypeTokens.mapOf(classOf[String], classOf[String])).asScala.toList - val isCertIssued = !issuedCertificates.isEmpty && !issuedCertificates.filter(cert => certName.equalsIgnoreCase(cert.getOrDefault(config.name,"").asInstanceOf[String])).isEmpty - val status = row.getInt(config.status) - val criteriaStatus = enrollmentCriteria.getOrElse(config.status, 2) - val oldId = if(isCertIssued && event.reIssue) issuedCertificates.filter(cert => certName.equalsIgnoreCase(cert.getOrDefault(config.name,"").asInstanceOf[String])) - .map(cert => cert.getOrDefault(config.identifier, "")).head else "" - val userId = if(active && (criteriaStatus == status) && (!isCertIssued || event.reIssue)) event.userId else "" - val issuedOn = row.getTimestamp(config.completedOn) - val addProps = enrolmentAdditionProps.map(prop => (prop -> row.getObject(prop.toLowerCase))).toMap - EnrolledUser(userId, oldId, issuedOn, {if(addProps.nonEmpty) Map[String, Any](config.enrollment -> addProps) else Map()}) - } else EnrolledUser("", "") - } else EnrolledUser(event.userId, "") - } - - def validateAssessmentCriteria(event: Event, assessmentCriteria: Map[String, AnyRef], enrolledUser: String, additionalProps: Map[String, List[String]])(metrics:Metrics, cassandraUtil: CassandraUtil, contentCache: DataCache, config:CollectionCertPreProcessorConfig):AssessedUser = { - if(!assessmentCriteria.isEmpty && !enrolledUser.isEmpty) { - val filteredUserAssessments = getMaxScore(event)(metrics, cassandraUtil, config, contentCache) - - val scoreMap = filteredUserAssessments.map(sc => sc._1 -> (sc._2.head.score * 100 / sc._2.head.totalScore)).toMap - - val score:Double = if (scoreMap.nonEmpty) scoreMap.values.max else 0d - val assessmentAdditionProps = additionalProps.getOrElse(config.assessment, List()) - val addProps = { - if (assessmentAdditionProps.nonEmpty && assessmentAdditionProps.contains("score")) Map("score" -> scoreMap) - else Map() - } - if(isValidAssessCriteria(assessmentCriteria, score)) { - AssessedUser(enrolledUser, {if(addProps.nonEmpty) Map[String, Any](config.assessment -> addProps) else Map()}) - } else AssessedUser("") - } else AssessedUser(enrolledUser) - } - - def validateUser(userId: String, userCriteria: Map[String, AnyRef], additionalProps: Map[String, List[String]])(metrics:Metrics, config:CollectionCertPreProcessorConfig, httpUtil: HttpUtil) = { - if(!userId.isEmpty) { - val url = config.learnerBasePath + config.userReadApi + "/" + userId + "?organisations,roles,locations,declarations,externalIds" - val result = getAPICall(url, "response")(config, httpUtil, metrics) - if(userCriteria.isEmpty || userCriteria.size == userCriteria.filter(uc => uc._2 == result.getOrElse(uc._1, null)).size) { - result - } else Map[String, AnyRef]() - } else Map[String, AnyRef]() - } - - def getMaxScore(event: Event)(metrics:Metrics, cassandraUtil: CassandraUtil, config:CollectionCertPreProcessorConfig, contentCache: DataCache):Map[String, Set[AssessmentUserAttempt]] = { - val contextId = "cb:" + event.batchId - val query = QueryBuilder.select().column("aggregates").column("agg").from(config.keyspace, config.useActivityAggTable) - .where(QueryBuilder.eq("activity_type", "Course")).and(QueryBuilder.eq("activity_id", event.courseId)) - .and(QueryBuilder.eq("user_id", event.userId)).and(QueryBuilder.eq("context_id", contextId)) - - val rows: java.util.List[Row] = cassandraUtil.find(query.toString) - metrics.incCounter(config.dbReadCount) - if(null != rows && !rows.isEmpty) { - val aggregates: Map[String, Double] = rows.asScala.toList.head - .getMap("aggregates", classOf[String], classOf[java.lang.Double]).asScala.map(e => e._1 -> e._2.toDouble) - .toMap - val agg: Map[String, Double] = rows.asScala.toList.head.getMap("agg", classOf[String], classOf[Integer]) - .asScala.map(e => e._1 -> e._2.toDouble).toMap - val aggs: Map[String, Double] = agg ++ aggregates - val userAssessments = aggs.keySet.filter(key => key.startsWith("score:")).map( - key => { - val id= key.replaceAll("score:", "") - AssessmentUserAttempt(id, aggs.getOrElse("score:" + id, 0d), aggs.getOrElse("max_score:" + id, 1d)) - }).groupBy(f => f.contentId) - - val filteredUserAssessments = userAssessments.filterKeys(key => { - val metadata = contentCache.getWithRetry(key) - if (metadata.nonEmpty) { - val contentType = metadata.getOrElse("contenttype", "") - config.assessmentContentTypes.contains(contentType) - } else if(!metadata.nonEmpty && config.enableSuppressException){ - logger.error("Suppressed exception: Metadata cache not available for: " + key) - false - } else throw new Exception("Metadata cache not available for: " + key) - }) - // TODO: Here we have an assumption that, we will consider max percentage from all the available attempts of different assessment contents. - if (filteredUserAssessments.nonEmpty) filteredUserAssessments else Map() - } else Map() - } - - def isValidAssessCriteria(assessmentCriteria: Map[String, AnyRef], score: Double): Boolean = { - if(assessmentCriteria.get("score").isInstanceOf[Number]) { - score == assessmentCriteria.get("score").asInstanceOf[Int].toDouble - } else { - val scoreCriteria = assessmentCriteria.getOrElse("score", Map[String, AnyRef]()).asInstanceOf[Map[String, Int]] - if(scoreCriteria.isEmpty) false - else { - val operation = scoreCriteria.head._1 - val criteriaScore = scoreCriteria.head._2.toDouble - operation match { - case "EQ" => score == criteriaScore - case "eq" => score == criteriaScore - case "=" => score == criteriaScore - case ">" => score > criteriaScore - case "<" => score < criteriaScore - case ">=" => score >= criteriaScore - case "<=" => score <= criteriaScore - case "ne" => score != criteriaScore - case "!=" => score != criteriaScore - case _ => false - } - } - - } - } - - def getAPICall(url: String, responseParam: String)(config:CollectionCertPreProcessorConfig, httpUtil: HttpUtil, metrics: Metrics): Map[String,AnyRef] = { - val response = httpUtil.get(url, config.defaultHeaders) - if(200 == response.status) { - ScalaJsonUtil.deserialize[Map[String, AnyRef]](response.body) - .getOrElse("result", Map[String, AnyRef]()).asInstanceOf[Map[String, AnyRef]] - .getOrElse(responseParam, Map[String, AnyRef]()).asInstanceOf[Map[String, AnyRef]] - } else if(400 == response.status && response.body.contains(config.userAccBlockedErrCode)) { - metrics.incCounter(config.skippedEventCount) - logger.error(s"Error while fetching user details for ${url}: " + response.status + " :: " + response.body) - Map[String, AnyRef]() - } else { - throw new Exception(s"Error from get API : ${url}, with response: ${response}") - } - } - - def getCourseName(courseId: String)(metrics:Metrics, config:CollectionCertPreProcessorConfig, cache:DataCache, httpUtil: HttpUtil): String = { - val courseMetadata = cache.getWithRetry(courseId) - if(null == courseMetadata || courseMetadata.isEmpty) { - val url = config.contentBasePath + config.contentReadApi + "/" + courseId + "?fields=name" - val response = getAPICall(url, "content")(config, httpUtil, metrics) - StringContext.processEscapes(response.getOrElse(config.name, "").asInstanceOf[String]).filter(_ >= ' ') - } else { - StringContext.processEscapes(courseMetadata.getOrElse(config.name, "").asInstanceOf[String]).filter(_ >= ' ') - } - } - - def generateCertificateEvent(event: Event, template: Map[String, String], userDetails: Map[String, AnyRef], enrolledUser: EnrolledUser, assessedUser: AssessedUser, additionalProps: Map[String, List[String]], certName: String)(metrics:Metrics, config:CollectionCertPreProcessorConfig, cache:DataCache, httpUtil: HttpUtil) = { - val firstName = Option(userDetails.getOrElse("firstName", "").asInstanceOf[String]).getOrElse("") - val lastName = Option(userDetails.getOrElse("lastName", "").asInstanceOf[String]).getOrElse("") - def nullStringCheck(name:String):String = {if(StringUtils.equalsIgnoreCase("null", name)) "" else name} - val recipientName = nullStringCheck(firstName).concat(" ").concat(nullStringCheck(lastName)).trim - val courseName = getCourseName(event.courseId)(metrics, config, cache, httpUtil) - val dateFormatter = new SimpleDateFormat("yyyy-MM-dd") - val related = getRelatedData(event, enrolledUser, assessedUser, userDetails, additionalProps, certName, courseName)(config) - val eData = Map[String, AnyRef] ( - "issuedDate" -> dateFormatter.format(enrolledUser.issuedOn), - "data" -> List(Map[String, AnyRef]("recipientName" -> recipientName, "recipientId" -> event.userId)), - "criteria" -> Map[String, String]("narrative" -> certName), - "svgTemplate" -> template.getOrElse("url", ""), - "oldId" -> enrolledUser.oldId, - "templateId" -> template.getOrElse(config.identifier, ""), - "userId" -> event.userId, - "orgId" -> userDetails.getOrElse("rootOrgId", ""), - "issuer" -> ScalaJsonUtil.deserialize[Map[String, AnyRef]](template.getOrElse(config.issuer, "{}")), - "signatoryList" -> ScalaJsonUtil.deserialize[List[Map[String, AnyRef]]](template.getOrElse(config.signatoryList, "[]")), - "courseName" -> courseName, - "basePath" -> config.certBasePath, - "related" -> related, - "name" -> certName, - "tag" -> event.batchId - ) - - ScalaJsonUtil.serialize(BEJobRequestEvent(edata = eData, `object` = EventObject(id= event.userId))) - } - - def getLocationDetails(userDetails: Map[String, AnyRef], additionalProps: Map[String, List[String]]): Map[String, Any] = { - if(additionalProps.getOrElse("location", List()).nonEmpty) { - val userLocations = userDetails.getOrElse("userLocations", List()).asInstanceOf[List[Map[String, AnyRef]]].map(l => l.getOrElse("type", "").asInstanceOf[String] -> l.getOrElse("name", "").asInstanceOf[String]).toMap - val locAdditionProps = additionalProps.getOrElse("location", List()).map(prop => prop -> userLocations.getOrElse(prop, null)).filter(p => null != p._2).toMap - if(locAdditionProps.nonEmpty) Map("location" -> locAdditionProps) else Map() - }else Map() - } - - def getRelatedData(event: Event, enrolledUser: EnrolledUser, assessedUser: AssessedUser, - userDetails: Map[String, AnyRef], additionalProps: Map[String, List[String]], certName: String, courseName: String)(config: CollectionCertPreProcessorConfig): Map[String, Any] = { - val userAdditionalProps = additionalProps.getOrElse(config.user, List()).filter(prop => userDetails.contains(prop)).map(prop => (prop -> userDetails.getOrElse(prop, null))).toMap - val locationProps = getLocationDetails(userDetails, additionalProps) - val courseAdditionalProps: Map[String, Any] = if(additionalProps.getOrElse("course", List()).nonEmpty) Map("course" -> Map("name" -> courseName)) else Map() - Map[String, Any]("batchId" -> event.batchId, "courseId" -> event.courseId, "type" -> certName) ++ - locationProps ++ enrolledUser.additionalProps ++ assessedUser.additionalProps ++ userAdditionalProps ++ courseAdditionalProps - } -} diff --git a/credential-generator/collection-cert-pre-processor/src/main/scala/org/sunbird/job/collectioncert/task/CollectionCertPreProcessorConfig.scala b/credential-generator/collection-cert-pre-processor/src/main/scala/org/sunbird/job/collectioncert/task/CollectionCertPreProcessorConfig.scala deleted file mode 100644 index f986cfc84..000000000 --- a/credential-generator/collection-cert-pre-processor/src/main/scala/org/sunbird/job/collectioncert/task/CollectionCertPreProcessorConfig.scala +++ /dev/null @@ -1,84 +0,0 @@ -package org.sunbird.job.collectioncert.task - -import com.typesafe.config.Config -import org.apache.flink.api.common.typeinfo.TypeInformation -import org.apache.flink.api.java.typeutils.TypeExtractor -import org.apache.flink.streaming.api.scala.OutputTag -import org.sunbird.job.BaseJobConfig - -import java.util - -class CollectionCertPreProcessorConfig(override val config: Config) extends BaseJobConfig(config, "collection-cert-pre-processor") { - - implicit val stringTypeInfo: TypeInformation[String] = TypeExtractor.getForClass(classOf[String]) - - //Redis config - val collectionCacheStore: Int = 0 - val contentCacheStore: Int = 5 - val metaRedisHost: String = config.getString("redis-meta.host") - val metaRedisPort: Int = config.getInt("redis-meta.port") - - - //kafka config - val kafkaInputTopic: String = config.getString("kafka.input.topic") - val kafkaOutputTopic: String = config.getString("kafka.output.topic") - val certificatePreProcessorConsumer: String = "collection-cert-pre-processor-consumer" - val generateCertificateProducer = "generate-certificate-sink" - override val kafkaConsumerParallelism: Int = config.getInt("task.consumer.parallelism") - val generateCertificateParallelism:Int = config.getInt("task.generate_certificate.parallelism") - - //Tags - val generateCertificateOutputTagName = "generate-certificate-request" - val generateCertificateOutputTag: OutputTag[String] = OutputTag[String](generateCertificateOutputTagName) - - //Cassandra config - val dbHost: String = config.getString("lms-cassandra.host") - val dbPort: Int = config.getInt("lms-cassandra.port") - val keyspace: String = config.getString("lms-cassandra.keyspace") - val courseTable: String = config.getString("lms-cassandra.course_batch.table") - val userEnrolmentsTable: String = config.getString("lms-cassandra.user_enrolments.table") - val assessmentTable: String = config.getString("lms-cassandra.assessment_aggregator.table") - val useActivityAggTable: String = config.getString("lms-cassandra.user_activity_agg.table") - val dbBatchId = "batchid" - val dbCourseId = "courseid" - val dbUserId = "userid" - - //API URL - val contentBasePath = config.getString("service.content.basePath") - val learnerBasePath = config.getString("service.learner.basePath") - val userReadApi = config.getString("user_read_api") - val contentReadApi = config.getString("content_read_api") - - // Metric List - val totalEventsCount = "total-events-count" - val successEventCount = "success-events-count" - val failedEventCount = "failed-events-count" - val skippedEventCount = "skipped-event-count" - val dbReadCount = "db-read-count" - val dbUpdateCount = "db-update-count" - val cacheHitCount = "cache-hit-cout" - - //Constants - val issueCertificate = "issue-certificate" - val certTemplates = "cert_templates" - val criteria: String = "criteria" - val enrollment: String = "enrollment" - val assessment: String = "assessment" - val users: String = "users" - val active: String = "active" - val issuedCertificates: String = "issued_certificates" - val status: String = "status" - val name: String = "name" - val user: String= "user" - val defaultHeaders = Map[String, String] ("Content-Type" -> "application/json") - val identifier: String = "identifier" - val completedOn: String = "completedon" - val issuer: String = "issuer" - val signatoryList: String = "signatoryList" - val certBasePath: String = config.getString("cert_domain_url") + "/certs" - val assessmentContentTypes = if(config.hasPath("assessment.metrics.supported.contenttype")) config.getStringList("assessment.metrics.supported.contenttype") else util.Arrays.asList("SelfAssess") - val userAccBlockedErrCode = "UOS_USRRED0006" - val enableSuppressException: Boolean = if(config.hasPath("enable.suppress.exception")) config.getBoolean("enable.suppress.exception") else false - - -} diff --git a/credential-generator/collection-cert-pre-processor/src/main/scala/org/sunbird/job/collectioncert/task/CollectionCertPreProcessorTask.scala b/credential-generator/collection-cert-pre-processor/src/main/scala/org/sunbird/job/collectioncert/task/CollectionCertPreProcessorTask.scala deleted file mode 100644 index 5c9b0143d..000000000 --- a/credential-generator/collection-cert-pre-processor/src/main/scala/org/sunbird/job/collectioncert/task/CollectionCertPreProcessorTask.scala +++ /dev/null @@ -1,58 +0,0 @@ -package org.sunbird.job.collectioncert.task - -import java.io.File -import com.typesafe.config.ConfigFactory -import org.apache.flink.api.common.typeinfo.TypeInformation -import org.apache.flink.api.java.functions.KeySelector -import org.apache.flink.api.java.typeutils.TypeExtractor -import org.apache.flink.api.java.utils.ParameterTool -import org.apache.flink.streaming.api.scala.StreamExecutionEnvironment -import org.sunbird.job.collectioncert.domain.Event -import org.sunbird.job.collectioncert.functions.CollectionCertPreProcessorFn -import org.sunbird.job.connector.FlinkKafkaConnector -import org.sunbird.job.util.{FlinkUtil, HttpUtil} - -class CollectionCertPreProcessorTask(config: CollectionCertPreProcessorConfig, kafkaConnector: FlinkKafkaConnector, httpUtil: HttpUtil) { - def process(): Unit = { - implicit val env: StreamExecutionEnvironment = FlinkUtil.getExecutionContext(config) - implicit val eventTypeInfo: TypeInformation[Event] = TypeExtractor.getForClass(classOf[Event]) - implicit val stringTypeInfo: TypeInformation[String] = TypeExtractor.getForClass(classOf[String]) - val source = kafkaConnector.kafkaJobRequestSource[Event](config.kafkaInputTopic) - - val progressStream = - env.addSource(source).name(config.certificatePreProcessorConsumer) - .uid(config.certificatePreProcessorConsumer).setParallelism(config.kafkaConsumerParallelism) - .rebalance - .keyBy(new CollectionCertPreProcessorKeySelector()) - .process(new CollectionCertPreProcessorFn(config, httpUtil)) - .name("collection-cert-pre-processor").uid("collection-cert-pre-processor") - .setParallelism(config.parallelism) - - progressStream.getSideOutput(config.generateCertificateOutputTag).addSink(kafkaConnector.kafkaStringSink(config.kafkaOutputTopic)) - .name(config.generateCertificateProducer).uid(config.generateCertificateProducer).setParallelism(config.generateCertificateParallelism) - env.execute(config.jobName) - } - -} - -// $COVERAGE-OFF$ Disabling scoverage as the below code can only be invoked within flink cluster - -object CollectionCertPreProcessorTask { - def main(args: Array[String]): Unit = { - val configFilePath = Option(ParameterTool.fromArgs(args).get("config.file.path")) - val config = configFilePath.map { - path => ConfigFactory.parseFile(new File(path)).resolve() - }.getOrElse(ConfigFactory.load("collection-cert-pre-processor.conf").withFallback(ConfigFactory.systemEnvironment())) - val certificatePreProcessorConfig = new CollectionCertPreProcessorConfig(config) - val kafkaUtil = new FlinkKafkaConnector(certificatePreProcessorConfig) - val httpUtil = new HttpUtil() - val task = new CollectionCertPreProcessorTask(certificatePreProcessorConfig, kafkaUtil, httpUtil) - task.process() - } -} - -// $COVERAGE-ON - -class CollectionCertPreProcessorKeySelector extends KeySelector[Event, String] { - override def getKey(event: Event): String = Set(event.userId, event.courseId, event.batchId).mkString("_") -} diff --git a/credential-generator/collection-cert-pre-processor/src/test/resources/test.conf b/credential-generator/collection-cert-pre-processor/src/test/resources/test.conf deleted file mode 100644 index efad514b6..000000000 --- a/credential-generator/collection-cert-pre-processor/src/test/resources/test.conf +++ /dev/null @@ -1,45 +0,0 @@ -include "base-test.conf" - -kafka { - input.topic = "flink.issue.certificate.request" - output.topic = "flink.generate.certificate.request" - output.failed.topic = "flink.issue.certificate.failed" - groupId = "flink-collection-cert-pre-processor-group" -} - -task { - consumer.parallelism = 1 - parallelism = 1 - generate_certificate.parallelism = 1 -} - -lms-cassandra { - keyspace = "sunbird_courses" - user_enrolments.table = "user_enrolments" - course_batch.table = "course_batch" - assessment_aggregator.table = "assessment_aggregator" - user_activity_agg.table = "user_activity_agg" - host = "localhost" - port = "9142" -} - -dp-redis { - host = localhost - port = 6340 - database.index = 5 -} - -cert_domain_url="https://dev.sunbirded.org" -user_read_api = "/private/user/v1/read" -content_read_api = "/content/v3/read" - -service { - content.basePath = "http://localhost:9000/content" - learner.basePath = "http://localhost:9000/learner" -} - -redis-meta { - host = localhost - port = 6379 -} -assessment.metrics.supported.contenttype = ["selfAssess"] diff --git a/credential-generator/collection-cert-pre-processor/src/test/resources/test.cql b/credential-generator/collection-cert-pre-processor/src/test/resources/test.cql deleted file mode 100644 index 5a5b056cf..000000000 --- a/credential-generator/collection-cert-pre-processor/src/test/resources/test.cql +++ /dev/null @@ -1,85 +0,0 @@ -CREATE KEYSPACE IF NOT EXISTS sunbird_courses with replication = {'class':'SimpleStrategy','replication_factor':1}; - -CREATE TABLE IF NOT EXISTS sunbird_courses.course_batch ( - courseid text, - batchid text, - cert_templates map>>, - createdby text, - createddate text, - createdfor list, - description text, - enddate text, - enrollmentenddate text, - enrollmenttype text, - mentors list, - name text, - startdate text, - status int, - updateddate text, - PRIMARY KEY (courseid, batchid) -); - -CREATE TABLE IF NOT EXISTS sunbird_courses.user_enrolments ( - userid text, - courseid text, - batchid text, - active boolean, - addedby text, - certificates list>>, - completedon timestamp, - completionpercentage int, - contentstatus map, - datetime timestamp, - issued_certificates list>>, - enrolleddate text, - lastreadcontentid text, - lastreadcontentstatus int, - progress int, - status int, - PRIMARY KEY (userid, courseid, batchid) -); - -CREATE TABLE IF NOT EXISTS sunbird_courses.assessment_aggregator ( - user_id text, - course_id text, - batch_id text, - content_id text, - attempt_id text, - created_on timestamp, - grand_total text, - last_attempted_on timestamp, - total_max_score double, - total_score double, - updated_on timestamp, - PRIMARY KEY ((user_id, course_id), batch_id, content_id, attempt_id) -); - -CREATE TABLE IF NOT EXISTS sunbird_courses.user_activity_agg ( - activity_id text, - user_id text, - activity_type text, - context_id text, - agg Map, - aggregates Map, - agg_last_updated Map, - PRIMARY KEY ((activity_type, activity_id), context_id, user_id) -); - - -//event 1 -INSERT INTO sunbird_courses.course_batch(courseid, batchid, cert_templates) VALUES ('do_11309999837886054415','0131000245281587206',{'template_01_dev_001':{'criteria': '{"enrollment":{"status":2}, "assessment": {"score": {">=": 80}}}', 'identifier': 'template_01_dev_001', 'url': 'template-url.svg', 'issuer': '{"name":"Gujarat Council of Educational Research and Training","publicKey":["7","8"],"url":"https://gcert.gujarat.gov.in/gcert/"}', 'name': 'Course Completion Certificate', 'notifyTemplate': '{"subject":"Completion certificate","stateImgUrl":"https://sunbirddev.blob.core.windows.net/orgemailtemplate/img/File-0128212938260643843.png","regardsperson":"Chairperson","regards":"Minister of Gujarat","emailTemplateType":"defaultCertTemp"}', 'signatoryList': '[{"image":"https://cdn.pixabay.com/photo/2014/11/09/08/06/signature-523237__340.jpg","name":"CEO Gujarat","id":"CEO","designation":"CEO"}]'}}); -INSERT INTO sunbird_courses.user_enrolments(userid, courseid, batchid, issued_certificates, completedon, status, active) VALUES ('user001','do_11309999837886054415','0131000245281587206',[{'identifier': 'certificateId', 'lastIssuedOn': '2019-08-21', 'name': 'Course Completion Certificate', 'token': 'P4L3Y9'}], toTimeStamp(toDate(now())), 2, true); -INSERT INTO sunbird_courses.assessment_aggregator(user_id, course_id, batch_id, content_id, attempt_id, total_max_score, total_score) VALUES ('user001','do_11309999837886054415','0131000245281587206', 'content_001', 'attempt_001', 1, 1); -INSERT INTO sunbird_courses.user_activity_agg(activity_id, user_id, activity_type, context_id, aggregates) VALUES ('do_11309999837886054415', 'user001', 'Course', 'cb:0131000245281587206', {'score:content_001': 1, 'max_score:content_001': 1}); -//event 2 -INSERT INTO sunbird_courses.course_batch(courseid, batchid,cert_templates) VALUES ('do_11309999837886054416','0131000245281587207',{'template_01_dev_001':{'criteria': '{"enrollment":{"status":2}}','url': 'template-url.svg', 'identifier': 'template_01_dev_001', 'issuer': '{"name":"Gujarat Council of Educational Research and Training","publicKey":["7","8"],"url":"https://gcert.gujarat.gov.in/gcert/"}', 'name': 'Course merit certificate', 'signatoryList': '[{"image":"https://cdn.pixabay.com/photo/2014/11/09/08/06/signature-523237__340.jpg","name":"CEO Gujarat","id":"CEO","designation":"CEO"}]'}}); -INSERT INTO sunbird_courses.user_enrolments(userid, courseid, batchid,completedon) VALUES ('user002','do_11309999837886054416','0131000245281587207', toTimeStamp(toDate(now()))); -//empty cert_template -INSERT INTO sunbird_courses.course_batch(courseid, batchid, cert_templates) VALUES ('course_002','batch_002',{}); - -//event 3 -INSERT INTO sunbird_courses.course_batch(courseid, batchid, cert_templates) VALUES ('course_003','batch_003',{'template_01_dev_001':{'criteria': '{"enrollment":{"status":2}, "assessment": {"score": {">=": 80}}}', 'identifier': 'template_01_dev_001', 'url': 'template-url.svg', 'issuer': '{"name":"Gujarat Council of Educational Research and Training","publicKey":["7","8"],"url":"https://gcert.gujarat.gov.in/gcert/"}', 'name': 'Course merit certificate', 'notifyTemplate': '{"subject":"Completion certificate","stateImgUrl":"https://sunbirddev.blob.core.windows.net/orgemailtemplate/img/File-0128212938260643843.png","regardsperson":"Chairperson","regards":"Minister of Gujarat","emailTemplateType":"defaultCertTemp"}', 'signatoryList': '[{"image":"https://cdn.pixabay.com/photo/2014/11/09/08/06/signature-523237__340.jpg","name":"CEO Gujarat","id":"CEO","designation":"CEO"}]'}}); -//user with empty issued certificate -INSERT INTO sunbird_courses.user_enrolments(userid, courseid, batchid, issued_certificates, completedon, status, active) VALUES ('user003','course_003','batch_003',[], toTimeStamp(toDate(now())), 2, true); -INSERT INTO sunbird_courses.assessment_aggregator(user_id, course_id, batch_id, content_id, attempt_id, total_max_score, total_score) VALUES ('user003','course_003','batch_003', 'content_001', 'attempt_001', 1, 1); -INSERT INTO sunbird_courses.user_activity_agg(activity_id, user_id, activity_type, context_id, agg) VALUES ('course_003', 'user003', 'Course', 'cb:batch_003', {'score:content_001': 1, 'max_score:content_001': 1}); diff --git a/credential-generator/collection-cert-pre-processor/src/test/scala/org/sunbird/job/collectioncert/fixture/EventFixture.scala b/credential-generator/collection-cert-pre-processor/src/test/scala/org/sunbird/job/collectioncert/fixture/EventFixture.scala deleted file mode 100644 index d3b665263..000000000 --- a/credential-generator/collection-cert-pre-processor/src/test/scala/org/sunbird/job/collectioncert/fixture/EventFixture.scala +++ /dev/null @@ -1,15 +0,0 @@ -package org.sunbird.job.collectioncert.fixture - -object EventFixture { - - val EVENT_1: String = """{"eid": "BE_JOB_REQUEST","ets": 1621833310477,"mid": "LP.1621833310477.429e795f-6d3e-4a11-8754-685984d62d10","actor": {"id": "Course Certificate Generator","type": "System"},"context": {"pdata": {"ver": "1.0","id": "org.sunbird.platform"}},"object": {"id": "0131000245281587206_do_11309999837886054416","type": "CourseCertificateGeneration"},"edata": {"userIds": ["user001"],"action": "issue-certificate","iteration": 1, "trigger": "auto-issue","batchId": "0131000245281587206","reIssue": true,"courseId": "do_11309999837886054415"}}""" - val USER_1: String = """{"id":".private.user.v1.read.c4cc494f-04c3-49f3-b3d5-7b1a1984abad","ver":"private","ts":"2021-05-27 10:25:05:836+0000","params":{"resmsgid":null,"msgid":"8e27cbf5-e299-43b0-bca7-8347f7e5abcf","err":null,"status":"success","errmsg":null},"responseCode":"OK","result":{"response":{"firstName":"user","lastName":"name","rootOrgId": "Org001","userLocations":[{"code":"29","name":"Karnataka","id":"027f81d8-0a2c-4fc6-96ac-59fe4cea3abf","type":"state","parentId":null},{"code":"2920","name":"BENGALURU URBAN SOUTH","id":"fa17379e-f8d5-4403-ae64-9e339f1dd599","type":"district","parentId":"027f81d8-0a2c-4fc6-96ac-59fe4cea3abf"}],"rootOrg":{"keys":{}}}}}""" - val CONTENT_1: String = """{"id":"api.content.read","ver":"3.0","ts":"2021-05-27T10:31:33ZZ","params":{"resmsgid":"316b05dd-df1a-4867-968a-042bb06a710f","msgid":null,"err":null,"status":"successful","errmsg":null},"responseCode":"OK","result":{"content":{"objectType":"Content","primaryCategory":"Course","contentType":"Course","identifier":"do_11309999837886054416","languageCode":["en"],"name":"test Course"}}}""" - val TEMPLATE_1: String = """{"criteria":"{\"enrollment\":{\"status\":2},\"assessment\":{\"score\":{\">=\":50}}}","identifier":"template_svg_04-prad","issuer":"{\"name\":\"Gujarat Council of Educational Research and Training\",\"url\":\"https://gcert.gujarat.gov.in/gcert/\"}","name":"Course Completion Certificate","signatoryList":"[{\"image\":\"https://cdn.pixabay.com/photo/2014/11/09/08/06/signature-523237__340.jpg\",\"name\":\"CEO Gujarat\",\"id\":\"CEO\",\"designation\":\"CEO\"}]","url":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/template_svg_04-prad/artifact/template-1.svg","additionalProps": "{\"enrollment\":[\"completedOn\"],\"location\":[\"state\",\"district\",\"school\"],\"assessment\":[\"score\"],\"course\":[\"name\"]}"}""" - - val USER_2_EMPTY_LASTNAME: String = """{"id":".private.user.v1.read.c4cc494f-04c3-49f3-b3d5-7b1a1984abad","ver":"private","ts":"2021-05-27 10:25:05:836+0000","params":{"resmsgid":null,"msgid":"8e27cbf5-e299-43b0-bca7-8347f7e5abcf","err":null,"status":"success","errmsg":null},"responseCode":"OK","result":{"response":{"firstName":"Rajesh","lastName":"","rootOrgId": "Org001","rootOrg":{"keys":{}}}}}""" - val USER_3_NULL_VALUE_LASTNAME: String = """{"id":".private.user.v1.read.c4cc494f-04c3-49f3-b3d5-7b1a1984abad","ver":"private","ts":"2021-05-27 10:25:05:836+0000","params":{"resmsgid":null,"msgid":"8e27cbf5-e299-43b0-bca7-8347f7e5abcf","err":null,"status":"success","errmsg":null},"responseCode":"OK","result":{"response":{"firstName":"Suresh","lastName":null,"rootOrgId": "Org001","rootOrg":{"keys":{}}}}}""" - val USER_4_NULL_STRING_VALUE_LASTNAME: String = """{"id":".private.user.v1.read.c4cc494f-04c3-49f3-b3d5-7b1a1984abad","ver":"private","ts":"2021-05-27 10:25:05:836+0000","params":{"resmsgid":null,"msgid":"8e27cbf5-e299-43b0-bca7-8347f7e5abcf","err":null,"status":"success","errmsg":null},"responseCode":"OK","result":{"response":{"firstName":"Manju","lastName":"null","rootOrgId": "Org001","rootOrg":{"keys":{}}}}}""" - - -} diff --git a/credential-generator/collection-cert-pre-processor/src/test/scala/org/sunbird/job/collectioncert/function/spec/CollectionCertPreProcessFnTestSpec.scala b/credential-generator/collection-cert-pre-processor/src/test/scala/org/sunbird/job/collectioncert/function/spec/CollectionCertPreProcessFnTestSpec.scala deleted file mode 100644 index 241f9f59b..000000000 --- a/credential-generator/collection-cert-pre-processor/src/test/scala/org/sunbird/job/collectioncert/function/spec/CollectionCertPreProcessFnTestSpec.scala +++ /dev/null @@ -1,127 +0,0 @@ -package org.sunbird.job.collectioncert.function.spec - -import com.typesafe.config.{Config, ConfigFactory} -import org.apache.flink.api.common.typeinfo.TypeInformation -import org.apache.flink.api.java.typeutils.TypeExtractor -import org.cassandraunit.CQLDataLoader -import org.cassandraunit.dataset.cql.FileCQLDataSet -import org.cassandraunit.utils.EmbeddedCassandraServerHelper -import org.mockito.Mockito.when -import org.mockito.{ArgumentMatchers, Mockito} -import org.sunbird.job.Metrics -import org.sunbird.job.cache.{DataCache, RedisConnect} -import org.sunbird.job.collectioncert.domain.Event -import org.sunbird.job.collectioncert.fixture.EventFixture -import org.sunbird.job.collectioncert.functions.CollectionCertPreProcessorFn -import org.sunbird.job.collectioncert.task.CollectionCertPreProcessorConfig -import org.sunbird.job.util._ -import org.sunbird.spec.BaseTestSpec -import redis.clients.jedis.Jedis -import redis.embedded.RedisServer - -class CollectionCertPreProcessFnTestSpec extends BaseTestSpec { - implicit val stringTypeInfo: TypeInformation[String] = TypeExtractor.getForClass(classOf[String]) - var cassandraUtil: CassandraUtil = _ - val config: Config = ConfigFactory.load("test.conf") - lazy val jobConfig: CollectionCertPreProcessorConfig = new CollectionCertPreProcessorConfig(config) - val httpUtil: HttpUtil = new HttpUtil - val mockHttpUtil:HttpUtil = mock[HttpUtil](Mockito.withSettings().serializable()) - val metricJson = s"""{"${jobConfig.totalEventsCount}": 0, "${jobConfig.skippedEventCount}": 0}""" - val mockMetrics = mock[Metrics](Mockito.withSettings().serializable()) - var jedis: Jedis = _ - var cache: DataCache = _ - var contentCache: DataCache = _ - var redisServer: RedisServer = _ - redisServer = new RedisServer(6340) - redisServer.start() - - override protected def beforeAll(): Unit = { - super.beforeAll() - val redisConnect = new RedisConnect(jobConfig) - jedis = redisConnect.getConnection(jobConfig.collectionCacheStore) - EmbeddedCassandraServerHelper.startEmbeddedCassandra(80000L) - cassandraUtil = new CassandraUtil(jobConfig.dbHost, jobConfig.dbPort) - val session = cassandraUtil.session - val dataLoader = new CQLDataLoader(session) - dataLoader.load(new FileCQLDataSet(getClass.getResource("/test.cql").getPath, true, true)) - cache = new DataCache(jobConfig, redisConnect, jobConfig.collectionCacheStore, null) - cache.init() - contentCache = new DataCache(jobConfig, redisConnect, jobConfig.contentCacheStore, List()) - contentCache.init() - jedis.flushDB() - - } - - override protected def afterAll(): Unit = { - super.afterAll() - try { - EmbeddedCassandraServerHelper.cleanEmbeddedCassandra() - } catch { - case ex: Exception => ex.printStackTrace() - } - redisServer.stop() - } - - "CertPreProcess " should " issue certificate to valid request" in { - val event = new Event(JSONUtil.deserialize[java.util.Map[String, Any]](EventFixture.EVENT_1), 0, 0) - val template = ScalaJsonUtil.deserialize[Map[String, String]](EventFixture.TEMPLATE_1) - when(mockHttpUtil.get(ArgumentMatchers.contains(jobConfig.userReadApi), ArgumentMatchers.any[Map[String, String]]())).thenReturn(HTTPResponse(200, EventFixture.USER_1)) - when(mockHttpUtil.get(ArgumentMatchers.contains(jobConfig.contentReadApi), ArgumentMatchers.any[Map[String, String]]())).thenReturn(HTTPResponse(200, EventFixture.CONTENT_1)) - jedis.select(jobConfig.contentCacheStore) - jedis.set("content_001", """{"identifier":"content_001","contentType": "selfAssess"}""") - val certEvent = new CollectionCertPreProcessorFn(jobConfig, mockHttpUtil)(stringTypeInfo, cassandraUtil).issueCertificate(event, template)(cassandraUtil, cache, contentCache, mockMetrics, jobConfig, mockHttpUtil) - certEvent shouldNot be(null) - } - - - "CertPreProcess When User Last Name is Empty " should " Should get Valid getRecipientName " in { - val event = new Event(JSONUtil.deserialize[java.util.Map[String, Any]](EventFixture.EVENT_1), 0, 0) - val template = ScalaJsonUtil.deserialize[Map[String, String]](EventFixture.TEMPLATE_1) - when(mockHttpUtil.get(ArgumentMatchers.contains(jobConfig.userReadApi), ArgumentMatchers.any[Map[String, String]]())).thenReturn(HTTPResponse(200, EventFixture.USER_2_EMPTY_LASTNAME)) - when(mockHttpUtil.get(ArgumentMatchers.contains(jobConfig.contentReadApi), ArgumentMatchers.any[Map[String, String]]())).thenReturn(HTTPResponse(200, EventFixture.CONTENT_1)) - jedis.select(jobConfig.contentCacheStore) - jedis.set("content_001", """{"identifier":"content_001","contentType": "selfAssess"}""") - val certEvent: String = new CollectionCertPreProcessorFn(jobConfig, mockHttpUtil)(stringTypeInfo, cassandraUtil).issueCertificate(event, template)(cassandraUtil, cache, contentCache, mockMetrics, jobConfig, mockHttpUtil) - certEvent shouldNot be(null) - getRecipientName(certEvent) should be("Rajesh") - } - - - "CertPreProcess When User Last Name is Null " should " Should get Valid getRecipientName " in { - val event = new Event(JSONUtil.deserialize[java.util.Map[String, Any]](EventFixture.EVENT_1), 0, 0) - val template = ScalaJsonUtil.deserialize[Map[String, String]](EventFixture.TEMPLATE_1) - when(mockHttpUtil.get(ArgumentMatchers.contains(jobConfig.userReadApi), ArgumentMatchers.any[Map[String, String]]())).thenReturn(HTTPResponse(200, EventFixture.USER_3_NULL_VALUE_LASTNAME)) - when(mockHttpUtil.get(ArgumentMatchers.contains(jobConfig.contentReadApi), ArgumentMatchers.any[Map[String, String]]())).thenReturn(HTTPResponse(200, EventFixture.CONTENT_1)) - jedis.select(jobConfig.contentCacheStore) - jedis.set("content_001", """{"identifier":"content_001","contentType": "selfAssess"}""") - val certEvent: String = new CollectionCertPreProcessorFn(jobConfig, mockHttpUtil)(stringTypeInfo, cassandraUtil).issueCertificate(event, template)(cassandraUtil, cache, contentCache, mockMetrics, jobConfig, mockHttpUtil) - certEvent shouldNot be(null) - getRecipientName(certEvent) should be("Suresh") - } - - - "CertPreProcess When User Last Name is String Null " should " Should get Valid getRecipientName " in { - val event = new Event(JSONUtil.deserialize[java.util.Map[String, Any]](EventFixture.EVENT_1), 0, 0) - val template = ScalaJsonUtil.deserialize[Map[String, String]](EventFixture.TEMPLATE_1) - when(mockHttpUtil.get(ArgumentMatchers.contains(jobConfig.userReadApi), ArgumentMatchers.any[Map[String, String]]())).thenReturn(HTTPResponse(200, EventFixture.USER_4_NULL_STRING_VALUE_LASTNAME)) - when(mockHttpUtil.get(ArgumentMatchers.contains(jobConfig.contentReadApi), ArgumentMatchers.any[Map[String, String]]())).thenReturn(HTTPResponse(200, EventFixture.CONTENT_1)) - jedis.select(jobConfig.contentCacheStore) - jedis.set("content_001", """{"identifier":"content_001","contentType": "selfAssess"}""") - val certEvent: String = new CollectionCertPreProcessorFn(jobConfig, mockHttpUtil)(stringTypeInfo, cassandraUtil).issueCertificate(event, template)(cassandraUtil, cache, contentCache, mockMetrics, jobConfig, mockHttpUtil) - certEvent shouldNot be(null) - getRecipientName(certEvent) should be("Manju") - } - - - def getRecipientName(certEvent: String): String = { - import java.util - import com.google.gson.Gson - import com.google.gson.internal.LinkedTreeMap - val gson = new Gson() - val certMapEvent = gson.fromJson(certEvent, new java.util.LinkedHashMap[String, Any]().getClass) - val certEdata = certMapEvent.getOrDefault("edata", new util.LinkedHashMap[String, Any]()).asInstanceOf[LinkedTreeMap[String, Any]] - val certUserData = certEdata.getOrDefault("data", new util.LinkedList[util.LinkedHashMap[String, Any]]()).asInstanceOf[util.ArrayList[LinkedTreeMap[String, Any]]] - certUserData.get(0).get("recipientName").asInstanceOf[String] - } - -} diff --git a/credential-generator/collection-cert-pre-processor/src/test/scala/org/sunbird/job/collectioncert/function/spec/CollectionCertPreProcessorTaskSpec.scala b/credential-generator/collection-cert-pre-processor/src/test/scala/org/sunbird/job/collectioncert/function/spec/CollectionCertPreProcessorTaskSpec.scala deleted file mode 100644 index e04c644b0..000000000 --- a/credential-generator/collection-cert-pre-processor/src/test/scala/org/sunbird/job/collectioncert/function/spec/CollectionCertPreProcessorTaskSpec.scala +++ /dev/null @@ -1,120 +0,0 @@ -package org.sunbird.job.collectioncert.function.spec - -import java.util - -import com.google.gson.Gson -import com.typesafe.config.{Config, ConfigFactory} -import org.apache.flink.api.common.typeinfo.TypeInformation -import org.apache.flink.api.java.typeutils.TypeExtractor -import org.apache.flink.runtime.testutils.MiniClusterResourceConfiguration -import org.apache.flink.streaming.api.functions.sink.SinkFunction -import org.apache.flink.streaming.api.functions.source.SourceFunction -import org.apache.flink.streaming.api.functions.source.SourceFunction.SourceContext -import org.apache.flink.test.util.MiniClusterWithClientResource -import org.cassandraunit.CQLDataLoader -import org.cassandraunit.dataset.cql.FileCQLDataSet -import org.cassandraunit.utils.EmbeddedCassandraServerHelper -import org.mockito.Mockito.when -import org.mockito.{ArgumentMatchers, Mockito} -import org.scalatest.DoNotDiscover -import org.sunbird.job.cache.RedisConnect -import org.sunbird.job.collectioncert.domain.Event -import org.sunbird.job.collectioncert.fixture.EventFixture -import org.sunbird.job.collectioncert.task.{CollectionCertPreProcessorConfig, CollectionCertPreProcessorTask} -import org.sunbird.job.connector.FlinkKafkaConnector -import org.sunbird.job.util.{CassandraUtil, HTTPResponse, HttpUtil, JSONUtil} -import org.sunbird.spec.{BaseMetricsReporter, BaseTestSpec} -import redis.clients.jedis.Jedis -import redis.embedded.RedisServer - -@DoNotDiscover -class CollectionCertPreProcessorTaskSpec extends BaseTestSpec { - implicit val mapTypeInfo: TypeInformation[java.util.Map[String, AnyRef]] = TypeExtractor.getForClass(classOf[java.util.Map[String, AnyRef]]) - implicit val eventTypeInfo: TypeInformation[Event] = TypeExtractor.getForClass(classOf[Event]) - implicit val stringTypeInfo: TypeInformation[String] = TypeExtractor.getForClass(classOf[String]) - - val flinkCluster = new MiniClusterWithClientResource(new MiniClusterResourceConfiguration.Builder() - .setConfiguration(testConfiguration()) - .setNumberSlotsPerTaskManager(1) - .setNumberTaskManagers(1) - .build) - - var redisServer: RedisServer = _ - redisServer = new RedisServer(6340) - redisServer.start() - var jedis: Jedis = _ - - val mockKafkaUtil: FlinkKafkaConnector = mock[FlinkKafkaConnector](Mockito.withSettings().serializable()) - implicit val mockHttpUtil: HttpUtil = mock[HttpUtil](Mockito.withSettings().serializable()) - val gson = new Gson() - val config: Config = ConfigFactory.load("test.conf") - val jobConfig: CollectionCertPreProcessorConfig = new CollectionCertPreProcessorConfig(config) - - - var cassandraUtil: CassandraUtil = _ - - override protected def beforeAll(): Unit = { - super.beforeAll() - val redisConnect = new RedisConnect(jobConfig) - jedis = redisConnect.getConnection(jobConfig.collectionCacheStore) - EmbeddedCassandraServerHelper.startEmbeddedCassandra(80000L) - cassandraUtil = new CassandraUtil(jobConfig.dbHost, jobConfig.dbPort) - val session = cassandraUtil.session - - val dataLoader = new CQLDataLoader(session) - dataLoader.load(new FileCQLDataSet(getClass.getResource("/test.cql").getPath, true, true)) - // Clear the metrics - BaseMetricsReporter.gaugeMetrics.clear() - jedis.flushDB() - flinkCluster.before() - } - - override protected def afterAll(): Unit = { - super.afterAll() - try { - EmbeddedCassandraServerHelper.cleanEmbeddedCassandra() - redisServer.stop() - } catch { - case ex: Exception => ex.printStackTrace() - } - flinkCluster.after() - } - - def initialize() { - when(mockKafkaUtil.kafkaJobRequestSource[Event](jobConfig.kafkaInputTopic)) - .thenReturn(new CollectionCertPreProcessorEventSource) - when(mockKafkaUtil.kafkaStringSink(jobConfig.kafkaOutputTopic)).thenReturn(new GenerateCertificateSink) - - when(mockHttpUtil.get(ArgumentMatchers.contains(jobConfig.userReadApi), ArgumentMatchers.any[Map[String, String]]())).thenReturn(HTTPResponse(200, EventFixture.USER_1)) - when(mockHttpUtil.get(ArgumentMatchers.contains(jobConfig.contentReadApi), ArgumentMatchers.any[Map[String, String]]())).thenReturn(HTTPResponse(200, EventFixture.CONTENT_1)) - } - "CollectionCertPreProcessor " should "validate metrics " in { - initialize() - new CollectionCertPreProcessorTask(jobConfig, mockKafkaUtil, mockHttpUtil).process() - BaseMetricsReporter.gaugeMetrics(s"${jobConfig.jobName}.${jobConfig.totalEventsCount}").getValue() should be(1) - BaseMetricsReporter.gaugeMetrics(s"${jobConfig.jobName}.${jobConfig.dbReadCount}").getValue() should be(2) - BaseMetricsReporter.gaugeMetrics(s"${jobConfig.jobName}.${jobConfig.successEventCount}").getValue() should be(1) - } - -} - -class CollectionCertPreProcessorEventSource extends SourceFunction[Event] { - override def run(ctx: SourceContext[Event]): Unit = { - ctx.collect(new Event(JSONUtil.deserialize[java.util.Map[String, Any]](EventFixture.EVENT_1), 0, 0)) - } - override def cancel(): Unit = {} -} - -class GenerateCertificateSink extends SinkFunction[String] { - override def invoke(value: String): Unit = { - synchronized { - println(value) - GenerateCertificateSink.values.add(value) - } - } -} - -object GenerateCertificateSink { - val values: util.List[String] = new util.ArrayList() -} - diff --git a/credential-generator/collection-certificate-generator/pom.xml b/credential-generator/collection-certificate-generator/pom.xml deleted file mode 100755 index 5b9e80b1d..000000000 --- a/credential-generator/collection-certificate-generator/pom.xml +++ /dev/null @@ -1,216 +0,0 @@ - - - 4.0.0 - - org.sunbird - credential-generator - 1.0 - - org.sunbird - collection-certificate-generator - 1.0.0 - jar - - - UTF-8 - 1.4.0 - - - - - org.apache.flink - flink-streaming-scala_${scala.version} - ${flink.version} - provided - - - org.sunbird - jobs-core - 1.0.0 - - - joda-time - joda-time - 2.10.6 - - - org.sunbird - jobs-core - 1.0.0 - test-jar - test - - - org.apache.flink - flink-test-utils_${scala.version} - ${flink.version} - test - - - org.apache.flink - flink-runtime_${scala.version} - ${flink.version} - test - tests - - - org.apache.flink - flink-streaming-java_${scala.version} - ${flink.version} - test - tests - - - org.scalatest - scalatest_${scala.version} - 3.0.6 - test - - - org.mockito - mockito-core - 3.3.3 - test - - - org.sunbird - certificate-processor - 1.0 - - - org.cassandraunit - cassandra-unit - 3.11.2.0 - test - - - - - - src/main/scala - src/test/scala - - - org.apache.maven.plugins - maven-compiler-plugin - 3.8.1 - - 11 - - - - org.apache.maven.plugins - maven-shade-plugin - 3.2.1 - - - - package - - shade - - - - - com.google.code.findbugs:jsr305 - - - - - - *:* - - META-INF/*.SF - META-INF/*.DSA - META-INF/*.RSA - - - - - - - org.sunbird.job.certgen.task.CertificateGeneratorStreamTask - - - - reference.conf - - - - - - - - - net.alchim31.maven - scala-maven-plugin - 4.4.0 - - 11 - 11 - ${scala.maj.version} - false - - - - scala-compile-first - process-resources - - add-source - compile - - - - scala-test-compile - process-test-resources - - testCompile - - - - - - - maven-surefire-plugin - 2.22.2 - - true - - - - - org.scalatest - scalatest-maven-plugin - 1.0 - - ${project.build.directory}/surefire-reports - . - collection-certificate-generator-testsuite.txt - - - - test - - test - - - - - - org.scoverage - scoverage-maven-plugin - ${scoverage.plugin.version} - - ${scala.version} - true - true - - - - - diff --git a/credential-generator/collection-certificate-generator/src/main/resources/collection-certificate-generator.conf b/credential-generator/collection-certificate-generator/src/main/resources/collection-certificate-generator.conf deleted file mode 100755 index 3cafbde91..000000000 --- a/credential-generator/collection-certificate-generator/src/main/resources/collection-certificate-generator.conf +++ /dev/null @@ -1,35 +0,0 @@ -include "base-config.conf" - -kafka { - input.topic = "sunbirddev.generate.certificate.request" - output.failed.topic = "sunbirddev.generate.certificate.failed" - groupId = "certificate-generator-group" - output.audit.topic = "sunbirddev.telemetry.raw" -} - -task { - consumer.parallelism = 1 - parallelism = 1 - notifier.parallelism = 1 - userfeed.parallelism = 1 - rc.badcharlist = "\\x00,\\\\aaa,\\aaa,Ø,Ý" -} - -service { - certreg.basePath = "http://localhost:9000/certreg" - learner.basePath = "http://localhost:9000/learner" - enc.basePath = "http://localhost:9000/enc" - rc.basePath = "http://localhost:8081/api/v1" - rc.entity = "TrainingCertificate" -} - -lms-cassandra { - keyspace = "sunbird_courses" - user_enrolments.table = "user_enrolments" - course_batch.table = "course_batch" - sbkeyspace = "sunbird" - certreg.table ="cert_registry" -} - -enable.suppress.exception = true -enable.rc.certificate = true diff --git a/credential-generator/collection-certificate-generator/src/main/resources/log4j.properties b/credential-generator/collection-certificate-generator/src/main/resources/log4j.properties deleted file mode 100755 index 87884853d..000000000 --- a/credential-generator/collection-certificate-generator/src/main/resources/log4j.properties +++ /dev/null @@ -1,11 +0,0 @@ -# log4j.appender.file=org.apache.log4j.FileAppender -log4j.appender.file=org.apache.log4j.RollingFileAppender -log4j.appender.file.file=collection-certificate-generator.log -log4j.appender.file.append=true -log4j.appender.file.layout=org.apache.log4j.PatternLayout -log4j.appender.file.MaxFileSize=256KB -log4j.appender.file.MaxBackupIndex=4 -log4j.appender.file.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss,SSS} %-5p %-60c %x - %m%n - -# Suppress the irrelevant (wrong) warnings from the Netty channel handler -log4j.logger.org.apache.flink.shaded.akka.org.jboss.netty.channel.DefaultChannelPipeline=ERROR, file \ No newline at end of file diff --git a/credential-generator/collection-certificate-generator/src/main/scala/org/sunbird/job/certgen/domain/Event.scala b/credential-generator/collection-certificate-generator/src/main/scala/org/sunbird/job/certgen/domain/Event.scala deleted file mode 100755 index df7ed101e..000000000 --- a/credential-generator/collection-certificate-generator/src/main/scala/org/sunbird/job/certgen/domain/Event.scala +++ /dev/null @@ -1,61 +0,0 @@ -package org.sunbird.job.certgen.domain - -import java.util - -import org.sunbird.job.domain.reader.JobRequest - -import scala.collection.JavaConverters - -class Event(eventMap: java.util.Map[String, Any], partition: Int, offset: Long) extends JobRequest(eventMap, partition , offset) { - - private val jobName = "CollectionCertificateGenerator" - - import scala.collection.JavaConverters._ - - def action: String = readOrDefault[String]("edata.action", "") - - def eData: Map[String, AnyRef] = readOrDefault[Map[String, AnyRef]]("edata", Map[String, AnyRef]()) - - def oldId: String = readOrDefault[String]("edata.oldId", "") - - def basePath: String = readOrDefault[String]("edata.basePath", "") - - def tag: String = readOrDefault[String]("edata.tag", "") - - def templateId: String = readOrDefault[String]("edata.templateId", "") - - def svgTemplate: String = readOrDefault[String]("edata.svgTemplate", "") - - def courseName: String = readOrDefault[String]("edata.courseName", "") - - def name: String = readOrDefault[String]("edata.name", "") - - def issuedDate: String = readOrDefault[String]("edata.issuedDate", "") - - def expiryDate: String = readOrDefault[String]("edata.expiry", "") - - //def data: List[Map[String, AnyRef]] = readOrDefault[List[java.util.Map[String, AnyRef]]]("edata.data", List[java.util.Map[String, AnyRef]]()).map(d => JavaConverters.mapAsScalaMap(d).toMap) - def data: List[Map[String, AnyRef]] = readOrDefault[List[Map[String, AnyRef]]]("edata.data", List[Map[String, AnyRef]]()).map(m => m.toMap).toList - - //def signatoryList: List[Map[String, AnyRef]] = readOrDefault[List[java.util.Map[String, AnyRef]]]("edata.signatoryList", List[java.util.Map[String, AnyRef]]()).map(d => JavaConverters.mapAsScalaMap(d).toMap) - def signatoryList: List[Map[String, AnyRef]] = readOrDefault[List[Map[String, AnyRef]]]("edata.signatoryList", List[Map[String, AnyRef]]()) - - def issuer: Map[String, AnyRef] = readOrDefault[Map[String, AnyRef]]("edata.issuer", Map[String, AnyRef]()) - - def criteria: Map[String, String] = readOrDefault[Map[String, String]]("edata.criteria", Map[String, String]()) - - def keys: Map[String, String] = readOrDefault[Map[String, String]]("edata.keys", Map[String, String]()) - - def logo: String = readOrDefault[String]("edata.logo", "") - - def certificateDescription: String = readOrDefault[String]("edata.description", "") - - def related: Map[String, AnyRef] = readOrDefault[Map[String, AnyRef]]("edata.related", Map[String, AnyRef]()) - - def batchId: String = related.getOrElse("batchId", "").asInstanceOf[String] - - def courseId: String = related.getOrElse("courseId", "").asInstanceOf[String] - - def userId: String = readOrDefault[String]("edata.userId", "") - -} diff --git a/credential-generator/collection-certificate-generator/src/main/scala/org/sunbird/job/certgen/domain/Models.scala b/credential-generator/collection-certificate-generator/src/main/scala/org/sunbird/job/certgen/domain/Models.scala deleted file mode 100755 index bb6baf196..000000000 --- a/credential-generator/collection-certificate-generator/src/main/scala/org/sunbird/job/certgen/domain/Models.scala +++ /dev/null @@ -1,66 +0,0 @@ -package org.sunbird.job.certgen.domain - -import java.util -import java.util.UUID - -import scala.collection.JavaConverters._ - -class Models extends Serializable {} - -case class Actor(id: String, `type`: String = "User") - -case class EventContext(channel: String = "in.ekstep", - env: String = "Course", - sid: String = UUID.randomUUID().toString, - did: String = UUID.randomUUID().toString, - pdata: util.Map[String, String] = Map("ver" -> "1.0", "id" -> "org.sunbird.learning.platform", "pid" -> "course-certificate-generator").asJava, - cdata: Array[util.Map[String, String]]) - - -case class EData(props: Array[String], `type`: String) - -case class EventObject(id: String, `type`: String, rollup: util.Map[String, String]) - -case class CertificateAuditEvent(eid: String = "AUDIT", - ets: Long = System.currentTimeMillis(), - mid: String = s"LP.AUDIT.${System.currentTimeMillis()}.${UUID.randomUUID().toString}", - ver: String = "3.0", - actor: Actor, - context: EventContext = EventContext( - cdata = Array[util.Map[String, String]]() - ), - `object`: EventObject, - edata: EData = EData(props = Array("certificates"), `type` = "certificate-issued-svg")) - -case class Certificate(id: String, - name: String, - token: String, - lastIssuedOn: String, - templateUrl: String, - `type`: String) { - def this() = this("", "", "", "", "", "") -} - -case class FailedEvent(errorCode: String, - error: String) { - def this() = this("", "") -} - -case class FailedEventMsg(jobName: String, - failInfo: FailedEvent) { - def this() = this("certificate-generator", null) -} - - -case class UserEnrollmentData(batchId: String, - userId: String, - courseId: String, - courseName: String, - templateId: String, - certificate: Certificate) { - def this() = this("", "", "", "", "", null) -} - -case class Recipient(id: String, name: String, `type`: String) -case class Training(id: String, name: String, `type`: String, batchId: String) -case class Issuer(url: String, name: String, kid: String) diff --git a/credential-generator/collection-certificate-generator/src/main/scala/org/sunbird/job/certgen/exceptions/ErrorMessages.scala b/credential-generator/collection-certificate-generator/src/main/scala/org/sunbird/job/certgen/exceptions/ErrorMessages.scala deleted file mode 100755 index 888060726..000000000 --- a/credential-generator/collection-certificate-generator/src/main/scala/org/sunbird/job/certgen/exceptions/ErrorMessages.scala +++ /dev/null @@ -1,16 +0,0 @@ -package org.sunbird.job.certgen.exceptions - -object ErrorMessages { - - val INVALID_REQUESTED_DATA: String = "Invalid Request! Please Provide Valid Request." - val INVALID_PARAM_VALUE: String = "Invalid value {0} for parameter {1}." - val MANDATORY_PARAMETER_MISSING: String = "Mandatory parameter {0} is missing." -} - -object ErrorCodes { - val MANDATORY_PARAMETER_MISSING: String = "MANDATORY_PARAMETER_MISSING" - val INVALID_PARAM_VALUE: String = "INVALID_PARAM_VALUE" - val SYSTEM_ERROR: String = "SYSTEM_ERROR" -} - - diff --git a/credential-generator/collection-certificate-generator/src/main/scala/org/sunbird/job/certgen/exceptions/ValidationException.scala b/credential-generator/collection-certificate-generator/src/main/scala/org/sunbird/job/certgen/exceptions/ValidationException.scala deleted file mode 100755 index 292bccfc8..000000000 --- a/credential-generator/collection-certificate-generator/src/main/scala/org/sunbird/job/certgen/exceptions/ValidationException.scala +++ /dev/null @@ -1,8 +0,0 @@ -package org.sunbird.job.certgen.exceptions - -case class ValidationException(errorCode: String, msg: String, ex: Exception = null) extends Exception(msg, ex) { - -} - -case class ServerException (errorCode: String, msg: String, ex: Exception = null) extends Exception(msg, ex) { -} diff --git a/credential-generator/collection-certificate-generator/src/main/scala/org/sunbird/job/certgen/functions/CertMapper.scala b/credential-generator/collection-certificate-generator/src/main/scala/org/sunbird/job/certgen/functions/CertMapper.scala deleted file mode 100755 index 44d08c25e..000000000 --- a/credential-generator/collection-certificate-generator/src/main/scala/org/sunbird/job/certgen/functions/CertMapper.scala +++ /dev/null @@ -1,95 +0,0 @@ -package org.sunbird.job.certgen.functions - -import java.time.LocalDateTime -import java.time.format.DateTimeFormatter - -import org.apache.commons.lang3.StringUtils -import org.sunbird.incredible.pojos.ob.{Criteria, Issuer, SignatoryExtension} -import org.sunbird.incredible.pojos.valuator.{ExpiryDateValuator, IssuedDateValuator} -import org.sunbird.incredible.processor.CertModel -import org.sunbird.incredible.{CertificateConfig, JsonKeys} -import org.sunbird.job.certgen.domain.Event - -import scala.collection.JavaConverters._ - - -class CertMapper(certConfig: CertificateConfig) { - - def mapReqToCertModel(certReq: Event): List[CertModel] = { - val dataList: List[Map[String, AnyRef]] = certReq.data - val signatoryArr = getSignatoryArray(certReq.signatoryList) - val issuedDate = new IssuedDateValuator().evaluates(if (StringUtils.isBlank(certReq.issuedDate)) getCurrentDate else certReq.issuedDate) - val expiryDate: String = if (StringUtils.isNotBlank(certReq.expiryDate)) new ExpiryDateValuator(issuedDate).evaluates(certReq.expiryDate) else "" - val certList: List[CertModel] = dataList.toStream.map((data: Map[String, AnyRef]) => { - val certModel: CertModel = CertModel(recipientName = data.getOrElse(JsonKeys.RECIPIENT_NAME, "").asInstanceOf[String], - recipientEmail = Option.apply(data.getOrElse(JsonKeys.RECIPIENT_EMAIl, "").asInstanceOf[String]), - recipientPhone = Option.apply(data.getOrElse(JsonKeys.RECIPIENT_PHONE, "").asInstanceOf[String]), - identifier = data.getOrElse(JsonKeys.RECIPIENT_ID, "").asInstanceOf[String], - validFrom = Option.apply(data.getOrElse(JsonKeys.VALID_FROM, null).asInstanceOf[String]), - issuer = getIssuer(certReq), - courseName = certReq.courseName, - issuedDate = issuedDate, - certificateLogo = Option.apply(certReq.logo), - certificateDescription = Option.apply(certReq.certificateDescription), - certificateName = certReq.name, - signatoryList = signatoryArr.toArray, - criteria = getCriteria(certReq.criteria), - keyId = certReq.keys.getOrElse(JsonKeys.ID, ""), - tag = certReq.tag, - expiry = Option.apply(expiryDate) - ) - certModel - }).toList - - certList - } - - private def getCurrentDate: String = { - val dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'") - dtf.format(LocalDateTime.now) - } - - private def getCriteria(criteriaData: Map[String, String]): Criteria = Criteria(narrative = criteriaData.getOrElse(JsonKeys.NARRATIVE, "")) - - - private def getIssuer(req: Event): Issuer = { - val issuerData: Map[String, AnyRef] = req.issuer - val publicKeys: List[String] = getPublicKeys(issuerData.getOrElse(JsonKeys.PUBLIC_KEY, List[String]()).asInstanceOf[List[String]], req.keys) - val issuer: Issuer = Issuer(context = certConfig.contextUrl, name = issuerData.getOrElse(JsonKeys.NAME, "").asInstanceOf[String], url = - issuerData.getOrElse(JsonKeys.URL, "").asInstanceOf[String], publicKey = Option.apply(publicKeys.toArray)) - issuer - } - - - private def getSignatoryArray(signatoryList: List[Map[String, AnyRef]]): List[SignatoryExtension] = - signatoryList.toStream.map((signatory: Map[String, AnyRef]) => - getSignatory(signatory)).toList - - - private def getSignatory(signatory: Map[String, AnyRef]): SignatoryExtension = { - SignatoryExtension(context = certConfig.signatoryExtension, identity = - signatory.getOrElse(JsonKeys.ID, "").asInstanceOf[String], - designation = signatory.getOrElse(JsonKeys.DESIGNATION, "").asInstanceOf[String], - image = signatory.getOrElse(JsonKeys.SIGNATORY_IMAGE, "").asInstanceOf[String], - name = signatory.getOrElse(JsonKeys.NAME, "").asInstanceOf[String]) - } - - private def getPublicKeys(issuerPublicKeys: List[String], keys: Map[String, AnyRef]): List[String] = { - var publicKeys = issuerPublicKeys - if (issuerPublicKeys.isEmpty && keys.nonEmpty) { - publicKeys = List() - publicKeys :+ keys.get(JsonKeys.ID).asInstanceOf[String] - } - val validatedPublicKeys: List[String] = List() - if (publicKeys.nonEmpty) - publicKeys.foreach((publicKey: String) => { - if (!publicKey.startsWith("http")) - validatedPublicKeys :+ (certConfig.basePath.concat("/") + JsonKeys.KEYS.concat("/") + publicKey.concat("_publicKey.json")) - else validatedPublicKeys :+ publicKey - }) - validatedPublicKeys - } - - -} - diff --git a/credential-generator/collection-certificate-generator/src/main/scala/org/sunbird/job/certgen/functions/CertValidator.scala b/credential-generator/collection-certificate-generator/src/main/scala/org/sunbird/job/certgen/functions/CertValidator.scala deleted file mode 100755 index a29aae1a0..000000000 --- a/credential-generator/collection-certificate-generator/src/main/scala/org/sunbird/job/certgen/functions/CertValidator.scala +++ /dev/null @@ -1,196 +0,0 @@ -package org.sunbird.job.certgen.functions - -import com.datastax.driver.core.TypeTokens -import com.datastax.driver.core.querybuilder.QueryBuilder - -import java.net.{MalformedURLException, URI, URISyntaxException, URL} -import java.text.MessageFormat -import java.util.regex.Pattern -import org.apache.commons.lang3.StringUtils -import org.slf4j.LoggerFactory -import org.sunbird.incredible.JsonKeys -import org.sunbird.job.Metrics -import org.sunbird.job.certgen.domain.Event -import org.sunbird.job.certgen.exceptions.{ErrorCodes, ErrorMessages, ValidationException} -import org.sunbird.job.certgen.task.CertificateGeneratorConfig -import org.sunbird.job.util.CassandraUtil - -import scala.collection.JavaConverters._ - -/** - * This class contains method to validate certificate api request - */ - -class CertValidator() { - - private var publicKeys: List[String] = _ - private val TAG_REGX: String = "[!@#$%^&*()+=,.?/\":;'{}|<>\\s-]" - private[this] val logger = LoggerFactory.getLogger(classOf[CertValidator]) - - - - /** - * This method will validate generate certificate request - * - */ - @throws[ValidationException] - def validateGenerateCertRequest(event: Event, enableSuppressException: Boolean): Unit = { - checkMandatoryParamsPresent(event.eData, "edata", List(JsonKeys.NAME, JsonKeys.SVG_TEMPLATE)) - validateCertData(event.data) - validateCertIssuer(event.issuer) - try { - validateCertSignatoryList(event.signatoryList) - } catch { - case e: Exception => - if (enableSuppressException) logger.error("SignatoryList Validation failed :: " + e.getMessage, e) - else throw e - } - validateCriteria(event.criteria) - validateTagId(event.tag) - val basePath: String = event.basePath - if (StringUtils.isNotBlank(basePath)) { - validateBasePath(basePath) - } - if (event.keys.nonEmpty) { - validateKeys(event.keys) - } - } - - private def validateCertSignatoryList(signatoryList: List[Map[String, AnyRef]]): Unit = { - checkMandatoryParamsPresent(signatoryList, JsonKeys.EDATA + "." + JsonKeys.SIGNATORY_LIST, List(JsonKeys.NAME, JsonKeys.ID, JsonKeys.DESIGNATION, - JsonKeys.SIGNATORY_IMAGE)) - } - - private def validateCertIssuer(issuer: Map[String, AnyRef]): Unit = { - checkMandatoryParamsPresent(issuer, JsonKeys.EDATA + "." + JsonKeys.ISSUER, List(JsonKeys.NAME, JsonKeys.URL)) - publicKeys = issuer.getOrElse(JsonKeys.PUBLIC_KEY, List[String]()).asInstanceOf[List[String]] - } - - private def validateCriteria(criteria: Map[String, AnyRef]): Unit = { - checkMandatoryParamsPresent(criteria, JsonKeys.EDATA + "." + JsonKeys.CRITERIA, List(JsonKeys.NARRATIVE)) - } - - private def validateCertData(data: List[Map[String, AnyRef]]): Unit = { - checkMandatoryParamsPresent(data, JsonKeys.EDATA + "." + JsonKeys.DATA, List(JsonKeys.RECIPIENT_NAME)) - } - - private def validateKeys(keys: Map[String, AnyRef]): Unit = { - checkMandatoryParamsPresent(keys, JsonKeys.EDATA + "." + JsonKeys.KEYS, List(JsonKeys.ID)) - if (publicKeys.nonEmpty) { - validateIssuerPublicKeys(keys) - } - } - - /** - * this method used to validate public keys of Issuer object ,if public key list is present, list must contain keys.id value of - * certificate request - * - * @param - */ - private def validateIssuerPublicKeys(keys: Map[String, AnyRef]): Unit = { - val keyIds = List[String]() - publicKeys.foreach(publicKey => - if (publicKey.startsWith("http")) { - keyIds :+ getKeyIdFromPublicKeyUrl(publicKey) - } else { - keyIds :+ publicKey - }) - if (!keyIds.contains(keys.getOrElse(JsonKeys.ID, ""))) { - throw ValidationException( - ErrorCodes.INVALID_PARAM_VALUE, - MessageFormat.format(ErrorMessages.INVALID_PARAM_VALUE, publicKeys, JsonKeys.EDATA + "." + JsonKeys.ISSUER + "." + JsonKeys.PUBLIC_KEY) + - " ,public key attribute must contain keys.id value") - } - } - - /** - * to get keyId from the publicKey url - * - * @return - */ - private def getKeyIdFromPublicKeyUrl(publicKey: String): String = { - var idStr: String = null - try { - val uri: URI = new URI(publicKey) - val path: String = uri.getPath - idStr = path.substring(path.lastIndexOf('/') + 1) - idStr = idStr.substring(0, 1) - } catch { - case e: URISyntaxException => - } - idStr - } - - @throws[ValidationException] - private def checkMandatoryParamsPresent(data: Map[String, AnyRef], parentKey: String, keys: List[String]): Unit = { - if (data.isEmpty) throw ValidationException(ErrorCodes.MANDATORY_PARAMETER_MISSING, MessageFormat.format(ErrorMessages.MANDATORY_PARAMETER_MISSING, parentKey)) - checkChildrenMapMandatoryParams(data, keys, parentKey) - } - - private def checkMandatoryParamsPresent(dataList: List[Map[String, AnyRef]], parentKey: String, keys: List[String]): Unit = { - if (dataList.isEmpty) { - throw ValidationException(ErrorCodes.MANDATORY_PARAMETER_MISSING, MessageFormat.format(ErrorMessages.MANDATORY_PARAMETER_MISSING, parentKey)) - } - dataList.foreach(data => checkChildrenMapMandatoryParams(data, keys, parentKey)) - } - - @throws[ValidationException] - private def checkChildrenMapMandatoryParams(data: Map[String, AnyRef], keys: List[String], parentKey: String): Unit = { - keys.foreach(key => { - if (StringUtils.isBlank(data.getOrElse(key, "").toString)) - throw ValidationException(ErrorCodes.MANDATORY_PARAMETER_MISSING, MessageFormat.format(ErrorMessages.MANDATORY_PARAMETER_MISSING, parentKey + "." + key)) - }) - - } - - @throws[ValidationException] - private def validateBasePath(basePath: String): Unit = { - val isValid: Boolean = isUrlValid(basePath) - if (!isValid) { - throw ValidationException(ErrorCodes.INVALID_PARAM_VALUE, MessageFormat.format(ErrorMessages.INVALID_PARAM_VALUE, basePath, JsonKeys.CERTIFICATE + "." + JsonKeys.BASE_PATH)) - } - } - - - private def isUrlValid(url: String): Boolean = try { - val obj: URL = new URL(url) - obj.toURI - true - } catch { - case e: MalformedURLException => - false - case e: URISyntaxException => - false - } - - /** - * validates tagId , if tagId contains any special character except '_' then tagId is invalid - * - */ - @throws[ValidationException] - private def validateTagId(tag: String): Unit = { - if (StringUtils.isNotBlank(tag)) { - val pattern = Pattern.compile(TAG_REGX) - if (pattern.matcher(tag).find()) { - throw ValidationException(ErrorCodes.INVALID_PARAM_VALUE, MessageFormat.format(ErrorMessages.INVALID_PARAM_VALUE, tag, JsonKeys.TAG)) - } - } - } - - def isNotIssued(event: Event)(config: CertificateGeneratorConfig, metrics: Metrics, cassandraUtil: CassandraUtil):Boolean = { - val query = QueryBuilder.select( "issued_certificates").from(config.dbKeyspace, config.dbEnrollmentTable) - .where(QueryBuilder.eq(config.dbUserId, event.eData.getOrElse("userId", ""))) - .and(QueryBuilder.eq(config.dbCourseId, event.related.getOrElse("courseId", ""))) - .and(QueryBuilder.eq(config.dbBatchId, event.related.getOrElse("batchId", ""))) - val row = cassandraUtil.findOne(query.toString) - metrics.incCounter(config.enrollmentDbReadCount) - if (null != row) { - val issuedCertificates = row - .getList(config.issuedCertificates, TypeTokens.mapOf(classOf[String], classOf[String])).asScala.toList - val isCertIssued = !issuedCertificates.isEmpty && !issuedCertificates - .filter(cert => event.name.equalsIgnoreCase(cert.getOrDefault(config.name, "").asInstanceOf[String])).isEmpty - ((null != event.oldId && !event.oldId.isEmpty) || !isCertIssued) - } else false - } - -} diff --git a/credential-generator/collection-certificate-generator/src/main/scala/org/sunbird/job/certgen/functions/CertificateGeneratorFunction.scala b/credential-generator/collection-certificate-generator/src/main/scala/org/sunbird/job/certgen/functions/CertificateGeneratorFunction.scala deleted file mode 100755 index a8d3a92d6..000000000 --- a/credential-generator/collection-certificate-generator/src/main/scala/org/sunbird/job/certgen/functions/CertificateGeneratorFunction.scala +++ /dev/null @@ -1,361 +0,0 @@ -package org.sunbird.job.certgen.functions - -import com.datastax.driver.core.querybuilder.{QueryBuilder, Update} -import com.datastax.driver.core.{Row, TypeTokens} -import com.google.gson.reflect.TypeToken -import kong.unirest.UnirestException -import org.apache.commons.io.FileUtils -import org.apache.commons.lang.StringUtils -import org.apache.flink.configuration.Configuration -import org.apache.flink.streaming.api.functions.KeyedProcessFunction -import org.slf4j.LoggerFactory -import org.sunbird.incredible.pojos.ob.CertificateExtension -import org.sunbird.incredible.processor.CertModel -import org.sunbird.incredible.processor.store.StorageService -import org.sunbird.incredible.processor.views.SvgGenerator -import org.sunbird.incredible.{CertificateConfig, CertificateGenerator, JsonKeys, ScalaModuleJsonUtils} -import org.sunbird.job.certgen.domain.Issuer -import org.sunbird.job.certgen.domain._ -import org.sunbird.job.certgen.exceptions.ServerException -import org.sunbird.job.certgen.task.CertificateGeneratorConfig -import org.sunbird.job.exception.InvalidEventException -import org.sunbird.job.util.{CassandraUtil, ElasticSearchUtil, HttpUtil, ScalaJsonUtil} -import org.sunbird.job.{BaseProcessKeyedFunction, Metrics} - -import java.io.{File, IOException} -import java.lang.reflect.Type -import java.text.SimpleDateFormat -import java.util -import java.util.stream.Collectors -import java.util.{Base64, Date} -import scala.collection.JavaConverters._ - -class CertificateGeneratorFunction(config: CertificateGeneratorConfig, httpUtil: HttpUtil, storageService: StorageService, @transient var cassandraUtil: CassandraUtil = null) - extends BaseProcessKeyedFunction[String, Event, String](config) { - - - private[this] val logger = LoggerFactory.getLogger(classOf[CertificateGeneratorFunction]) - val mapType: Type = new TypeToken[java.util.Map[String, AnyRef]]() {}.getType - val directory: String = "certificates/" - val formatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ") - implicit val certificateConfig: CertificateConfig = CertificateConfig(basePath = config.basePath, encryptionServiceUrl = config.encServiceUrl, contextUrl = config.CONTEXT, issuerUrl = config.ISSUER_URL, - evidenceUrl = config.EVIDENCE_URL, signatoryExtension = config.SIGNATORY_EXTENSION) - implicit var esUtil: ElasticSearchUtil = null - - override def open(parameters: Configuration): Unit = { - super.open(parameters) - cassandraUtil = new CassandraUtil(config.dbHost, config.dbPort) - if(esUtil==null) - esUtil = new ElasticSearchUtil(config.esConnection, config.certIndex, "config.auditHistoryIndexType") - } - - override def close(): Unit = { - cassandraUtil.close() - if(esUtil!=null) esUtil.close() - super.close() - } - - override def metricsList(): List[String] = { - List(config.successEventCount, config.failedEventCount, config.skippedEventCount, config.totalEventsCount, config.dbUpdateCount, config.enrollmentDbReadCount, config.totalEventsCount) - } - - - override def processElement(event: Event, - context: KeyedProcessFunction[String, Event, String]#Context, - metrics: Metrics): Unit = { - println("Certificate data: " + event) - metrics.incCounter(config.totalEventsCount) - try { - val certValidator = new CertValidator() - logger.info("Certificate generator | is rc integration enabled: " + config.enableRcCertificate) - certValidator.validateGenerateCertRequest(event, config.enableSuppressException) - if(certValidator.isNotIssued(event)(config, metrics, cassandraUtil)) { - if(config.enableRcCertificate) generateCertificateUsingRC(event, context)(metrics) - else generateCertificate(event, context)(metrics) - - } else { - metrics.incCounter(config.skippedEventCount) - logger.info(s"Certificate already issued for: ${event.eData.getOrElse("userId", "")} ${event.related}") - } - } catch { - case e: Exception => - metrics.incCounter(config.failedEventCount) - throw new InvalidEventException(e.getMessage, Map("partition" -> event.partition, "offset" -> event.offset), e) - } - } - - @throws[Exception] - def generateCertificate(event: Event, context: KeyedProcessFunction[String, Event, String]#Context)(implicit metrics: Metrics): Unit = { - val certModelList: List[CertModel] = new CertMapper(certificateConfig).mapReqToCertModel(event) - val certificateGenerator = new CertificateGenerator - certModelList.foreach(certModel => { - var uuid: String = null - try { - val certificateExtension: CertificateExtension = certificateGenerator.getCertificateExtension(certModel) - uuid = certificateGenerator.getUUID(certificateExtension) - val qrMap = certificateGenerator.generateQrCode(uuid, directory, certificateConfig.basePath) - val encodedQrCode: String = encodeQrCode(qrMap.qrFile) - val printUri = SvgGenerator.generate(certificateExtension, encodedQrCode, event.svgTemplate) - certificateExtension.printUri = Option(printUri) - val jsonUrl = uploadJson(certificateExtension, directory.concat(uuid).concat(".json"), event.tag.concat("/")) - //adding certificate to registry - val addReq = Map[String, AnyRef](JsonKeys.REQUEST -> {Map[String, AnyRef]( - JsonKeys.ID -> uuid, JsonKeys.JSON_URL -> certificateConfig.basePath.concat(jsonUrl), - JsonKeys.JSON_DATA -> certificateExtension, JsonKeys.ACCESS_CODE -> qrMap.accessCode, - JsonKeys.RECIPIENT_NAME -> certModel.recipientName, JsonKeys.RECIPIENT_ID -> certModel.identifier, - config.RELATED -> event.related - ) ++ {if (!event.oldId.isEmpty) Map[String, AnyRef](config.OLD_ID -> event.oldId) else Map[String, AnyRef]()}}) - addCertToRegistry(event, addReq, context)(metrics) - //cert-registry end - val related = event.related - val userEnrollmentData = UserEnrollmentData(related.getOrElse(config.BATCH_ID, "").asInstanceOf[String], certModel.identifier, - related.getOrElse(config.COURSE_ID, "").asInstanceOf[String], event.courseName, event.templateId, - Certificate(uuid, event.name, qrMap.accessCode, formatter.format(new Date()), "", "")) - updateUserEnrollmentTable(event, userEnrollmentData, context) - metrics.incCounter(config.successEventCount) - } finally { - cleanUp(uuid, directory) - } - }) - } - - @throws[Exception] - def generateCertificateUsingRC(event: Event, context: KeyedProcessFunction[String, Event, String]#Context)(implicit metrics: Metrics): Unit = { - val certModelList: List[CertModel] = new CertMapper(certificateConfig).mapReqToCertModel(event) - certModelList.foreach(certModel => { - var uuid: String = null - val reIssue: Boolean = !event.oldId.isEmpty - //if reissue then read rc for oldId and call rc delete api - if(reIssue){ - try { - callCertificateRc(config.rcDeleteApi, event.oldId, null) - } catch { - case ex: ServerException => - logger.error("Rc deletion failed | old id is not present :: identifier " + event.oldId + " :: " + ex.getMessage) - //when record not present for oldId in rc registry, calls old registry deletion - deleteOldRegistry(event.oldId) - case e: UnirestException => - logger.error("Rc deletion failed due to connection :: identifier " + event.oldId + " :: " + e.getMessage) - } - } - val related = event.related - val certReq = generateRequest(event, certModel, reIssue) - //make api call to registry - uuid = callCertificateRc(config.rcCreateApi, null, certReq) - val userEnrollmentData = UserEnrollmentData(related.getOrElse(config.BATCH_ID, "").asInstanceOf[String], certModel.identifier, - related.getOrElse(config.COURSE_ID, "").asInstanceOf[String], event.courseName, event.templateId, - Certificate(uuid, event.name, "", formatter.format(new Date()), event.svgTemplate, config.rcEntity)) - updateUserEnrollmentTable(event, userEnrollmentData, context) - metrics.incCounter(config.successEventCount) - }) - } - - def deleteOldRegistry(id: String) = { - try { - deleteCassandraRecord(id) - deleteEsRecord(id) - } catch { - case ex: Exception => - logger.error("Old registry deletion failed | old id is not present :: identifier " + id+ " :: " + ex.getMessage) - - } - } - - def deleteCassandraRecord(id: String): Unit = { - val query = QueryBuilder.delete().from(config.sbKeyspace, config.certRegTable) - .where(QueryBuilder.eq("identifier", id)) - .ifExists - cassandraUtil.executePreparedStatement(query.toString) - } - - def deleteEsRecord(id: String): Unit = { - esUtil.deleteDocument(id) - } - - @throws[ServerException] - def addCertToRegistry(certReq: Event, request: Map[String, AnyRef], context: KeyedProcessFunction[String, Event, String]#Context)(implicit metrics: Metrics): Unit = { - logger.info("adding certificate to the registry") - val httpRequest = ScalaModuleJsonUtils.serialize(request) - val httpResponse = httpUtil.post(config.certRegistryBaseUrl + config.addCertRegApi, httpRequest) - if (httpResponse.status == 200) { - logger.info("certificate added successfully to the registry " + httpResponse.body) - } else { - logger.error("certificate addition to registry failed: " + httpResponse.status + " :: " + httpResponse.body) - throw ServerException("ERR_API_CALL", "Something Went Wrong While Making API Call | Status is: " + httpResponse.status + " :: " + httpResponse.body) - } - } - - @throws[IOException] - private def encodeQrCode(file: File): String = { - val fileContent = FileUtils.readFileToByteArray(file) - file.delete - Base64.getEncoder.encodeToString(fileContent) - } - - @throws[IOException] - private def uploadJson(certificateExtension: CertificateExtension, fileName: String, cloudPath: String): String = { - logger.info("uploadJson: uploading json file started {}", fileName) - val file = new File(fileName) - ScalaModuleJsonUtils.writeToJsonFile(file, certificateExtension) - storageService.uploadFile(cloudPath, file) - } - - def generateRequest(event: Event, certModel: CertModel, reIssue: Boolean): Map[String, AnyRef] = { - val req = Map("filters" -> Map()) - val publicKeyId: String = callCertificateRc(config.rcSearchApi, null, req) - val createCertReq = Map[String, AnyRef]( - "certificateLabel" -> certModel.certificateName, - "status" -> "ACTIVE", - "templateUrl" -> event.svgTemplate, - "training" -> Training(event.related.getOrElse(config.COURSE_ID, "").asInstanceOf[String], event.courseName, "Course", event.related.getOrElse(config.BATCH_ID, "").asInstanceOf[String]), - "recipient" -> Recipient(certModel.identifier, certModel.recipientName, null), - "issuer" -> Issuer(certModel.issuer.url, certModel.issuer.name, publicKeyId), - "signatory" -> event.signatoryList, - ) ++ {if (reIssue) Map[String, AnyRef](config.OLD_ID -> event.oldId) else Map[String, AnyRef]()} - createCertReq - } - - @throws[ServerException] - @throws[UnirestException] - def callCertificateRc(api: String, identifier: String, request: Map[String, AnyRef]): String = { - logger.info("Certificate rc called | Api:: " + api) - var id: String = null - val uri: String = config.rcBaseUrl + "/" + config.rcEntity - val status = api match { - case config.rcDeleteApi => httpUtil.delete(uri + "/" +identifier).status - case config.rcCreateApi => - val plainReq: String = ScalaModuleJsonUtils.serialize(request) - val req = removeBadChars(plainReq) - logger.info("RC Create API request: " + req) - val httpResponse = httpUtil.post(uri, req) - if(httpResponse.status == 200) { - val response = ScalaJsonUtil.deserialize[Map[String, AnyRef]](httpResponse.body) - id = response.getOrElse("result", Map[String, AnyRef]()).asInstanceOf[Map[String, AnyRef]].getOrElse(config.rcEntity, Map[String, AnyRef]()).asInstanceOf[Map[String, AnyRef]].getOrElse("osid","").asInstanceOf[String] - } else { - logger.error("RC Create Error Response: " + httpResponse.status + " :: Response: " + httpResponse.body) - } - httpResponse.status - case config.rcSearchApi => - val req: String = ScalaModuleJsonUtils.serialize(request) - val searchUri = config.rcBaseUrl + "/" + "PublicKey" + "/search" - val httpResponse = httpUtil.post(searchUri, req) - if(httpResponse.status == 200) { - val resp = ScalaJsonUtil.deserialize[List[Map[String, AnyRef]]](httpResponse.body) - id = resp.head.getOrElse("osid", null).asInstanceOf[String] - } - httpResponse.status - } - if (status == 200) { - logger.info("certificate rc successfully executed for api: " + api) - } else { - logger.error("certificate rc failed for api: " + api + " | Status is: " + status) - throw ServerException("ERR_API_CALL", "Something Went Wrong While Making API Call: " + api + " | Status is: " + status) - } - id - } - - private def removeBadChars(request: String): String = { - config.badCharList.split(",").foldLeft(request)((curReq, removeChar) => StringUtils.remove(curReq, removeChar)) - } - - private def cleanUp(fileName: String, path: String): Unit = { - try { - val directory = new File(path) - val files: Array[File] = directory.listFiles - if (files != null && files.length > 0) - files.foreach(file => { - if (file.getName.startsWith(fileName)) file.delete - }) - logger.info("cleanUp completed") - } catch { - case ex: Exception => - logger.error(ex.getMessage, ex) - } - } - - def updateUserEnrollmentTable(event: Event, certMetaData: UserEnrollmentData, context: KeyedProcessFunction[String, Event, String]#Context)(implicit metrics: Metrics): Unit = { - logger.info("updating user enrollment table {}", certMetaData) - val primaryFields = Map(config.userId.toLowerCase() -> certMetaData.userId, config.batchId.toLowerCase -> certMetaData.batchId, config.courseId.toLowerCase -> certMetaData.courseId) - val records = getIssuedCertificatesFromUserEnrollmentTable(primaryFields) - if (records.nonEmpty) { - records.foreach((row: Row) => { - val issuedOn = row.getTimestamp("completedOn") - var certificatesList = row.getList(config.issued_certificates, TypeTokens.mapOf(classOf[String], classOf[String])) - if (null == certificatesList && certificatesList.isEmpty) { - certificatesList = new util.ArrayList[util.Map[String, String]]() - } - val updatedCerts: util.List[util.Map[String, String]] = certificatesList.stream().filter(cert => !StringUtils.equalsIgnoreCase(certMetaData.certificate.name, cert.get("name"))).collect(Collectors.toList()) - updatedCerts.add(mapAsJavaMap(Map[String, String]( - config.name -> certMetaData.certificate.name, - config.identifier -> certMetaData.certificate.id, - config.token -> certMetaData.certificate.token, - ) ++ {if(!certMetaData.certificate.lastIssuedOn.isEmpty) Map[String, String](config.lastIssuedOn -> certMetaData.certificate.lastIssuedOn) - else Map[String, String]()} - ++ {if(config.enableRcCertificate) Map[String, String](config.templateUrl -> certMetaData.certificate.templateUrl, config.`type`->certMetaData.certificate.`type`) - else Map[String, String]()} - )) - - val query = getUpdateIssuedCertQuery(updatedCerts, certMetaData.userId, certMetaData.courseId, certMetaData.batchId, config) - logger.info("update query {}", query.toString) - val result = cassandraUtil.update(query) - logger.info("update result {}", result) - if (result) { - logger.info("issued certificates in user-enrollment table updated successfully") - metrics.incCounter(config.dbUpdateCount) - val certificateAuditEvent = generateAuditEvent(certMetaData) - logger.info("pushAuditEvent: audit event generated for certificate : " + certificateAuditEvent) - val audit = ScalaJsonUtil.serialize(certificateAuditEvent) - context.output(config.auditEventOutputTag, audit) - logger.info("pushAuditEvent: certificate audit event success {}", audit) - context.output(config.notifierOutputTag, NotificationMetaData(certMetaData.userId, certMetaData.courseName, issuedOn, certMetaData.courseId, certMetaData.batchId, certMetaData.templateId, event.partition, event.offset)) - context.output(config.userFeedOutputTag, UserFeedMetaData(certMetaData.userId, certMetaData.courseName, issuedOn, certMetaData.courseId, event.partition, event.offset)) - } else { - metrics.incCounter(config.failedEventCount) - throw new Exception(s"Update certificates to enrolments failed: ${event}") - } - - }) - } - - } - - - /** - * returns query for updating issued_certificates in user_enrollment table - */ - def getUpdateIssuedCertQuery(updatedCerts: util.List[util.Map[String, String]], userId: String, courseId: String, batchId: String, config: CertificateGeneratorConfig): - Update.Where = QueryBuilder.update(config.dbKeyspace, config.dbEnrollmentTable).where() - .`with`(QueryBuilder.set(config.issued_certificates, updatedCerts)) - .where(QueryBuilder.eq(config.userId.toLowerCase, userId)) - .and(QueryBuilder.eq(config.courseId.toLowerCase, courseId)) - .and(QueryBuilder.eq(config.batchId.toLowerCase, batchId)) - - - private def getIssuedCertificatesFromUserEnrollmentTable(columns: Map[String, AnyRef])(implicit metrics: Metrics) = { - logger.info("primary columns {}", columns) - val selectWhere = QueryBuilder.select().all() - .from(config.dbKeyspace, config.dbEnrollmentTable). - where() - columns.map(col => { - col._2 match { - case value: List[Any] => - selectWhere.and(QueryBuilder.in(col._1, value.asJava)) - case _ => - selectWhere.and(QueryBuilder.eq(col._1, col._2)) - } - }) - logger.info("select query {}", selectWhere.toString) - metrics.incCounter(config.enrollmentDbReadCount) - cassandraUtil.find(selectWhere.toString).asScala.toList - } - - - private def generateAuditEvent(data: UserEnrollmentData): CertificateAuditEvent = { - CertificateAuditEvent( - actor = Actor(id = data.userId), - context = EventContext(cdata = Array(Map("type" -> config.courseBatch, config.id -> data.batchId).asJava)), - `object` = EventObject(id = data.certificate.id, `type` = "Certificate", rollup = Map(config.l1 -> data.courseId).asJava)) - } - - -} diff --git a/credential-generator/collection-certificate-generator/src/main/scala/org/sunbird/job/certgen/functions/CreateUserFeedFunction.scala b/credential-generator/collection-certificate-generator/src/main/scala/org/sunbird/job/certgen/functions/CreateUserFeedFunction.scala deleted file mode 100755 index f015b2894..000000000 --- a/credential-generator/collection-certificate-generator/src/main/scala/org/sunbird/job/certgen/functions/CreateUserFeedFunction.scala +++ /dev/null @@ -1,49 +0,0 @@ -package org.sunbird.job.certgen.functions - -import java.util.Date -import org.apache.flink.api.common.typeinfo.TypeInformation -import org.apache.flink.configuration.Configuration -import org.apache.flink.streaming.api.functions.ProcessFunction -import org.slf4j.LoggerFactory -import org.sunbird.job.certgen.task.CertificateGeneratorConfig -import org.sunbird.job.exception.InvalidEventException -import org.sunbird.job.util.{HTTPResponse, HttpUtil} -import org.sunbird.job.{BaseProcessFunction, Metrics} - -case class UserFeedMetaData(userId: String, courseName: String, issuedOn: Date, courseId: String, partition: Int, offset: Long) - -class CreateUserFeedFunction(config: CertificateGeneratorConfig, httpUtil: HttpUtil)(implicit val stringTypeInfo: TypeInformation[String]) - extends BaseProcessFunction[UserFeedMetaData, String](config) { - - private[this] val logger = LoggerFactory.getLogger(classOf[CreateUserFeedFunction]) - - override def open(parameters: Configuration): Unit = { - super.open(parameters) - } - - override def close(): Unit = { - super.close() - } - - override def processElement(metaData: UserFeedMetaData, - context: ProcessFunction[UserFeedMetaData, String]#Context, - metrics: Metrics): Unit = { - val req = - s"""{"request":{"userId":"${metaData.userId}","category":"Notification","priority":1,"data":{"type":1,"actionData":{"actionType":"certificateUpdate","title":"${metaData.courseName}","description":"${config.userFeedMsg}","identifier":"${metaData.courseId}"}}}}""" - val url = config.learnerServiceBaseUrl + config.userFeedCreateEndPoint - val response: HTTPResponse = httpUtil.post(url, req) - if (response.status == 200) { - metrics.incCounter(config.userFeedCount) - logger.info("user feed response status {} :: {}", response.status, response.body) - } - else { - metrics.incCounter(config.failedEventCount) - logger.error(s"Error response from createUserFeed API for request :: ${req} :: response is :: ${response.status} :: ${response.body}") - throw new InvalidEventException(s"Error in UserFeed Response: ${response}", Map("partition" -> metaData.partition, "offset" -> metaData.offset), null) - } - } - - override def metricsList(): List[String] = { - List(config.userFeedCount, config.failedEventCount) - } -} diff --git a/credential-generator/collection-certificate-generator/src/main/scala/org/sunbird/job/certgen/functions/NotifierFunction.scala b/credential-generator/collection-certificate-generator/src/main/scala/org/sunbird/job/certgen/functions/NotifierFunction.scala deleted file mode 100755 index 765798529..000000000 --- a/credential-generator/collection-certificate-generator/src/main/scala/org/sunbird/job/certgen/functions/NotifierFunction.scala +++ /dev/null @@ -1,154 +0,0 @@ -package org.sunbird.job.certgen.functions - -import java.lang.reflect.Type -import java.text.SimpleDateFormat -import java.util -import java.util.Date -import com.datastax.driver.core.querybuilder.{QueryBuilder, Select} -import com.datastax.driver.core.{Row, TypeTokens} -import com.google.gson.reflect.TypeToken -import org.apache.commons.lang3.StringUtils -import org.apache.flink.api.common.typeinfo.TypeInformation -import org.apache.flink.configuration.Configuration -import org.apache.flink.streaming.api.functions.ProcessFunction -import org.slf4j.LoggerFactory -import org.sunbird.job.certgen.task.CertificateGeneratorConfig -import org.sunbird.job.exception.InvalidEventException -import org.sunbird.job.util.{CassandraUtil, HttpUtil, JSONUtil, ScalaJsonUtil} -import org.sunbird.job.{BaseProcessFunction, Metrics} - -import scala.collection.JavaConverters._ -import scala.collection.mutable - -case class NotificationMetaData(userId: String, courseName: String, issuedOn: Date, courseId: String, batchId: String, templateId: String, partition: Int, offset: Long) - -class NotifierFunction(config: CertificateGeneratorConfig, httpUtil: HttpUtil, @transient var cassandraUtil: CassandraUtil = null)(implicit val stringTypeInfo: TypeInformation[String]) - extends BaseProcessFunction[NotificationMetaData, String](config) { - - private val dateFormatter = new SimpleDateFormat("yyyy-MM-dd") - - private[this] val logger = LoggerFactory.getLogger(classOf[NotifierFunction]) - - val mapType: Type = new TypeToken[java.util.Map[String, AnyRef]]() {}.getType - - override def open(parameters: Configuration): Unit = { - super.open(parameters) - cassandraUtil = new CassandraUtil(config.dbHost, config.dbPort) - } - - override def close(): Unit = { - cassandraUtil.close() - super.close() - } - - override def processElement(metaData: NotificationMetaData, - context: ProcessFunction[NotificationMetaData, String]#Context, - metrics: Metrics): Unit = { - - val userResponse: Map[String, AnyRef] = getUserDetails(metaData.userId)(metrics) // call user Service - if (null != userResponse && userResponse.nonEmpty) { - val primaryFields = Map(config.courseId.toLowerCase() -> metaData.courseId, - config.batchId.toLowerCase -> metaData.batchId) - val row = getNotificationTemplates(primaryFields, metrics) - val certTemplate = row.getMap(config.cert_templates, com.google.common.reflect.TypeToken.of(classOf[String]), - TypeTokens.mapOf(classOf[String], classOf[String])) - val url = config.learnerServiceBaseUrl + config.notificationEndPoint - if (certTemplate != null && StringUtils.isNotBlank(metaData.templateId) && - certTemplate.containsKey(metaData.templateId) && - certTemplate.get(metaData.templateId).containsKey(config.notifyTemplate)) { - logger.info("notification template is present in the cert-templates object {}", - certTemplate.get(metaData.templateId).containsKey(config.notifyTemplate)) - val notifyTemplate = getNotifyTemplateFromRes(certTemplate.get(metaData.templateId)) - val request = mutable.Map[String, AnyRef]("request" -> (notifyTemplate ++ mutable.Map[String, AnyRef]( - config.firstName -> userResponse.getOrElse(config.firstName, "").asInstanceOf[String], - config.trainingName -> metaData.courseName, - config.heldDate -> dateFormatter.format(metaData.issuedOn), - config.recipientUserIds -> List[String](metaData.userId), - config.body -> "email body"))) - - val response = httpUtil.post(url, ScalaJsonUtil.serialize(request)) - if (response.status == 200) { - metrics.incCounter(config.notifiedUserCount) - logger.info("email response status {} :: {}", response.status, response.body) - } - else { - metrics.incCounter(config.failedEventCount) - logger.error(s"Error response from email notification for request :: ${request} :: response is :: ${response.status} :: ${response.body}") - throw new InvalidEventException(s"Error in email notification response : ${response}", Map("partition" -> metaData.partition, "offset" -> metaData.offset), null) - } - if (StringUtils.isNoneBlank(userResponse.getOrElse("maskedPhone", "").asInstanceOf[String])) { - request.put(config.body, "sms") - val smsBody = config.notificationSmsBody.replaceAll("@@TRAINING_NAME@@", metaData.courseName) - .replaceAll("@@HELD_DATE@@", dateFormatter.format(metaData.issuedOn)) - request.getOrElse("request", mutable.Map[String, AnyRef]()).asInstanceOf[mutable.Map[String, AnyRef]] - .put("body", smsBody) - val response = httpUtil.post(url, ScalaJsonUtil.serialize(request)) - if (response.status == 200) - logger.info("phone response status {} :: {}", response.status, response.body) - else { - metrics.incCounter(config.failedEventCount) - logger.error(s"Error response from sms notification for request :: ${request} :: response is :: ${response.status} :: ${response.body}") - throw new InvalidEventException(s"Error in sms notification response : ${response}", Map("partition" -> metaData.partition, "offset" -> metaData.offset), null) - } - } - } else { - logger.info("notification template is not present in the cert-templates object {}") - metrics.incCounter(config.skipNotifyUserCount) - } - } - - } - - - private def getNotifyTemplateFromRes(certTemplate: util.Map[String, String]): mutable.Map[String, String] = { - val notifyTemplate = certTemplate.get(config.notifyTemplate) - if (notifyTemplate.isInstanceOf[String]) ScalaJsonUtil.deserialize[mutable.Map[String, String]](notifyTemplate) - else notifyTemplate.asInstanceOf[mutable.Map[String, String]] - } - - - /** - * get notify template from course-batch table - * - * @param columns - * @param metrics - * @return - */ - private def getNotificationTemplates(columns: Map[String, AnyRef], metrics: Metrics): Row = { - val selectWhere: Select.Where = QueryBuilder.select().all() - .from(config.dbKeyspace, config.dbCourseBatchTable). - where() - columns.map(col => { - col._2 match { - case value: List[Any] => - selectWhere.and(QueryBuilder.in(col._1, value.asJava)) - case _ => - selectWhere.and(QueryBuilder.eq(col._1, col._2)) - } - }) - metrics.incCounter(config.courseBatchdbReadCount) - cassandraUtil.findOne(selectWhere.toString) - } - - - private def getUserDetails(userId: String)(metrics: Metrics): Map[String, AnyRef] = { - logger.info("getting user info for id {}", userId) - val httpResponse = httpUtil.get(config.learnerServiceBaseUrl + "/private/user/v1/read/" + userId) - if (200 == httpResponse.status) { - logger.info("user search response status {} :: {} ", httpResponse.status, httpResponse.body) - val response = ScalaJsonUtil.deserialize[Map[String, AnyRef]](httpResponse.body) - val result = response.getOrElse("result", Map[String, AnyRef]()).asInstanceOf[Map[String, AnyRef]].getOrElse("response", Map[String, AnyRef]()).asInstanceOf[Map[String, AnyRef]] - result - } else if((400 == httpResponse.status) && httpResponse.body.contains("USER_ACCOUNT_BLOCKED")){ - logger.error(s"Error while fetching user details for ${userId}: " + httpResponse.status + " :: " + httpResponse.body) - metrics.incCounter(config.skipNotifyUserCount) - Map[String, AnyRef]() - } else throw new Exception(s"Error while reading user for notification for userId: ${userId}") - } - - override def metricsList(): List[String] = { - List(config.courseBatchdbReadCount, config.skipNotifyUserCount, config.notifiedUserCount, config.failedEventCount) - } - - -} diff --git a/credential-generator/collection-certificate-generator/src/main/scala/org/sunbird/job/certgen/task/CertificateGeneratorConfig.scala b/credential-generator/collection-certificate-generator/src/main/scala/org/sunbird/job/certgen/task/CertificateGeneratorConfig.scala deleted file mode 100755 index d39d38b3f..000000000 --- a/credential-generator/collection-certificate-generator/src/main/scala/org/sunbird/job/certgen/task/CertificateGeneratorConfig.scala +++ /dev/null @@ -1,159 +0,0 @@ -package org.sunbird.job.certgen.task - -import java.util - -import com.typesafe.config.Config -import org.apache.flink.api.common.typeinfo.TypeInformation -import org.apache.flink.api.java.typeutils.TypeExtractor -import org.apache.flink.streaming.api.scala.OutputTag -import org.sunbird.job.BaseJobConfig -import org.sunbird.job.certgen.functions.{NotificationMetaData, UserFeedMetaData} - -class CertificateGeneratorConfig(override val config: Config) extends BaseJobConfig(config, "collection-certificate-generator") { - - private val serialVersionUID = 2905979434303791379L - - implicit val mapTypeInfo: TypeInformation[util.Map[String, AnyRef]] = TypeExtractor.getForClass(classOf[util.Map[String, AnyRef]]) - implicit val stringTypeInfo: TypeInformation[String] = TypeExtractor.getForClass(classOf[String]) - implicit val notificationMetaTypeInfo: TypeInformation[NotificationMetaData] = TypeExtractor.getForClass(classOf[NotificationMetaData]) - implicit val userFeeMetaTypeInfo: TypeInformation[UserFeedMetaData] = TypeExtractor.getForClass(classOf[UserFeedMetaData]) - - // Kafka Topics Configuration - val kafkaInputTopic: String = config.getString("kafka.input.topic") - val kafkaAuditEventTopic: String = config.getString("kafka.output.audit.topic") - - val enableSuppressException: Boolean = if(config.hasPath("enable.suppress.exception")) config.getBoolean("enable.suppress.exception") else false - val enableRcCertificate: Boolean = if(config.hasPath("enable.rc.certificate")) config.getBoolean("enable.rc.certificate") else false - - - // Producers - val certificateGeneratorAuditProducer = "collection-certificate-generator-audit-events-sink" - - override val kafkaConsumerParallelism: Int = config.getInt("task.consumer.parallelism") - val notifierParallelism: Int = if(config.hasPath("task.notifier.parallelism")) config.getInt("task.notifier.parallelism") else 1 - val userFeedParallelism: Int = if(config.hasPath("task.userfeed.parallelism")) config.getInt("task.userfeed.parallelism") else 1 - - //ES configuration - val esConnection: String = config.getString("es.basePath") - val certIndex: String = "certs" - val certIndexType: String = "_doc" - - - // Cassandra Configurations - val sbKeyspace: String = config.getString("lms-cassandra.sbkeyspace") - val certRegTable: String = config.getString("lms-cassandra.certreg.table") - val dbEnrollmentTable: String = config.getString("lms-cassandra.user_enrolments.table") - val dbKeyspace: String = config.getString("lms-cassandra.keyspace") - val dbHost: String = config.getString("lms-cassandra.host") - val dbPort: Int = config.getInt("lms-cassandra.port") - val dbCourseBatchTable: String = config.getString("lms-cassandra.course_batch.table") - val dbBatchId = "batchid" - val dbCourseId = "courseid" - val dbUserId = "userid" - val active: String = "active" - val issuedCertificates: String = "issued_certificates" - - // Metric List - val totalEventsCount = "total-events-count" - val successEventCount = "success-events-count" - val failedEventCount = "failed-events-count" - val skippedEventCount = "skipped-event-count" - val enrollmentDbReadCount = "enrollment-db-read-count" - val dbUpdateCount = "db-update-user-enrollment-count" - val notifiedUserCount = "notified-user-count" - val skipNotifyUserCount = "skipped-notify-user-count" - val courseBatchdbReadCount = "db-course-batch-read-count" - - // Consumers - val certificateGeneratorConsumer = "certificate" - - // env vars - val storageType: String = config.getString("cert_cloud_storage_type") - val containerName: String = config.getString("cert_container_name") - val azureStorageSecret: String = config.getString("cert_azure_storage_secret") - val azureStorageKey: String = config.getString("cert_azure_storage_key") - val domainUrl: String = config.getString("cert_domain_url") - val encServiceUrl: String = config.getString("service.enc.basePath") - val certRegistryBaseUrl: String = config.getString("service.certreg.basePath") - val learnerServiceBaseUrl: String = config.getString("service.learner.basePath") - val basePath: String = domainUrl.concat("/").concat("certs") - val awsStorageSecret: String = "" - val awsStorageKey: String = "" - val addCertRegApi = "/certs/v2/registry/add" - val userFeedCreateEndPoint:String = "/private/user/feed/v1/create" - val notificationEndPoint: String = "/v2/notification" - val rcBaseUrl: String = config.getString("service.rc.basePath") - val rcEntity: String = config.getString("service.rc.entity") - val rcCreateApi: String = "service.rc.create.api" - val rcDeleteApi: String = "service.rc.delete.api" - val rcSearchApi: String = "service.rc.search.api" - - - //constant - val DATA: String = "data" - val RECIPIENT_NAME: String = "recipientName" - val ISSUER: String = "issuer" - val BADGE_URL: String = "/Badge.json" - val ISSUER_URL: String = basePath.concat("/Issuer.json") - val EVIDENCE_URL: String = basePath.concat("/Evidence.json") - val CONTEXT: String = basePath.concat( "/v1/context.json") - val PUBLIC_KEY_URL: String = "_publicKey.json" - val VERIFICATION_TYPE: String = "SignedBadge" - val SIGNATORY_EXTENSION: String = basePath.concat("v1/extensions/SignatoryExtension/context.json") - val ACCESS_CODE_LENGTH: String = "6" - val EDATA: String = "edata" - val RELATED: String = "related" - val OLD_ID: String = "oldId" - val BATCH_ID: String = "batchId" - val COURSE_ID: String = "courseId" - val TEMPLATE_ID: String = "templateId" - val USER_ID: String = "userId" - - - val courseId = "courseId" - val batchId = "batchId" - val userId = "userId" - val notifyTemplate = "notifyTemplate" - val firstName = "firstName" - val trainingName = "TrainingName" - val heldDate = "heldDate" - val recipientUserIds = "recipientUserIds" - val identifier = "identifier" - val body = "body" - val notificationSmsBody = "Congratulations! Download your course certificate from your profile page. If you have a problem downloading it on the mobile, update your DIKSHA app" - val request = "request" - val filters = "filters" - val fields = "fields" - val issued_certificates = "issued_certificates" - val eData = "edata" - val name = "name" - val token = "token" - val lastIssuedOn = "lastIssuedOn" - val templateUrl = "templateUrl" - val `type` = "type" - val certificate = "certificate" - val action = "action" - val courseName = "courseName" - val templateId = "templateId" - val cert_templates = "cert_templates" - val courseBatch = "CourseBatch" - val l1 = "l1" - val id = "id" - val data = "data" - val category = "category" - val certificates = "certificates" - val badCharList = if(config.hasPath("task.rc.badcharlist")) config.getString("task.rc.badcharlist") else "\\x00,\\\\aaa,\\aaa,Ø,Ý" - - // Tags - val auditEventOutputTagName = "audit-events" - val auditEventOutputTag: OutputTag[String] = OutputTag[String](auditEventOutputTagName) - val notifierOutputTag: OutputTag[NotificationMetaData] = OutputTag[NotificationMetaData]("notifier") - val userFeedOutputTag: OutputTag[UserFeedMetaData] = OutputTag[UserFeedMetaData]("user-feed") - - //UserFeed constants - val priority: String = "priority" - val userFeedMsg: String = "You have earned a certificate! Download it from your profile page." - val priorityValue = 1 - val userFeedCount = "user-feed-count" - -} diff --git a/credential-generator/collection-certificate-generator/src/main/scala/org/sunbird/job/certgen/task/CertificateGeneratorStreamTask.scala b/credential-generator/collection-certificate-generator/src/main/scala/org/sunbird/job/certgen/task/CertificateGeneratorStreamTask.scala deleted file mode 100755 index a73c0ea46..000000000 --- a/credential-generator/collection-certificate-generator/src/main/scala/org/sunbird/job/certgen/task/CertificateGeneratorStreamTask.scala +++ /dev/null @@ -1,85 +0,0 @@ -package org.sunbird.job.certgen.task - -import java.io.File -import java.util -import com.typesafe.config.ConfigFactory -import org.apache.flink.api.common.typeinfo.TypeInformation -import org.apache.flink.api.java.functions.KeySelector -import org.apache.flink.api.java.typeutils.TypeExtractor -import org.apache.flink.api.java.utils.ParameterTool -import org.apache.flink.streaming.api.scala.StreamExecutionEnvironment -import org.sunbird.incredible.StorageParams -import org.sunbird.incredible.processor.store.StorageService -import org.sunbird.job.certgen.domain.Event -import org.sunbird.job.certgen.functions.{CertificateGeneratorFunction, CreateUserFeedFunction, NotificationMetaData, NotifierFunction, UserFeedMetaData} -import org.sunbird.job.connector.FlinkKafkaConnector -import org.sunbird.job.util.{FlinkUtil, HttpUtil} - -class CertificateGeneratorStreamTask(config: CertificateGeneratorConfig, kafkaConnector: FlinkKafkaConnector, httpUtil: HttpUtil, storageService: StorageService) { - - def process(): Unit = { - implicit val env: StreamExecutionEnvironment = FlinkUtil.getExecutionContext(config) - implicit val eventTypeInfo: TypeInformation[Event] = TypeExtractor.getForClass(classOf[Event]) - implicit val mapTypeInfo: TypeInformation[util.Map[String, AnyRef]] = TypeExtractor.getForClass(classOf[util.Map[String, AnyRef]]) - implicit val stringTypeInfo: TypeInformation[String] = TypeExtractor.getForClass(classOf[String]) - implicit val notificationMetaTypeInfo: TypeInformation[NotificationMetaData] = TypeExtractor.getForClass(classOf[NotificationMetaData]) - implicit val userFeedMetaTypeInfo: TypeInformation[UserFeedMetaData] = TypeExtractor.getForClass(classOf[UserFeedMetaData]) - - val source = kafkaConnector.kafkaJobRequestSource[Event](config.kafkaInputTopic) - - val processStreamTask = env.addSource(source) - .name(config.certificateGeneratorConsumer) - .uid(config.certificateGeneratorConsumer).setParallelism(config.kafkaConsumerParallelism) - .rebalance - .keyBy(new CertificateGeneratorKeySelector) - .process(new CertificateGeneratorFunction(config, httpUtil, storageService)) - .name("collection-certificate-generator") - .uid("collection-certificate-generator") - .setParallelism(config.parallelism) - - processStreamTask.getSideOutput(config.auditEventOutputTag) - .addSink(kafkaConnector.kafkaStringSink(config.kafkaAuditEventTopic)) - .name(config.certificateGeneratorAuditProducer) - .uid(config.certificateGeneratorAuditProducer) - - processStreamTask.getSideOutput(config.notifierOutputTag) - .process(new NotifierFunction(config, httpUtil)) - .name("notifier") - .uid("notifier") - .setParallelism(config.notifierParallelism) - - processStreamTask.getSideOutput(config.userFeedOutputTag) - .process(new CreateUserFeedFunction(config, httpUtil)) - .name("user-feed") - .uid("user-feed") - .setParallelism(config.userFeedParallelism) - - - env.execute(config.jobName) - } - -} - -// $COVERAGE-OFF$ Disabling scoverage as the below code can only be invoked within flink cluster -object CertificateGeneratorStreamTask { - - def main(args: Array[String]): Unit = { - val configFilePath = Option(ParameterTool.fromArgs(args).get("config.file.path")) - val config = configFilePath.map { - path => ConfigFactory.parseFile(new File(path)).resolve() - }.getOrElse(ConfigFactory.load("collection-certificate-generator.conf").withFallback(ConfigFactory.systemEnvironment())) - val ccgConfig = new CertificateGeneratorConfig(config) - val kafkaUtil = new FlinkKafkaConnector(ccgConfig) - val httpUtil = new HttpUtil - val storageParams: StorageParams = StorageParams(ccgConfig.storageType, ccgConfig.azureStorageKey, ccgConfig.azureStorageSecret, ccgConfig.containerName) - val storageService: StorageService = new StorageService(storageParams) - val task = new CertificateGeneratorStreamTask(ccgConfig, kafkaUtil, httpUtil, storageService) - task.process() - } -} - -// $COVERAGE-ON$ - -class CertificateGeneratorKeySelector extends KeySelector[Event, String] { - override def getKey(event: Event): String = Set(event.userId, event.courseId, event.batchId).mkString("_") -} \ No newline at end of file diff --git a/credential-generator/collection-certificate-generator/src/test/resources/logback-test.xml b/credential-generator/collection-certificate-generator/src/test/resources/logback-test.xml deleted file mode 100755 index 77fa2fc44..000000000 --- a/credential-generator/collection-certificate-generator/src/test/resources/logback-test.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n - - - - - - - - - - - - \ No newline at end of file diff --git a/credential-generator/collection-certificate-generator/src/test/resources/test.conf b/credential-generator/collection-certificate-generator/src/test/resources/test.conf deleted file mode 100755 index b87ac1946..000000000 --- a/credential-generator/collection-certificate-generator/src/test/resources/test.conf +++ /dev/null @@ -1,40 +0,0 @@ -include "base-test.conf" - -kafka { - input.topic = "generate.certificate.request" - output.failed.topic = "generate.certificate.failed" - output.audit.topic = "generate.certificate.audit" - groupId = "certificate-generator-group" -} - -task { - consumer.parallelism = 1 -} - -service { - certreg.basePath = "http://localhost:9000/certreg" - learner.basePath = "http://localhost:9000/learner" - enc.basePath = "http://localhost:9000/enc" - rc.basePath = "http://localhost:8081/api/v1" - rc.entity = "TrainingCertificate" -} - -cert_domain_url="https://dev.sunbirded.org" -cert_cloud_storage_type="azure" -cert_azure_storage_secret="secret" -cert_container_name="credential" -cert_azure_storage_key="key" - -lms-cassandra { - keyspace = "sunbird_courses" - user_enrolments.table = "user_enrolments" - course_batch.table = "course_batch" - host = "localhost" - port = "9142" - sbkeyspace = "sunbird" - certreg.table ="cert_registry" -} - -enable.suppress.exception = true -enable.rc.certificate = true - diff --git a/credential-generator/collection-certificate-generator/src/test/resources/test.cql b/credential-generator/collection-certificate-generator/src/test/resources/test.cql deleted file mode 100755 index 2b75e890b..000000000 --- a/credential-generator/collection-certificate-generator/src/test/resources/test.cql +++ /dev/null @@ -1,53 +0,0 @@ -CREATE KEYSPACE sunbird_courses with replication = {'class':'SimpleStrategy','replication_factor':1}; - -CREATE TABLE sunbird_courses.user_enrolments ( - userid text, - courseid text, - batchid text, - active boolean, - addedby text, - certificates list>>, - completedon timestamp, - completionpercentage int, - contentstatus map, - datetime timestamp, - enrolleddate text, - issued_certificates list>>, - lastreadcontentid text, - lastreadcontentstatus int, - progress int, - status int, - PRIMARY KEY (userid, courseid, batchid) -) WITH CLUSTERING ORDER BY (courseid ASC, batchid ASC); - - -CREATE TABLE sunbird_courses.course_batch ( - courseid text, - batchid text, - cert_templates map>>, - createdby text, - createddate text, - createdfor list, - description text, - enddate text, - enrollmentenddate text, - enrollmenttype text, - mentors list, - name text, - startdate text, - status int, - updateddate text, - PRIMARY KEY (courseid, batchid) -) WITH CLUSTERING ORDER BY (batchid ASC); - -//event 1 - -INSERT INTO sunbird_courses.course_batch(courseid, batchid,cert_templates) VALUES ('do_11309999837886054415','0131000245281587206',{'template_01_dev_001':{'criteria': '{"enrollment":{"status":2}}', 'identifier': 'template_01_dev_001', 'issuer': '{"name":"Gujarat Council of Educational Research and Training","publicKey":["7","8"],"url":"https://gcert.gujarat.gov.in/gcert/"}', 'name': 'Course merit certificate', 'notifyTemplate': '{"subject":"Completion certificate","stateImgUrl":"https://sunbirddev.blob.core.windows.net/orgemailtemplate/img/File-0128212938260643843.png","regardsperson":"Chairperson","regards":"Minister of Gujarat","emailTemplateType":"defaultCertTemp"}', 'signatoryList': '[{"image":"https://cdn.pixabay.com/photo/2014/11/09/08/06/signature-523237__340.jpg","name":"CEO Gujarat","id":"CEO","designation":"CEO"}]'}}) ; - -INSERT INTO sunbird_courses.user_enrolments(userid, courseid, batchid,issued_certificates,completedon) VALUES ('user001','do_11309999837886054415','0131000245281587206',[{'identifier': 'certificateId', 'lastIssuedOn': '2019-08-21', 'name': '100PercentCompletionCertificate', 'token': 'P4L3Y9'}], toTimeStamp(toDate(now()))) ; - -//event 2 - -INSERT INTO sunbird_courses.course_batch(courseid, batchid,cert_templates) VALUES ('do_11309999837886054416','0131000245281587207',{'template_01_dev_001':{'criteria': '{"enrollment":{"status":2}}', 'identifier': 'template_01_dev_001', 'issuer': '{"name":"Gujarat Council of Educational Research and Training","publicKey":["7","8"],"url":"https://gcert.gujarat.gov.in/gcert/"}', 'name': 'Course merit certificate', 'signatoryList': '[{"image":"https://cdn.pixabay.com/photo/2014/11/09/08/06/signature-523237__340.jpg","name":"CEO Gujarat","id":"CEO","designation":"CEO"}]'}}) ; - -INSERT INTO sunbird_courses.user_enrolments(userid, courseid, batchid,completedon) VALUES ('user002','do_11309999837886054416','0131000245281587207', toTimeStamp(toDate(now()))) ; diff --git a/credential-generator/collection-certificate-generator/src/test/scala/org/sunbird/job/certgen/fixture/EventFixture.scala b/credential-generator/collection-certificate-generator/src/test/scala/org/sunbird/job/certgen/fixture/EventFixture.scala deleted file mode 100644 index 17decf521..000000000 --- a/credential-generator/collection-certificate-generator/src/test/scala/org/sunbird/job/certgen/fixture/EventFixture.scala +++ /dev/null @@ -1,24 +0,0 @@ -package org.sunbird.job.certgen.fixture - -object EventFixture { - - val EVENT_1: String = - """ - |{"eid":"BE_JOB_REQUEST","ets":1563788371969,"mid":"LMS.1563788371969.590c5fa0-0ce8-46ed-bf6c-681c0a1fdac8","actor":{"type":"System","id":"Certificate Generator"},"context":{"pdata":{"ver":"1.0","id":"org.sunbird.platform"}},"object":{"type":"GenerateCertificate","id":"874ed8a5-782e-4f6c-8f36-e0288455901e"},"edata":{"userId": "user001", "oldId": "certificateId", "svgTemplate":"https://ntpstagingall.blob.core.windows.net/user/cert/File-01311849840255795242.svg", "templateId":"template_01_dev_001","courseName":"new course may23","data":[{"recipientName":"Creation ","recipientId":"user001"}],"name":"100PercentCompletionCertificate","tag":"0125450863553740809","issuer":{"name":"Gujarat Council of Educational Research and Training","url":"https://gcert.gujarat.gov.in/gcert/","publicKey":["1","2"]},"signatoryList":[{"name":"CEO Gujarat","id":"CEO","designation":"CEO","image":"https://cdn.pixabay.com/photo/2014/11/09/08/06/signature-523237__340.jpg"}],"criteria":{"narrative":"course completion certificate"},"basePath":"https://dev.sunbirded.org/certs","related":{"type":"course","batchId":"0131000245281587206","courseId":"do_11309999837886054415"}}} - |""".stripMargin - - val EVENT_2: String = - """ - |{"eid":"BE_JOB_REQUEST","ets":1563788371969,"mid":"LMS.1563788371969.590c5fa0-0ce8-46ed-bf6c-681c0a1fdac8","actor":{"type":"System","id":"Certificate Generator"},"context":{"pdata":{"ver":"1.0","id":"org.sunbird.platform"}},"object":{"type":"GenerateCertificate","id":"874ed8a5-782e-4f6c-8f36-e0288455901e"},"edata":{"userId": "user002","svgTemplate":"", "templateId":"template_01_dev_001","courseName":"new course may23","data":[{"recipientName":"Creation ","recipientId":"874ed8a5-782e-4f6c-8f36-e0288455901e"}],"name":"100PercentCompletionCertificate","tag":"0125450863553740809","issuer":{"name":"Gujarat Council of Educational Research and Training","url":"https://gcert.gujarat.gov.in/gcert/","publicKey":["1","2"]},"signatoryList":[{"name":"CEO Gujarat","id":"CEO","designation":"CEO","image":"https://cdn.pixabay.com/photo/2014/11/09/08/06/signature-523237__340.jpg"}],"criteria":{"narrative":"course completion certificate"},"basePath":"https://dev.sunbirded.org/certs","related":{"type":"course","batchId":"123","courseId":"543535"}}} - |""".stripMargin - - val EVENT_3: String = - """ - |{"eid":"BE_JOB_REQUEST","ets":1563788371969,"mid":"LMS.1563788371969.590c5fa0-0ce8-46ed-bf6c-681c0a1fdac8","actor":{"type":"System","id":"Certificate Generator"},"context":{"pdata":{"ver":"1.0","id":"org.sunbird.platform"}},"object":{"type":"GenerateCertificate","id":"874ed8a5-782e-4f6c-8f36-e0288455901e"},"edata":{"userId": "user001", "svgTemplate":"https://ntpstagingall.blob.core.windows.net/user/cert/File-01311849840255795242.svg", "templateId":"template_01_dev_001","courseName":"new course may23","data":[{"recipientName":"Creation ","recipientId":"user001"}],"name":"100PercentCompletionCertificate","tag":"0125450863553740809","issuer":{"name":"Gujarat Council of Educational Research and Training","url":"https://gcert.gujarat.gov.in/gcert/","publicKey":["1","2"]},"signatoryList":[{"name":"CEO Gujarat","id":"CEO","designation":"CEO","image":"https://cdn.pixabay.com/photo/2014/11/09/08/06/signature-523237__340.jpg"}],"criteria":{"narrative":"course completion certificate"},"basePath":"https://dev.sunbirded.org/certs","related":{"type":"course","batchId":"0131000245281587206","courseId":"do_11309999837886054415"}}} - |""".stripMargin - - val EVENT_4: String = - """ - |{"eid":"BE_JOB_REQUEST","ets":1563788371969,"mid":"LMS.1563788371969.590c5fa0-0ce8-46ed-bf6c-681c0a1fdac8","actor":{"type":"System","id":"Certificate Generator"},"context":{"pdata":{"ver":"1.0","id":"org.sunbird.platform"}},"object":{"type":"GenerateCertificate","id":"874ed8a5-782e-4f6c-8f36-e0288455901e"},"edata":{"userId": "user001", "svgTemplate":"https://ntpstagingall.blob.core.windows.net/user/cert/File-01311849840255795242.svg", "templateId":"template_01_dev_001","courseName":"new course may23","data":[{"recipientName":"Creation ","recipientId":"user001"}],"name":"100PercentCompletionCertificate","tag":"0125450863553740809","issuer":{"name":"Gujarat Council of Educational Research and Training","url":"https://gcert.gujarat.gov.in/gcert/","publicKey":["1","2"]},"signatoryList":[{"name":"","id":"","designation":"","image":""}],"criteria":{"narrative":"course completion certificate"},"basePath":"https://dev.sunbirded.org/certs","related":{"type":"course","batchId":"0131000245281587206","courseId":"do_11309999837886054415"}}} - |""".stripMargin -} diff --git a/credential-generator/collection-certificate-generator/src/test/scala/org/sunbird/job/certgen/spec/CertValidatorTest.scala b/credential-generator/collection-certificate-generator/src/test/scala/org/sunbird/job/certgen/spec/CertValidatorTest.scala deleted file mode 100644 index cb3e4d9d1..000000000 --- a/credential-generator/collection-certificate-generator/src/test/scala/org/sunbird/job/certgen/spec/CertValidatorTest.scala +++ /dev/null @@ -1,70 +0,0 @@ -package org.sunbird.job.certgen.spec - -import com.typesafe.config.{Config, ConfigFactory} -import org.cassandraunit.CQLDataLoader -import org.cassandraunit.dataset.cql.FileCQLDataSet -import org.cassandraunit.utils.EmbeddedCassandraServerHelper -import org.mockito.Mockito -import org.sunbird.job.Metrics -import org.sunbird.job.certgen.domain.Event -import org.sunbird.job.certgen.exceptions.ValidationException -import org.sunbird.job.certgen.fixture.EventFixture -import org.sunbird.job.certgen.functions.CertValidator -import org.sunbird.job.certgen.task.CertificateGeneratorConfig -import org.sunbird.job.util.{CassandraUtil, HttpUtil, JSONUtil} -import org.sunbird.spec.BaseTestSpec - -import java.util - -class CertValidatorTest extends BaseTestSpec{ - var cassandraUtil: CassandraUtil = _ - val config: Config = ConfigFactory.load("test.conf") - lazy val jobConfig: CertificateGeneratorConfig = new CertificateGeneratorConfig(config) - val httpUtil: HttpUtil = new HttpUtil - val mockHttpUtil:HttpUtil = mock[HttpUtil](Mockito.withSettings().serializable()) - val metricJson = s"""{"${jobConfig.enrollmentDbReadCount}": 0, "${jobConfig.skippedEventCount}": 0}""" - val mockMetrics = mock[Metrics](Mockito.withSettings().serializable()) - - override protected def beforeAll(): Unit = { - super.beforeAll() - EmbeddedCassandraServerHelper.startEmbeddedCassandra(80000L) - cassandraUtil = new CassandraUtil(jobConfig.dbHost, jobConfig.dbPort) - val session = cassandraUtil.session - session.execute(s"DROP KEYSPACE IF EXISTS ${jobConfig.dbKeyspace}") - val dataLoader = new CQLDataLoader(session) - dataLoader.load(new FileCQLDataSet(getClass.getResource("/test.cql").getPath, true, true)) - - } - - override protected def afterAll(): Unit = { - super.afterAll() - try { - EmbeddedCassandraServerHelper.cleanEmbeddedCassandra() - } catch { - case ex: Exception => ex.printStackTrace() - } - } - - "CertValidator isNotIssued" should "return true if re-Issued" in { - val event = new Event(JSONUtil.deserialize[java.util.Map[String, Any]](EventFixture.EVENT_1), 0, 0) - val isCertificateIssued = new CertValidator().isNotIssued(event)(jobConfig, mockMetrics, cassandraUtil) - assert(true == isCertificateIssued) - } - - "CertValidator isNotIssued" should "return false if already issued" in { - val event = new Event(JSONUtil.deserialize[java.util.Map[String, Any]](EventFixture.EVENT_3), 0, 0) - val isCertificateIssued = new CertValidator().isNotIssued(event)(jobConfig, mockMetrics, cassandraUtil) - assert(false == isCertificateIssued) - } - - "CertValidator with enabled suppress exception on signatoryList with empty field values" should "not throw exception" in { - val event = new Event(JSONUtil.deserialize[java.util.Map[String, Any]](EventFixture.EVENT_4), 0, 0) - noException should be thrownBy new CertValidator().validateGenerateCertRequest(event, true) - } - - "CertValidator on signatoryList with empty field values" should " throw exception" in { - val event = new Event(JSONUtil.deserialize[java.util.Map[String, Any]](EventFixture.EVENT_4), 0, 0) - an [ValidationException] should be thrownBy new CertValidator().validateGenerateCertRequest(event, false) - } - -} diff --git a/credential-generator/collection-certificate-generator/src/test/scala/org/sunbird/job/certgen/spec/CertificateGeneratorFunctionTaskTestSpec.scala b/credential-generator/collection-certificate-generator/src/test/scala/org/sunbird/job/certgen/spec/CertificateGeneratorFunctionTaskTestSpec.scala deleted file mode 100755 index cdf6f8c82..000000000 --- a/credential-generator/collection-certificate-generator/src/test/scala/org/sunbird/job/certgen/spec/CertificateGeneratorFunctionTaskTestSpec.scala +++ /dev/null @@ -1,127 +0,0 @@ -package org.sunbird.job.certgen.spec - -import java.io.File -import java.util - -import com.typesafe.config.{Config, ConfigFactory} -import org.apache.flink.api.common.typeinfo.TypeInformation -import org.apache.flink.api.java.typeutils.TypeExtractor -import org.apache.flink.runtime.client.JobExecutionException -import org.apache.flink.runtime.testutils.MiniClusterResourceConfiguration -import org.apache.flink.streaming.api.functions.sink.SinkFunction -import org.apache.flink.streaming.api.functions.source.SourceFunction -import org.apache.flink.streaming.api.functions.source.SourceFunction.SourceContext -import org.apache.flink.test.util.MiniClusterWithClientResource -import org.cassandraunit.CQLDataLoader -import org.cassandraunit.dataset.cql.FileCQLDataSet -import org.cassandraunit.utils.EmbeddedCassandraServerHelper -import org.mockito.ArgumentMatchers.{any, contains, endsWith} -import org.mockito.Mockito -import org.mockito.Mockito._ -import org.scalatest.DoNotDiscover -import org.sunbird.incredible.processor.store.StorageService -import org.sunbird.job.certgen.domain.Event -import org.sunbird.job.certgen.fixture.EventFixture -import org.sunbird.job.certgen.task.{CertificateGeneratorConfig, CertificateGeneratorStreamTask} -import org.sunbird.job.connector.FlinkKafkaConnector -import org.sunbird.job.util.{CassandraUtil, HTTPResponse, HttpUtil, JSONUtil} -import org.sunbird.spec.{BaseMetricsReporter, BaseTestSpec} - -@DoNotDiscover -class CertificateGeneratorFunctionTaskTestSpec extends BaseTestSpec { - - implicit val mapTypeInfo: TypeInformation[java.util.Map[String, AnyRef]] = TypeExtractor.getForClass(classOf[java.util.Map[String, AnyRef]]) - - var cassandraUtil: CassandraUtil = _ - - val flinkCluster = new MiniClusterWithClientResource(new MiniClusterResourceConfiguration.Builder() - .setConfiguration(testConfiguration()) - .setNumberSlotsPerTaskManager(1) - .setNumberTaskManagers(1) - .build) - - val mockKafkaUtil: FlinkKafkaConnector = mock[FlinkKafkaConnector](Mockito.withSettings().serializable()) - val config: Config = ConfigFactory.load("test.conf") - val jobConfig: CertificateGeneratorConfig = new CertificateGeneratorConfig(config) - val mockHttpUtil: HttpUtil = mock[HttpUtil](Mockito.withSettings().serializable()) - val storageService: StorageService = mock[StorageService](Mockito.withSettings().serializable()) - - - override protected def beforeAll(): Unit = { - super.beforeAll() - EmbeddedCassandraServerHelper.startEmbeddedCassandra(80000L) - cassandraUtil = new CassandraUtil(jobConfig.dbHost, jobConfig.dbPort) - val session = cassandraUtil.session - - val dataLoader = new CQLDataLoader(session) - dataLoader.load(new FileCQLDataSet(getClass.getResource("/test.cql").getPath, true, true)) - flinkCluster.before() - // Clear the metrics - - BaseMetricsReporter.gaugeMetrics.clear() - when(mockHttpUtil.post(endsWith("/certs/v2/registry/add"), any[String], any[Map[String, String]]())).thenReturn(HTTPResponse(200, """{"id":"api.certs.registry.add","ver":"v2","ts":"1602590393507","params":null,"responseCode":"OK","result":{"id":"c96d60f8-9c76-4a73-9ef0-9e01d0f726c6"}}""")) - when(mockHttpUtil.get(contains("/private/user/v1/read"), any[Map[String, String]]())).thenReturn(HTTPResponse(200, """{"id":"","ver":"private","ts":"2020-10-21 14:10:49:964+0000","params":{"resmsgid":null,"msgid":"a6f3e248-c504-4c2f-9bfa-90f54abd2e30","err":null,"status":"success","errmsg":null},"responseCode":"OK","result":{"response":{"firstName":"test12","lastName":"A","maskedPhone":"******0183","rootOrgName":"ORG_002","userName":"teast123","rootOrgId":"01246944855007232011"}}}""")) - when(mockHttpUtil.post(endsWith("/v2/notification"), any[String], any[Map[String, String]]())).thenReturn(HTTPResponse(200, """{"id":"api.notification","ver":"v2","ts":"2020-10-21 14:12:09:065+0000","params":{"resmsgid":null,"msgid":"0df38787-1168-4ae0-aa4b-dcea23ea81e4","err":null,"status":"success","errmsg":null},"responseCode":"OK","result":{"response":"SUCCESS"}}""")) - when(mockHttpUtil.post(endsWith("/private/user/feed/v1/create"), any[String], any[Map[String, String]]())).thenReturn(HTTPResponse(200, """{"id":"api.user.feed.create","ver":"v1","ts":"2020-10-30 13:20:54:940+0000","params":{"resmsgid":null,"msgid":"518d3404-cf1f-4001-81a5-0c58647b32fe","err":null,"status":"success","errmsg":null},"responseCode":"OK","result":{"response":"SUCCESS"}}""")) - when(storageService.uploadFile(any[String], any[File])).thenReturn("jsonUrl") - } - - - override protected def afterAll(): Unit = { - super.afterAll() - try { - EmbeddedCassandraServerHelper.cleanEmbeddedCassandra() - } catch { - case ex: Exception => { - } - } finally - flinkCluster.after() - } - - - /** - * this test works on intellij , but using mvn scoverge:report is not working - */ - it should "generate certificate and add to the registry" in { - when(mockKafkaUtil.kafkaStringSink(jobConfig.kafkaAuditEventTopic)).thenReturn(new auditEventSink) - when(mockKafkaUtil.kafkaJobRequestSource[Event](jobConfig.kafkaInputTopic)).thenReturn(new CertificateGeneratorEventSource) - intercept[JobExecutionException](new CertificateGeneratorStreamTask(jobConfig, mockKafkaUtil, mockHttpUtil, storageService).process()) - BaseMetricsReporter.gaugeMetrics(s"${jobConfig.jobName}.${jobConfig.totalEventsCount}").getValue() should be(3) - BaseMetricsReporter.gaugeMetrics(s"${jobConfig.jobName}.${jobConfig.successEventCount}").getValue() should be(1) - BaseMetricsReporter.gaugeMetrics(s"${jobConfig.jobName}.${jobConfig.failedEventCount}").getValue() should be(1) - BaseMetricsReporter.gaugeMetrics(s"${jobConfig.jobName}.${jobConfig.skippedEventCount}").getValue() should be(1) - BaseMetricsReporter.gaugeMetrics(s"${jobConfig.jobName}.${jobConfig.enrollmentDbReadCount}").getValue() should be(3) - BaseMetricsReporter.gaugeMetrics(s"${jobConfig.jobName}.${jobConfig.courseBatchdbReadCount}").getValue() should be(1) - BaseMetricsReporter.gaugeMetrics(s"${jobConfig.jobName}.${jobConfig.dbUpdateCount}").getValue() should be(1) - BaseMetricsReporter.gaugeMetrics(s"${jobConfig.jobName}.${jobConfig.notifiedUserCount}").getValue() should be(1) - auditEventSink.values.size() should be(1) - } - - -} - -class CertificateGeneratorEventSource extends SourceFunction[Event] { - override def run(ctx: SourceContext[Event]): Unit = { - val eventMap1: util.Map[String, Any] = JSONUtil.deserialize[java.util.Map[String, Any]](EventFixture.EVENT_1) - val eventMap2: util.Map[String, Any] = JSONUtil.deserialize[java.util.Map[String, Any]](EventFixture.EVENT_2) - val eventMap3: util.Map[String, Any] = JSONUtil.deserialize[java.util.Map[String, Any]](EventFixture.EVENT_3) - ctx.collect(new Event(eventMap1.asInstanceOf[util.Map[String, Any]],0,0)) - ctx.collect(new Event(eventMap3.asInstanceOf[util.Map[String, Any]],0, 1)) - ctx.collect(new Event(eventMap2.asInstanceOf[util.Map[String, Any]],0, 2)) - } - - override def cancel() = {} -} - -class auditEventSink extends SinkFunction[String] { - - override def invoke(value: String): Unit = { - synchronized { - auditEventSink.values.add(value) - } - } -} - -object auditEventSink { - val values: util.List[String] = new util.ArrayList() -} diff --git a/credential-generator/collection-certificate-generator/src/test/scala/org/sunbird/job/certgen/spec/CertificateGeneratorFunctionTest.scala b/credential-generator/collection-certificate-generator/src/test/scala/org/sunbird/job/certgen/spec/CertificateGeneratorFunctionTest.scala deleted file mode 100644 index 55ee40390..000000000 --- a/credential-generator/collection-certificate-generator/src/test/scala/org/sunbird/job/certgen/spec/CertificateGeneratorFunctionTest.scala +++ /dev/null @@ -1,240 +0,0 @@ -package org.sunbird.job.certgen.spec - -import com.datastax.driver.core.{ResultSet, Row} -import com.datastax.driver.core.querybuilder.QueryBuilder -import com.typesafe.config.{Config, ConfigFactory} -import kong.unirest.UnirestException -import org.cassandraunit.CQLDataLoader -import org.cassandraunit.dataset.cql.FileCQLDataSet -import org.cassandraunit.utils.EmbeddedCassandraServerHelper -import org.mockito.ArgumentMatchers.{any, anyString} -import org.mockito.Mockito -import org.mockito.Mockito.{doNothing, when} -import org.sunbird.incredible.processor.CertModel -import org.sunbird.incredible.{CertificateConfig, JsonKeys, ScalaModuleJsonUtils, StorageParams} -import org.sunbird.incredible.processor.store.StorageService -import org.sunbird.job.Metrics -import org.sunbird.job.certgen.domain.{Certificate, Event, Issuer, Recipient, Training, UserEnrollmentData} -import org.sunbird.job.certgen.exceptions.ServerException -import org.sunbird.job.certgen.fixture.EventFixture -import org.sunbird.job.certgen.functions.{CertMapper, CertValidator, CertificateGeneratorFunction} -import org.sunbird.job.certgen.task.CertificateGeneratorConfig -import org.sunbird.job.util.{CassandraUtil, ElasticSearchUtil, HTTPResponse, HttpUtil, JSONUtil} -import org.sunbird.spec.BaseTestSpec - -import java.text.SimpleDateFormat -import java.util -import java.util.Date - -class CertificateGeneratorFunctionTest extends BaseTestSpec { - - var cassandraUtil: CassandraUtil = _ - val mockCassandraUtil = mock[CassandraUtil] - val config: Config = ConfigFactory.load("test.conf") - lazy val jobConfig: CertificateGeneratorConfig = new CertificateGeneratorConfig(config) - val httpUtil: HttpUtil = new HttpUtil - val mockHttpUtil = mock[HttpUtil](Mockito.withSettings().serializable()) - val storageParams: StorageParams = StorageParams(jobConfig.storageType, jobConfig.azureStorageKey, jobConfig.azureStorageSecret, jobConfig.containerName) - val storageService: StorageService = new StorageService(storageParams) - val metricJson = s"""{"${jobConfig.enrollmentDbReadCount}": 0, "${jobConfig.skippedEventCount}": 0}""" - val mockMetrics = mock[Metrics](Mockito.withSettings().serializable()) - val certificateConfig: CertificateConfig = CertificateConfig(basePath = jobConfig.basePath, encryptionServiceUrl = jobConfig.encServiceUrl, contextUrl = jobConfig.CONTEXT, issuerUrl = jobConfig.ISSUER_URL, - evidenceUrl = jobConfig.EVIDENCE_URL, signatoryExtension = jobConfig.SIGNATORY_EXTENSION) - val mockEsUtil: ElasticSearchUtil = mock[ElasticSearchUtil](Mockito.withSettings().serializable()) - val formatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ") - - - - override protected def beforeAll(): Unit = { - super.beforeAll() - EmbeddedCassandraServerHelper.startEmbeddedCassandra(80000L) - cassandraUtil = new CassandraUtil(jobConfig.dbHost, jobConfig.dbPort) - val session = cassandraUtil.session - session.execute(s"DROP KEYSPACE IF EXISTS ${jobConfig.dbKeyspace}") - val dataLoader = new CQLDataLoader(session) - dataLoader.load(new FileCQLDataSet(getClass.getResource("/test.cql").getPath, true, true)) - - } - - override protected def afterAll(): Unit = { - super.afterAll() - try { - EmbeddedCassandraServerHelper.cleanEmbeddedCassandra() - } catch { - case ex: Exception => ex.printStackTrace() - } - } - - "Certificate generation process " should " not throw exception on enabled suppress exception for signatorylist with empty field values" in { - val event = new Event(JSONUtil.deserialize[java.util.Map[String, Any]](EventFixture.EVENT_4), 0, 0) - noException should be thrownBy new CertificateGeneratorFunction(jobConfig, httpUtil, storageService, cassandraUtil).processElement(event, null, mockMetrics) - } - - "Certificate rc delete api call for valid identifier " should " not throw serverException " in { - when(mockHttpUtil.delete(any[String])).thenReturn(HTTPResponse(200, """{}""")) - noException should be thrownBy new CertificateGeneratorFunction(jobConfig, mockHttpUtil, storageService, cassandraUtil).callCertificateRc(jobConfig.rcDeleteApi, "validId", null) - } - - "Certificate rc delete api call for invalid identifier " should " throw serverException " in { - when(mockHttpUtil.delete(any[String])).thenReturn(HTTPResponse(500, """{}""")) - an [ServerException] should be thrownBy new CertificateGeneratorFunction(jobConfig, mockHttpUtil, storageService, cassandraUtil).callCertificateRc(jobConfig.rcDeleteApi, "invalidId", null) - } - - "Certificate rc create api call for for !200 response status " should " not throw serverException and returns validId" in { - val certReq = Map[String, AnyRef]( - JsonKeys.NAME -> "name", - JsonKeys.CERTIFICATE_NAME -> "name" - ) - when(mockHttpUtil.post(jobConfig.rcBaseUrl + "/" + jobConfig.rcEntity, ScalaModuleJsonUtils.serialize(certReq))).thenReturn(HTTPResponse(200, """{"id":"sunbird-rc.registry.create","ver":"1.0","ets":1646765130993,"params":{"resmsgid":"","msgid":"cca2e242-fce7-47ec-b5d0-61cebe56c31d","err":"","status":"SUCCESSFUL","errmsg":""},"responseCode":"OK","result":{"TrainingCertificate":{"osid":"validId"}}}""")) - var id: String = null - noException should be thrownBy { - id = new CertificateGeneratorFunction(jobConfig, mockHttpUtil, storageService, cassandraUtil).callCertificateRc(jobConfig.rcCreateApi, null, certReq) - } - assert(id equals "validId") - } - - "Certificate rc create api call for !200 response status " should " throw serverException " in { - val certReq = Map[String, AnyRef]( - JsonKeys.NAME -> "name", - JsonKeys.CERTIFICATE_NAME -> "name" - ) - when(mockHttpUtil.post(jobConfig.rcBaseUrl + "/" + jobConfig.rcEntity, ScalaModuleJsonUtils.serialize(certReq))).thenReturn(HTTPResponse(500, """{}""")) - an [ServerException] should be thrownBy new CertificateGeneratorFunction(jobConfig, mockHttpUtil, storageService, cassandraUtil).callCertificateRc(jobConfig.rcCreateApi, null, certReq) - } - - "Certificate Update enrolment with valid event " should " not throw exception " in { - val event = new Event(JSONUtil.deserialize[java.util.Map[String, Any]](EventFixture.EVENT_3), 0, 0) - val createCertReq = generateRequest(event,"1-25a8c96b-b254-4720-bbc9-29b37c3c2bec") - val recipient = createCertReq.getOrElse("recipient", null).asInstanceOf[Recipient] - val userEnrollmentData = UserEnrollmentData(event.related.getOrElse(jobConfig.BATCH_ID, "").asInstanceOf[String], recipient.id, - event.related.getOrElse(jobConfig.COURSE_ID, "").asInstanceOf[String], event.courseName, event.templateId, - Certificate("validId", event.name, "", formatter.format(new Date()), event.svgTemplate, jobConfig.rcEntity)) - val batchId = event.related.getOrElse(jobConfig.COURSE_ID, "").asInstanceOf[String] - val courseId = event.related.getOrElse(jobConfig.BATCH_ID, "").asInstanceOf[String] - val req = Map("filters" -> Map()) - - when(mockHttpUtil.post(jobConfig.rcBaseUrl + "/PublicKey/search", ScalaModuleJsonUtils.serialize(req))).thenReturn(HTTPResponse(200, """[{"osUpdatedAt":"2022-03-17T06:43:48.070698Z","osCreatedAt":"2022-03-17T06:43:48.070698Z","osUpdatedBy":"anonymous","osCreatedBy":"anonymous","osid":"1-25a8c96b-b254-4720-bbc9-29b37c3c2bec","value":"keyvalue","alg":"RSA"}]""")) - when(mockHttpUtil.post(jobConfig.rcBaseUrl + "/" + jobConfig.rcEntity, ScalaModuleJsonUtils.serialize(createCertReq))).thenReturn(HTTPResponse(200, """{"id":"sunbird-rc.registry.create","ver":"1.0","ets":1646765130993,"params":{"resmsgid":"","msgid":"cca2e242-fce7-47ec-b5d0-61cebe56c31d","err":"","status":"SUCCESSFUL","errmsg":""},"responseCode":"OK","result":{"TrainingCertificate":{"osid":"validId"}}}""")) - when(mockCassandraUtil.find("SELECT * FROM sunbird_courses.user_enrolments WHERE userid='"+event.userId+"' AND batchid='"+batchId+"' AND courseid='"+courseId+"';")).thenReturn(new util.ArrayList[Row]()) - noException should be thrownBy new CertificateGeneratorFunction(jobConfig, mockHttpUtil, storageService, mockCassandraUtil).updateUserEnrollmentTable(event,userEnrollmentData,null )(mockMetrics) - } - - "Certificate generation with valid event " should " not throw exception " in { - val event = new Event(JSONUtil.deserialize[java.util.Map[String, Any]](EventFixture.EVENT_3), 0, 0) - val createCertReq = generateRequest(event,"1-25a8c96b-b254-4720-bbc9-29b37c3c2bec") - val batchId = event.related.getOrElse(jobConfig.COURSE_ID, "").asInstanceOf[String] - val courseId = event.related.getOrElse(jobConfig.BATCH_ID, "").asInstanceOf[String] - val req = Map("filters" -> Map()) - when(mockHttpUtil.post(jobConfig.rcBaseUrl + "/PublicKey/search", ScalaModuleJsonUtils.serialize(req))).thenReturn(HTTPResponse(200, """[{"osUpdatedAt":"2022-03-17T06:43:48.070698Z","osCreatedAt":"2022-03-17T06:43:48.070698Z","osUpdatedBy":"anonymous","osCreatedBy":"anonymous","osid":"1-25a8c96b-b254-4720-bbc9-29b37c3c2bec","value":"keyvalue","alg":"RSA"}]""")) - when(mockHttpUtil.post(jobConfig.rcBaseUrl + "/" + jobConfig.rcEntity, ScalaModuleJsonUtils.serialize(createCertReq))).thenReturn(HTTPResponse(200, """{"id":"sunbird-rc.registry.create","ver":"1.0","ets":1646765130993,"params":{"resmsgid":"","msgid":"cca2e242-fce7-47ec-b5d0-61cebe56c31d","err":"","status":"SUCCESSFUL","errmsg":""},"responseCode":"OK","result":{"TrainingCertificate":{"osid":"validId"}}}""")) - when(mockCassandraUtil.find("SELECT * FROM sunbird_courses.user_enrolments WHERE userid='"+event.userId+"' AND batchid='"+batchId+"' AND courseid='"+courseId+"';")).thenReturn(new util.ArrayList[Row]()) - noException should be thrownBy new CertificateGeneratorFunction(jobConfig, mockHttpUtil, storageService, mockCassandraUtil).generateCertificateUsingRC(event, null)(mockMetrics) - - } - - - private def generateRequest(event: Event, kid: String): Map[String, AnyRef] = { - val certModel: CertModel = new CertMapper(certificateConfig).mapReqToCertModel(event).head - val reIssue: Boolean = !event.oldId.isEmpty - val related = event.related - val createCertReq = Map[String, AnyRef]( - "certificateLabel" -> certModel.certificateName, - "status" -> "ACTIVE", - "templateUrl" -> event.svgTemplate, - "training" -> Training(related.getOrElse(jobConfig.COURSE_ID, "").asInstanceOf[String], event.courseName, "Course", related.getOrElse(jobConfig.BATCH_ID, "").asInstanceOf[String]), - "recipient" -> Recipient(certModel.identifier, certModel.recipientName, null), - "issuer" -> Issuer(certModel.issuer.url, certModel.issuer.name, kid), - "signatory" -> event.signatoryList, - ) ++ {if (reIssue) Map[String, AnyRef](jobConfig.OLD_ID -> event.oldId) else Map[String, AnyRef]()} - createCertReq - } - - // functional test cases commented. - // To be used while running locally -/* "Certificate rc create api call for for valid request " should " not throw serverException and returns id" in { - val event = new Event(JSONUtil.deserialize[java.util.Map[String, Any]](EventFixture.EVENT_3), 0, 0) - val createCertReq = generateRequest(event) - var id: String = null - noException should be thrownBy { - id = new CertificateGeneratorFunction(jobConfig, httpUtil, storageService, cassandraUtil).callCertificateRc(jobConfig.rcCreateApi, null, createCertReq) - } - assert(id != null) - } - - "Certificate rc create api call for for empty request " should " throw serverException and returns null" in { - val createCertReq = Map[String, AnyRef]() - var id: String = null - an [ServerException] should be thrownBy { - id = new CertificateGeneratorFunction(jobConfig, httpUtil, storageService, cassandraUtil).callCertificateRc(jobConfig.rcCreateApi, null, createCertReq) - } - assert(id == null) - }*/ - - "Certificate rc delete api call for for missing id " should " throw server Exception " in { - when(mockHttpUtil.delete(jobConfig.rcBaseUrl + "/" + jobConfig.rcEntity + "/" +"missingId")).thenReturn(HTTPResponse(500, """{}""")) - an [ServerException] should be thrownBy { - new CertificateGeneratorFunction(jobConfig, mockHttpUtil, storageService, cassandraUtil).callCertificateRc(jobConfig.rcDeleteApi, "missingId", null) - } - } - - "Certificate old registry delete api call for for missing id " should " not throw serverException " in { - - val query = QueryBuilder.delete().from(jobConfig.sbKeyspace, jobConfig.certRegTable) - .where(QueryBuilder.eq("identifier", "missingId")) - .ifExists - when(mockCassandraUtil.executePreparedStatement(query.toString)).thenReturn(new util.ArrayList[Row]()) - noException should be thrownBy { - new CertificateGeneratorFunction(jobConfig, mockHttpUtil, storageService, mockCassandraUtil).deleteOldRegistry("missingId") - } - } - - "Certificate old registry delete api call with valid id " should " not throw exception " in { - - doNothing().when(mockEsUtil).deleteDocument("validId") - val query = QueryBuilder.delete().from(jobConfig.sbKeyspace, jobConfig.certRegTable) - .where(QueryBuilder.eq("identifier", "validId")) - .ifExists - when(mockCassandraUtil.executePreparedStatement(query.toString)).thenReturn(new util.ArrayList[Row]()) - noException should be thrownBy { - new CertificateGeneratorFunction(jobConfig, mockHttpUtil, storageService, mockCassandraUtil).deleteOldRegistry("validId") - } - } - - "Certificate generation for event with missing oldId in rc and old registry " should " not throw server exception while re issuing" in { - val event = new Event(JSONUtil.deserialize[java.util.Map[String, Any]](EventFixture.EVENT_1), 0, 0) - val batchId = event.related.getOrElse(jobConfig.COURSE_ID, "").asInstanceOf[String] - val courseId = event.related.getOrElse(jobConfig.BATCH_ID, "").asInstanceOf[String] - when(mockHttpUtil.delete(jobConfig.rcBaseUrl + "/" + jobConfig.rcEntity + "/" +event.oldId)).thenReturn(HTTPResponse(500, """{}""")) - when(mockCassandraUtil.find("SELECT * FROM sunbird_courses.user_enrolments WHERE userid='"+event.userId+"' AND batchid='"+batchId+"' AND courseid='"+courseId+"';")).thenReturn(new util.ArrayList[Row]()) - an [Exception] should be thrownBy new CertificateGeneratorFunction(jobConfig, mockHttpUtil, storageService, mockCassandraUtil).generateCertificateUsingRC(event, null)(mockMetrics) - - } - - "Certificate generation for event with connection issue on rc " should " not throw unirest exception while re issuing" in { - val event = new Event(JSONUtil.deserialize[java.util.Map[String, Any]](EventFixture.EVENT_1), 0, 0) - when(mockHttpUtil.delete(jobConfig.rcBaseUrl + "/" + jobConfig.rcEntity + "/" +event.oldId)).thenThrow(new UnirestException("")) - an [Exception] should be thrownBy new CertificateGeneratorFunction(jobConfig, mockHttpUtil, storageService, mockCassandraUtil).generateCertificateUsingRC(event, null)(mockMetrics) - - } - - "Certificate rc search api call for publicKey with valid request" should " returns id" in { - val req = Map("filters" -> Map()) - when(mockHttpUtil.post(jobConfig.rcBaseUrl + "/PublicKey/search", ScalaModuleJsonUtils.serialize(req))).thenReturn(HTTPResponse(200, """[{"osUpdatedAt":"2022-03-17T06:43:48.070698Z","osCreatedAt":"2022-03-17T06:43:48.070698Z","osUpdatedBy":"anonymous","osCreatedBy":"anonymous","osid":"1-25a8c96b-b254-4720-bbc9-29b37c3c2bec","value":"keyvalue","alg":"RSA"}]""")) - val id: String = new CertificateGeneratorFunction(jobConfig, mockHttpUtil, storageService, mockCassandraUtil).callCertificateRc(jobConfig.rcSearchApi, null, req) - assert(id != null) -} - - "Certificate rc search api call for publicKey with invalid request" should " throw exception " in { - var id: String = null - val req = Map("key"->"") - when(mockHttpUtil.post(jobConfig.rcBaseUrl + "/PublicKey/search", ScalaModuleJsonUtils.serialize(req))).thenReturn(HTTPResponse(200, """{"id":"sunbird-rc.registry.search","ver":"1.0","ets":1647501412770,"params":{"resmsgid":"","msgid":"1c33747f-c2f5-4c36-89d9-7efdf8b4021a","err":"","status":"UNSUCCESSFUL","errmsg":"filters or queries missing from search request!"},"responseCode":"OK","result":""}p""")) - an [Exception] should be thrownBy new CertificateGeneratorFunction(jobConfig, mockHttpUtil, storageService, mockCassandraUtil).callCertificateRc(jobConfig.rcSearchApi, null, req) - } - - //Functional test case for search service -/* "Certificate rc search api call for publicKey with invalid request" should " returns id" in { - val req = Map("filters" -> Map()) - val id: String = new CertificateGeneratorFunction(jobConfig, httpUtil, storageService, mockCassandraUtil).callCertificateRc(jobConfig.rcSearchApi, null, req) - assert(id != null) - }*/ -} diff --git a/credential-generator/collection-certificate-generator/src/test/scala/org/sunbird/job/certgen/spec/CreateUserFeedFunctionTest.scala b/credential-generator/collection-certificate-generator/src/test/scala/org/sunbird/job/certgen/spec/CreateUserFeedFunctionTest.scala deleted file mode 100755 index 3e8a946a1..000000000 --- a/credential-generator/collection-certificate-generator/src/test/scala/org/sunbird/job/certgen/spec/CreateUserFeedFunctionTest.scala +++ /dev/null @@ -1,52 +0,0 @@ -package org.sunbird.job.certgen.spec - -import java.util.Date -import java.util.concurrent.ConcurrentHashMap -import java.util.concurrent.atomic.AtomicLong - -import com.typesafe.config.{Config, ConfigFactory} -import org.apache.flink.api.common.typeinfo.TypeInformation -import org.apache.flink.api.java.typeutils.TypeExtractor -import org.mockito.ArgumentMatchers.any -import org.mockito.Mockito -import org.mockito.Mockito.when -import org.sunbird.job.Metrics -import org.sunbird.job.certgen.functions.{CreateUserFeedFunction, UserFeedMetaData} -import org.sunbird.job.certgen.task.CertificateGeneratorConfig -import org.sunbird.job.util.{HTTPResponse, HttpUtil} -import org.sunbird.spec.{BaseMetricsReporter, BaseTestSpec} - -class CreateUserFeedFunctionTest extends BaseTestSpec { - - implicit val stringTypeInfo: TypeInformation[String] = TypeExtractor.getForClass(classOf[String]) - val config: Config = ConfigFactory.load("test.conf") - val userFeedConfig: CertificateGeneratorConfig = new CertificateGeneratorConfig(config) - val mockHttpUtil: HttpUtil = mock[HttpUtil](Mockito.withSettings().serializable()) - val metrics = Metrics(new ConcurrentHashMap[String, AtomicLong]() { - { - put(userFeedConfig.userFeedCount, new AtomicLong()) - } - }) - - - override protected def beforeAll(): Unit = { - super.beforeAll() - // Clear the metrics - - BaseMetricsReporter.gaugeMetrics.clear() - when(mockHttpUtil.post(any[String], any[String], any[Map[String, String]]())).thenReturn(HTTPResponse(200, """{"id":"api.user.feed.create","ver":"v1","ts":"2020-10-30 13:20:54:940+0000","params":{"resmsgid":null,"msgid":"518d3404-cf1f-4001-81a5-0c58647b32fe","err":null,"status":"success","errmsg":null},"responseCode":"OK","result":{"response":"SUCCESS"}}""")) - } - - - override protected def afterAll(): Unit = { - super.afterAll() - - } - - "UserFeedFunction " should "should send user feed" in { - implicit val userFeedMetaTypeInfo: TypeInformation[UserFeedMetaData] = TypeExtractor.getForClass(classOf[UserFeedMetaData]) - new CreateUserFeedFunction(userFeedConfig, mockHttpUtil).processElement(UserFeedMetaData("userId", "CourseName", new Date(), "courseId", 0, 0), null, metrics) - metrics.get(s"${userFeedConfig.userFeedCount}") should be(1) - - } -} diff --git a/credential-generator/collection-certificate-generator/src/test/scala/org/sunbird/job/certgen/spec/NotifierFunctionTest.scala b/credential-generator/collection-certificate-generator/src/test/scala/org/sunbird/job/certgen/spec/NotifierFunctionTest.scala deleted file mode 100755 index 2cd445226..000000000 --- a/credential-generator/collection-certificate-generator/src/test/scala/org/sunbird/job/certgen/spec/NotifierFunctionTest.scala +++ /dev/null @@ -1,73 +0,0 @@ -package org.sunbird.job.certgen.spec - -import java.util.Date -import java.util.concurrent.ConcurrentHashMap -import java.util.concurrent.atomic.AtomicLong - -import com.typesafe.config.{Config, ConfigFactory} -import org.apache.flink.api.common.typeinfo.TypeInformation -import org.apache.flink.api.java.typeutils.TypeExtractor -import org.cassandraunit.CQLDataLoader -import org.cassandraunit.dataset.cql.FileCQLDataSet -import org.cassandraunit.utils.EmbeddedCassandraServerHelper -import org.mockito.{ArgumentMatchers, Mockito} -import org.mockito.Mockito._ -import org.sunbird.job.util.{CassandraUtil, HTTPResponse, HttpUtil} -import org.sunbird.spec.{BaseMetricsReporter, BaseTestSpec} -import org.mockito.ArgumentMatchers.{any, endsWith} -import org.sunbird.job.Metrics -import org.sunbird.job.certgen.functions.{NotificationMetaData, NotifierFunction} -import org.sunbird.job.certgen.task.CertificateGeneratorConfig - - -class NotifierFunctionTest extends BaseTestSpec { - implicit val stringTypeInfo: TypeInformation[String] = TypeExtractor.getForClass(classOf[String]) - var cassandraUtil: CassandraUtil = _ - val config: Config = ConfigFactory.load("test.conf") - val notifierConfig: CertificateGeneratorConfig = new CertificateGeneratorConfig(config) - val mockHttpUtil: HttpUtil = mock[HttpUtil](Mockito.withSettings().serializable()) - val metrics = Metrics(new ConcurrentHashMap[String, AtomicLong]() { - { - put(notifierConfig.courseBatchdbReadCount, new AtomicLong()) - put(notifierConfig.skipNotifyUserCount, new AtomicLong()) - put(notifierConfig.notifiedUserCount, new AtomicLong()) - } - }) - - - override protected def beforeAll(): Unit = { - super.beforeAll() - EmbeddedCassandraServerHelper.startEmbeddedCassandra(80000L) - cassandraUtil = new CassandraUtil(notifierConfig.dbHost, notifierConfig.dbPort) - val session = cassandraUtil.session - - session.execute(s"DROP KEYSPACE IF EXISTS ${notifierConfig.dbKeyspace}") - val dataLoader = new CQLDataLoader(session) - dataLoader.load(new FileCQLDataSet(getClass.getResource("/test.cql").getPath, true, true)) - // Clear the metrics - - BaseMetricsReporter.gaugeMetrics.clear() - when(mockHttpUtil.get(ArgumentMatchers.contains("/private/user/v1/read/"), any[Map[String, String]]())).thenReturn(HTTPResponse(200, """{"id":"","ver":"private","ts":"2020-10-21 14:10:49:964+0000","params":{"resmsgid":null,"msgid":"a6f3e248-c504-4c2f-9bfa-90f54abd2e30","err":null,"status":"success","errmsg":null},"responseCode":"OK","result":{"response":{"firstName":"test12","lastName":"A","maskedPhone":"******0183","rootOrgName":"ORG_002","userName":"teast123","rootOrgId":"01246944855007232011"}}}""")) - when(mockHttpUtil.post(endsWith("/v2/notification"), any[String], any[Map[String, String]]())).thenReturn(HTTPResponse(200, """{"id":"api.notification","ver":"v2","ts":"2020-10-21 14:12:09:065+0000","params":{"resmsgid":null,"msgid":"0df38787-1168-4ae0-aa4b-dcea23ea81e4","err":null,"status":"success","errmsg":null},"responseCode":"OK","result":{"response":"SUCCESS"}}""")) - } - - - override protected def afterAll(): Unit = { - super.afterAll() - try { - EmbeddedCassandraServerHelper.cleanEmbeddedCassandra() - } catch { - case ex: Exception => { - } - } - } - - "NotifierFunction " should "should send notify user" in { - implicit val notificationMetaTypeInfo: TypeInformation[NotificationMetaData] = TypeExtractor.getForClass(classOf[NotificationMetaData]) - new NotifierFunction(notifierConfig, mockHttpUtil,cassandraUtil).processElement(NotificationMetaData("userId", "Course Name", new Date(), "do_11309999837886054415", "0131000245281587206", "template_01_dev_001",0, 0), null, metrics) - metrics.get(s"${notifierConfig.courseBatchdbReadCount}") should be(1) - metrics.get(s"${notifierConfig.notifiedUserCount}") should be(1) - metrics.get(s"${notifierConfig.skipNotifyUserCount}") should be(0) - } -} - diff --git a/credential-generator/pom.xml b/credential-generator/pom.xml deleted file mode 100644 index 2c7fadf4a..000000000 --- a/credential-generator/pom.xml +++ /dev/null @@ -1,47 +0,0 @@ - - - org.sunbird - knowledge-platform-jobs - 1.0 - - 4.0.0 - - credential-generator - pom - credential-generator - - - collection-cert-pre-processor - certificate-processor - collection-certificate-generator - - - - - - - org.apache.maven.plugins - maven-compiler-plugin - 3.8.1 - - 11 - 11 - - - - org.scoverage - scoverage-maven-plugin - ${scoverage.plugin.version} - - ${scala.version} - true - true - org.sunbird.incredible - - - - - - - - diff --git a/activity-aggregate-updater/pom.xml b/csp-migrator/pom.xml similarity index 85% rename from activity-aggregate-updater/pom.xml rename to csp-migrator/pom.xml index d501f1ca4..e24493acc 100644 --- a/activity-aggregate-updater/pom.xml +++ b/csp-migrator/pom.xml @@ -9,12 +9,12 @@ knowledge-platform-jobs 1.0 - activity-aggregate-updater + csp-migrator 1.0.0 jar - activity-aggregate-updater + csp-migrator - Course Progress Computation + CSP Migrator Flink Job @@ -39,11 +39,6 @@ joda-time 2.10.6 - - com.twitter - storehaus-cache_${scala.version} - 0.15.0 - org.sunbird jobs-core @@ -64,12 +59,6 @@ test tests - - it.ozimov - embedded-redis - 0.7.1 - test - org.apache.flink flink-streaming-java_${scala.version} @@ -89,12 +78,6 @@ 3.3.3 test - - com.fiftyonred - mock-jedis - 0.4.0 - test - org.cassandraunit cassandra-unit @@ -102,22 +85,43 @@ test - it.ozimov - embedded-redis - 0.7.1 - test + com.google.apis + google-api-services-youtube + v3-rev182-1.22.0 com.google.guava - guava + guava-jdk5 + + com.google.api-client + google-api-client + 1.20.0 + + + com.google.http-client + google-http-client + 1.20.0 + compile + + + com.google.apis + google-api-services-drive + v3-rev83-1.20.0 + + + com.google.oauth-client + google-oauth-client-jetty + 1.20.0 + + - src/main/scala - src/test/scala + src/main/scala + src/test/scala org.apache.maven.plugins @@ -127,7 +131,6 @@ 11 - org.apache.maven.plugins maven-shade-plugin @@ -158,9 +161,10 @@ + - org.sunbird.job.aggregate.task.ActivityAggregateUpdaterStreamTask + org.sunbird.job.cspmigrator.task.CSPMigratorStreamTask ${project.build.directory}/surefire-reports . - dp-duplication-testsuite.txt + csp-migrator-testsuite.txt diff --git a/csp-migrator/src/main/resources/csp-migrator.conf b/csp-migrator/src/main/resources/csp-migrator.conf new file mode 100644 index 000000000..dfed1c541 --- /dev/null +++ b/csp-migrator/src/main/resources/csp-migrator.conf @@ -0,0 +1,98 @@ +include "base-config.conf" + +job { + env = "sunbirddev" +} + +kafka { + input.topic = "sunbirddev.csp.migration.job.request" + failed.topic = "sunbirddev.csp.migration.job.request.failed" + groupId = "sunbirddev-csp-migrator-group" + live_video_stream.topic = "sunbirddev.live.video.stream.request" + live_content_node_republish.topic = "sunbirddev.republish.job.request" + live_question_node_republish.topic = "sunbirddev.assessment.republish.request" +} + +task { + consumer.parallelism = 1 + parallelism = 1 + cassandra-migrator.parallelism = 1 + checkpointing.timeout = 4200000 +} + +redis { + database { + relationCache.id = 10 + collectionCache.id = 5 + } +} + +hierarchy { + keyspace = "hierarchy_store" + table = "content_hierarchy" +} + +content { + keyspace = "content_store" + content_table = "content_data" + assessment_table = "question_data" +} + +key_value_strings_to_migrate = { + "https://sunbirdstagingpublic.blob.core.windows.net/sunbird-content-staging": "CLOUD_STORAGE_BASE_PATH" + "https://ekstep-public-dev.s3-ap-south-1.amazonaws.com": "CLOUD_STORAGE_BASE_PATH", + "https://community.ekstep.in/assets/public": "CLOUD_STORAGE_BASE_PATH", + "https://vdn.diksha.gov.in/assets/public": "CLOUD_STORAGE_BASE_PATH" +} + +neo4j_fields_to_migrate = { + "asset": ["artifactUrl","thumbnail"], + "content": ["appIcon","artifactUrl", "posterImage", "previewUrl", "thumbnail", "assetsMap", "certTemplate", "itemSetPreviewUrl", "grayScaleAppIcon", "sourceURL", "downloadUrl", "variants"], + "contentimage": ["appIcon","artifactUrl", "posterImage", "previewUrl", "thumbnail", "assetsMap", "certTemplate", "itemSetPreviewUrl", "grayScaleAppIcon", "sourceURL", "downloadUrl", "variants"], + "collection": ["appIcon","artifactUrl", "posterImage", "previewUrl", "thumbnail", "toc_url", "grayScaleAppIcon","variants"], + "collectionimage": ["appIcon","artifactUrl", "posterImage", "previewUrl", "thumbnail", "toc_url", "grayScaleAppIcon","variants"], + "plugins": ["artifactUrl"], + "itemset": ["previewUrl"], + "assessmentitem": ["data", "question", "solutions", "editorState", "media"], + "question": ["appIcon","artifactUrl", "posterImage", "previewUrl","downloadUrl", "variants","pdfUrl"], + "questionimage": ["appIcon","artifactUrl", "posterImage", "previewUrl","downloadUrl", "variants","pdfUrl"], + "questionset": ["appIcon","artifactUrl", "posterImage", "previewUrl","downloadUrl", "variants","pdfUrl"], + "questionsetimage": ["appIcon","artifactUrl", "posterImage", "previewUrl","downloadUrl", "variants","pdfUrl"] +} + +cassandra_fields_to_migrate = { + "assessmentitem": ["body", "editorState", "answer", "solutions", "instructions", "media"] +} + +cloud_storage { + folder { + content = "content" + artifact = "artifact" + } +} + +migrationVersion = 1 + +video_stream_regeneration_enable = false +live_node_republish_enable = false +copy_missing_files_to_cloud = false + +download_path = /tmp + +cloudstorage.metadata.replace_absolute_path=false +cloudstorage.relative_path_prefix= "CONTENT_STORAGE_BASE_PATH" +cloudstorage.read_base_path="https://sunbirddev.blob.core.windows.net" +cloudstorage.write_base_path=["https://sunbirddev.blob.core.windows.net","https://obj.dev.sunbird.org"] +cloudstorage.metadata.list=["appIcon","posterImage","artifactUrl","downloadUrl","variants","previewUrl","pdfUrl", "streamingUrl", "toc_url"] + +cloud_storage_type="" +cloud_storage_key="" +cloud_storage_secret="" +cloud_storage_container="" +cloud_storage_endpoint="" + +questionset.hierarchy.keyspace="hierarchy_store" +questionset.hierarchy.table="questionset_hierarchy" + +gdrive.application_name="{{ gdrive_application_name }}" +g_service_acct_cred="{{ auto_creator_g_service_acct_cred }}" diff --git a/relation-cache-updater/src/main/resources/log4j.properties b/csp-migrator/src/main/resources/log4j.properties similarity index 91% rename from relation-cache-updater/src/main/resources/log4j.properties rename to csp-migrator/src/main/resources/log4j.properties index 37a85c438..d86b28fce 100644 --- a/relation-cache-updater/src/main/resources/log4j.properties +++ b/csp-migrator/src/main/resources/log4j.properties @@ -1,6 +1,6 @@ # log4j.appender.file=org.apache.log4j.FileAppender log4j.appender.file=org.apache.log4j.RollingFileAppender -log4j.appender.file.file=relation-cache-updater.log +log4j.appender.file.file=csp-migrator.log log4j.appender.file.append=true log4j.appender.file.layout=org.apache.log4j.PatternLayout log4j.appender.file.MaxFileSize=256KB diff --git a/csp-migrator/src/main/scala/org/sunbird/job/cspmigrator/domain/Event.scala b/csp-migrator/src/main/scala/org/sunbird/job/cspmigrator/domain/Event.scala new file mode 100644 index 000000000..04576c5cc --- /dev/null +++ b/csp-migrator/src/main/scala/org/sunbird/job/cspmigrator/domain/Event.scala @@ -0,0 +1,37 @@ +package org.sunbird.job.cspmigrator.domain + +import org.apache.commons.lang3.StringUtils +import org.sunbird.job.cspmigrator.task.CSPMigratorConfig +import org.sunbird.job.domain.reader.JobRequest + +class Event(eventMap: java.util.Map[String, Any], partition: Int, offset: Long) extends JobRequest(eventMap, partition, offset) { + + val jobName = "csp-migrator" + + def eData: Map[String, AnyRef] = readOrDefault("edata", Map()).asInstanceOf[Map[String, AnyRef]] + + def context: Map[String, AnyRef] = readOrDefault("context", Map()).asInstanceOf[Map[String, AnyRef]] + + def obj: Map[String, AnyRef] = readOrDefault("object", Map()).asInstanceOf[Map[String, AnyRef]] + + def channel: String = readOrDefault[String]("context.channel", "") + + def metadata: Map[String, AnyRef] = readOrDefault("edata.metadata", Map()) + + def action: String = readOrDefault[String]("edata.action", "") + + def mimeType: String = readOrDefault[String]("edata.metadata.mimeType", "") + + def status: String = readOrDefault[String]("edata.metadata.status", "") + + def objectType: String = readOrDefault[String]("edata.metadata.objectType", "") + + def identifier: String = readOrDefault[String]("object.id", "") + + def currentIteration: Int = readOrDefault[Int]("edata.iteration", 1) + + def isValid(objMetadata: Map[String, AnyRef], config: CSPMigratorConfig): Boolean = { + (StringUtils.equals("csp-migration", action) && StringUtils.isNotBlank(identifier)) && StringUtils.isNotBlank(status) + } + +} \ No newline at end of file diff --git a/csp-migrator/src/main/scala/org/sunbird/job/cspmigrator/functions/CSPCassandraMigratorFunction.scala b/csp-migrator/src/main/scala/org/sunbird/job/cspmigrator/functions/CSPCassandraMigratorFunction.scala new file mode 100644 index 000000000..9edfcf4db --- /dev/null +++ b/csp-migrator/src/main/scala/org/sunbird/job/cspmigrator/functions/CSPCassandraMigratorFunction.scala @@ -0,0 +1,110 @@ +package org.sunbird.job.cspmigrator.functions + +import org.apache.commons.lang3.StringUtils +import org.apache.flink.api.common.typeinfo.TypeInformation +import org.apache.flink.configuration.Configuration +import org.apache.flink.streaming.api.functions.ProcessFunction +import org.slf4j.LoggerFactory +import org.sunbird.job.cspmigrator.domain.Event +import org.sunbird.job.cspmigrator.helpers.CSPCassandraMigrator +import org.sunbird.job.cspmigrator.task.CSPMigratorConfig +import org.sunbird.job.domain.`object`.DefinitionCache +import org.sunbird.job.exception.ServerException +import org.sunbird.job.helper.FailedEventHelper +import org.sunbird.job.util._ +import org.sunbird.job.{BaseProcessFunction, Metrics} + +import scala.collection.JavaConverters._ + +import java.util + +class CSPCassandraMigratorFunction(config: CSPMigratorConfig, httpUtil: HttpUtil, + @transient var neo4JUtil: Neo4JUtil = null, + @transient var cassandraUtil: CassandraUtil = null, + @transient var cloudStorageUtil: CloudStorageUtil = null) + (implicit mapTypeInfo: TypeInformation[util.Map[String, AnyRef]], stringTypeInfo: TypeInformation[String]) + extends BaseProcessFunction[Event, String](config) with CSPCassandraMigrator with FailedEventHelper { + + private[this] lazy val logger = LoggerFactory.getLogger(classOf[CSPCassandraMigratorFunction]) + lazy val defCache: DefinitionCache = new DefinitionCache() + + override def metricsList(): List[String] = { + List(config.totalEventsCount, config.successEventCount, config.failedEventCount, config.skippedEventCount, config.errorEventCount, config.assetVideoStreamCount, config.liveContentNodePublishCount, config.liveQuestionNodePublishCount, config.liveQuestionSetNodePublishCount) + } + + override def open(parameters: Configuration): Unit = { + super.open(parameters) + neo4JUtil = new Neo4JUtil(config.graphRoutePath, config.graphName, config) + cassandraUtil = new CassandraUtil(config.cassandraHost, config.cassandraPort, config) + cloudStorageUtil = new CloudStorageUtil(config) + } + + override def close(): Unit = { + super.close() + } + + override def processElement(event: Event, + context: ProcessFunction[Event, String]#Context, + metrics: Metrics): Unit = { + metrics.incCounter(config.totalEventsCount) + logger.info("CSPCassandraMigratorFunction::processElement:: event context : " + event.context) + logger.info("CSPCassandraMigratorFunction::processElement:: event edata : " + event.eData) + + val objMetadata: Map[String, AnyRef] = getMetadata(event.identifier)(neo4JUtil) + val identifier: String = objMetadata.getOrElse("identifier", "").asInstanceOf[String] + + try { + val fieldsToMigrate: List[String] = if (config.getConfig.hasPath("neo4j_fields_to_migrate."+event.objectType.toLowerCase())) config.getConfig.getStringList("neo4j_fields_to_migrate."+event.objectType.toLowerCase()).asScala.toList + else throw new ServerException("ERR_CONFIG_NOT_FOUND", "Fields to migrate configuration not found for objectType: " + event.objectType) + val migratedMetadataFields: Map[String, String] = fieldsToMigrate.flatMap(migrateField => { + if(objMetadata.contains(migrateField)) { + val metadataFieldValue = objMetadata.getOrElse(migrateField, "").asInstanceOf[String] + + val tempMetadataFieldValue = handleExternalURLS(metadataFieldValue, identifier, config, httpUtil, cloudStorageUtil) + + val migrateValue: String = if (StringUtils.isNotBlank(tempMetadataFieldValue)) + StringUtils.replaceEach(tempMetadataFieldValue, config.keyValueMigrateStrings.keySet().toArray().map(_.asInstanceOf[String]), config.keyValueMigrateStrings.values().toArray().map(_.asInstanceOf[String])) + else null + if (config.copyMissingFiles && StringUtils.isNotBlank(migrateValue)) verifyFile(event.identifier, tempMetadataFieldValue, migrateValue, migrateField, config)(httpUtil, cloudStorageUtil) + + Map(migrateField -> migrateValue) + } else Map.empty[String, String] + }).filter(record => record._1.nonEmpty).toMap[String, String] + + val migratedObjMetadata = objMetadata ++ migratedMetadataFields + logger.info("CSPCassandraMigratorFunction::processElement:: migratedObjMetadata : " + migratedObjMetadata) + + updateNeo4j(migratedObjMetadata, event)(defCache, neo4JUtil, config) + + process(migratedObjMetadata, config, httpUtil, cassandraUtil, cloudStorageUtil) + + event.objectType match { + case "Content" | "Collection" => + finalizeMigration(migratedObjMetadata, event, metrics, config)(defCache, neo4JUtil) + if(config.liveNodeRepublishEnabled && (event.status.equalsIgnoreCase("Live") || + event.status.equalsIgnoreCase("Unlisted"))) { + pushLiveNodePublishEvent(objMetadata, context, metrics, config, config.liveCollectionNodePublishEventOutTag) + metrics.incCounter(config.liveContentNodePublishCount) + } + case "QuestionSet"| "QuestionSetImage" => + finalizeMigration(migratedObjMetadata, event, metrics, config)(defCache, neo4JUtil) + if(config.liveNodeRepublishEnabled && (event.status.equalsIgnoreCase("Live") || + event.status.equalsIgnoreCase("Unlisted"))) { + pushQuestionPublishEvent(migratedObjMetadata, context, metrics, config, config.liveQuestionSetNodePublishEventOutTag, config.liveQuestionSetNodePublishCount) + } + case _ => finalizeMigration(migratedObjMetadata, event, metrics, config)(defCache, neo4JUtil) + } + } catch { + case se: Exception => + logger.error("CSPCassandraMigratorFunction :: Message processing failed for mid : " + event.mid() + " || " + event , se) + logger.error("CSPCassandraMigratorFunction :: Error while migrating content :: " + se.getMessage) + metrics.incCounter(config.failedEventCount) + se.printStackTrace() + logger.info(s"""{ identifier: \"${objMetadata.getOrElse("identifier", "").asInstanceOf[String]}\", mimetype: \"${objMetadata.getOrElse("mimeType", "").asInstanceOf[String]}\", status: \"Failed\", stage: \"Static Migration\"}""") + // Insert into neo4j with migrationVersion as 0.1 + updateNeo4j(objMetadata + ("migrationVersion" -> 0.1.asInstanceOf[Number]), event)(defCache, neo4JUtil, config) + } + } + + +} diff --git a/csp-migrator/src/main/scala/org/sunbird/job/cspmigrator/functions/CSPNeo4jMigratorFunction.scala b/csp-migrator/src/main/scala/org/sunbird/job/cspmigrator/functions/CSPNeo4jMigratorFunction.scala new file mode 100644 index 000000000..840f4e8b2 --- /dev/null +++ b/csp-migrator/src/main/scala/org/sunbird/job/cspmigrator/functions/CSPNeo4jMigratorFunction.scala @@ -0,0 +1,124 @@ +package org.sunbird.job.cspmigrator.functions + +import org.apache.flink.api.common.typeinfo.TypeInformation +import org.apache.flink.configuration.Configuration +import org.apache.flink.streaming.api.functions.ProcessFunction +import org.slf4j.LoggerFactory +import org.sunbird.job.cspmigrator.domain.Event +import org.sunbird.job.cspmigrator.helpers.CSPNeo4jMigrator +import org.sunbird.job.cspmigrator.task.CSPMigratorConfig +import org.sunbird.job.domain.`object`.DefinitionCache +import org.sunbird.job.helper.FailedEventHelper +import org.sunbird.job.util._ +import org.sunbird.job.{BaseProcessFunction, Metrics} + +import java.util + +class CSPNeo4jMigratorFunction(config: CSPMigratorConfig, httpUtil: HttpUtil, + @transient var neo4JUtil: Neo4JUtil = null, + @transient var cassandraUtil: CassandraUtil = null, + @transient var cloudStorageUtil: CloudStorageUtil = null) + (implicit mapTypeInfo: TypeInformation[util.Map[String, AnyRef]], stringTypeInfo: TypeInformation[String]) + extends BaseProcessFunction[Event, String](config) with CSPNeo4jMigrator with FailedEventHelper { + + private[this] lazy val logger = LoggerFactory.getLogger(classOf[CSPNeo4jMigratorFunction]) + lazy val defCache: DefinitionCache = new DefinitionCache() + + override def metricsList(): List[String] = { + List(config.totalEventsCount, config.successEventCount, config.failedEventCount, config.skippedEventCount, config.errorEventCount, config.assetVideoStreamCount, config.liveContentNodePublishCount, config.liveQuestionNodePublishCount, config.liveQuestionSetNodePublishCount) + } + + override def open(parameters: Configuration): Unit = { + super.open(parameters) + neo4JUtil = new Neo4JUtil(config.graphRoutePath, config.graphName, config) + cassandraUtil = new CassandraUtil(config.cassandraHost, config.cassandraPort, config) + cloudStorageUtil = new CloudStorageUtil(config) + } + + override def close(): Unit = { + super.close() + } + + override def processElement(event: Event, + context: ProcessFunction[Event, String]#Context, + metrics: Metrics): Unit = { + metrics.incCounter(config.totalEventsCount) + logger.info("CSPNeo4jMigratorFunction::processElement:: event context : " + event.context) + logger.info("CSPNeo4jMigratorFunction::processElement:: event edata : " + event.eData) + + val objMetadata: Map[String, AnyRef] = getMetadata(event.identifier)(neo4JUtil) + logger.info("CSPNeo4jMigratorFunction::processElement:: objMetadata : " + objMetadata) + try { + if (event.isValid(objMetadata, config)) { + val migratedMetadataFields = process(objMetadata, config, httpUtil, cloudStorageUtil) + + val migratedMap = objMetadata ++ migratedMetadataFields + + event.objectType match { + case "Asset" => + event.mimeType.toLowerCase match { + case "video/mp4" | "video/webm" => + finalizeMigration(migratedMap, event, metrics, config)(defCache, neo4JUtil) + if (config.videStreamRegenerationEnabled && event.status.equalsIgnoreCase("Live")) { + logger.info("CSPNeo4jMigratorFunction :: Sending Asset For streaming URL generation: " + event.identifier) + pushStreamingUrlEvent(migratedMap, context, config)(metrics) + metrics.incCounter(config.assetVideoStreamCount) + } + case _ => finalizeMigration(migratedMap, event, metrics, config)(defCache, neo4JUtil) + } + case "Content" | "ContentImage" | "Collection" | "CollectionImage" => + event.mimeType.toLowerCase match { + case "application/vnd.ekstep.ecml-archive" | "application/vnd.ekstep.content-collection" => + updateNeo4j(migratedMap, event)(defCache, neo4JUtil, config) + logger.info("CSPNeo4jMigratorFunction :: Sending Content/Collection For cassandra migration: " + event.identifier) + context.output(config.cassandraMigrationOutputTag, event) + case _ => + finalizeMigration(migratedMap, event, metrics, config)(defCache, neo4JUtil) + if(config.liveNodeRepublishEnabled && (event.status.equalsIgnoreCase("Live") || + event.status.equalsIgnoreCase("Unlisted"))) { + pushLiveNodePublishEvent(objMetadata, context, metrics, config, config.liveContentNodePublishEventOutTag) + metrics.incCounter(config.liveContentNodePublishCount) + } + } + case "AssessmentItem" => { + updateNeo4j(migratedMap, event)(defCache, neo4JUtil, config) + logger.info("CSPNeo4jMigratorFunction :: Sending AssessmentItem For cassandra migration: " + event.identifier) + context.output(config.cassandraMigrationOutputTag, event) + } + case "Question" | "QuestionImage" => { + finalizeMigration(migratedMap, event, metrics, config)(defCache, neo4JUtil) + if(config.liveNodeRepublishEnabled && (event.status.equalsIgnoreCase("Live") || + event.status.equalsIgnoreCase("Unlisted"))) { + pushQuestionPublishEvent(objMetadata, context, metrics, config, config.liveQuestionNodePublishEventOutTag, config.liveQuestionNodePublishCount) + } + } + case "QuestionSet" | "QuestionSetImage" => { + updateNeo4j(migratedMap, event)(defCache, neo4JUtil, config) + logger.info("CSPNeo4jMigratorFunction :: Sending QuestionSet/QuestionSetImage For cassandra migration: " + event.identifier) + context.output(config.cassandraMigrationOutputTag, event) + } + case _ => finalizeMigration(migratedMap, event, metrics, config)(defCache, neo4JUtil) + } + } else { + if(!objMetadata.contains("migrationVersion")) { + // Insert into neo4j with migrationVersion as 0.5 for skipped events for easy identification + updateNeo4j(objMetadata + ("migrationVersion" -> 0.5.asInstanceOf[Number]), event)(defCache, neo4JUtil, config) + } + logger.info("CSPNeo4jMigratorFunction::processElement:: Event is not qualified for csp migration having identifier : " + event.identifier + " | objectType : " + event.objectType) + metrics.incCounter(config.skippedEventCount) + } + } catch { + case se: Exception => + logger.error("CSPNeo4jMigratorFunction :: Message processing failed for mid : " + event.mid() + " || " + event , se) + logger.error("CSPNeo4jMigratorFunction :: Error while migrating content :: " + se.getMessage) + metrics.incCounter(config.failedEventCount) + se.printStackTrace() + logger.info(s"""{ identifier: \"${objMetadata.getOrElse("identifier", "").asInstanceOf[String]}\", mimetype: \"${objMetadata.getOrElse("mimeType", "").asInstanceOf[String]}\", status: \"Failed\", stage: \"Static Migration\"}""") + // Insert into neo4j with migrationVersion as 0.1 for failed scenarios + updateNeo4j(objMetadata + ("migrationVersion" -> 0.1.asInstanceOf[Number]), event)(defCache, neo4JUtil, config) + + } + } + + +} diff --git a/csp-migrator/src/main/scala/org/sunbird/job/cspmigrator/helpers/CSPCassandraMigrator.scala b/csp-migrator/src/main/scala/org/sunbird/job/cspmigrator/helpers/CSPCassandraMigrator.scala new file mode 100644 index 000000000..c02b1b35b --- /dev/null +++ b/csp-migrator/src/main/scala/org/sunbird/job/cspmigrator/helpers/CSPCassandraMigrator.scala @@ -0,0 +1,55 @@ +package org.sunbird.job.cspmigrator.helpers + +import com.datastax.driver.core.Row +import org.apache.commons.lang3.StringUtils +import org.slf4j.LoggerFactory +import org.sunbird.job.cspmigrator.task.CSPMigratorConfig +import org.sunbird.job.util._ + +import scala.collection.JavaConverters._ + +trait CSPCassandraMigrator extends MigrationObjectReader with MigrationObjectUpdater{ + + private[this] val logger = LoggerFactory.getLogger(classOf[CSPCassandraMigrator]) + + def process(objMetadata: Map[String, AnyRef], config: CSPMigratorConfig, httpUtil: HttpUtil, cassandraUtil: CassandraUtil, cloudStorageUtil: CloudStorageUtil): Unit = { + + val objectType: String = objMetadata.getOrElse("objectType","").asInstanceOf[String] + val identifier: String = objMetadata.getOrElse("identifier", "").asInstanceOf[String] + + objectType match { + case "AssessmentItem" => + val row: Row = getAssessmentItemData(identifier, config)(cassandraUtil) + val extProps = config.getConfig.getStringList("cassandra_fields_to_migrate.assessmentitem").asScala.toList + val data: Map[String, String] = if (null != row) extProps.map(prop => prop -> row.getString(prop.toLowerCase())).toMap.filter(p => StringUtils.isNotBlank(p._2)) else Map[String, String]() + logger.info(s"""CSPCassandraMigrator:: process:: $identifier - $objectType :: Fetched Cassandra data:: $data""") + val migrateData = data.flatMap(rec => { + Map(rec._1 -> StringUtils.replaceEach(rec._2, config.keyValueMigrateStrings.keySet().toArray().map(_.asInstanceOf[String]), config.keyValueMigrateStrings.values().toArray().map(_.asInstanceOf[String]))) + }) + updateAssessmentItemData(identifier, migrateData, config)(cassandraUtil) + logger.info(s"""CSPCassandraMigrator:: process:: $identifier - $objectType :: Migrated Cassandra data:: $migrateData""") + case "Content" | "ContentImage" => + val ecmlBody: String = getContentBody(identifier, config)(cassandraUtil) + logger.info(s"""CSPCassandraMigrator:: process:: $identifier - $objectType :: ECML Fetched body:: $ecmlBody""") + val migratedECMLBody: String = extractAndValidateUrls(identifier, ecmlBody, config, httpUtil, cloudStorageUtil) + updateContentBody(identifier, migratedECMLBody, config)(cassandraUtil) + logger.info(s"""CSPCassandraMigrator:: process:: $identifier - $objectType :: ECML Migrated body:: $migratedECMLBody""") + case "Collection" | "CollectionImage" => + val collectionHierarchy: String = getCollectionHierarchy(identifier, config)(cassandraUtil) + logger.info(s"""CSPCassandraMigrator:: process:: $identifier - $objectType :: Fetched Hierarchy:: $collectionHierarchy""") + if(collectionHierarchy != null && collectionHierarchy.nonEmpty) { + val migratedCollectionHierarchy: String = extractAndValidateUrls(identifier, collectionHierarchy, config, httpUtil, cloudStorageUtil, false) + updateCollectionHierarchy(identifier, migratedCollectionHierarchy, config)(cassandraUtil) + logger.info(s"""CSPCassandraMigrator:: process:: $identifier - $objectType :: Migrated Hierarchy:: $migratedCollectionHierarchy""") + } + case "QuestionSet" | "QuestionSetImage" => { + val qsH: String = getQuestionSetHierarchy(identifier, config)(cassandraUtil) + logger.info(s"""CSPCassandraMigrator:: process:: $identifier - $objectType :: Fetched Hierarchy:: $qsH""") + val migratedQsHierarchy: String = extractAndValidateUrls(identifier, qsH, config, httpUtil, cloudStorageUtil) + updateQuestionSetHierarchy(identifier, migratedQsHierarchy, config)(cassandraUtil) + logger.info(s"""CSPCassandraMigrator:: process:: $identifier - $objectType :: Migrated Hierarchy:: $migratedQsHierarchy""") + } + case _ => logger.info(s"""CSPCassandraMigrator:: process:: $identifier - $objectType :: NO CASSANDRA MIGRATION PERFORMED!! """) + } + } +} \ No newline at end of file diff --git a/csp-migrator/src/main/scala/org/sunbird/job/cspmigrator/helpers/CSPNeo4jMigrator.scala b/csp-migrator/src/main/scala/org/sunbird/job/cspmigrator/helpers/CSPNeo4jMigrator.scala new file mode 100644 index 000000000..010887c75 --- /dev/null +++ b/csp-migrator/src/main/scala/org/sunbird/job/cspmigrator/helpers/CSPNeo4jMigrator.scala @@ -0,0 +1,60 @@ +package org.sunbird.job.cspmigrator.helpers + +import org.apache.commons.lang3.StringUtils +import org.slf4j.LoggerFactory +import org.sunbird.job.cspmigrator.task.CSPMigratorConfig +import org.sunbird.job.exception.ServerException +import org.sunbird.job.util.{CloudStorageUtil, HttpUtil} + +import scala.collection.JavaConverters._ + +trait CSPNeo4jMigrator extends MigrationObjectReader with MigrationObjectUpdater { + + private[this] val logger = LoggerFactory.getLogger(classOf[CSPNeo4jMigrator]) + val streamableMimeTypes=List("video/mp4", "video/webm") + val imageAttributes=List("appIcon", "thumbnail", "posterImage") + + def process(objMetadata: Map[String, AnyRef], config: CSPMigratorConfig, httpUtil: HttpUtil, cloudStorageUtil: CloudStorageUtil): Map[String, AnyRef] = { + + // fetch the objectType. + // check if the object has only Draft OR only Live OR live/image if the objectType is content/collection. + // fetch the string replace data from config + // fetch the list of attributes for string replace from config + // check the string of each property and perform string replace. + // commit updated data to DB. + // if objectType is Asset and mimeType is video, trigger streamingUrl generation + // if objectType is content and mimeType is ECML, need to update ECML content body + // if objectType is collection, fetch hierarchy data and update cassandra data with the replaced string + // if objectType is content/collection and the node is Live, trigger the LiveNodePublisher flink job + // if objectType is AssessmentItem, migrate cassandra data as well + // update the migrationVersion of the object + + // validation of the replace URL paths to be done. If not available, Fail migration + // For Migration Failed contents set migrationVersion to 0.1 + // For collection, verify if all childNodes are having live migrated contents - REQUIRED for Draft/Image version? + + val objectType: String = objMetadata.getOrElse("objectType","").asInstanceOf[String] + val mimeType: String = objMetadata.getOrElse("mimeType","").asInstanceOf[String] + val identifier: String = objMetadata.getOrElse("identifier", "").asInstanceOf[String] + val fieldsToMigrate: List[String] = if (config.getConfig.hasPath("neo4j_fields_to_migrate."+objectType.toLowerCase())) config.getConfig.getStringList("neo4j_fields_to_migrate."+objectType.toLowerCase()).asScala.toList + else throw new ServerException("ERR_CONFIG_NOT_FOUND", "Fields to migrate configuration not found for objectType: " + objectType) + + logger.info(s"""CSPNeo4jMigrator:: process:: starting neo4j fields migration for $identifier - $objectType fields:: $fieldsToMigrate""") + val migratedMetadataFields: Map[String, String] = fieldsToMigrate.flatMap(migrateField => { + if(objMetadata.contains(migrateField) && (!migrateField.equalsIgnoreCase("streamingUrl") || !streamableMimeTypes.contains(mimeType)) && (!mimeType.equalsIgnoreCase("video/x-youtube") || imageAttributes.contains(migrateField))) { + val metadataFieldValue = objMetadata.getOrElse(migrateField, "").asInstanceOf[String] + + val tempMetadataFieldValue = handleExternalURLS(metadataFieldValue,identifier, config, httpUtil, cloudStorageUtil) + + val migrateValue: String = if(StringUtils.isNotBlank(tempMetadataFieldValue)) + StringUtils.replaceEach(tempMetadataFieldValue, config.keyValueMigrateStrings.keySet().toArray().map(_.asInstanceOf[String]), config.keyValueMigrateStrings.values().toArray().map(_.asInstanceOf[String])) + else null + if(config.copyMissingFiles && StringUtils.isNotBlank(migrateValue)) verifyFile(identifier, tempMetadataFieldValue, migrateValue, migrateField, config)(httpUtil, cloudStorageUtil) + Map(migrateField -> migrateValue) + } else Map.empty[String, String] + }).filter(record => record._1.nonEmpty).toMap[String, String] + + logger.info(s"""CSPNeo4jMigrator:: process:: $identifier - $objectType migratedMetadataFields:: $migratedMetadataFields""") + migratedMetadataFields + } +} diff --git a/csp-migrator/src/main/scala/org/sunbird/job/cspmigrator/helpers/GoogleDriveUtil.scala b/csp-migrator/src/main/scala/org/sunbird/job/cspmigrator/helpers/GoogleDriveUtil.scala new file mode 100644 index 000000000..41fdabc3a --- /dev/null +++ b/csp-migrator/src/main/scala/org/sunbird/job/cspmigrator/helpers/GoogleDriveUtil.scala @@ -0,0 +1,84 @@ +package org.sunbird.job.cspmigrator.helpers + +import com.google.api.client.googleapis.auth.oauth2.GoogleCredential +import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport +import com.google.api.client.googleapis.json.GoogleJsonResponseException +import com.google.api.client.http.HttpResponseException +import com.google.api.client.json.jackson2.JacksonFactory +import com.google.api.services.drive.{Drive, DriveScopes} +import org.apache.commons.lang.StringUtils +import org.slf4j.LoggerFactory +import org.sunbird.job.BaseJobConfig +import org.sunbird.job.exception.ServerException +import org.sunbird.job.util.Slug + +import java.io.{ByteArrayInputStream, File, FileOutputStream} +import java.nio.charset.Charset +import java.util + + +object GoogleDriveUtil { + private[this] val logger = LoggerFactory.getLogger("GoogleDriveUtil") + + private def initialize(config: BaseJobConfig): Drive = { + val jacksonFactory = new JacksonFactory + val gDriveAppName = config.getString("gdrive.application_name","drive-download-sunbird") + + try { + val httpTransport = GoogleNetHttpTransport.newTrustedTransport + new Drive.Builder(httpTransport, jacksonFactory, getCredentials(config)).setApplicationName(gDriveAppName).build + } catch { + case e: Exception => + logger.error("GoogleDriveUtil:: Error occurred while creating google drive client ::: " + e.getMessage, e) + throw new Exception("Error occurred while creating google drive client ::: " + e.getMessage) + } + } + + @throws[Exception] + private def getCredentials(config: BaseJobConfig): GoogleCredential = { + val scope = util.Arrays.asList(DriveScopes.DRIVE_FILE, DriveScopes.DRIVE, DriveScopes.DRIVE_METADATA) + val gDriveCredentials = config.getString("g_service_acct_cred","") + val credentialsStream = new ByteArrayInputStream(gDriveCredentials.getBytes(Charset.forName("UTF-8"))) + val credential = GoogleCredential.fromStream(credentialsStream).createScoped(scope) + credential + } + + @throws[Exception] + def downloadFile(fileId: String, saveDir: String)(implicit config: BaseJobConfig): File = { + var file: File = null + try { + val drive = initialize(config) + logger.info("GoogleDriveUtil:: downloadFile:: drive: " + drive + " || drive.files: " + drive.files().list()) + val getFile = drive.files.get(fileId) + logger.info("GoogleDriveUtil:: downloadFile:: getFile: " + getFile) + getFile.setFields("id,name,size,owners,mimeType,properties,permissionIds,webContentLink") + logger.info("GoogleDriveUtil:: downloadFile:: getFile.setFields: ") + val googleDriveFile = getFile.execute + logger.info("GoogleDriveUtil :: downloadFile ::: Drive File Details:: " + googleDriveFile) + val fileName = Slug.makeSlug(googleDriveFile.getName) + logger.info("GoogleDriveUtil :: downloadFile ::: Slug fileName :: " + fileName) + val saveFile = new File(saveDir) + if (!saveFile.exists) saveFile.mkdirs + val saveFilePath: String = saveDir + File.separator + fileName + logger.info("GoogleDriveUtil :: downloadFile :: File Id :" + fileId + " | Save File Path: " + saveFilePath) + val outputStream = new FileOutputStream(saveFilePath) + getFile.executeMediaAndDownloadTo(outputStream) + outputStream.close() + file = new File(saveFilePath) + file = Slug.createSlugFile(file) + logger.info("GoogleDriveUtil :: downloadFile :: File Downloaded Successfully. Sluggified File Name: " + file.getAbsolutePath) + return file + } catch { + case ge: GoogleJsonResponseException => logger.error("GoogleDriveUtil :: downloadFile :: GoogleJsonResponseException :: Error Occurred while downloading file having id " + fileId + " | Error is ::" + ge.getDetails.toString, ge) + throw new ServerException("ERR_INVALID_UPLOAD_FILE_URL", "Invalid Response Received From Google API for file Id : " + fileId + " | Error is : " + ge.getDetails.toString) + case he: HttpResponseException => logger.error("GoogleDriveUtil :: downloadFile :: HttpResponseException :: Error Occurred while downloading file having id " + fileId + " | Error is ::" + he.getContent, he) + throw new ServerException("ERR_INVALID_UPLOAD_FILE_URL", "Invalid Response Received From Google API for file Id : " + fileId + " | Error is : " + he.getContent) + case e: Exception => + logger.error("GoogleDriveUtil :: downloadFile :: Exception :: Error Occurred While Downloading Google Drive File having Id " + fileId + " : " + e.getMessage, e) + if (e.isInstanceOf[ServerException]) throw e + else throw new ServerException("ERR_INVALID_UPLOAD_FILE_URL", "Invalid Response Received From Google API for file Id : " + fileId + " | Error is : " + e.getMessage) + } + file + } + +} diff --git a/csp-migrator/src/main/scala/org/sunbird/job/cspmigrator/helpers/MigrationObjectReader.scala b/csp-migrator/src/main/scala/org/sunbird/job/cspmigrator/helpers/MigrationObjectReader.scala new file mode 100644 index 000000000..a0c69b7f8 --- /dev/null +++ b/csp-migrator/src/main/scala/org/sunbird/job/cspmigrator/helpers/MigrationObjectReader.scala @@ -0,0 +1,122 @@ +package org.sunbird.job.cspmigrator.helpers + +import com.datastax.driver.core.Row +import com.datastax.driver.core.querybuilder.{QueryBuilder, Select} +import org.apache.commons.lang3 +import org.apache.flink.streaming.api.functions.ProcessFunction +import org.slf4j.LoggerFactory +import org.sunbird.job.Metrics +import org.sunbird.job.cspmigrator.domain.Event +import org.sunbird.job.cspmigrator.task.CSPMigratorConfig +import org.sunbird.job.util.{CassandraUtil, Neo4JUtil} +import java.util.UUID + +import org.apache.flink.streaming.api.scala.OutputTag + +import scala.collection.JavaConverters._ + +trait MigrationObjectReader { + + private[this] val logger = LoggerFactory.getLogger(classOf[MigrationObjectReader]) + + def getMetadata(identifier: String)(implicit neo4JUtil: Neo4JUtil): Map[String, AnyRef] = { + val metaData = neo4JUtil.getNodeProperties(identifier).asScala.toMap + val id = metaData.getOrElse("IL_UNIQUE_ID", identifier).asInstanceOf[String] + val objType = metaData.getOrElse("IL_FUNC_OBJECT_TYPE", "").asInstanceOf[String] + logger.info("MigrationObjectReader:: getMetadata:: identifier: " + identifier + " with objType: " + objType) + metaData ++ Map[String, AnyRef]("identifier" -> id, "objectType" -> objType) - ("IL_UNIQUE_ID", "IL_FUNC_OBJECT_TYPE", "IL_SYS_NODE_TYPE") + } + + def getContentBody(identifier: String, config: CSPMigratorConfig)(implicit cassandraUtil: CassandraUtil): String = { + // fetch content body from cassandra + val selectId = QueryBuilder.select() + selectId.fcall("blobAsText", QueryBuilder.column("body")).as("body") + val selectWhereId: Select.Where = selectId.from(config.contentKeyspaceName, config.contentTableName).where().and(QueryBuilder.eq("content_id", identifier)) + logger.info("MigrationObjectReader:: getContentBody:: ECML Body Fetch Query :: " + selectWhereId.toString) + val rowId = cassandraUtil.findOne(selectWhereId.toString) + if (null != rowId && null != rowId.getString("body") ) rowId.getString("body") else "" + } + + def getAssessmentItemData(identifier: String, config: CSPMigratorConfig)(implicit cassandraUtil: CassandraUtil): Row = { + logger.info("MigrationObjectReader ::: getAssessmentItemData ::: Reading Question External Data For : " + identifier) + val select = QueryBuilder.select() + val extProps = config.getConfig.getStringList("cassandra_fields_to_migrate.assessmentitem").asScala.toList + extProps.foreach(prop => select.fcall("blobAsText", QueryBuilder.column(prop.toLowerCase())).as(prop.toLowerCase())) + val selectWhere: Select.Where = select.from(config.contentKeyspaceName, config.assessmentTableName).where().and(QueryBuilder.eq("question_id", identifier)) + logger.info("MigrationObjectReader ::: getAssessmentItemData:: Cassandra Fetch Query :: " + selectWhere.toString) + cassandraUtil.findOne(selectWhere.toString) + } + + def getCollectionHierarchy(identifier: String, config: CSPMigratorConfig)(implicit cassandraUtil: CassandraUtil): String = { + val selectId = QueryBuilder.select().column("hierarchy") + val selectWhereId: Select.Where = selectId.from(config.hierarchyKeyspaceName, config.hierarchyTableName).where().and(QueryBuilder.eq("identifier", identifier)) + logger.info("MigrationObjectReader:: getCollectionHierarchy:: Hierarchy Fetch Query :: " + selectWhereId.toString) + val rowId = cassandraUtil.findOne(selectWhereId.toString) + if (null != rowId && null != rowId.getString("hierarchy")) rowId.getString("hierarchy") else "" + } + + def getQuestionSetHierarchy(identifier: String, config: CSPMigratorConfig)(implicit cassandraUtil: CassandraUtil): String = { + val selectId = QueryBuilder.select().column("hierarchy") + val selectWhereId: Select.Where = selectId.from(config.qsHierarchyKeyspaceName, config.qsHierarchyTableName).where().and(QueryBuilder.eq("identifier", identifier)) + logger.info("MigrationObjectReader:: getQuestionSetHierarchy:: Hierarchy Fetch Query :: " + selectWhereId.toString) + val rowId = cassandraUtil.findOne(selectWhereId.toString) + if (null != rowId && null != rowId.getString("hierarchy")) rowId.getString("hierarchy") else "" + } + + + def pushStreamingUrlEvent(objMetadata: Map[String, AnyRef], context: ProcessFunction[Event, String]#Context, config: CSPMigratorConfig)(implicit metrics: Metrics): Unit = { + val event = getStreamingEvent(objMetadata, config) + context.output(config.generateVideoStreamingOutTag, event) + } + + def getStreamingEvent(objMetadata: Map[String, AnyRef], config: CSPMigratorConfig): String = { + val ets = System.currentTimeMillis + val mid = s"""LP.$ets.${UUID.randomUUID}""" + val channelId = objMetadata.getOrElse("channel", "").asInstanceOf[String] + val ver = objMetadata.getOrElse("versionKey", "").asInstanceOf[String] + val artifactUrl = objMetadata.getOrElse("artifactUrl", "").asInstanceOf[String] + val contentType = objMetadata.getOrElse("contentType", "").asInstanceOf[String] + val mimeType = objMetadata.getOrElse("mimeType", "").asInstanceOf[String] + val status = objMetadata.getOrElse("status", "").asInstanceOf[String] + val identifier = objMetadata.getOrElse("identifier", "").asInstanceOf[String] + val pkgVersion = objMetadata.getOrElse("pkgVersion", "").asInstanceOf[Number] + val event = s"""{"eid":"BE_JOB_REQUEST", "ets": $ets, "mid": "$mid", "actor": {"id": "Live Video Stream Generator", "type": "System"}, "context":{"pdata":{"ver":"1.0","id":"org.ekstep.platform"}, "channel":"$channelId","env":"${config.jobEnv}"},"object":{"ver":"$ver","id":"$identifier"},"edata": {"action":"live-video-stream-generate","iteration":1,"identifier":"$identifier","channel":"$channelId","artifactUrl":"$artifactUrl","mimeType":"$mimeType","contentType":"$contentType","pkgVersion":$pkgVersion,"status":"$status"}}""".stripMargin + logger.info(s"MigrationObjectReader :: Asset Video Streaming Event for identifier $identifier is : $event") + event + } + + def pushLiveNodePublishEvent(objMetadata: Map[String, AnyRef], context: ProcessFunction[Event, String]#Context, metrics: Metrics, config: CSPMigratorConfig, tag: OutputTag[String]): Unit = { + val epochTime = System.currentTimeMillis + val identifier = objMetadata.getOrElse("identifier", "").asInstanceOf[String] + val pkgVersion = objMetadata.getOrElse("pkgVersion", "").asInstanceOf[Number] + val objectType = objMetadata.getOrElse("objectType", "").asInstanceOf[String] + val contentType = objMetadata.getOrElse("contentType", "").asInstanceOf[String] + val mimeType = objMetadata.getOrElse("mimeType", "").asInstanceOf[String] + val status = objMetadata.getOrElse("status", "").asInstanceOf[String] + val publishType = if(status.equalsIgnoreCase("Live")) "Public" else "Unlisted" + + val event = s"""{"eid":"BE_JOB_REQUEST","ets":$epochTime,"mid":"LP.$epochTime.${UUID.randomUUID()}","actor":{"id":"content-republish","type":"System"},"context":{"pdata":{"ver":"1.0","id":"org.ekstep.platform"},"channel":"sunbird","env":"${config.jobEnv}"},"object":{"ver":"$pkgVersion","id":"$identifier"},"edata":{"publish_type":"$publishType","metadata":{"identifier":"$identifier", "mimeType":"$mimeType","objectType":"$objectType","lastPublishedBy":"System","pkgVersion":$pkgVersion},"action":"republish","iteration":1,"contentType":"$contentType"}}""" + context.output(tag, event) + metrics.incCounter(config.liveContentNodePublishCount) + logger.info("MigrationObjectReader :: Live content publish triggered for " + identifier) + logger.info("MigrationObjectReader :: Live content publish event: " + event) + } + + def pushQuestionPublishEvent(objMetadata: Map[String, AnyRef], context: ProcessFunction[Event, String]#Context, metrics: Metrics, config: CSPMigratorConfig, tag: OutputTag[String], countMetric: String): Unit = { + val epochTime = System.currentTimeMillis + val identifier = objMetadata.getOrElse("identifier", "").asInstanceOf[String] + val pkgVersion = objMetadata.getOrElse("pkgVersion", "").asInstanceOf[Number] + val objectType = objMetadata.getOrElse("objectType", "").asInstanceOf[String] + val mimeType = objMetadata.getOrElse("mimeType", "").asInstanceOf[String] + val status = objMetadata.getOrElse("status", "").asInstanceOf[String] + val publishType = if(status.equalsIgnoreCase("Live")) "Public" else "Unlisted" + val channel = objMetadata.getOrElse("channel", "").asInstanceOf[String] + val lastPublishedBy = objMetadata.getOrElse("lastPublishedBy", "System").asInstanceOf[String] + val event = s"""{"eid":"BE_JOB_REQUEST","ets":$epochTime,"mid":"LP.$epochTime.${UUID.randomUUID()}","actor":{"id":"question-republish","type":"System"},"context":{"pdata":{"ver":"1.0","id":"org.sunbird.platform"},"channel":"${channel}","env":"${config.jobEnv}"},"object":{"ver":"$pkgVersion","id":"$identifier"},"edata":{"publish_type":"$publishType","metadata":{"identifier":"$identifier", "mimeType":"$mimeType","objectType":"$objectType","lastPublishedBy":"${lastPublishedBy}","pkgVersion":$pkgVersion},"action":"republish","iteration":1}}""" + context.output(tag, event) + metrics.incCounter(countMetric) + logger.info(s"MigrationObjectReader :: Live ${objectType} publish triggered for " + identifier) + logger.info(s"MigrationObjectReader :: Live ${objectType} publish event: " + event) + } + +} diff --git a/csp-migrator/src/main/scala/org/sunbird/job/cspmigrator/helpers/MigrationObjectUpdater.scala b/csp-migrator/src/main/scala/org/sunbird/job/cspmigrator/helpers/MigrationObjectUpdater.scala new file mode 100644 index 000000000..890c7b4cc --- /dev/null +++ b/csp-migrator/src/main/scala/org/sunbird/job/cspmigrator/helpers/MigrationObjectUpdater.scala @@ -0,0 +1,283 @@ +package org.sunbird.job.cspmigrator.helpers + +import com.datastax.driver.core.querybuilder.QueryBuilder +import org.apache.commons.io.{FileUtils, FilenameUtils} +import org.apache.commons.lang3.StringUtils +import org.neo4j.driver.v1.StatementResult +import org.slf4j.LoggerFactory +import org.sunbird.job.Metrics +import org.sunbird.job.cspmigrator.domain.Event +import org.sunbird.job.cspmigrator.task.CSPMigratorConfig +import org.sunbird.job.domain.`object`.DefinitionCache +import org.sunbird.job.exception.{InvalidInputException, ServerException} +import org.sunbird.job.util.CSPMetaUtil.updateAbsolutePath +import org.sunbird.job.util.{CassandraUtil, CloudStorageUtil, HttpUtil, JSONUtil, Neo4JUtil, ScalaJsonUtil, Slug} +import org.sunbird.job.util.CSPMetaUtil.updateAbsolutePath +import org.sunbird.job.util.{CassandraUtil, CloudStorageUtil, HttpUtil, JSONUtil, Neo4JUtil, ScalaJsonUtil, Slug} + +import scala.collection.JavaConverters._ +import scala.collection.JavaConverters._ +import java.io.{File, IOException} +import java.net.URL +import java.util + +trait MigrationObjectUpdater extends URLExtractor { + + private[this] val logger = LoggerFactory.getLogger(classOf[MigrationObjectUpdater]) + + def updateContentBody(identifier: String, ecmlBody: String, config: CSPMigratorConfig)(implicit cassandraUtil: CassandraUtil): Unit = { + val updateQuery = QueryBuilder.update(config.contentKeyspaceName, config.contentTableName) + .where(QueryBuilder.eq("content_id", identifier)) + .`with`(QueryBuilder.set("body", QueryBuilder.fcall("textAsBlob", ecmlBody))) + logger.info(s"""MigrationObjectUpdater:: updateContentBody:: Updating Content Body in Cassandra For $identifier : ${updateQuery.toString}""") + val result = cassandraUtil.upsert(updateQuery.toString) + if (result) logger.info(s"""MigrationObjectUpdater:: updateContentBody:: Content Body Updated Successfully For $identifier""") + else { + logger.error(s"""MigrationObjectUpdater:: updateContentBody:: Content Body Update Failed For $identifier""") + throw new InvalidInputException(s"""Content Body Update Failed For $identifier""") + } + } + + def updateAssessmentItemData(identifier: String, updatedData: Map[String, String], config: CSPMigratorConfig)(implicit cassandraUtil: CassandraUtil): Unit = { + val updateQuery = QueryBuilder.update(config.contentKeyspaceName, config.assessmentTableName) + .where(QueryBuilder.eq("question_id", identifier)) + .`with`(QueryBuilder.set("body", QueryBuilder.fcall("textAsBlob", updatedData.getOrElse("body", null)))) + .and(QueryBuilder.set("question", QueryBuilder.fcall("textAsBlob", updatedData.getOrElse("question", null)))) + .and(QueryBuilder.set("editorstate", QueryBuilder.fcall("textAsBlob", updatedData.getOrElse("editorstate", null)))) + .and(QueryBuilder.set("solutions", QueryBuilder.fcall("textAsBlob", updatedData.getOrElse("solutions", null)))) + + logger.info(s"""MigrationObjectUpdater:: updateAssessmentItemData:: Updating Assessment Body in Cassandra For $identifier : ${updateQuery.toString}""") + val result = cassandraUtil.upsert(updateQuery.toString) + if (result) logger.info(s"""MigrationObjectUpdater:: updateAssessmentItemData:: Assessment Body Updated Successfully For $identifier""") + else { + logger.error(s"""MigrationObjectUpdater:: updateAssessmentItemData:: Assessment Body Update Failed For $identifier""") + throw new InvalidInputException(s"""Assessment Body Update Failed For $identifier""") + } + } + + def updateCollectionHierarchy(identifier: String, hierarchy: String, config: CSPMigratorConfig)(implicit cassandraUtil: CassandraUtil): Unit = { + val updateQuery = QueryBuilder.update(config.hierarchyKeyspaceName, config.hierarchyTableName) + .where(QueryBuilder.eq("identifier", identifier)) + .`with`(QueryBuilder.set("hierarchy", hierarchy)) + logger.info(s"""MigrationObjectUpdater:: updateCollectionHierarchy:: Updating Hierarchy in Cassandra For $identifier : ${updateQuery.toString}""") + val result = cassandraUtil.upsert(updateQuery.toString) + if (result) logger.info(s"""MigrationObjectUpdater:: updateCollectionHierarchy:: Hierarchy Updated Successfully For $identifier""") + else { + logger.error(s"""MigrationObjectUpdater:: updateCollectionHierarchy:: Hierarchy Update Failed For $identifier""") + throw new InvalidInputException(s"""Hierarchy Update Failed For $identifier""") + } + } + + def updateQuestionSetHierarchy(identifier: String, hierarchy: String, config: CSPMigratorConfig)(implicit cassandraUtil: CassandraUtil): Unit = { + val updateQuery = QueryBuilder.update(config.qsHierarchyKeyspaceName, config.qsHierarchyTableName) + .where(QueryBuilder.eq("identifier", identifier)) + .`with`(QueryBuilder.set("hierarchy", hierarchy)) + logger.info(s"""MigrationObjectUpdater:: updateQuestionSetHierarchy:: Updating Hierarchy in Cassandra For $identifier : ${updateQuery.toString}""") + val result = cassandraUtil.upsert(updateQuery.toString) + if (result) logger.info(s"""MigrationObjectUpdater:: updateQuestionSetHierarchy:: Hierarchy Updated Successfully For $identifier""") + else { + logger.error(s"""MigrationObjectUpdater:: updateQuestionSetHierarchy:: Hierarchy Update Failed For $identifier""") + throw new InvalidInputException(s"""Hierarchy Update Failed For $identifier""") + } + } + + def updateNeo4j(updatedMetadata: Map[String, AnyRef], event: Event)(definitionCache: DefinitionCache, neo4JUtil: Neo4JUtil, config: CSPMigratorConfig): Unit = { + logger.info(s"""MigrationObjectUpdater:: process:: ${event.identifier} - ${event.objectType} updated fields data:: $updatedMetadata""") + val metadataUpdateQuery = metaDataQuery(event.objectType, updatedMetadata)(definitionCache, config) + val query = s"""MATCH (n:domain{IL_UNIQUE_ID:"${event.identifier}"}) SET $metadataUpdateQuery return n;""" + logger.info(s"""MigrationObjectUpdater:: process:: ${event.identifier} - ${event.objectType} updated fields :: Query: """ + query) + val sResult: StatementResult = neo4JUtil.executeQuery(query) + if(sResult !=null) logger.info("MigrationObjectUpdater:: process:: sResult:: " + sResult) + logger.info("MigrationObjectUpdater:: process:: static fields migration completed for " + event.identifier) + } + + + def extractAndValidateUrls(identifier: String, contentString: String, config: CSPMigratorConfig, httpUtil: HttpUtil, cloudStorageUtil: CloudStorageUtil, isContent: Boolean = true): String = { + val extractedUrls: List[String] = extractUrls(contentString) + logger.info("MigrationObjectUpdater::extractAndValidateUrls:: extractedUrls : " + extractedUrls) + if(extractedUrls.nonEmpty) { + var tempContentString: String = contentString + + val migratedString = if(config.copyMissingFiles) { + extractedUrls.toSet[String].foreach(urlString => { + // TODO: call a method to validate the url, upload to cloud set the url to migrated value + val tempUrlString = if(isContent) handleExternalURLS(urlString, identifier, config, httpUtil, cloudStorageUtil) else urlString + + config.keyValueMigrateStrings.keySet().toArray().map(migrateDomain => { + if (StringUtils.isNotBlank(tempUrlString) && tempUrlString.contains(migrateDomain.asInstanceOf[String])) { + tempContentString = StringUtils.replace(tempContentString, urlString, tempUrlString) + + val migrateValue: String = StringUtils.replaceEach(tempUrlString, config.keyValueMigrateStrings.keySet().toArray().map(_.asInstanceOf[String]), config.keyValueMigrateStrings.values().toArray().map(_.asInstanceOf[String])) + verifyFile(identifier, tempUrlString, migrateValue, migrateDomain.asInstanceOf[String], config)(httpUtil, cloudStorageUtil) + } + }) + }) + tempContentString + }else{ + extractedUrls.toSet[String].foreach(urlString => { + logger.info("MigrationObjectUpdater::extractAndValidateUrls:: urlString : " + urlString) + val tempUrlString = if(isContent) handleExternalURLS(urlString, identifier, config, httpUtil, cloudStorageUtil) else urlString + logger.info("MigrationObjectUpdater::extractAndValidateUrls:: tempUrlString : " + tempUrlString) + tempContentString = if(StringUtils.isNotBlank(tempUrlString)) StringUtils.replace(tempContentString, urlString, tempUrlString) else StringUtils.replace(tempContentString, urlString, "") + }) + tempContentString + } + + StringUtils.replaceEach(migratedString, config.keyValueMigrateStrings.keySet().toArray().map(_.asInstanceOf[String]), config.keyValueMigrateStrings.values().toArray().map(_.asInstanceOf[String])) + } else contentString + } + + + def downloadFile(downloadPath: String, fileUrl: String): File = try { + createDirectory(downloadPath) + var file = new File(downloadPath + File.separator + Slug.makeSlug(FilenameUtils.getName(fileUrl))) + FileUtils.copyURLToFile(new URL(fileUrl), file) + file = Slug.createSlugFile(file) + logger.info("MigrationObjectUpdater:: downloadFile:: external URL file download status:: " + file.exists() + " || " + file.getAbsolutePath) + file + } catch { + case e: IOException => logger.info("ERR_INVALID_FILE_URL", "File not found in the old path to migrate: " + fileUrl) + null + } + + private def createDirectory(directoryName: String): Unit = { + val theDir = new File(directoryName) + if (!theDir.exists) theDir.mkdirs + } + + def finalizeMigration(migratedMap: Map[String, AnyRef], event: Event, metrics: Metrics, config: CSPMigratorConfig)(defCache: DefinitionCache, neo4JUtil: Neo4JUtil): Unit = { + updateNeo4j(migratedMap + ("migrationVersion" -> config.migrationVersion.asInstanceOf[AnyRef]), event)(defCache, neo4JUtil, config) + logger.info("MigrationObjectUpdater::finalizeMigration:: CSP migration operation completed for : " + event.identifier) + metrics.incCounter(config.successEventCount) + } + + def verifyFile(identifier: String, originalUrl: String, migrateUrl: String, migrateDomain: String, config: CSPMigratorConfig)(implicit httpUtil: HttpUtil, cloudStorageUtil: CloudStorageUtil): Unit = { + val updateMigrateUrl = updateAbsolutePath(migrateUrl)(config) + logger.info("MigrationObjectUpdater::verifyFile:: originalUrl :: " + originalUrl + " || updateMigrateUrl:: " + updateMigrateUrl + " || identifier: " + identifier) + if(httpUtil.getSize(updateMigrateUrl) <= 0) { + if (config.copyMissingFiles) { + if(FilenameUtils.getExtension(originalUrl) != null && !FilenameUtils.getExtension(originalUrl).isBlank && FilenameUtils.getExtension(originalUrl).nonEmpty) { + try { + // code to download file from old cloud path and upload to new cloud path + val downloadedFile: File = downloadFile(s"/tmp/$identifier", originalUrl) + val exDomain: String = originalUrl.replace(migrateDomain, "") + val folderName: String = exDomain.substring(1, exDomain.indexOf(FilenameUtils.getName(originalUrl)) - 1) + cloudStorageUtil.uploadFile(folderName, downloadedFile) + } catch { + case f: IllegalArgumentException => logger.info("ERR_INVALID_FILE_URL", "File is not valid : " + originalUrl + " || identifier: " + identifier) + } + } + } else throw new ServerException("ERR_NEW_PATH_NOT_FOUND", "File not found in the new path to migrate: " + updateMigrateUrl) + } + } + + + def metaDataQuery(objectType: String, objMetadata: Map[String, AnyRef])(definitionCache: DefinitionCache, config: CSPMigratorConfig): String = { + val version = if(objectType.equalsIgnoreCase("itemset")) "2.0" else "1.0" + val definition = definitionCache.getDefinition(objectType, version, config.definitionBasePath) + val metadata = objMetadata - ("IL_UNIQUE_ID", "identifier", "IL_FUNC_OBJECT_TYPE", "IL_SYS_NODE_TYPE", "pkgVersion", "lastStatusChangedOn", "lastUpdatedOn", "status", "objectType", "publish_type") + metadata.map(prop => { + if (null == prop._2) s"n.${prop._1}=${prop._2}" + else if (definition.objectTypeProperties.contains(prop._1)) { + prop._2 match { + case _: Map[String, AnyRef] => + val strValue = JSONUtil.serialize(ScalaJsonUtil.serialize(prop._2)) + s"""n.${prop._1}=$strValue""" + case _: util.Map[String, AnyRef] => + val strValue = JSONUtil.serialize(JSONUtil.serialize(prop._2)) + s"""n.${prop._1}=$strValue""" + case _ => + val strValue = JSONUtil.serialize(prop._2) + s"""n.${prop._1}=$strValue""" + } + } else { + prop._2 match { + case _: Map[String, AnyRef] => + val strValue = JSONUtil.serialize(ScalaJsonUtil.serialize(prop._2)) + s"""n.${prop._1}=$strValue""" + case _: util.Map[String, AnyRef] => + val strValue = JSONUtil.serialize(JSONUtil.serialize(prop._2)) + s"""n.${prop._1}=$strValue""" + case _: List[String] => + val strValue = ScalaJsonUtil.serialize(prop._2) + s"""n.${prop._1}=$strValue""" + case _: util.List[String] => + val strValue = JSONUtil.serialize(prop._2) + s"""n.${prop._1}=$strValue""" + case _ => + val strValue = JSONUtil.serialize(prop._2) + s"""n.${prop._1}=$strValue""" + } + } + }).mkString(",") + } + + @throws[Exception] + private def getFile(identifier: String, fileUrl: String, config: CSPMigratorConfig, httpUtil: HttpUtil): File = { + try { + val fileId = fileUrl.split("download&id=")(1) + if (StringUtils.isBlank(fileId)) { + logger.info("Invalid fileUrl received for : " + identifier + " | fileUrl : " + fileUrl) + null + } else { + GoogleDriveUtil.downloadFile(fileId, getBasePath(identifier, config))(config) + } + } catch { + case e: ServerException =>{ + logger.info("Invalid fileUrl received for : " + identifier + " | fileUrl : " + fileUrl + "Exception is : " + e.getMessage) + null + } + case ex: Exception => { + logger.info("Invalid fileUrl received for : " + identifier + " | fileUrl : " + fileUrl + "Exception is : " + ex.getMessage) + null + } + } + } + + private def getBasePath(objectId: String, config: CSPMigratorConfig): String = { + if (StringUtils.isNotBlank(objectId)) config.temp_file_location + File.separator + objectId + File.separator + "_temp_" + System.currentTimeMillis + else config.temp_file_location + File.separator + "_temp_" + System.currentTimeMillis + } + + def handleExternalURLS(fileUrl: String, contentId: String, config: CSPMigratorConfig, httpUtil: HttpUtil, cloudStorageUtil: CloudStorageUtil): String = { + val validCSPSource: List[String] = config.config.getStringList("cloudstorage.write_base_path").asScala.toList + val relativePathPrefix: String = config.config.getString("cloudstorage.relative_path_prefix") + + if (StringUtils.isNotBlank(fileUrl) && !fileUrl.contains(relativePathPrefix) && !validCSPSource.exists(writeURL=> fileUrl.contains(writeURL))) { + try { + val file = if (fileUrl.contains("drive.google.com")) getFile(contentId, fileUrl, config, httpUtil) else downloadFile(getBasePath(contentId, config), fileUrl) + logger.info("MigrationObjectUpdater :: update :: Icon downloaded for : " + contentId + " | appIconUrl : " + fileUrl) + + if (null == file || !file.exists) { + logger.info("Error Occurred while downloading appIcon file for " + contentId + " | File Url : " + fileUrl) + null + } else { + val urls = uploadArtifact(file, contentId, config, cloudStorageUtil) + val url = if (null != urls && StringUtils.isNotBlank(urls(1))) { + val blobUrl = urls(1) + logger.info("CSPNeo4jMigrator :: handleExternalURLS :: Icon Uploaded Successfully to cloud for : " + contentId + " | appIconUrl : " + fileUrl + " | appIconBlobUrl : " + blobUrl) + FileUtils.deleteQuietly(file) + blobUrl + } else null + url + } + } catch { + case f: Exception => logger.info("ERR_INVALID_FILE_URL", "File is not valid to migrate: " + fileUrl + " || identifier: " + contentId) + fileUrl + } + } else fileUrl + } + + private def uploadArtifact(uploadedFile: File, identifier: String, config: CSPMigratorConfig, cloudStorageUtil: CloudStorageUtil): Array[String] = { + try { + var folder = config.contentFolder + folder = folder + "/" + Slug.makeSlug(identifier, isTransliterate = true) + "/" + config.artifactFolder + cloudStorageUtil.uploadFile(folder, uploadedFile, Option(true)) + } catch { + case e: Exception => e.printStackTrace() + logger.info("MigrationObjectUpdater :: uploadArtifact :: Exception occurred while uploading artifact for : " + identifier + "Exception is : " + e.getMessage) + throw new ServerException("ERR_CONTENT_UPLOAD_FILE", "Error while uploading the File.", e) + } + } + +} diff --git a/csp-migrator/src/main/scala/org/sunbird/job/cspmigrator/helpers/URLExtractor.scala b/csp-migrator/src/main/scala/org/sunbird/job/cspmigrator/helpers/URLExtractor.scala new file mode 100644 index 000000000..907e57014 --- /dev/null +++ b/csp-migrator/src/main/scala/org/sunbird/job/cspmigrator/helpers/URLExtractor.scala @@ -0,0 +1,34 @@ +package org.sunbird.job.cspmigrator.helpers + +import java.util +import java.util.regex.Pattern +import scala.collection.JavaConverters._ + +trait URLExtractor { + + // Function to extract all the URL// Function to extract all the URL from the string + def extractUrls(str: String): List[String] = { + // Creating an empty ArrayList + val list = new util.ArrayList[String] + + // Regular Expression to extract URL from the string + val regex = "\\b((?:https?):" + "//[-a-zA-Z0-9+&@#/%?=" + "~_|!:, .;]*[-a-zA-Z0-9+" + "&@#/%=~_|])" //(?<=")https?:\/\/[^\"]+ + // Compile the Regular Expression + val p = Pattern.compile(regex, Pattern.CASE_INSENSITIVE) + // Find the match between string and the regular expression + val m = p.matcher(str) + // Find the next subsequence of the input subsequence that find the pattern + while (m.find) { + // Find the substring from the first index of match result to the last index of match result and add in the list + list.add(str.substring(m.start(0), m.end(0))) + } + // IF there no URL present + if (list.size == 0) { + System.out.println("-1") + return List.empty[String] + } + // Print all the URLs stored + + list.asScala.toList + } +} \ No newline at end of file diff --git a/csp-migrator/src/main/scala/org/sunbird/job/cspmigrator/task/CSPMigratorConfig.scala b/csp-migrator/src/main/scala/org/sunbird/job/cspmigrator/task/CSPMigratorConfig.scala new file mode 100644 index 000000000..8fa4d6523 --- /dev/null +++ b/csp-migrator/src/main/scala/org/sunbird/job/cspmigrator/task/CSPMigratorConfig.scala @@ -0,0 +1,91 @@ +package org.sunbird.job.cspmigrator.task + +import com.typesafe.config.Config +import org.apache.flink.api.common.typeinfo.TypeInformation +import org.apache.flink.api.java.typeutils.TypeExtractor +import org.apache.flink.streaming.api.scala.OutputTag +import org.sunbird.job.BaseJobConfig +import org.sunbird.job.cspmigrator.domain.Event + +import java.util + +class CSPMigratorConfig(override val config: Config) extends BaseJobConfig(config, "csp-migrator") { + + implicit val mapTypeInfo: TypeInformation[util.Map[String, AnyRef]] = TypeExtractor.getForClass(classOf[util.Map[String, AnyRef]]) + implicit val stringTypeInfo: TypeInformation[String] = TypeExtractor.getForClass(classOf[String]) + implicit val contentAutoCreatorTypeInfo: TypeInformation[Event] = TypeExtractor.getForClass(classOf[Event]) + + // Kafka Topics Configuration + val kafkaInputTopic: String = config.getString("kafka.input.topic") + val kafkaFailedTopic: String = config.getString("kafka.failed.topic") + val liveVideoStreamingTopic: String = config.getString("kafka.live_video_stream.topic") + val liveContentNodeRepublishTopic: String = config.getString("kafka.live_content_node_republish.topic") + val liveQuestionNodeRepublishTopic: String = config.getString("kafka.live_question_node_republish.topic") + + val jobEnv: String = config.getString("job.env") + + val cassandraMigrationOutputTag: OutputTag[Event] = OutputTag[Event]("csp-cassandra-migration") + + override val kafkaConsumerParallelism: Int = config.getInt("task.consumer.parallelism") + override val parallelism: Int = config.getInt("task.parallelism") + val cassandraMigratorParallelism: Int = if (config.hasPath("task.cassandra-migrator.parallelism")) + config.getInt("task.cassandra-migrator.parallelism") else 1 + + // Metric List + val totalEventsCount = "csp-total-message-count" + val successEventCount = "csp-success-message-count" + val failedEventCount = "csp-failed-message-count" + val skippedEventCount = "csp-skipped-message-count" + val errorEventCount = "csp-error-message-count" + val liveContentNodePublishCount = "live-content-node-publish-count" + val assetVideoStreamCount = "asset-video-stream-count" + val liveQuestionNodePublishCount = "live-question-node-publish-count" + val liveQuestionSetNodePublishCount = "live-questionset-node-publish-count" + + // Consumers + val eventConsumer = "csp-migrator-consumer" + val cspMigratorFunction = "csp-migrator-process" + val cspMigratorEventProducer = "csp-migrator-producer" + val cassandraMigratorFunction = "csp-cassandra-migrator-process" + + // Tags + val contentAutoCreatorOutputTag: OutputTag[Event] = OutputTag[Event]("csp-migrator") + val failedEventOutTag: OutputTag[String] = OutputTag[String]("csp-migrator-failed-event") + val generateVideoStreamingOutTag: OutputTag[String] = OutputTag[String]("live-video-streaming-generator-request") + val liveContentNodePublishEventOutTag: OutputTag[String] = OutputTag[String]("live-content-node-republish-request") + val liveQuestionSetNodePublishEventOutTag: OutputTag[String] = OutputTag[String]("live-questionset-node-republish-request") + val liveQuestionNodePublishEventOutTag: OutputTag[String] = OutputTag[String]("live-question-node-republish-request") + val liveCollectionNodePublishEventOutTag: OutputTag[String] = OutputTag[String]("live-collection-node-republish-request") + + val configVersion = "1.0" + + // Cassandra Configurations + val cassandraHost: String = config.getString("lms-cassandra.host") + val cassandraPort: Int = config.getInt("lms-cassandra.port") + val contentKeyspaceName: String = config.getString("content.keyspace") + val contentTableName: String = config.getString("content.content_table") + val assessmentTableName: String = config.getString("content.assessment_table") + val hierarchyKeyspaceName: String = config.getString("hierarchy.keyspace") + val hierarchyTableName: String = config.getString("hierarchy.table") + val qsHierarchyKeyspaceName: String = config.getString("questionset.hierarchy.keyspace") + val qsHierarchyTableName: String = config.getString("questionset.hierarchy.table") + + // Neo4J Configurations + val graphRoutePath: String = config.getString("neo4j.routePath") + val graphName: String = config.getString("neo4j.graph") + + val fieldsToMigrate: util.Map[String, AnyRef] = if(config.hasPath("neo4j_fields_to_migrate")) config.getAnyRef("neo4j_fields_to_migrate").asInstanceOf[util.Map[String, AnyRef]] else new util.HashMap[String, AnyRef]() + val keyValueMigrateStrings: util.Map[String, String] = config.getAnyRef("key_value_strings_to_migrate").asInstanceOf[util.Map[String, String]] + val migrationVersion: Double = if(config.hasPath("migrationVersion")) config.getDouble("migrationVersion") else 1.0 + + val videStreamRegenerationEnabled: Boolean = if(config.hasPath("video_stream_regeneration_enable")) config.getBoolean("video_stream_regeneration_enable") else true + val liveNodeRepublishEnabled: Boolean = if(config.hasPath("live_node_republish_enable")) config.getBoolean("live_node_republish_enable") else true + val copyMissingFiles: Boolean = if(config.hasPath("copy_missing_files_to_cloud")) config.getBoolean("copy_missing_files_to_cloud") else true + + val definitionBasePath: String = if (config.hasPath("schema.basePath")) config.getString("schema.basePath") else "https://sunbirddev.blob.core.windows.net/sunbird-content-dev/schemas/local" + + val contentFolder: String = if (config.hasPath("cloud_storage.folder.content")) config.getString("cloud_storage.folder.content") else "content" + val artifactFolder: String = if (config.hasPath("cloud_storage.folder.artifact")) config.getString("cloud_storage.folder.artifact") else "artifact" + val temp_file_location: String = if (config.hasPath("download_path")) config.getString("download_path") else "/tmp" + def getConfig: Config = config +} diff --git a/csp-migrator/src/main/scala/org/sunbird/job/cspmigrator/task/CSPMigratorStreamTask.scala b/csp-migrator/src/main/scala/org/sunbird/job/cspmigrator/task/CSPMigratorStreamTask.scala new file mode 100644 index 000000000..81cc189cf --- /dev/null +++ b/csp-migrator/src/main/scala/org/sunbird/job/cspmigrator/task/CSPMigratorStreamTask.scala @@ -0,0 +1,65 @@ +package org.sunbird.job.cspmigrator.task + +import com.typesafe.config.ConfigFactory +import org.apache.flink.api.common.typeinfo.TypeInformation +import org.apache.flink.api.java.typeutils.TypeExtractor +import org.apache.flink.api.java.utils.ParameterTool +import org.apache.flink.streaming.api.scala.StreamExecutionEnvironment +import org.sunbird.job.connector.FlinkKafkaConnector +import org.sunbird.job.cspmigrator.domain.Event +import org.sunbird.job.cspmigrator.functions.{CSPNeo4jMigratorFunction, CSPCassandraMigratorFunction} +import org.sunbird.job.util.{FlinkUtil, HttpUtil} + +import java.io.File +import java.util + + +class CSPMigratorStreamTask(config: CSPMigratorConfig, kafkaConnector: FlinkKafkaConnector, httpUtil: HttpUtil) { + + def process(): Unit = { + implicit val env: StreamExecutionEnvironment = FlinkUtil.getExecutionContext(config) + implicit val eventTypeInfo: TypeInformation[Event] = TypeExtractor.getForClass(classOf[Event]) + implicit val mapTypeInfo: TypeInformation[util.Map[String, AnyRef]] = TypeExtractor.getForClass(classOf[util.Map[String, AnyRef]]) + implicit val stringTypeInfo: TypeInformation[String] = TypeExtractor.getForClass(classOf[String]) + + val processStreamTask = env.addSource(kafkaConnector.kafkaJobRequestSource[Event](config.kafkaInputTopic)).name(config.eventConsumer) + .uid(config.eventConsumer).setParallelism(config.kafkaConsumerParallelism) + .rebalance + .process(new CSPNeo4jMigratorFunction(config, httpUtil)) + .name(config.cspMigratorFunction) + .uid(config.cspMigratorFunction) + .setParallelism(config.parallelism) + + + val cassandraStream = processStreamTask.getSideOutput(config.cassandraMigrationOutputTag).process(new CSPCassandraMigratorFunction(config, httpUtil)) + .name(config.cassandraMigratorFunction).uid(config.cassandraMigratorFunction).setParallelism(config.cassandraMigratorParallelism) + + processStreamTask.getSideOutput(config.failedEventOutTag).addSink(kafkaConnector.kafkaStringSink(config.kafkaFailedTopic)) + processStreamTask.getSideOutput(config.generateVideoStreamingOutTag).addSink(kafkaConnector.kafkaStringSink(config.liveVideoStreamingTopic)) + processStreamTask.getSideOutput(config.liveContentNodePublishEventOutTag).addSink(kafkaConnector.kafkaStringSink(config.liveContentNodeRepublishTopic)) + processStreamTask.getSideOutput(config.liveQuestionNodePublishEventOutTag).addSink(kafkaConnector.kafkaStringSink(config.liveQuestionNodeRepublishTopic)) + + cassandraStream.getSideOutput(config.liveCollectionNodePublishEventOutTag).addSink(kafkaConnector.kafkaStringSink(config.liveContentNodeRepublishTopic)) + cassandraStream.getSideOutput(config.liveQuestionSetNodePublishEventOutTag).addSink(kafkaConnector.kafkaStringSink(config.liveQuestionNodeRepublishTopic)) + + env.execute(config.jobName) + } +} + +// $COVERAGE-OFF$ Disabling scoverage as the below code can only be invoked within flink cluster +object CSPMigratorStreamTask { + + def main(args: Array[String]): Unit = { + val configFilePath = Option(ParameterTool.fromArgs(args).get("config.file.path")) + val config = configFilePath.map { + path => ConfigFactory.parseFile(new File(path)).resolve() + }.getOrElse(ConfigFactory.load("csp-migrator.conf").withFallback(ConfigFactory.systemEnvironment())) + val CSPMigratorConfig = new CSPMigratorConfig(config) + val kafkaUtil = new FlinkKafkaConnector(CSPMigratorConfig) + val httpUtil = new HttpUtil + val task = new CSPMigratorStreamTask(CSPMigratorConfig, kafkaUtil, httpUtil) + task.process() + } +} + +// $COVERAGE-ON$ diff --git a/csp-migrator/src/test/resources/base-test.conf b/csp-migrator/src/test/resources/base-test.conf new file mode 100644 index 000000000..e03e30144 --- /dev/null +++ b/csp-migrator/src/test/resources/base-test.conf @@ -0,0 +1,54 @@ +kafka { + broker-servers = "localhost:9093" + zookeeper = "localhost:2183" + map.input.topic = "local.telemetry.map.input" + map.output.topic = "local.telemetry.map.output" + string.input.topic = "local.telemetry.string.input" + string.output.topic = "local.telemetry.string.output" + jobRequest.input.topic = "local.jobrequest.input" + jobRequest.output.topic = "local.jobrequest.output" + groupId = "test-consumer-group" + auto.offset.reset = "earliest" + producer { + max-request-size = 102400 + } +} + +task { + checkpointing.compressed = true + checkpointing.pause.between.seconds = 30000 + checkpointing.interval = 60000 + restart-strategy.attempts = 1 + restart-strategy.delay = 10000 + parallelism = 1 + consumer.parallelism = 1 +} + +redisdb.connection.timeout = 30000 + +redis { + host = localhost + port = 6340 + database { + key.expiry.seconds = 3600 + } +} + +lms-cassandra { + host = "localhost" + port = 9142 +} + +neo4j { + routePath = "bolt://localhost:7687" + graph = "domain" +} + +es { + basePath = "localhost:9200" +} + +schema { + basePath = "https://sunbirddev.blob.core.windows.net/sunbird-content-dev/schemas/local" + supportedVersion = {"itemset": "2.0"} +} \ No newline at end of file diff --git a/csp-migrator/src/test/resources/test.conf b/csp-migrator/src/test/resources/test.conf new file mode 100644 index 000000000..89b23ee41 --- /dev/null +++ b/csp-migrator/src/test/resources/test.conf @@ -0,0 +1,87 @@ +include "base-test.conf" + +job.env = "local" + +kafka { + input.topic = "sunbirddev.csp.migration.request" + failed.topic = "sunbirddev.csp.migration.job.request.failed" + groupId = "sunbirddev-csp-migrator-group" + live_video_stream.topic = "sunbirddev.live.video.stream.request" + live_content_node_republish.topic = "sunbirddev.republish.job.request" + live_question_node_republish.topic = "sunbirddev.republish.job.request" +} + +task { + consumer.parallelism = 1 + parallelism = 1 + csp-migrator.parallelism = 1 +} + +redis { + database { + relationCache.id = 10 + collectionCache.id = 5 + } +} + +service { + search.basePath = "http://11.2.6.6/search" + lms.basePath = "http://11.2.6.6/lms" + learning_service.basePath = "http://11.2.4.22:8080/learning-service" + content_service.basePath = "http://11.2.6.6/content" +} + + +hierarchy { + keyspace = "hierarchy_store" + table = "content_hierarchy" +} + +content { + keyspace = "content_store" + content_table = "content_data" + assessment_table = "question_data" +} + +key_value_strings_to_migrate = { + "https://sunbirddev.blob.core.windows.net": "https://store.migrationdomain", + "https://ekstep-public-dev.s3-ap-south-1.amazonaws.com": "https://store.migrationdomain", + "https://community.ekstep.in/assets/public": "https://store.migrationdomain" + "https://ntpproductionall.blob.core.windows.net/ntp-content-production": "https://store.migrationdomain" + "https://sunbirdstagingpublic.blob.core.windows.net/sunbird-content-staging": "https://store.migrationdomain" +} + +neo4j_fields_to_migrate = { + "asset": ["artifactUrl","thumbnail"], + "content": ["appIcon","artifactUrl", "posterImage", "previewUrl", "thumbnail", "assetsMap", "certTemplate", "itemSetPreviewUrl", "grayScaleAppIcon"], + "contentimage": ["appIcon","artifactUrl", "posterImage", "previewUrl", "thumbnail", "assetsMap", "certTemplate", "itemSetPreviewUrl", "grayScaleAppIcon"], + "collection": ["appIcon","artifactUrl", "posterImage", "previewUrl", "thumbnail", "toc_url", "grayScaleAppIcon"], + "collectionimage": ["appIcon","artifactUrl", "posterImage", "previewUrl", "thumbnail", "toc_url", "grayScaleAppIcon"], + "plugins": ["artifactUrl"], + "itemset": ["previewUrl"], + "assessmentitem": ["data", "question", "solutions", "editorState", "media"], + "question": ["appIcon","artifactUrl", "posterImage", "previewUrl","downloadUrl", "variants","pdfUrl"], + "questionimage": ["appIcon","artifactUrl", "posterImage", "previewUrl","downloadUrl", "variants","pdfUrl"], + "questionset": ["appIcon","artifactUrl", "posterImage", "previewUrl","downloadUrl", "variants","pdfUrl"], + "questionsetimage": ["appIcon","artifactUrl", "posterImage", "previewUrl","downloadUrl", "variants","pdfUrl"], +} + +cassandra_fields_to_migrate = { + "assessmentitem": ["body", "editorState", "answer", "solutions", "instructions", "media"] +} + +questionset.hierarchy.keyspace="hierarchy_store" +questionset.hierarchy.table="questionset_hierarchy" + +migrationVersion = 1 + +video_stream_regeneration_enable = false +live_node_republish_enable = false +copy_missing_files_to_cloud = false + +cloud_storage_container = sample-content +cloudstorage.metadata.replace_absolute_path=false +cloudstorage.relative_path_prefix= "CONTENT_STORAGE_BASE_PATH" +cloudstorage.read_base_path="https://sunbirddev.blob.core.windows.net" +cloudstorage.write_base_path=["https://sunbirddev.blob.core.windows.net","https://obj.dev.sunbird.org"] +cloudstorage.metadata.list=["appIcon","posterImage","artifactUrl","downloadUrl","variants","previewUrl","pdfUrl", "streamingUrl", "toc_url"] diff --git a/csp-migrator/src/test/resources/test.cql b/csp-migrator/src/test/resources/test.cql new file mode 100644 index 000000000..de3e9cfd8 --- /dev/null +++ b/csp-migrator/src/test/resources/test.cql @@ -0,0 +1,21 @@ +CREATE KEYSPACE IF NOT EXISTS content_store WITH replication = {'class':'SimpleStrategy','replication_factor':1}; +CREATE TABLE IF NOT EXISTS content_store.content_data ( + content_id text, + body blob, + PRIMARY KEY (content_id) +); + +CREATE KEYSPACE IF NOT EXISTS hierarchy_store WITH replication = {'class':'SimpleStrategy','replication_factor':1}; +CREATE TABLE IF NOT EXISTS hierarchy_store.content_hierarchy ( + identifier text, + hierarchy text, + relational_metadata text, + PRIMARY KEY (identifier) +); + +INSERT INTO content_store.content_data(content_id, body) VALUES ('do_31270597860728832015700', textAsBlob('{"theme":{"id":"theme","version":"1.0","startStage":"b8b47094-1d69-43a1-9c88-c02e760996c5","stage":[{"x":0,"y":0,"w":100,"h":100,"id":"b8b47094-1d69-43a1-9c88-c02e760996c5","rotate":null,"config":{"__cdata":"{\"opacity\":100,\"strokeWidth\":1,\"stroke\":\"rgba(255, 255, 255, 0)\",\"autoplay\":false,\"visible\":true,\"color\":\"#FFFFFF\",\"genieControls\":false,\"instructions\":\"\"}"},"param":[{"name":"next","value":"85c23e06-51c9-416b-ac9b-b9d5386a37ee"}],"manifest":{"media":[{"assetId":"385467069"},{"assetId":"QuizImage"},{"assetId":"org.ekstep.questionset.audioicon"},{"assetId":"org.ekstep.questionset.default-imgageicon"}]},"org.ekstep.questionset":[{"x":9,"y":6,"w":80,"h":85,"rotate":0,"z-index":0,"id":"b27c19a3-768b-4b07-9809-00d7a1e0e5c6","data":{"__cdata":"[{\"code\":\"NA\",\"isShuffleOption\":false,\"body\":\"{\\\"data\\\":{\\\"plugin\\\":{\\\"id\\\":\\\"org.ekstep.questionunit.mcq\\\",\\\"version\\\":\\\"1.1\\\",\\\"templateId\\\":\\\"horizontalMCQ\\\"},\\\"data\\\":{\\\"question\\\":{\\\"text\\\":\\\"

Q)జిప్సమ్ మరియు ప్లాస్టర్ ఆఫ్ పారిస్ ల నందు ఉండే నీటి అణువులలో తేడా ఎంత?

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"hint\\\":\\\"\\\"},\\\"options\\\":[{\\\"text\\\":\\\"

A)3/2

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"hint\\\":\\\"\\\",\\\"isCorrect\\\":true,\\\"$$hashKey\\\":\\\"object:1034\\\"},{\\\"text\\\":\\\"

B)1/2

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"hint\\\":\\\"\\\",\\\"isCorrect\\\":false,\\\"$$hashKey\\\":\\\"object:1035\\\"},{\\\"text\\\":\\\"

C)2

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"isCorrect\\\":false,\\\"$$hashKey\\\":\\\"object:1045\\\"},{\\\"text\\\":\\\"

D)5/2

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"isCorrect\\\":false,\\\"$$hashKey\\\":\\\"object:1050\\\"}],\\\"questionCount\\\":0,\\\"media\\\":[{\\\"id\\\":385467069,\\\"src\\\":\\\"/assets/public//content/do_31270089678451507213885/artifact/ice_screenshot_20190217-164338_1550402439889.png\\\",\\\"assetId\\\":\\\"do_31270089678451507213885\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":false},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true}]},\\\"config\\\":{\\\"metadata\\\":{\\\"data\\\":{\\\"plugin\\\":{\\\"id\\\":\\\"org.ekstep.questionunit.mcq\\\",\\\"version\\\":\\\"1.1\\\",\\\"templateId\\\":\\\"horizontalMCQ\\\"},\\\"data\\\":{\\\"question\\\":{\\\"text\\\":\\\"

Q)జిప్సమ్ మరియు ప్లాస్టర్ ఆఫ్ పారిస్ ల నందు ఉండే నీటి అణువులలో తేడా ఎంత?

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"hint\\\":\\\"\\\"},\\\"options\\\":[{\\\"text\\\":\\\"

A)3/2

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"hint\\\":\\\"\\\",\\\"isCorrect\\\":true,\\\"$$hashKey\\\":\\\"object:1034\\\"},{\\\"text\\\":\\\"

B)1/2

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"hint\\\":\\\"\\\",\\\"isCorrect\\\":false,\\\"$$hashKey\\\":\\\"object:1035\\\"},{\\\"text\\\":\\\"

C)2

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"isCorrect\\\":false,\\\"$$hashKey\\\":\\\"object:1045\\\"},{\\\"text\\\":\\\"

D)5/2

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"isCorrect\\\":false,\\\"$$hashKey\\\":\\\"object:1050\\\"}],\\\"questionCount\\\":0,\\\"media\\\":[{\\\"id\\\":385467069,\\\"src\\\":\\\"/assets/public//content/do_31270089678451507213885/artifact/ice_screenshot_20190217-164338_1550402439889.png\\\",\\\"assetId\\\":\\\"do_31270089678451507213885\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":false},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true}]},\\\"config\\\":{\\\"metadata\\\":{\\\"data\\\":{\\\"plugin\\\":{\\\"id\\\":\\\"org.ekstep.questionunit.mcq\\\",\\\"version\\\":\\\"1.1\\\",\\\"templateId\\\":\\\"horizontalMCQ\\\"},\\\"data\\\":{\\\"question\\\":{\\\"text\\\":\\\"

Q)The difference of the molecules of water in gypsum and plaster       

\\\\n\\\\n

  of Paris is

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"hint\\\":\\\"\\\"},\\\"options\\\":[{\\\"text\\\":\\\"

A)3/2

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"hint\\\":\\\"\\\",\\\"isCorrect\\\":true,\\\"$$hashKey\\\":\\\"object:1034\\\"},{\\\"text\\\":\\\"

B)1/2

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"hint\\\":\\\"\\\",\\\"isCorrect\\\":false,\\\"$$hashKey\\\":\\\"object:1035\\\"},{\\\"text\\\":\\\"

C)2

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"isCorrect\\\":false,\\\"$$hashKey\\\":\\\"object:1045\\\"},{\\\"text\\\":\\\"

D)5/2

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"isCorrect\\\":false,\\\"$$hashKey\\\":\\\"object:1050\\\"}],\\\"questionCount\\\":0,\\\"media\\\":[{\\\"id\\\":385467069,\\\"src\\\":\\\"/assets/public//content/do_31270089678451507213885/artifact/ice_screenshot_20190217-164338_1550402439889.png\\\",\\\"assetId\\\":\\\"do_31270089678451507213885\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":false},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true}]},\\\"config\\\":{\\\"metadata\\\":{\\\"data\\\":{\\\"plugin\\\":{\\\"id\\\":\\\"org.ekstep.questionunit.mcq\\\",\\\"version\\\":\\\"1.1\\\",\\\"templateId\\\":\\\"horizontalMCQ\\\"},\\\"data\\\":{\\\"question\\\":{\\\"text\\\":\\\"

Q) కార్బన్ ఎలక్ట్రాన్ విన్యాసాన్ని ఈ క్రింది విధంగా రాయరాదని సాత్విక చెప్పింది. ఎందువలన?

\\\\n\\\",\\\"image\\\":\\\"/assets/public//content/do_31270089678451507213885/artifact/ice_screenshot_20190217-164338_1550402439889.png\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"hint\\\":\\\"\\\"},\\\"options\\\":[{\\\"text\\\":\\\"

A)ఆఫ్  బౌ నియమాన్ని పాటించుట లేదు

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"hint\\\":\\\"\\\",\\\"isCorrect\\\":false,\\\"$$hashKey\\\":\\\"object:1034\\\"},{\\\"text\\\":\\\"

B)హుండు నియమాన్ని పాటించుట లేదు

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"hint\\\":\\\"\\\",\\\"isCorrect\\\":true,\\\"$$hashKey\\\":\\\"object:1035\\\"},{\\\"text\\\":\\\"

C)పౌలీవర్జన నియమాన్ని పాటించుట లేదు

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"isCorrect\\\":false,\\\"$$hashKey\\\":\\\"object:1045\\\"},{\\\"text\\\":\\\"

D)పై వన్నీ సరైనవే

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"isCorrect\\\":false,\\\"$$hashKey\\\":\\\"object:1050\\\"}],\\\"questionCount\\\":0,\\\"media\\\":[{\\\"id\\\":385467069,\\\"src\\\":\\\"/assets/public//content/do_31270089678451507213885/artifact/ice_screenshot_20190217-164338_1550402439889.png\\\",\\\"assetId\\\":\\\"do_31270089678451507213885\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":false},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true}]},\\\"config\\\":{\\\"metadata\\\":{\\\"data\\\":{\\\"plugin\\\":{\\\"id\\\":\\\"org.ekstep.questionunit.mcq\\\",\\\"version\\\":\\\"1.1\\\",\\\"templateId\\\":\\\"horizontalMCQ\\\"},\\\"data\\\":{\\\"question\\\":{\\\"text\\\":\\\"

Q) Satwika told that the electronic configuration of carbon cannot be written like this……why?

\\\\n\\\\n

Be cause this electronic configuration does not obey

\\\\n\\\",\\\"image\\\":\\\"/assets/public//content/do_31270089678451507213885/artifact/ice_screenshot_20190217-164338_1550402439889.png\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"hint\\\":\\\"\\\"},\\\"options\\\":[{\\\"text\\\":\\\"

A) aufbau principle  

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"hint\\\":\\\"\\\",\\\"isCorrect\\\":false,\\\"$$hashKey\\\":\\\"object:1034\\\"},{\\\"text\\\":\\\"

B) Hund”s rule

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"hint\\\":\\\"\\\",\\\"isCorrect\\\":true,\\\"$$hashKey\\\":\\\"object:1035\\\"},{\\\"text\\\":\\\"

C) Paul”s exclusion princple 

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"isCorrect\\\":false,\\\"$$hashKey\\\":\\\"object:1045\\\"},{\\\"text\\\":\\\"

D) All the above

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"isCorrect\\\":false,\\\"$$hashKey\\\":\\\"object:1050\\\"}],\\\"questionCount\\\":0,\\\"media\\\":[{\\\"id\\\":385467069,\\\"src\\\":\\\"/assets/public//content/do_31270089678451507213885/artifact/ice_screenshot_20190217-164338_1550402439889.png\\\",\\\"assetId\\\":\\\"do_31270089678451507213885\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":false},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true}]},\\\"config\\\":{\\\"metadata\\\":{\\\"data\\\":{\\\"plugin\\\":{\\\"id\\\":\\\"org.ekstep.questionunit.mcq\\\",\\\"version\\\":\\\"1.1\\\",\\\"templateId\\\":\\\"horizontalMCQ\\\"},\\\"data\\\":{\\\"question\\\":{\\\"text\\\":\\\"

Q) Satwika told that the electronic configuration of carbon cannot be written like this……why?

\\\\n\\\\n

Be cause this electronic configuration does not obey

\\\\n\\\",\\\"image\\\":\\\"/assets/public//content/do_31270089678451507213885/artifact/ice_screenshot_20190217-164338_1550402439889.png\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"hint\\\":\\\"\\\"},\\\"options\\\":[{\\\"text\\\":\\\"

A) aufbau principle  

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"hint\\\":\\\"\\\",\\\"isCorrect\\\":false,\\\"$$hashKey\\\":\\\"object:1034\\\"},{\\\"text\\\":\\\"

B) Hund”s rule

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"hint\\\":\\\"\\\",\\\"isCorrect\\\":true,\\\"$$hashKey\\\":\\\"object:1035\\\"},{\\\"text\\\":\\\"

C) Paul”s exclusion princple 

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"isCorrect\\\":false,\\\"$$hashKey\\\":\\\"object:1045\\\"},{\\\"text\\\":\\\"

D) All the above

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"isCorrect\\\":false,\\\"$$hashKey\\\":\\\"object:1050\\\"}],\\\"questionCount\\\":0,\\\"media\\\":[]},\\\"config\\\":{\\\"metadata\\\":{\\\"data\\\":{\\\"plugin\\\":{\\\"id\\\":\\\"org.ekstep.questionunit.mcq\\\",\\\"version\\\":\\\"1.1\\\",\\\"templateId\\\":\\\"horizontalMCQ\\\"},\\\"data\\\":{\\\"question\\\":{\\\"text\\\":\\\"

Q) ఒక మూలకం పరమాణువ సంఖ్య 20 అయినా ఆవర్తన పట్టికలో ఆ మూలక స్థానం

\\\\n\\\\n
    \\\\n\\\\t
  1. 4 వ పీరియడ్ మరియు 2 వ గ్రూప్  B) 1 వ పీరియడ్ మరియు 2 వ గ్రూప్ C) 3 వ పీరియడ్ మరియు 13 వ గ్రూప్
  2. \\\\n
\\\\n\\\\n

D) 2 వ పీరియడ్ మరియు 2 వ గ్రూప్

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"hint\\\":\\\"\\\"},\\\"options\\\":[{\\\"text\\\":\\\"

A) 4 వ పీరియడ్ మరియు 2 వ గ్రూప్  

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"hint\\\":\\\"\\\",\\\"isCorrect\\\":true,\\\"$$hashKey\\\":\\\"object:1034\\\"},{\\\"text\\\":\\\"

B) 1 వ పీరియడ్ మరియు 2 వ గ్రూప్

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"hint\\\":\\\"\\\",\\\"isCorrect\\\":false,\\\"$$hashKey\\\":\\\"object:1035\\\"},{\\\"text\\\":\\\"

C) 3 వ పీరియడ్ మరియు 13 వ గ్రూప్

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"isCorrect\\\":false,\\\"$$hashKey\\\":\\\"object:1045\\\"},{\\\"text\\\":\\\"

D) 2 వ పీరియడ్ మరియు 2 వ గ్రూప్

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"isCorrect\\\":false,\\\"$$hashKey\\\":\\\"object:1050\\\"}],\\\"questionCount\\\":0,\\\"media\\\":[]},\\\"config\\\":{\\\"metadata\\\":{\\\"data\\\":{\\\"plugin\\\":{\\\"id\\\":\\\"org.ekstep.questionunit.mcq\\\",\\\"version\\\":\\\"1.1\\\",\\\"templateId\\\":\\\"horizontalMCQ\\\"},\\\"data\\\":{\\\"question\\\":{\\\"text\\\":\\\"

Q) If an atomic number of an element is 20 then what is the position of that element in a modern periodic table

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"hint\\\":\\\"\\\"},\\\"options\\\":[{\\\"text\\\":\\\"

A) 4th period and 2nd group  

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"hint\\\":\\\"\\\",\\\"isCorrect\\\":true,\\\"$$hashKey\\\":\\\"object:1034\\\"},{\\\"text\\\":\\\"

B) 1st period and 2nd group

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"hint\\\":\\\"\\\",\\\"isCorrect\\\":false,\\\"$$hashKey\\\":\\\"object:1035\\\"},{\\\"text\\\":\\\"

C) 3rd period and 13th group

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"isCorrect\\\":false,\\\"$$hashKey\\\":\\\"object:1045\\\"},{\\\"text\\\":\\\"

 D) 2nd period and 2nd group

\\\\n\\\\n

 

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"isCorrect\\\":false,\\\"$$hashKey\\\":\\\"object:1050\\\"}],\\\"questionCount\\\":0,\\\"media\\\":[]},\\\"config\\\":{\\\"metadata\\\":{\\\"data\\\":{\\\"plugin\\\":{\\\"id\\\":\\\"org.ekstep.questionunit.mcq\\\",\\\"version\\\":\\\"1.1\\\",\\\"templateId\\\":\\\"horizontalMCQ\\\"},\\\"data\\\":{\\\"question\\\":{\\\"text\\\":\\\"

Q) In a Doberiner triad the atomic weight of first and third elements are 32 and 125 , then what is the atomic weight of second one

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"hint\\\":\\\"\\\"},\\\"options\\\":[{\\\"text\\\":\\\"

A) 78.5

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"hint\\\":\\\"\\\",\\\"isCorrect\\\":true,\\\"$$hashKey\\\":\\\"object:1034\\\"},{\\\"text\\\":\\\"

B)80 

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"hint\\\":\\\"\\\",\\\"isCorrect\\\":false,\\\"$$hashKey\\\":\\\"object:1035\\\"},{\\\"text\\\":\\\"

C)23

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"isCorrect\\\":false,\\\"$$hashKey\\\":\\\"object:1045\\\"},{\\\"text\\\":\\\"

D)60

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"isCorrect\\\":false,\\\"$$hashKey\\\":\\\"object:1050\\\"}],\\\"questionCount\\\":0,\\\"media\\\":[]},\\\"config\\\":{\\\"metadata\\\":{\\\"data\\\":{\\\"plugin\\\":{\\\"id\\\":\\\"org.ekstep.questionunit.mcq\\\",\\\"version\\\":\\\"1.1\\\",\\\"templateId\\\":\\\"horizontalMCQ\\\"},\\\"data\\\":{\\\"question\\\":{\\\"text\\\":\\\"

Q) Find the frequency of a radiowave of wave lenth 100 metres?

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"hint\\\":\\\"\\\"},\\\"options\\\":[{\\\"text\\\":\\\"

A) 3 HZ  

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"hint\\\":\\\"\\\",\\\"isCorrect\\\":false,\\\"$$hashKey\\\":\\\"object:1034\\\"},{\\\"text\\\":\\\"

B)3 KHZ  

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"hint\\\":\\\"\\\",\\\"isCorrect\\\":false,\\\"$$hashKey\\\":\\\"object:1035\\\"},{\\\"text\\\":\\\"

C)3 MHZ 

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"isCorrect\\\":true,\\\"$$hashKey\\\":\\\"object:1045\\\"},{\\\"text\\\":\\\"

D)30 MHZ

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"isCorrect\\\":false,\\\"$$hashKey\\\":\\\"object:1050\\\"}],\\\"questionCount\\\":0,\\\"media\\\":[]},\\\"config\\\":{\\\"metadata\\\":{\\\"data\\\":{\\\"plugin\\\":{\\\"id\\\":\\\"org.ekstep.questionunit.mcq\\\",\\\"version\\\":\\\"1.1\\\",\\\"templateId\\\":\\\"horizontalMCQ\\\"},\\\"data\\\":{\\\"question\\\":{\\\"text\\\":\\\"

 Q .When l=2 , what is the maximum value of ml

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"hint\\\":\\\"\\\"},\\\"options\\\":[{\\\"text\\\":\\\"

A) 2

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"hint\\\":\\\"\\\",\\\"isCorrect\\\":true,\\\"$$hashKey\\\":\\\"object:1034\\\"},{\\\"text\\\":\\\"

B)-2

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"hint\\\":\\\"\\\",\\\"isCorrect\\\":false,\\\"$$hashKey\\\":\\\"object:1035\\\"},{\\\"text\\\":\\\"

C)2(2)+1

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"isCorrect\\\":false,\\\"$$hashKey\\\":\\\"object:1045\\\"},{\\\"text\\\":\\\"

D)0

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"isCorrect\\\":false,\\\"$$hashKey\\\":\\\"object:1050\\\"}],\\\"questionCount\\\":0,\\\"media\\\":[]},\\\"config\\\":{\\\"metadata\\\":{\\\"data\\\":{\\\"plugin\\\":{\\\"id\\\":\\\"org.ekstep.questionunit.mcq\\\",\\\"version\\\":\\\"1.1\\\",\\\"templateId\\\":\\\"horizontalMCQ\\\"},\\\"data\\\":{\\\"question\\\":{\\\"text\\\":\\\"

1) l=2 అయినపుడు  యొక్క mlగరిష్ట విలువ ఎంత?

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"hint\\\":\\\"\\\"},\\\"options\\\":[{\\\"text\\\":\\\"

A) 2

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"hint\\\":\\\"\\\",\\\"isCorrect\\\":true,\\\"$$hashKey\\\":\\\"object:1034\\\"},{\\\"text\\\":\\\"

B)-2

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"hint\\\":\\\"\\\",\\\"isCorrect\\\":false,\\\"$$hashKey\\\":\\\"object:1035\\\"},{\\\"text\\\":\\\"

C)2(2)+1

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"isCorrect\\\":false,\\\"$$hashKey\\\":\\\"object:1045\\\"},{\\\"text\\\":\\\"

D)0

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"isCorrect\\\":false,\\\"$$hashKey\\\":\\\"object:1050\\\"}],\\\"questionCount\\\":0,\\\"media\\\":[]},\\\"config\\\":{\\\"metadata\\\":{\\\"data\\\":{\\\"plugin\\\":{\\\"id\\\":\\\"org.ekstep.questionunit.mcq\\\",\\\"version\\\":\\\"1.1\\\",\\\"templateId\\\":\\\"horizontalMCQ\\\"},\\\"data\\\":{\\\"question\\\":{\\\"text\\\":\\\"

1) l=2 అయినపుడు  యొక్క mlగరిష్ట విలువ ఎంత?

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"hint\\\":\\\"\\\"},\\\"options\\\":[{\\\"text\\\":\\\"

A) 2

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"hint\\\":\\\"\\\",\\\"isCorrect\\\":false,\\\"$$hashKey\\\":\\\"object:1034\\\"},{\\\"text\\\":\\\"

B)-2

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"hint\\\":\\\"\\\",\\\"isCorrect\\\":false,\\\"$$hashKey\\\":\\\"object:1035\\\"},{\\\"text\\\":\\\"

C)2(2)+1

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"isCorrect\\\":true,\\\"$$hashKey\\\":\\\"object:1045\\\"},{\\\"text\\\":\\\"

D)0

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"isCorrect\\\":false,\\\"$$hashKey\\\":\\\"object:1050\\\"}],\\\"questionCount\\\":0,\\\"media\\\":[]},\\\"config\\\":{\\\"metadata\\\":{\\\"max_score\\\":1,\\\"isShuffleOption\\\":false,\\\"isPartialScore\\\":true,\\\"templateType\\\":\\\"Horizontal\\\",\\\"name\\\":\\\"\\\\n\\\\tఈ క్రింది వాటిలో విశిష్టోష్ణానికి ప్రమాణం\\\\n\\\\n\\\",\\\"title\\\":\\\"Copy of - \\\\n\\\\tఈ క్రింది వాటిలో విశిష్టోష్ణానికి ప్రమాణం\\\\n\\\\n\\\",\\\"board\\\":\\\"State (Andhra Pradesh)\\\",\\\"topic\\\":[],\\\"medium\\\":\\\"Telugu\\\",\\\"gradeLevel\\\":[\\\"Class 10\\\"],\\\"subject\\\":\\\"Physical Science\\\",\\\"qlevel\\\":\\\"EASY\\\",\\\"description\\\":\\\"10 th ps bits\\\",\\\"category\\\":\\\"MCQ\\\",\\\"topicData\\\":\\\"(0) topics selected\\\"},\\\"max_time\\\":0,\\\"max_score\\\":1,\\\"partial_scoring\\\":true,\\\"layout\\\":\\\"Horizontal\\\",\\\"isShuffleOption\\\":false,\\\"questionCount\\\":1},\\\"media\\\":[]},\\\"medium\\\":\\\"Telugu\\\",\\\"questionTitle\\\":\\\"Copy of - \\\\n\\\\tఈ క్రింది వాటిలో విశిష్టోష్ణానికి ప్రమాణం\\\\n\\\\n\\\",\\\"qlevel\\\":\\\"MEDIUM\\\",\\\"subject\\\":\\\"Physical Science\\\",\\\"board\\\":\\\"State (Andhra Pradesh)\\\",\\\"templateType\\\":\\\"Horizontal\\\",\\\"isPartialScore\\\":true,\\\"gradeLevel\\\":[\\\"Class 10\\\"],\\\"isShuffleOption\\\":false,\\\"topic\\\":[\\\"Structure Of Atom\\\"],\\\"questionDesc\\\":\\\"10 th ps bits\\\",\\\"max_score\\\":1,\\\"name\\\":\\\"Examprep_10tm_ps_cha8_Q1\\\",\\\"title\\\":\\\"Copy of - Copy of - Copy of - Examprep_10tm_ps_cha8_Q1\\\",\\\"topicData\\\":\\\"(0) topics selected\\\",\\\"description\\\":\\\"10 PS BITS\\\",\\\"category\\\":\\\"MCQ\\\"},\\\"max_time\\\":0,\\\"max_score\\\":1,\\\"partial_scoring\\\":true,\\\"layout\\\":\\\"Horizontal\\\",\\\"isShuffleOption\\\":false,\\\"questionCount\\\":1},\\\"media\\\":[]},\\\"medium\\\":\\\"Telugu\\\",\\\"questionTitle\\\":\\\"Copy of - Examprep_10tm_ps_cha8_Q1\\\",\\\"qlevel\\\":\\\"MEDIUM\\\",\\\"subject\\\":\\\"Physical Science\\\",\\\"board\\\":\\\"State (Andhra Pradesh)\\\",\\\"templateType\\\":\\\"Horizontal\\\",\\\"isPartialScore\\\":true,\\\"gradeLevel\\\":[\\\"Class 10\\\"],\\\"isShuffleOption\\\":false,\\\"topic\\\":[\\\"Structure Of Atom\\\"],\\\"questionDesc\\\":\\\"10 PS BITS\\\",\\\"max_score\\\":1,\\\"name\\\":\\\"Examprep_10tm_ps_cha8_Q1\\\",\\\"title\\\":\\\"Copy of - Examprep_10tm_ps_cha8_Q1\\\",\\\"topicData\\\":\\\"(1) topics selected\\\",\\\"category\\\":\\\"MCQ\\\"},\\\"max_time\\\":0,\\\"max_score\\\":1,\\\"partial_scoring\\\":true,\\\"layout\\\":\\\"Horizontal\\\",\\\"isShuffleOption\\\":false,\\\"questionCount\\\":1},\\\"media\\\":[]},\\\"medium\\\":\\\"Telugu\\\",\\\"questionTitle\\\":\\\"Copy of - Examprep_10tm_ps_cha8_Q1\\\",\\\"qlevel\\\":\\\"DIFFICULT\\\",\\\"subject\\\":\\\"Physical Science\\\",\\\"board\\\":\\\"State (Andhra Pradesh)\\\",\\\"templateType\\\":\\\"Horizontal\\\",\\\"isPartialScore\\\":true,\\\"gradeLevel\\\":[\\\"Class 10\\\"],\\\"isShuffleOption\\\":false,\\\"topic\\\":[\\\"Structure Of Atom\\\"],\\\"max_score\\\":1,\\\"name\\\":\\\"Examprep_10em_ps_cha8_Q1\\\",\\\"title\\\":\\\"Copy of - Examprep_10em_ps_cha8_Q1\\\",\\\"topicData\\\":\\\"(1) topics selected\\\",\\\"description\\\":\\\"10 ps bits\\\",\\\"category\\\":\\\"MCQ\\\"},\\\"max_time\\\":0,\\\"max_score\\\":1,\\\"partial_scoring\\\":true,\\\"layout\\\":\\\"Horizontal\\\",\\\"isShuffleOption\\\":false,\\\"questionCount\\\":1},\\\"media\\\":[]},\\\"medium\\\":\\\"English\\\",\\\"questionTitle\\\":\\\"Copy of - Examprep_10em_ps_cha8_Q1\\\",\\\"qlevel\\\":\\\"DIFFICULT\\\",\\\"subject\\\":\\\"Physical Science\\\",\\\"board\\\":\\\"State (Andhra Pradesh)\\\",\\\"templateType\\\":\\\"Horizontal\\\",\\\"isPartialScore\\\":true,\\\"gradeLevel\\\":[\\\"Class 10\\\"],\\\"isShuffleOption\\\":false,\\\"topic\\\":[],\\\"questionDesc\\\":\\\"10 ps bits\\\",\\\"max_score\\\":1,\\\"name\\\":\\\"Examprep_10em_ps_cha8_Q2\\\",\\\"title\\\":\\\"Copy of - Examprep_10em_ps_cha8_Q2\\\",\\\"topicData\\\":\\\"(1) topics selected\\\",\\\"description\\\":\\\"10 PS BITS\\\",\\\"category\\\":\\\"MCQ\\\"},\\\"max_time\\\":0,\\\"max_score\\\":1,\\\"partial_scoring\\\":true,\\\"layout\\\":\\\"Horizontal\\\",\\\"isShuffleOption\\\":false,\\\"questionCount\\\":1},\\\"media\\\":[]},\\\"medium\\\":\\\"English\\\",\\\"questionTitle\\\":\\\"Copy of - Examprep_10em_ps_cha8_Q2\\\",\\\"qlevel\\\":\\\"DIFFICULT\\\",\\\"subject\\\":\\\"Physical Science\\\",\\\"board\\\":\\\"State (Andhra Pradesh)\\\",\\\"templateType\\\":\\\"Horizontal\\\",\\\"isPartialScore\\\":true,\\\"gradeLevel\\\":[\\\"Class 10\\\"],\\\"isShuffleOption\\\":false,\\\"topic\\\":[\\\"Classification Of Elements\\\"],\\\"questionDesc\\\":\\\"10 PS BITS\\\",\\\"max_score\\\":1,\\\"name\\\":\\\"Examprep_10em_ps_cha8_Q2\\\",\\\"title\\\":\\\"Copy of - Examprep_10em_ps_cha8_Q2\\\",\\\"topicData\\\":\\\"(0) topics selected\\\",\\\"category\\\":\\\"MCQ\\\"},\\\"max_time\\\":0,\\\"max_score\\\":1,\\\"partial_scoring\\\":true,\\\"layout\\\":\\\"Horizontal\\\",\\\"isShuffleOption\\\":false,\\\"questionCount\\\":1},\\\"media\\\":[]},\\\"medium\\\":\\\"English\\\",\\\"questionTitle\\\":\\\"Copy of - Examprep_10em_ps_cha8_Q2\\\",\\\"qlevel\\\":\\\"DIFFICULT\\\",\\\"subject\\\":\\\"Physical Science\\\",\\\"board\\\":\\\"State (Andhra Pradesh)\\\",\\\"templateType\\\":\\\"Horizontal\\\",\\\"isPartialScore\\\":true,\\\"gradeLevel\\\":[\\\"Class 10\\\"],\\\"isShuffleOption\\\":false,\\\"topic\\\":[\\\"Classification Of Elements\\\"],\\\"max_score\\\":1,\\\"name\\\":\\\"Copy of - Examprep_10em_ps_cha8_Q2\\\",\\\"title\\\":\\\"Copy of - Copy of - Examprep_10em_ps_cha8_Q2\\\",\\\"topicData\\\":\\\"(1) topics selected\\\",\\\"description\\\":\\\"10 ps bits\\\",\\\"category\\\":\\\"MCQ\\\"},\\\"max_time\\\":0,\\\"max_score\\\":1,\\\"partial_scoring\\\":true,\\\"layout\\\":\\\"Horizontal\\\",\\\"isShuffleOption\\\":false,\\\"questionCount\\\":1},\\\"media\\\":[]},\\\"medium\\\":\\\"Telugu\\\",\\\"questionTitle\\\":\\\"Copy of - Copy of - Examprep_10em_ps_cha8_Q2\\\",\\\"qlevel\\\":\\\"DIFFICULT\\\",\\\"subject\\\":\\\"Physical Science\\\",\\\"board\\\":\\\"State (Andhra Pradesh)\\\",\\\"templateType\\\":\\\"Horizontal\\\",\\\"isPartialScore\\\":true,\\\"gradeLevel\\\":[\\\"Class 10\\\"],\\\"isShuffleOption\\\":false,\\\"topic\\\":[\\\"Classification Of Elements\\\"],\\\"questionDesc\\\":\\\"10 ps bits\\\",\\\"max_score\\\":1,\\\"name\\\":\\\"Examprep_10tm_ps_cha8_Q2\\\",\\\"title\\\":\\\"Copy of - Copy of - Examprep_10tm_ps_cha8_Q2\\\",\\\"topicData\\\":\\\"(1) topics selected\\\",\\\"description\\\":\\\"10 PS BITS\\\",\\\"category\\\":\\\"MCQ\\\"},\\\"max_time\\\":0,\\\"max_score\\\":1,\\\"partial_scoring\\\":true,\\\"layout\\\":\\\"Horizontal\\\",\\\"isShuffleOption\\\":false,\\\"questionCount\\\":1},\\\"media\\\":[]},\\\"medium\\\":\\\"Telugu\\\",\\\"questionTitle\\\":\\\"Copy of - Examprep_10tm_ps_cha8_Q2\\\",\\\"qlevel\\\":\\\"MEDIUM\\\",\\\"subject\\\":\\\"Physical Science\\\",\\\"board\\\":\\\"State (Andhra Pradesh)\\\",\\\"templateType\\\":\\\"Horizontal\\\",\\\"isPartialScore\\\":true,\\\"gradeLevel\\\":[\\\"Class 10\\\"],\\\"isShuffleOption\\\":false,\\\"topic\\\":[\\\"Classification Of Elements\\\"],\\\"questionDesc\\\":\\\"10 PS BITS\\\",\\\"max_score\\\":1,\\\"name\\\":\\\"Examprep_10em ps_cha4 Q3\\\",\\\"title\\\":\\\"Examprep_10em ps_cha4 Q3\\\",\\\"topicData\\\":\\\"(1) topics selected\\\",\\\"description\\\":\\\"10 ps bits\\\",\\\"category\\\":\\\"MCQ\\\"},\\\"max_time\\\":0,\\\"max_score\\\":1,\\\"partial_scoring\\\":true,\\\"layout\\\":\\\"Horizontal\\\",\\\"isShuffleOption\\\":false,\\\"questionCount\\\":1},\\\"media\\\":[{\\\"id\\\":385467069,\\\"src\\\":\\\"/assets/public//content/do_31270089678451507213885/artifact/ice_screenshot_20190217-164338_1550402439889.png\\\",\\\"assetId\\\":\\\"do_31270089678451507213885\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":false},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true}]},\\\"medium\\\":\\\"Telugu\\\",\\\"questionTitle\\\":\\\"Examprep_10em ps_cha4 Q3\\\",\\\"qlevel\\\":\\\"MEDIUM\\\",\\\"subject\\\":\\\"Physical Science\\\",\\\"board\\\":\\\"State (Andhra Pradesh)\\\",\\\"templateType\\\":\\\"Horizontal\\\",\\\"isPartialScore\\\":true,\\\"gradeLevel\\\":[\\\"Class 10\\\"],\\\"isShuffleOption\\\":false,\\\"topic\\\":[\\\"Classification Of Elements\\\"],\\\"questionDesc\\\":\\\"10 ps bits\\\",\\\"max_score\\\":1,\\\"name\\\":\\\"Examprep_10em ps_cha8- Q3\\\",\\\"title\\\":\\\"Examprep_10em ps_cha8- Q3\\\",\\\"topicData\\\":\\\"(1) topics selected\\\",\\\"category\\\":\\\"MCQ\\\"},\\\"max_time\\\":0,\\\"max_score\\\":1,\\\"partial_scoring\\\":true,\\\"layout\\\":\\\"Horizontal\\\",\\\"isShuffleOption\\\":false,\\\"questionCount\\\":1},\\\"media\\\":[{\\\"id\\\":385467069,\\\"src\\\":\\\"/assets/public//content/do_31270089678451507213885/artifact/ice_screenshot_20190217-164338_1550402439889.png\\\",\\\"assetId\\\":\\\"do_31270089678451507213885\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":false},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true}]},\\\"medium\\\":\\\"Telugu\\\",\\\"questionTitle\\\":\\\"Examprep_10em ps_cha8- Q3\\\",\\\"qlevel\\\":\\\"DIFFICULT\\\",\\\"subject\\\":\\\"Physical Science\\\",\\\"board\\\":\\\"State (Andhra Pradesh)\\\",\\\"templateType\\\":\\\"Horizontal\\\",\\\"isPartialScore\\\":true,\\\"gradeLevel\\\":[\\\"Class 10\\\"],\\\"isShuffleOption\\\":false,\\\"topic\\\":[\\\"Classification Of Elements\\\"],\\\"max_score\\\":1,\\\"name\\\":\\\"Examprep_10tm ps_cha8- Q3\\\",\\\"title\\\":\\\"Copy of - Examprep_10tm ps_cha8- Q3\\\",\\\"topicData\\\":\\\"(1) topics selected\\\",\\\"description\\\":\\\"10 ps bits\\\",\\\"category\\\":\\\"MCQ\\\"},\\\"max_time\\\":0,\\\"max_score\\\":1,\\\"partial_scoring\\\":true,\\\"layout\\\":\\\"Horizontal\\\",\\\"isShuffleOption\\\":false,\\\"questionCount\\\":1},\\\"media\\\":[{\\\"id\\\":385467069,\\\"src\\\":\\\"/assets/public//content/do_31270089678451507213885/artifact/ice_screenshot_20190217-164338_1550402439889.png\\\",\\\"assetId\\\":\\\"do_31270089678451507213885\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":false},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true}]},\\\"medium\\\":\\\"English\\\",\\\"questionTitle\\\":\\\"Copy of - Examprep_10tm ps_cha8- Q3\\\",\\\"qlevel\\\":\\\"MEDIUM\\\",\\\"subject\\\":\\\"Physical Science\\\",\\\"board\\\":\\\"State (Andhra Pradesh)\\\",\\\"templateType\\\":\\\"Horizontal\\\",\\\"isPartialScore\\\":true,\\\"gradeLevel\\\":[\\\"Class 10\\\"],\\\"isShuffleOption\\\":false,\\\"topic\\\":[],\\\"questionDesc\\\":\\\"10 ps bits\\\",\\\"max_score\\\":1,\\\"name\\\":\\\"Examprep_10em ps_cha8- Q5\\\",\\\"title\\\":\\\"Examprep_10em ps_cha8- Q5\\\",\\\"topicData\\\":\\\"(1) topics selected\\\",\\\"description\\\":\\\"10 PS BITS\\\",\\\"category\\\":\\\"MCQ\\\"},\\\"max_time\\\":0,\\\"max_score\\\":1,\\\"partial_scoring\\\":true,\\\"layout\\\":\\\"Horizontal\\\",\\\"isShuffleOption\\\":false,\\\"questionCount\\\":1},\\\"media\\\":[{\\\"id\\\":385467069,\\\"src\\\":\\\"/assets/public//content/do_31270089678451507213885/artifact/ice_screenshot_20190217-164338_1550402439889.png\\\",\\\"assetId\\\":\\\"do_31270089678451507213885\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":false},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true}]},\\\"medium\\\":\\\"English\\\",\\\"questionTitle\\\":\\\"Examprep_10em ps_cha8- Q5\\\",\\\"qlevel\\\":\\\"MEDIUM\\\",\\\"subject\\\":\\\"Physical Science\\\",\\\"board\\\":\\\"State (Andhra Pradesh)\\\",\\\"templateType\\\":\\\"Horizontal\\\",\\\"isPartialScore\\\":true,\\\"gradeLevel\\\":[\\\"Class 10\\\"],\\\"isShuffleOption\\\":false,\\\"topic\\\":[\\\"Structure Of Atom\\\"],\\\"questionDesc\\\":\\\"10 PS BITS\\\",\\\"max_score\\\":1,\\\"name\\\":\\\"Examprep_10em ps_cha8- Q5\\\",\\\"title\\\":\\\"Examprep_10em ps_cha8- Q5\\\",\\\"description\\\":\\\"10 PS BITS\\\",\\\"category\\\":\\\"MCQ\\\"},\\\"max_time\\\":0,\\\"max_score\\\":1,\\\"partial_scoring\\\":true,\\\"layout\\\":\\\"Horizontal\\\",\\\"isShuffleOption\\\":false,\\\"questionCount\\\":1},\\\"media\\\":[{\\\"id\\\":385467069,\\\"src\\\":\\\"/assets/public//content/do_31270089678451507213885/artifact/ice_screenshot_20190217-164338_1550402439889.png\\\",\\\"assetId\\\":\\\"do_31270089678451507213885\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":false},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true}]}}\",\"itemType\":\"UNIT\",\"version\":2,\"category\":\"MCQ\",\"createdBy\":\"c252c844-4178-498f-9006-d63540319254\",\"channel\":\"0123207707019919361056\",\"type\":\"mcq\",\"template\":\"NA\",\"template_id\":\"NA\",\"framework\":\"ap_k-12_1\",\"data\":{\"plugin\":{\"id\":\"org.ekstep.questionunit.mcq\",\"version\":\"1.1\",\"templateId\":\"horizontalMCQ\"},\"data\":{\"question\":{\"text\":\"

Q)జిప్సమ్ మరియు ప్లాస్టర్ ఆఫ్ పారిస్ ల నందు ఉండే నీటి అణువులలో తేడా ఎంత?

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\"},\"options\":[{\"text\":\"

A)3/2

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\",\"isCorrect\":true,\"$$hashKey\":\"object:1034\"},{\"text\":\"

B)1/2

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1035\"},{\"text\":\"

C)2

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1045\"},{\"text\":\"

D)5/2

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1050\"}],\"questionCount\":0,\"media\":[{\"id\":385467069,\"src\":\"/assets/public//content/do_31270089678451507213885/artifact/ice_screenshot_20190217-164338_1550402439889.png\",\"assetId\":\"do_31270089678451507213885\",\"type\":\"image\",\"preload\":false},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true}]},\"config\":{\"metadata\":{\"data\":{\"plugin\":{\"id\":\"org.ekstep.questionunit.mcq\",\"version\":\"1.1\",\"templateId\":\"horizontalMCQ\"},\"data\":{\"question\":{\"text\":\"

Q)The difference of the molecules of water in gypsum and plaster       

\\n\\n

  of Paris is

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\"},\"options\":[{\"text\":\"

A)3/2

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\",\"isCorrect\":true,\"$$hashKey\":\"object:1034\"},{\"text\":\"

B)1/2

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1035\"},{\"text\":\"

C)2

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1045\"},{\"text\":\"

D)5/2

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1050\"}],\"questionCount\":0,\"media\":[{\"id\":385467069,\"src\":\"/assets/public//content/do_31270089678451507213885/artifact/ice_screenshot_20190217-164338_1550402439889.png\",\"assetId\":\"do_31270089678451507213885\",\"type\":\"image\",\"preload\":false},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true}]},\"config\":{\"metadata\":{\"data\":{\"plugin\":{\"id\":\"org.ekstep.questionunit.mcq\",\"version\":\"1.1\",\"templateId\":\"horizontalMCQ\"},\"data\":{\"question\":{\"text\":\"

Q) కార్బన్ ఎలక్ట్రాన్ విన్యాసాన్ని ఈ క్రింది విధంగా రాయరాదని సాత్విక చెప్పింది. ఎందువలన?

\\n\",\"image\":\"/assets/public//content/do_31270089678451507213885/artifact/ice_screenshot_20190217-164338_1550402439889.png\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\"},\"options\":[{\"text\":\"

A)ఆఫ్  బౌ నియమాన్ని పాటించుట లేదు

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1034\"},{\"text\":\"

B)హుండు నియమాన్ని పాటించుట లేదు

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\",\"isCorrect\":true,\"$$hashKey\":\"object:1035\"},{\"text\":\"

C)పౌలీవర్జన నియమాన్ని పాటించుట లేదు

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1045\"},{\"text\":\"

D)పై వన్నీ సరైనవే

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1050\"}],\"questionCount\":0,\"media\":[{\"id\":385467069,\"src\":\"/assets/public//content/do_31270089678451507213885/artifact/ice_screenshot_20190217-164338_1550402439889.png\",\"assetId\":\"do_31270089678451507213885\",\"type\":\"image\",\"preload\":false},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true}]},\"config\":{\"metadata\":{\"data\":{\"plugin\":{\"id\":\"org.ekstep.questionunit.mcq\",\"version\":\"1.1\",\"templateId\":\"horizontalMCQ\"},\"data\":{\"question\":{\"text\":\"

Q) Satwika told that the electronic configuration of carbon cannot be written like this……why?

\\n\\n

Be cause this electronic configuration does not obey

\\n\",\"image\":\"/assets/public//content/do_31270089678451507213885/artifact/ice_screenshot_20190217-164338_1550402439889.png\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\"},\"options\":[{\"text\":\"

A) aufbau principle  

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1034\"},{\"text\":\"

B) Hund”s rule

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\",\"isCorrect\":true,\"$$hashKey\":\"object:1035\"},{\"text\":\"

C) Paul”s exclusion princple 

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1045\"},{\"text\":\"

D) All the above

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1050\"}],\"questionCount\":0,\"media\":[{\"id\":385467069,\"src\":\"/assets/public//content/do_31270089678451507213885/artifact/ice_screenshot_20190217-164338_1550402439889.png\",\"assetId\":\"do_31270089678451507213885\",\"type\":\"image\",\"preload\":false},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true}]},\"config\":{\"metadata\":{\"data\":{\"plugin\":{\"id\":\"org.ekstep.questionunit.mcq\",\"version\":\"1.1\",\"templateId\":\"horizontalMCQ\"},\"data\":{\"question\":{\"text\":\"

Q) Satwika told that the electronic configuration of carbon cannot be written like this……why?

\\n\\n

Be cause this electronic configuration does not obey

\\n\",\"image\":\"/assets/public//content/do_31270089678451507213885/artifact/ice_screenshot_20190217-164338_1550402439889.png\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\"},\"options\":[{\"text\":\"

A) aufbau principle  

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1034\"},{\"text\":\"

B) Hund”s rule

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\",\"isCorrect\":true,\"$$hashKey\":\"object:1035\"},{\"text\":\"

C) Paul”s exclusion princple 

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1045\"},{\"text\":\"

D) All the above

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1050\"}],\"questionCount\":0,\"media\":[]},\"config\":{\"metadata\":{\"data\":{\"plugin\":{\"id\":\"org.ekstep.questionunit.mcq\",\"version\":\"1.1\",\"templateId\":\"horizontalMCQ\"},\"data\":{\"question\":{\"text\":\"

Q) ఒక మూలకం పరమాణువ సంఖ్య 20 అయినా ఆవర్తన పట్టికలో ఆ మూలక స్థానం

\\n\\n
    \\n\\t
  1. 4 వ పీరియడ్ మరియు 2 వ గ్రూప్  B) 1 వ పీరియడ్ మరియు 2 వ గ్రూప్ C) 3 వ పీరియడ్ మరియు 13 వ గ్రూప్
  2. \\n
\\n\\n

D) 2 వ పీరియడ్ మరియు 2 వ గ్రూప్

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\"},\"options\":[{\"text\":\"

A) 4 వ పీరియడ్ మరియు 2 వ గ్రూప్  

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\",\"isCorrect\":true,\"$$hashKey\":\"object:1034\"},{\"text\":\"

B) 1 వ పీరియడ్ మరియు 2 వ గ్రూప్

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1035\"},{\"text\":\"

C) 3 వ పీరియడ్ మరియు 13 వ గ్రూప్

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1045\"},{\"text\":\"

D) 2 వ పీరియడ్ మరియు 2 వ గ్రూప్

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1050\"}],\"questionCount\":0,\"media\":[]},\"config\":{\"metadata\":{\"data\":{\"plugin\":{\"id\":\"org.ekstep.questionunit.mcq\",\"version\":\"1.1\",\"templateId\":\"horizontalMCQ\"},\"data\":{\"question\":{\"text\":\"

Q) If an atomic number of an element is 20 then what is the position of that element in a modern periodic table

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\"},\"options\":[{\"text\":\"

A) 4th period and 2nd group  

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\",\"isCorrect\":true,\"$$hashKey\":\"object:1034\"},{\"text\":\"

B) 1st period and 2nd group

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1035\"},{\"text\":\"

C) 3rd period and 13th group

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1045\"},{\"text\":\"

 D) 2nd period and 2nd group

\\n\\n

 

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1050\"}],\"questionCount\":0,\"media\":[]},\"config\":{\"metadata\":{\"data\":{\"plugin\":{\"id\":\"org.ekstep.questionunit.mcq\",\"version\":\"1.1\",\"templateId\":\"horizontalMCQ\"},\"data\":{\"question\":{\"text\":\"

Q) In a Doberiner triad the atomic weight of first and third elements are 32 and 125 , then what is the atomic weight of second one

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\"},\"options\":[{\"text\":\"

A) 78.5

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\",\"isCorrect\":true,\"$$hashKey\":\"object:1034\"},{\"text\":\"

B)80 

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1035\"},{\"text\":\"

C)23

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1045\"},{\"text\":\"

D)60

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1050\"}],\"questionCount\":0,\"media\":[]},\"config\":{\"metadata\":{\"data\":{\"plugin\":{\"id\":\"org.ekstep.questionunit.mcq\",\"version\":\"1.1\",\"templateId\":\"horizontalMCQ\"},\"data\":{\"question\":{\"text\":\"

Q) Find the frequency of a radiowave of wave lenth 100 metres?

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\"},\"options\":[{\"text\":\"

A) 3 HZ  

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1034\"},{\"text\":\"

B)3 KHZ  

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1035\"},{\"text\":\"

C)3 MHZ 

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"isCorrect\":true,\"$$hashKey\":\"object:1045\"},{\"text\":\"

D)30 MHZ

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1050\"}],\"questionCount\":0,\"media\":[]},\"config\":{\"metadata\":{\"data\":{\"plugin\":{\"id\":\"org.ekstep.questionunit.mcq\",\"version\":\"1.1\",\"templateId\":\"horizontalMCQ\"},\"data\":{\"question\":{\"text\":\"

 Q .When l=2 , what is the maximum value of ml

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\"},\"options\":[{\"text\":\"

A) 2

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\",\"isCorrect\":true,\"$$hashKey\":\"object:1034\"},{\"text\":\"

B)-2

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1035\"},{\"text\":\"

C)2(2)+1

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1045\"},{\"text\":\"

D)0

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1050\"}],\"questionCount\":0,\"media\":[]},\"config\":{\"metadata\":{\"data\":{\"plugin\":{\"id\":\"org.ekstep.questionunit.mcq\",\"version\":\"1.1\",\"templateId\":\"horizontalMCQ\"},\"data\":{\"question\":{\"text\":\"

1) l=2 అయినపుడు  యొక్క mlగరిష్ట విలువ ఎంత?

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\"},\"options\":[{\"text\":\"

A) 2

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\",\"isCorrect\":true,\"$$hashKey\":\"object:1034\"},{\"text\":\"

B)-2

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1035\"},{\"text\":\"

C)2(2)+1

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1045\"},{\"text\":\"

D)0

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1050\"}],\"questionCount\":0,\"media\":[]},\"config\":{\"metadata\":{\"data\":{\"plugin\":{\"id\":\"org.ekstep.questionunit.mcq\",\"version\":\"1.1\",\"templateId\":\"horizontalMCQ\"},\"data\":{\"question\":{\"text\":\"

1) l=2 అయినపుడు  యొక్క mlగరిష్ట విలువ ఎంత?

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\"},\"options\":[{\"text\":\"

A) 2

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1034\"},{\"text\":\"

B)-2

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1035\"},{\"text\":\"

C)2(2)+1

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"isCorrect\":true,\"$$hashKey\":\"object:1045\"},{\"text\":\"

D)0

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1050\"}],\"questionCount\":0,\"media\":[]},\"config\":{\"metadata\":{\"max_score\":1,\"isShuffleOption\":false,\"isPartialScore\":true,\"templateType\":\"Horizontal\",\"name\":\"\\n\\tఈ క్రింది వాటిలో విశిష్టోష్ణానికి ప్రమాణం\\n\\n\",\"title\":\"Copy of - \\n\\tఈ క్రింది వాటిలో విశిష్టోష్ణానికి ప్రమాణం\\n\\n\",\"board\":\"State (Andhra Pradesh)\",\"topic\":[],\"medium\":\"Telugu\",\"gradeLevel\":[\"Class 10\"],\"subject\":\"Physical Science\",\"qlevel\":\"EASY\",\"description\":\"10 th ps bits\",\"category\":\"MCQ\",\"topicData\":\"(0) topics selected\"},\"max_time\":0,\"max_score\":1,\"partial_scoring\":true,\"layout\":\"Horizontal\",\"isShuffleOption\":false,\"questionCount\":1},\"media\":[]},\"medium\":\"Telugu\",\"questionTitle\":\"Copy of - \\n\\tఈ క్రింది వాటిలో విశిష్టోష్ణానికి ప్రమాణం\\n\\n\",\"qlevel\":\"MEDIUM\",\"subject\":\"Physical Science\",\"board\":\"State (Andhra Pradesh)\",\"templateType\":\"Horizontal\",\"isPartialScore\":true,\"gradeLevel\":[\"Class 10\"],\"isShuffleOption\":false,\"topic\":[\"Structure Of Atom\"],\"questionDesc\":\"10 th ps bits\",\"max_score\":1,\"name\":\"Examprep_10tm_ps_cha8_Q1\",\"title\":\"Copy of - Copy of - Copy of - Examprep_10tm_ps_cha8_Q1\",\"topicData\":\"(0) topics selected\",\"description\":\"10 PS BITS\",\"category\":\"MCQ\"},\"max_time\":0,\"max_score\":1,\"partial_scoring\":true,\"layout\":\"Horizontal\",\"isShuffleOption\":false,\"questionCount\":1},\"media\":[]},\"medium\":\"Telugu\",\"questionTitle\":\"Copy of - Examprep_10tm_ps_cha8_Q1\",\"qlevel\":\"MEDIUM\",\"subject\":\"Physical Science\",\"board\":\"State (Andhra Pradesh)\",\"templateType\":\"Horizontal\",\"isPartialScore\":true,\"gradeLevel\":[\"Class 10\"],\"isShuffleOption\":false,\"topic\":[\"Structure Of Atom\"],\"questionDesc\":\"10 PS BITS\",\"max_score\":1,\"name\":\"Examprep_10tm_ps_cha8_Q1\",\"title\":\"Copy of - Examprep_10tm_ps_cha8_Q1\",\"topicData\":\"(1) topics selected\",\"category\":\"MCQ\"},\"max_time\":0,\"max_score\":1,\"partial_scoring\":true,\"layout\":\"Horizontal\",\"isShuffleOption\":false,\"questionCount\":1},\"media\":[]},\"medium\":\"Telugu\",\"questionTitle\":\"Copy of - Examprep_10tm_ps_cha8_Q1\",\"qlevel\":\"DIFFICULT\",\"subject\":\"Physical Science\",\"board\":\"State (Andhra Pradesh)\",\"templateType\":\"Horizontal\",\"isPartialScore\":true,\"gradeLevel\":[\"Class 10\"],\"isShuffleOption\":false,\"topic\":[\"Structure Of Atom\"],\"max_score\":1,\"name\":\"Examprep_10em_ps_cha8_Q1\",\"title\":\"Copy of - Examprep_10em_ps_cha8_Q1\",\"topicData\":\"(1) topics selected\",\"description\":\"10 ps bits\",\"category\":\"MCQ\"},\"max_time\":0,\"max_score\":1,\"partial_scoring\":true,\"layout\":\"Horizontal\",\"isShuffleOption\":false,\"questionCount\":1},\"media\":[]},\"medium\":\"English\",\"questionTitle\":\"Copy of - Examprep_10em_ps_cha8_Q1\",\"qlevel\":\"DIFFICULT\",\"subject\":\"Physical Science\",\"board\":\"State (Andhra Pradesh)\",\"templateType\":\"Horizontal\",\"isPartialScore\":true,\"gradeLevel\":[\"Class 10\"],\"isShuffleOption\":false,\"topic\":[],\"questionDesc\":\"10 ps bits\",\"max_score\":1,\"name\":\"Examprep_10em_ps_cha8_Q2\",\"title\":\"Copy of - Examprep_10em_ps_cha8_Q2\",\"topicData\":\"(1) topics selected\",\"description\":\"10 PS BITS\",\"category\":\"MCQ\"},\"max_time\":0,\"max_score\":1,\"partial_scoring\":true,\"layout\":\"Horizontal\",\"isShuffleOption\":false,\"questionCount\":1},\"media\":[]},\"medium\":\"English\",\"questionTitle\":\"Copy of - Examprep_10em_ps_cha8_Q2\",\"qlevel\":\"DIFFICULT\",\"subject\":\"Physical Science\",\"board\":\"State (Andhra Pradesh)\",\"templateType\":\"Horizontal\",\"isPartialScore\":true,\"gradeLevel\":[\"Class 10\"],\"isShuffleOption\":false,\"topic\":[\"Classification Of Elements\"],\"questionDesc\":\"10 PS BITS\",\"max_score\":1,\"name\":\"Examprep_10em_ps_cha8_Q2\",\"title\":\"Copy of - Examprep_10em_ps_cha8_Q2\",\"topicData\":\"(0) topics selected\",\"category\":\"MCQ\"},\"max_time\":0,\"max_score\":1,\"partial_scoring\":true,\"layout\":\"Horizontal\",\"isShuffleOption\":false,\"questionCount\":1},\"media\":[]},\"medium\":\"English\",\"questionTitle\":\"Copy of - Examprep_10em_ps_cha8_Q2\",\"qlevel\":\"DIFFICULT\",\"subject\":\"Physical Science\",\"board\":\"State (Andhra Pradesh)\",\"templateType\":\"Horizontal\",\"isPartialScore\":true,\"gradeLevel\":[\"Class 10\"],\"isShuffleOption\":false,\"topic\":[\"Classification Of Elements\"],\"max_score\":1,\"name\":\"Copy of - Examprep_10em_ps_cha8_Q2\",\"title\":\"Copy of - Copy of - Examprep_10em_ps_cha8_Q2\",\"topicData\":\"(1) topics selected\",\"description\":\"10 ps bits\",\"category\":\"MCQ\"},\"max_time\":0,\"max_score\":1,\"partial_scoring\":true,\"layout\":\"Horizontal\",\"isShuffleOption\":false,\"questionCount\":1},\"media\":[]},\"medium\":\"Telugu\",\"questionTitle\":\"Copy of - Copy of - Examprep_10em_ps_cha8_Q2\",\"qlevel\":\"DIFFICULT\",\"subject\":\"Physical Science\",\"board\":\"State (Andhra Pradesh)\",\"templateType\":\"Horizontal\",\"isPartialScore\":true,\"gradeLevel\":[\"Class 10\"],\"isShuffleOption\":false,\"topic\":[\"Classification Of Elements\"],\"questionDesc\":\"10 ps bits\",\"max_score\":1,\"name\":\"Examprep_10tm_ps_cha8_Q2\",\"title\":\"Copy of - Copy of - Examprep_10tm_ps_cha8_Q2\",\"topicData\":\"(1) topics selected\",\"description\":\"10 PS BITS\",\"category\":\"MCQ\"},\"max_time\":0,\"max_score\":1,\"partial_scoring\":true,\"layout\":\"Horizontal\",\"isShuffleOption\":false,\"questionCount\":1},\"media\":[]},\"medium\":\"Telugu\",\"questionTitle\":\"Copy of - Examprep_10tm_ps_cha8_Q2\",\"qlevel\":\"MEDIUM\",\"subject\":\"Physical Science\",\"board\":\"State (Andhra Pradesh)\",\"templateType\":\"Horizontal\",\"isPartialScore\":true,\"gradeLevel\":[\"Class 10\"],\"isShuffleOption\":false,\"topic\":[\"Classification Of Elements\"],\"questionDesc\":\"10 PS BITS\",\"max_score\":1,\"name\":\"Examprep_10em ps_cha4 Q3\",\"title\":\"Examprep_10em ps_cha4 Q3\",\"topicData\":\"(1) topics selected\",\"description\":\"10 ps bits\",\"category\":\"MCQ\"},\"max_time\":0,\"max_score\":1,\"partial_scoring\":true,\"layout\":\"Horizontal\",\"isShuffleOption\":false,\"questionCount\":1},\"media\":[{\"id\":385467069,\"src\":\"/assets/public//content/do_31270089678451507213885/artifact/ice_screenshot_20190217-164338_1550402439889.png\",\"assetId\":\"do_31270089678451507213885\",\"type\":\"image\",\"preload\":false},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true}]},\"medium\":\"Telugu\",\"questionTitle\":\"Examprep_10em ps_cha4 Q3\",\"qlevel\":\"MEDIUM\",\"subject\":\"Physical Science\",\"board\":\"State (Andhra Pradesh)\",\"templateType\":\"Horizontal\",\"isPartialScore\":true,\"gradeLevel\":[\"Class 10\"],\"isShuffleOption\":false,\"topic\":[\"Classification Of Elements\"],\"questionDesc\":\"10 ps bits\",\"max_score\":1,\"name\":\"Examprep_10em ps_cha8- Q3\",\"title\":\"Examprep_10em ps_cha8- Q3\",\"topicData\":\"(1) topics selected\",\"category\":\"MCQ\"},\"max_time\":0,\"max_score\":1,\"partial_scoring\":true,\"layout\":\"Horizontal\",\"isShuffleOption\":false,\"questionCount\":1},\"media\":[{\"id\":385467069,\"src\":\"/assets/public//content/do_31270089678451507213885/artifact/ice_screenshot_20190217-164338_1550402439889.png\",\"assetId\":\"do_31270089678451507213885\",\"type\":\"image\",\"preload\":false},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true}]},\"medium\":\"Telugu\",\"questionTitle\":\"Examprep_10em ps_cha8- Q3\",\"qlevel\":\"DIFFICULT\",\"subject\":\"Physical Science\",\"board\":\"State (Andhra Pradesh)\",\"templateType\":\"Horizontal\",\"isPartialScore\":true,\"gradeLevel\":[\"Class 10\"],\"isShuffleOption\":false,\"topic\":[\"Classification Of Elements\"],\"max_score\":1,\"name\":\"Examprep_10tm ps_cha8- Q3\",\"title\":\"Copy of - Examprep_10tm ps_cha8- Q3\",\"topicData\":\"(1) topics selected\",\"description\":\"10 ps bits\",\"category\":\"MCQ\"},\"max_time\":0,\"max_score\":1,\"partial_scoring\":true,\"layout\":\"Horizontal\",\"isShuffleOption\":false,\"questionCount\":1},\"media\":[{\"id\":385467069,\"src\":\"/assets/public//content/do_31270089678451507213885/artifact/ice_screenshot_20190217-164338_1550402439889.png\",\"assetId\":\"do_31270089678451507213885\",\"type\":\"image\",\"preload\":false},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true}]},\"medium\":\"English\",\"questionTitle\":\"Copy of - Examprep_10tm ps_cha8- Q3\",\"qlevel\":\"MEDIUM\",\"subject\":\"Physical Science\",\"board\":\"State (Andhra Pradesh)\",\"templateType\":\"Horizontal\",\"isPartialScore\":true,\"gradeLevel\":[\"Class 10\"],\"isShuffleOption\":false,\"topic\":[],\"questionDesc\":\"10 ps bits\",\"max_score\":1,\"name\":\"Examprep_10em ps_cha8- Q5\",\"title\":\"Examprep_10em ps_cha8- Q5\",\"topicData\":\"(1) topics selected\",\"description\":\"10 PS BITS\",\"category\":\"MCQ\"},\"max_time\":0,\"max_score\":1,\"partial_scoring\":true,\"layout\":\"Horizontal\",\"isShuffleOption\":false,\"questionCount\":1},\"media\":[{\"id\":385467069,\"src\":\"/assets/public//content/do_31270089678451507213885/artifact/ice_screenshot_20190217-164338_1550402439889.png\",\"assetId\":\"do_31270089678451507213885\",\"type\":\"image\",\"preload\":false},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true}]},\"medium\":\"English\",\"questionTitle\":\"Examprep_10em ps_cha8- Q5\",\"qlevel\":\"MEDIUM\",\"subject\":\"Physical Science\",\"board\":\"State (Andhra Pradesh)\",\"templateType\":\"Horizontal\",\"isPartialScore\":true,\"gradeLevel\":[\"Class 10\"],\"topic\":[\"Structure Of Atom\"],\"questionDesc\":\"10 PS BITS\",\"max_score\":1,\"name\":\"Examprep_10em ps_cha8- Q5\",\"title\":\"Examprep_10em ps_cha8- Q5\",\"description\":\"10 PS BITS\",\"options\":[{\"answer\":true,\"value\":{\"type\":\"text\",\"asset\":\"1\"}}],\"identifier\":\"do_31270597409132544015699\",\"isSelected\":true,\"$$hashKey\":\"object:1142\"}]"},"config":{"__cdata":"{\"title\":\"10 PS BITS\",\"max_score\":1,\"allow_skip\":true,\"show_feedback\":true,\"shuffle_questions\":false,\"shuffle_options\":false,\"total_items\":1,\"btn_edit\":\"Edit\"}"},"org.ekstep.question":[{"id":"6d5e4460-7393-49e9-b158-430441c0e6c0","type":"mcq","pluginId":"org.ekstep.questionunit.mcq","pluginVer":"1.1","templateId":"horizontalMCQ","data":{"__cdata":"{\"question\":{\"text\":\"

Q)జిప్సమ్ మరియు ప్లాస్టర్ ఆఫ్ పారిస్ ల నందు ఉండే నీటి అణువులలో తేడా ఎంత?

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\"},\"options\":[{\"text\":\"

A)3/2

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\",\"isCorrect\":true,\"$$hashKey\":\"object:1034\"},{\"text\":\"

B)1/2

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1035\"},{\"text\":\"

C)2

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1045\"},{\"text\":\"

D)5/2

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1050\"}],\"questionCount\":0,\"media\":[{\"id\":385467069,\"src\":\"/assets/public//content/do_31270089678451507213885/artifact/ice_screenshot_20190217-164338_1550402439889.png\",\"assetId\":\"do_31270089678451507213885\",\"type\":\"image\",\"preload\":false},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true}]}"},"config":{"__cdata":"{\"metadata\":{\"data\":{\"plugin\":{\"id\":\"org.ekstep.questionunit.mcq\",\"version\":\"1.1\",\"templateId\":\"horizontalMCQ\"},\"data\":{\"question\":{\"text\":\"

Q)జిప్సమ్ మరియు ప్లాస్టర్ ఆఫ్ పారిస్ ల నందు ఉండే నీటి అణువులలో తేడా ఎంత?

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\"},\"options\":[{\"text\":\"

A)3/2

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\",\"isCorrect\":true,\"$$hashKey\":\"object:1034\"},{\"text\":\"

B)1/2

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1035\"},{\"text\":\"

C)2

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1045\"},{\"text\":\"

D)5/2

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1050\"}],\"questionCount\":0,\"media\":[{\"id\":385467069,\"src\":\"/assets/public//content/do_31270089678451507213885/artifact/ice_screenshot_20190217-164338_1550402439889.png\",\"assetId\":\"do_31270089678451507213885\",\"type\":\"image\",\"preload\":false},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true}]},\"config\":{\"metadata\":{\"data\":{\"plugin\":{\"id\":\"org.ekstep.questionunit.mcq\",\"version\":\"1.1\",\"templateId\":\"horizontalMCQ\"},\"data\":{\"question\":{\"text\":\"

Q)The difference of the molecules of water in gypsum and plaster       

\\n\\n

  of Paris is

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\"},\"options\":[{\"text\":\"

A)3/2

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\",\"isCorrect\":true,\"$$hashKey\":\"object:1034\"},{\"text\":\"

B)1/2

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1035\"},{\"text\":\"

C)2

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1045\"},{\"text\":\"

D)5/2

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1050\"}],\"questionCount\":0,\"media\":[{\"id\":385467069,\"src\":\"/assets/public//content/do_31270089678451507213885/artifact/ice_screenshot_20190217-164338_1550402439889.png\",\"assetId\":\"do_31270089678451507213885\",\"type\":\"image\",\"preload\":false},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true}]},\"config\":{\"metadata\":{\"data\":{\"plugin\":{\"id\":\"org.ekstep.questionunit.mcq\",\"version\":\"1.1\",\"templateId\":\"horizontalMCQ\"},\"data\":{\"question\":{\"text\":\"

Q) కార్బన్ ఎలక్ట్రాన్ విన్యాసాన్ని ఈ క్రింది విధంగా రాయరాదని సాత్విక చెప్పింది. ఎందువలన?

\\n\",\"image\":\"/assets/public//content/do_31270089678451507213885/artifact/ice_screenshot_20190217-164338_1550402439889.png\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\"},\"options\":[{\"text\":\"

A)ఆఫ్  బౌ నియమాన్ని పాటించుట లేదు

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1034\"},{\"text\":\"

B)హుండు నియమాన్ని పాటించుట లేదు

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\",\"isCorrect\":true,\"$$hashKey\":\"object:1035\"},{\"text\":\"

C)పౌలీవర్జన నియమాన్ని పాటించుట లేదు

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1045\"},{\"text\":\"

D)పై వన్నీ సరైనవే

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1050\"}],\"questionCount\":0,\"media\":[{\"id\":385467069,\"src\":\"/assets/public//content/do_31270089678451507213885/artifact/ice_screenshot_20190217-164338_1550402439889.png\",\"assetId\":\"do_31270089678451507213885\",\"type\":\"image\",\"preload\":false},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true}]},\"config\":{\"metadata\":{\"data\":{\"plugin\":{\"id\":\"org.ekstep.questionunit.mcq\",\"version\":\"1.1\",\"templateId\":\"horizontalMCQ\"},\"data\":{\"question\":{\"text\":\"

Q) Satwika told that the electronic configuration of carbon cannot be written like this……why?

\\n\\n

Be cause this electronic configuration does not obey

\\n\",\"image\":\"/assets/public//content/do_31270089678451507213885/artifact/ice_screenshot_20190217-164338_1550402439889.png\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\"},\"options\":[{\"text\":\"

A) aufbau principle  

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1034\"},{\"text\":\"

B) Hund”s rule

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\",\"isCorrect\":true,\"$$hashKey\":\"object:1035\"},{\"text\":\"

C) Paul”s exclusion princple 

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1045\"},{\"text\":\"

D) All the above

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1050\"}],\"questionCount\":0,\"media\":[{\"id\":385467069,\"src\":\"/assets/public//content/do_31270089678451507213885/artifact/ice_screenshot_20190217-164338_1550402439889.png\",\"assetId\":\"do_31270089678451507213885\",\"type\":\"image\",\"preload\":false},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true}]},\"config\":{\"metadata\":{\"data\":{\"plugin\":{\"id\":\"org.ekstep.questionunit.mcq\",\"version\":\"1.1\",\"templateId\":\"horizontalMCQ\"},\"data\":{\"question\":{\"text\":\"

Q) Satwika told that the electronic configuration of carbon cannot be written like this……why?

\\n\\n

Be cause this electronic configuration does not obey

\\n\",\"image\":\"/assets/public//content/do_31270089678451507213885/artifact/ice_screenshot_20190217-164338_1550402439889.png\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\"},\"options\":[{\"text\":\"

A) aufbau principle  

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1034\"},{\"text\":\"

B) Hund”s rule

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\",\"isCorrect\":true,\"$$hashKey\":\"object:1035\"},{\"text\":\"

C) Paul”s exclusion princple 

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1045\"},{\"text\":\"

D) All the above

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1050\"}],\"questionCount\":0,\"media\":[]},\"config\":{\"metadata\":{\"data\":{\"plugin\":{\"id\":\"org.ekstep.questionunit.mcq\",\"version\":\"1.1\",\"templateId\":\"horizontalMCQ\"},\"data\":{\"question\":{\"text\":\"

Q) ఒక మూలకం పరమాణువ సంఖ్య 20 అయినా ఆవర్తన పట్టికలో ఆ మూలక స్థానం

\\n\\n
    \\n\\t
  1. 4 వ పీరియడ్ మరియు 2 వ గ్రూప్  B) 1 వ పీరియడ్ మరియు 2 వ గ్రూప్ C) 3 వ పీరియడ్ మరియు 13 వ గ్రూప్
  2. \\n
\\n\\n

D) 2 వ పీరియడ్ మరియు 2 వ గ్రూప్

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\"},\"options\":[{\"text\":\"

A) 4 వ పీరియడ్ మరియు 2 వ గ్రూప్  

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\",\"isCorrect\":true,\"$$hashKey\":\"object:1034\"},{\"text\":\"

B) 1 వ పీరియడ్ మరియు 2 వ గ్రూప్

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1035\"},{\"text\":\"

C) 3 వ పీరియడ్ మరియు 13 వ గ్రూప్

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1045\"},{\"text\":\"

D) 2 వ పీరియడ్ మరియు 2 వ గ్రూప్

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1050\"}],\"questionCount\":0,\"media\":[]},\"config\":{\"metadata\":{\"data\":{\"plugin\":{\"id\":\"org.ekstep.questionunit.mcq\",\"version\":\"1.1\",\"templateId\":\"horizontalMCQ\"},\"data\":{\"question\":{\"text\":\"

Q) If an atomic number of an element is 20 then what is the position of that element in a modern periodic table

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\"},\"options\":[{\"text\":\"

A) 4th period and 2nd group  

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\",\"isCorrect\":true,\"$$hashKey\":\"object:1034\"},{\"text\":\"

B) 1st period and 2nd group

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1035\"},{\"text\":\"

C) 3rd period and 13th group

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1045\"},{\"text\":\"

 D) 2nd period and 2nd group

\\n\\n

 

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1050\"}],\"questionCount\":0,\"media\":[]},\"config\":{\"metadata\":{\"data\":{\"plugin\":{\"id\":\"org.ekstep.questionunit.mcq\",\"version\":\"1.1\",\"templateId\":\"horizontalMCQ\"},\"data\":{\"question\":{\"text\":\"

Q) In a Doberiner triad the atomic weight of first and third elements are 32 and 125 , then what is the atomic weight of second one

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\"},\"options\":[{\"text\":\"

A) 78.5

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\",\"isCorrect\":true,\"$$hashKey\":\"object:1034\"},{\"text\":\"

B)80 

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1035\"},{\"text\":\"

C)23

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1045\"},{\"text\":\"

D)60

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1050\"}],\"questionCount\":0,\"media\":[]},\"config\":{\"metadata\":{\"data\":{\"plugin\":{\"id\":\"org.ekstep.questionunit.mcq\",\"version\":\"1.1\",\"templateId\":\"horizontalMCQ\"},\"data\":{\"question\":{\"text\":\"

Q) Find the frequency of a radiowave of wave lenth 100 metres?

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\"},\"options\":[{\"text\":\"

A) 3 HZ  

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1034\"},{\"text\":\"

B)3 KHZ  

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1035\"},{\"text\":\"

C)3 MHZ 

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"isCorrect\":true,\"$$hashKey\":\"object:1045\"},{\"text\":\"

D)30 MHZ

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1050\"}],\"questionCount\":0,\"media\":[]},\"config\":{\"metadata\":{\"data\":{\"plugin\":{\"id\":\"org.ekstep.questionunit.mcq\",\"version\":\"1.1\",\"templateId\":\"horizontalMCQ\"},\"data\":{\"question\":{\"text\":\"

 Q .When l=2 , what is the maximum value of ml

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\"},\"options\":[{\"text\":\"

A) 2

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\",\"isCorrect\":true,\"$$hashKey\":\"object:1034\"},{\"text\":\"

B)-2

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1035\"},{\"text\":\"

C)2(2)+1

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1045\"},{\"text\":\"

D)0

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1050\"}],\"questionCount\":0,\"media\":[]},\"config\":{\"metadata\":{\"data\":{\"plugin\":{\"id\":\"org.ekstep.questionunit.mcq\",\"version\":\"1.1\",\"templateId\":\"horizontalMCQ\"},\"data\":{\"question\":{\"text\":\"

1) l=2 అయినపుడు  యొక్క mlగరిష్ట విలువ ఎంత?

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\"},\"options\":[{\"text\":\"

A) 2

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\",\"isCorrect\":true,\"$$hashKey\":\"object:1034\"},{\"text\":\"

B)-2

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1035\"},{\"text\":\"

C)2(2)+1

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1045\"},{\"text\":\"

D)0

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1050\"}],\"questionCount\":0,\"media\":[]},\"config\":{\"metadata\":{\"data\":{\"plugin\":{\"id\":\"org.ekstep.questionunit.mcq\",\"version\":\"1.1\",\"templateId\":\"horizontalMCQ\"},\"data\":{\"question\":{\"text\":\"

1) l=2 అయినపుడు  యొక్క mlగరిష్ట విలువ ఎంత?

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\"},\"options\":[{\"text\":\"

A) 2

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1034\"},{\"text\":\"

B)-2

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1035\"},{\"text\":\"

C)2(2)+1

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"isCorrect\":true,\"$$hashKey\":\"object:1045\"},{\"text\":\"

D)0

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1050\"}],\"questionCount\":0,\"media\":[]},\"config\":{\"metadata\":{\"max_score\":1,\"isShuffleOption\":false,\"isPartialScore\":true,\"templateType\":\"Horizontal\",\"name\":\"\\n\\tఈ క్రింది వాటిలో విశిష్టోష్ణానికి ప్రమాణం\\n\\n\",\"title\":\"Copy of - \\n\\tఈ క్రింది వాటిలో విశిష్టోష్ణానికి ప్రమాణం\\n\\n\",\"board\":\"State (Andhra Pradesh)\",\"topic\":[],\"medium\":\"Telugu\",\"gradeLevel\":[\"Class 10\"],\"subject\":\"Physical Science\",\"qlevel\":\"EASY\",\"description\":\"10 th ps bits\",\"category\":\"MCQ\",\"topicData\":\"(0) topics selected\"},\"max_time\":0,\"max_score\":1,\"partial_scoring\":true,\"layout\":\"Horizontal\",\"isShuffleOption\":false,\"questionCount\":1},\"media\":[]},\"medium\":\"Telugu\",\"questionTitle\":\"Copy of - \\n\\tఈ క్రింది వాటిలో విశిష్టోష్ణానికి ప్రమాణం\\n\\n\",\"qlevel\":\"MEDIUM\",\"subject\":\"Physical Science\",\"board\":\"State (Andhra Pradesh)\",\"templateType\":\"Horizontal\",\"isPartialScore\":true,\"gradeLevel\":[\"Class 10\"],\"isShuffleOption\":false,\"topic\":[\"Structure Of Atom\"],\"questionDesc\":\"10 th ps bits\",\"max_score\":1,\"name\":\"Examprep_10tm_ps_cha8_Q1\",\"title\":\"Copy of - Copy of - Copy of - Examprep_10tm_ps_cha8_Q1\",\"topicData\":\"(0) topics selected\",\"description\":\"10 PS BITS\",\"category\":\"MCQ\"},\"max_time\":0,\"max_score\":1,\"partial_scoring\":true,\"layout\":\"Horizontal\",\"isShuffleOption\":false,\"questionCount\":1},\"media\":[]},\"medium\":\"Telugu\",\"questionTitle\":\"Copy of - Examprep_10tm_ps_cha8_Q1\",\"qlevel\":\"MEDIUM\",\"subject\":\"Physical Science\",\"board\":\"State (Andhra Pradesh)\",\"templateType\":\"Horizontal\",\"isPartialScore\":true,\"gradeLevel\":[\"Class 10\"],\"isShuffleOption\":false,\"topic\":[\"Structure Of Atom\"],\"questionDesc\":\"10 PS BITS\",\"max_score\":1,\"name\":\"Examprep_10tm_ps_cha8_Q1\",\"title\":\"Copy of - Examprep_10tm_ps_cha8_Q1\",\"topicData\":\"(1) topics selected\",\"category\":\"MCQ\"},\"max_time\":0,\"max_score\":1,\"partial_scoring\":true,\"layout\":\"Horizontal\",\"isShuffleOption\":false,\"questionCount\":1},\"media\":[]},\"medium\":\"Telugu\",\"questionTitle\":\"Copy of - Examprep_10tm_ps_cha8_Q1\",\"qlevel\":\"DIFFICULT\",\"subject\":\"Physical Science\",\"board\":\"State (Andhra Pradesh)\",\"templateType\":\"Horizontal\",\"isPartialScore\":true,\"gradeLevel\":[\"Class 10\"],\"isShuffleOption\":false,\"topic\":[\"Structure Of Atom\"],\"max_score\":1,\"name\":\"Examprep_10em_ps_cha8_Q1\",\"title\":\"Copy of - Examprep_10em_ps_cha8_Q1\",\"topicData\":\"(1) topics selected\",\"description\":\"10 ps bits\",\"category\":\"MCQ\"},\"max_time\":0,\"max_score\":1,\"partial_scoring\":true,\"layout\":\"Horizontal\",\"isShuffleOption\":false,\"questionCount\":1},\"media\":[]},\"medium\":\"English\",\"questionTitle\":\"Copy of - Examprep_10em_ps_cha8_Q1\",\"qlevel\":\"DIFFICULT\",\"subject\":\"Physical Science\",\"board\":\"State (Andhra Pradesh)\",\"templateType\":\"Horizontal\",\"isPartialScore\":true,\"gradeLevel\":[\"Class 10\"],\"isShuffleOption\":false,\"topic\":[],\"questionDesc\":\"10 ps bits\",\"max_score\":1,\"name\":\"Examprep_10em_ps_cha8_Q2\",\"title\":\"Copy of - Examprep_10em_ps_cha8_Q2\",\"topicData\":\"(1) topics selected\",\"description\":\"10 PS BITS\",\"category\":\"MCQ\"},\"max_time\":0,\"max_score\":1,\"partial_scoring\":true,\"layout\":\"Horizontal\",\"isShuffleOption\":false,\"questionCount\":1},\"media\":[]},\"medium\":\"English\",\"questionTitle\":\"Copy of - Examprep_10em_ps_cha8_Q2\",\"qlevel\":\"DIFFICULT\",\"subject\":\"Physical Science\",\"board\":\"State (Andhra Pradesh)\",\"templateType\":\"Horizontal\",\"isPartialScore\":true,\"gradeLevel\":[\"Class 10\"],\"isShuffleOption\":false,\"topic\":[\"Classification Of Elements\"],\"questionDesc\":\"10 PS BITS\",\"max_score\":1,\"name\":\"Examprep_10em_ps_cha8_Q2\",\"title\":\"Copy of - Examprep_10em_ps_cha8_Q2\",\"topicData\":\"(0) topics selected\",\"category\":\"MCQ\"},\"max_time\":0,\"max_score\":1,\"partial_scoring\":true,\"layout\":\"Horizontal\",\"isShuffleOption\":false,\"questionCount\":1},\"media\":[]},\"medium\":\"English\",\"questionTitle\":\"Copy of - Examprep_10em_ps_cha8_Q2\",\"qlevel\":\"DIFFICULT\",\"subject\":\"Physical Science\",\"board\":\"State (Andhra Pradesh)\",\"templateType\":\"Horizontal\",\"isPartialScore\":true,\"gradeLevel\":[\"Class 10\"],\"isShuffleOption\":false,\"topic\":[\"Classification Of Elements\"],\"max_score\":1,\"name\":\"Copy of - Examprep_10em_ps_cha8_Q2\",\"title\":\"Copy of - Copy of - Examprep_10em_ps_cha8_Q2\",\"topicData\":\"(1) topics selected\",\"description\":\"10 ps bits\",\"category\":\"MCQ\"},\"max_time\":0,\"max_score\":1,\"partial_scoring\":true,\"layout\":\"Horizontal\",\"isShuffleOption\":false,\"questionCount\":1},\"media\":[]},\"medium\":\"Telugu\",\"questionTitle\":\"Copy of - Copy of - Examprep_10em_ps_cha8_Q2\",\"qlevel\":\"DIFFICULT\",\"subject\":\"Physical Science\",\"board\":\"State (Andhra Pradesh)\",\"templateType\":\"Horizontal\",\"isPartialScore\":true,\"gradeLevel\":[\"Class 10\"],\"isShuffleOption\":false,\"topic\":[\"Classification Of Elements\"],\"questionDesc\":\"10 ps bits\",\"max_score\":1,\"name\":\"Examprep_10tm_ps_cha8_Q2\",\"title\":\"Copy of - Copy of - Examprep_10tm_ps_cha8_Q2\",\"topicData\":\"(1) topics selected\",\"description\":\"10 PS BITS\",\"category\":\"MCQ\"},\"max_time\":0,\"max_score\":1,\"partial_scoring\":true,\"layout\":\"Horizontal\",\"isShuffleOption\":false,\"questionCount\":1},\"media\":[]},\"medium\":\"Telugu\",\"questionTitle\":\"Copy of - Examprep_10tm_ps_cha8_Q2\",\"qlevel\":\"MEDIUM\",\"subject\":\"Physical Science\",\"board\":\"State (Andhra Pradesh)\",\"templateType\":\"Horizontal\",\"isPartialScore\":true,\"gradeLevel\":[\"Class 10\"],\"isShuffleOption\":false,\"topic\":[\"Classification Of Elements\"],\"questionDesc\":\"10 PS BITS\",\"max_score\":1,\"name\":\"Examprep_10em ps_cha4 Q3\",\"title\":\"Examprep_10em ps_cha4 Q3\",\"topicData\":\"(1) topics selected\",\"description\":\"10 ps bits\",\"category\":\"MCQ\"},\"max_time\":0,\"max_score\":1,\"partial_scoring\":true,\"layout\":\"Horizontal\",\"isShuffleOption\":false,\"questionCount\":1},\"media\":[{\"id\":385467069,\"src\":\"/assets/public//content/do_31270089678451507213885/artifact/ice_screenshot_20190217-164338_1550402439889.png\",\"assetId\":\"do_31270089678451507213885\",\"type\":\"image\",\"preload\":false},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true}]},\"medium\":\"Telugu\",\"questionTitle\":\"Examprep_10em ps_cha4 Q3\",\"qlevel\":\"MEDIUM\",\"subject\":\"Physical Science\",\"board\":\"State (Andhra Pradesh)\",\"templateType\":\"Horizontal\",\"isPartialScore\":true,\"gradeLevel\":[\"Class 10\"],\"isShuffleOption\":false,\"topic\":[\"Classification Of Elements\"],\"questionDesc\":\"10 ps bits\",\"max_score\":1,\"name\":\"Examprep_10em ps_cha8- Q3\",\"title\":\"Examprep_10em ps_cha8- Q3\",\"topicData\":\"(1) topics selected\",\"category\":\"MCQ\"},\"max_time\":0,\"max_score\":1,\"partial_scoring\":true,\"layout\":\"Horizontal\",\"isShuffleOption\":false,\"questionCount\":1},\"media\":[{\"id\":385467069,\"src\":\"/assets/public//content/do_31270089678451507213885/artifact/ice_screenshot_20190217-164338_1550402439889.png\",\"assetId\":\"do_31270089678451507213885\",\"type\":\"image\",\"preload\":false},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true}]},\"medium\":\"Telugu\",\"questionTitle\":\"Examprep_10em ps_cha8- Q3\",\"qlevel\":\"DIFFICULT\",\"subject\":\"Physical Science\",\"board\":\"State (Andhra Pradesh)\",\"templateType\":\"Horizontal\",\"isPartialScore\":true,\"gradeLevel\":[\"Class 10\"],\"isShuffleOption\":false,\"topic\":[\"Classification Of Elements\"],\"max_score\":1,\"name\":\"Examprep_10tm ps_cha8- Q3\",\"title\":\"Copy of - Examprep_10tm ps_cha8- Q3\",\"topicData\":\"(1) topics selected\",\"description\":\"10 ps bits\",\"category\":\"MCQ\"},\"max_time\":0,\"max_score\":1,\"partial_scoring\":true,\"layout\":\"Horizontal\",\"isShuffleOption\":false,\"questionCount\":1},\"media\":[{\"id\":385467069,\"src\":\"/assets/public//content/do_31270089678451507213885/artifact/ice_screenshot_20190217-164338_1550402439889.png\",\"assetId\":\"do_31270089678451507213885\",\"type\":\"image\",\"preload\":false},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true}]},\"medium\":\"English\",\"questionTitle\":\"Copy of - Examprep_10tm ps_cha8- Q3\",\"qlevel\":\"MEDIUM\",\"subject\":\"Physical Science\",\"board\":\"State (Andhra Pradesh)\",\"templateType\":\"Horizontal\",\"isPartialScore\":true,\"gradeLevel\":[\"Class 10\"],\"isShuffleOption\":false,\"topic\":[],\"questionDesc\":\"10 ps bits\",\"max_score\":1,\"name\":\"Examprep_10em ps_cha8- Q5\",\"title\":\"Examprep_10em ps_cha8- Q5\",\"topicData\":\"(1) topics selected\",\"description\":\"10 PS BITS\",\"category\":\"MCQ\"},\"max_time\":0,\"max_score\":1,\"partial_scoring\":true,\"layout\":\"Horizontal\",\"isShuffleOption\":false,\"questionCount\":1},\"media\":[{\"id\":385467069,\"src\":\"/assets/public//content/do_31270089678451507213885/artifact/ice_screenshot_20190217-164338_1550402439889.png\",\"assetId\":\"do_31270089678451507213885\",\"type\":\"image\",\"preload\":false},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true}]},\"medium\":\"English\",\"questionTitle\":\"Examprep_10em ps_cha8- Q5\",\"qlevel\":\"MEDIUM\",\"subject\":\"Physical Science\",\"board\":\"State (Andhra Pradesh)\",\"templateType\":\"Horizontal\",\"isPartialScore\":true,\"gradeLevel\":[\"Class 10\"],\"isShuffleOption\":false,\"topic\":[\"Structure Of Atom\"],\"questionDesc\":\"10 PS BITS\",\"max_score\":1,\"name\":\"Examprep_10em ps_cha8- Q5\",\"title\":\"Examprep_10em ps_cha8- Q5\",\"description\":\"10 PS BITS\",\"category\":\"MCQ\"},\"max_time\":0,\"max_score\":1,\"partial_scoring\":true,\"layout\":\"Horizontal\",\"isShuffleOption\":false,\"questionCount\":1}"},"w":80,"h":85,"x":9,"y":6}]}]},{"x":0,"y":0,"w":100,"h":100,"id":"85c23e06-51c9-416b-ac9b-b9d5386a37ee","rotate":null,"config":{"__cdata":"{\"opacity\":100,\"strokeWidth\":1,\"stroke\":\"rgba(255, 255, 255, 0)\",\"autoplay\":false,\"visible\":true,\"color\":\"#FFFFFF\",\"genieControls\":false,\"instructions\":\"\"}"},"param":[{"name":"previous","value":"b8b47094-1d69-43a1-9c88-c02e760996c5"}],"manifest":{"media":[{"assetId":"8dbbc49b-6776-458c-b211-c1a260437421"}]},"org.ekstep.video":[{"y":7.9,"x":10.97,"w":78.4,"h":79.51,"rotate":0,"z-index":0,"id":"8dbbc49b-6776-458c-b211-c1a260437421","config":{"__cdata":"{\"autoplay\":true,\"controls\":true,\"muted\":false,\"visible\":true,\"url\":\"https://ntpproductionall.blob.core.windows.net/ntp-content-production/content/do_3128517134878638081614/artifact/ms-new-bit-1.mp4\"}"}}]}],"manifest":{"media":[{"id":"2381510b-4d49-47d5-b765-c130233028dc","plugin":"org.ekstep.navigation","ver":"1.0","src":"/content-plugins/org.ekstep.navigation-1.0/renderer/controller/navigation_ctrl.js","type":"js"},{"id":"e34e434b-c4de-4907-b025-56c156957112","plugin":"org.ekstep.navigation","ver":"1.0","src":"/content-plugins/org.ekstep.navigation-1.0/renderer/templates/navigation.html","type":"js"},{"id":"org.ekstep.navigation","plugin":"org.ekstep.navigation","ver":"1.0","src":"/content-plugins/org.ekstep.navigation-1.0/renderer/plugin.js","type":"plugin"},{"id":"org.ekstep.navigation_manifest","plugin":"org.ekstep.navigation","ver":"1.0","src":"/content-plugins/org.ekstep.navigation-1.0/manifest.json","type":"json"},{"id":"org.ekstep.questionunit.renderer.audioicon","plugin":"org.ekstep.questionunit","ver":"1.0","src":"/content-plugins/org.ekstep.questionunit-1.0/renderer/assets/audio-icon.png","type":"image"},{"id":"org.ekstep.questionunit.renderer.downarrow","plugin":"org.ekstep.questionunit","ver":"1.0","src":"/content-plugins/org.ekstep.questionunit-1.0/renderer/assets/down_arrow.png","type":"image"},{"id":"aa57e9c4-e978-4ab1-be74-09a657aa092c","plugin":"org.ekstep.questionunit","ver":"1.0","src":"/content-plugins/org.ekstep.questionunit-1.0/renderer/components/js/components.js","type":"js"},{"id":"07bac729-0b71-42d1-b4e3-d5398474d0d0","plugin":"org.ekstep.questionunit","ver":"1.0","src":"/content-plugins/org.ekstep.questionunit-1.0/renderer/components/css/components.css","type":"css"},{"id":"6b74a0ea-d9dc-4ae9-9134-abe3124260da","plugin":"org.ekstep.questionunit","ver":"1.0","src":"/content-plugins/org.ekstep.questionunit-1.0/renderer/libs/katex/katex.min.js","type":"js"},{"id":"d5b4d576-7477-490b-9713-d9857b5beab1","plugin":"org.ekstep.questionunit","ver":"1.0","src":"/content-plugins/org.ekstep.questionunit-1.0/renderer/libs/katex/katex.min.css","type":"css"},{"id":"8aa00822-5d00-400c-bdbd-9847737db6a5","plugin":"org.ekstep.questionunit","ver":"1.0","src":"/content-plugins/org.ekstep.questionunit-1.0/renderer/libs/katex/fonts/katex_main-bold.ttf","type":"js"},{"id":"bdace240-7a3b-4b7b-b9d1-564747334578","plugin":"org.ekstep.questionunit","ver":"1.0","src":"/content-plugins/org.ekstep.questionunit-1.0/renderer/libs/katex/fonts/katex_main-bolditalic.ttf","type":"js"},{"id":"363edb27-4782-4ee5-b7c7-cb8e85e29d52","plugin":"org.ekstep.questionunit","ver":"1.0","src":"/content-plugins/org.ekstep.questionunit-1.0/renderer/libs/katex/fonts/katex_main-italic.ttf","type":"js"},{"id":"939ed1b9-7e48-4c1d-aeee-831d44698d6c","plugin":"org.ekstep.questionunit","ver":"1.0","src":"/content-plugins/org.ekstep.questionunit-1.0/renderer/libs/katex/fonts/katex_main-regular.ttf","type":"js"},{"id":"9e8dca64-6a00-4dde-b840-3ee96d50a2b8","plugin":"org.ekstep.questionunit","ver":"1.0","src":"/content-plugins/org.ekstep.questionunit-1.0/renderer/libs/katex/fonts/katex_math-bolditalic.ttf","type":"js"},{"id":"d7135270-d403-48ce-be16-c449d26e6da7","plugin":"org.ekstep.questionunit","ver":"1.0","src":"/content-plugins/org.ekstep.questionunit-1.0/renderer/libs/katex/fonts/katex_math-italic.ttf","type":"js"},{"id":"3d410ae3-3072-43d8-b452-9620eb5585ed","plugin":"org.ekstep.questionunit","ver":"1.0","src":"/content-plugins/org.ekstep.questionunit-1.0/renderer/libs/katex/fonts/katex_math-regular.ttf","type":"js"},{"id":"5447cb80-7597-411c-90dc-13a0c02cc863","plugin":"org.ekstep.questionunit","ver":"1.0","src":"/content-plugins/org.ekstep.questionunit-1.0/renderer/libs/katex/fonts/katex_size1-regular.ttf","type":"js"},{"id":"08358247-a9e0-4cb3-8bb3-9b97b7d6174e","plugin":"org.ekstep.questionunit","ver":"1.0","src":"/content-plugins/org.ekstep.questionunit-1.0/renderer/libs/katex/fonts/katex_size2-regular.ttf","type":"js"},{"id":"03556441-a8cd-45a6-93b7-6e98743490b1","plugin":"org.ekstep.questionunit","ver":"1.0","src":"/content-plugins/org.ekstep.questionunit-1.0/renderer/libs/katex/fonts/katex_size3-regular.ttf","type":"js"},{"id":"4713a36c-49dc-4b66-a387-79e04c050598","plugin":"org.ekstep.questionunit","ver":"1.0","src":"/content-plugins/org.ekstep.questionunit-1.0/renderer/libs/katex/fonts/katex_size4-regular.ttf","type":"js"},{"id":"org.ekstep.questionunit","plugin":"org.ekstep.questionunit","ver":"1.0","src":"/content-plugins/org.ekstep.questionunit-1.0/renderer/plugin.js","type":"plugin"},{"id":"org.ekstep.questionunit_manifest","plugin":"org.ekstep.questionunit","ver":"1.0","src":"/content-plugins/org.ekstep.questionunit-1.0/manifest.json","type":"json"},{"id":"b6a388cc-6d04-4cb1-9ca0-5919a75ddb9c","plugin":"org.ekstep.questionunit.mcq","ver":"1.1","src":"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/styles/style.css","type":"css"},{"id":"97b66a43-6b00-4753-aebc-d2b270e1c702","plugin":"org.ekstep.questionunit.mcq","ver":"1.1","src":"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/styles/horizontal_and_vertical.css","type":"css"},{"id":"367e9c5d-aa82-436c-9767-31a4e3d89356","plugin":"org.ekstep.questionunit.mcq","ver":"1.1","src":"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/template/mcq-layouts.js","type":"js"},{"id":"95e6a633-711c-4e51-ae0a-4207874b6be9","plugin":"org.ekstep.questionunit.mcq","ver":"1.1","src":"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/template/template_controller.js","type":"js"},{"id":"28903dd9-4012-4866-9d1c-061a53db19a3","plugin":"org.ekstep.questionunit.mcq","ver":"1.1","src":"/content-plugins/org.ekstep.questionunit.mcq-1.1//renderer/assets/tick_icon.png","type":"image"},{"id":"008a394a-399c-4010-9b83-a727abe607c0","plugin":"org.ekstep.questionunit.mcq","ver":"1.1","src":"/content-plugins/org.ekstep.questionunit.mcq-1.1//renderer/assets/audio-icon2.png","type":"image"},{"id":"030d52c7-669a-49e7-ad10-2632bdb949e8","plugin":"org.ekstep.questionunit.mcq","ver":"1.1","src":"/content-plugins/org.ekstep.questionunit.mcq-1.1//renderer/assets/music-blue.png","type":"image"},{"id":"org.ekstep.questionunit.mcq","plugin":"org.ekstep.questionunit.mcq","ver":"1.1","src":"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/plugin.js","type":"plugin"},{"id":"org.ekstep.questionunit.mcq_manifest","plugin":"org.ekstep.questionunit.mcq","ver":"1.1","src":"/content-plugins/org.ekstep.questionunit.mcq-1.1/manifest.json","type":"json"},{"id":"org.ekstep.questionset.quiz","plugin":"org.ekstep.questionset.quiz","ver":"1.0","src":"/content-plugins/org.ekstep.questionset.quiz-1.0/renderer/plugin.js","type":"plugin"},{"id":"org.ekstep.questionset.quiz_manifest","plugin":"org.ekstep.questionset.quiz","ver":"1.0","src":"/content-plugins/org.ekstep.questionset.quiz-1.0/manifest.json","type":"json"},{"id":"org.ekstep.iterator","plugin":"org.ekstep.iterator","ver":"1.0","src":"/content-plugins/org.ekstep.iterator-1.0/renderer/plugin.js","type":"plugin"},{"id":"org.ekstep.iterator_manifest","plugin":"org.ekstep.iterator","ver":"1.0","src":"/content-plugins/org.ekstep.iterator-1.0/manifest.json","type":"json"},{"id":"ec943bc6-5684-483c-95a0-db88990c2ea6","plugin":"org.ekstep.questionset","ver":"1.0","src":"/content-plugins/org.ekstep.questionset-1.0/renderer/utils/telemetry_logger.js","type":"js"},{"id":"87862711-b63c-4140-a355-dd4cd5d336b4","plugin":"org.ekstep.questionset","ver":"1.0","src":"/content-plugins/org.ekstep.questionset-1.0/renderer/utils/html_audio_plugin.js","type":"js"},{"id":"4b619cde-e3da-429c-bc05-b329365bc9b0","plugin":"org.ekstep.questionset","ver":"1.0","src":"/content-plugins/org.ekstep.questionset-1.0/renderer/utils/qs_feedback_popup.js","type":"js"},{"id":"org.ekstep.questionset","plugin":"org.ekstep.questionset","ver":"1.0","src":"/content-plugins/org.ekstep.questionset-1.0/renderer/plugin.js","type":"plugin"},{"id":"org.ekstep.questionset_manifest","plugin":"org.ekstep.questionset","ver":"1.0","src":"/content-plugins/org.ekstep.questionset-1.0/manifest.json","type":"json"},{"id":"2caa1e61-bc5a-4104-a74c-65dcda49a148","plugin":"org.ekstep.video","ver":"1.3","src":"/content-plugins/org.ekstep.video-1.3/renderer/libs/video.js","type":"js"},{"id":"7a98d0c5-d4c0-4eda-bd52-5c793ab29aab","plugin":"org.ekstep.video","ver":"1.3","src":"/content-plugins/org.ekstep.video-1.3/renderer/libs/videoyoutube.js","type":"js"},{"id":"026257be-900a-4929-ae15-8ba4632d1225","plugin":"org.ekstep.video","ver":"1.3","src":"/content-plugins/org.ekstep.video-1.3/renderer/libs/videojs.css","type":"css"},{"id":"org.ekstep.video","plugin":"org.ekstep.video","ver":"1.3","src":"/content-plugins/org.ekstep.video-1.3/renderer/videoplugin.js","type":"plugin"},{"id":"org.ekstep.video_manifest","plugin":"org.ekstep.video","ver":"1.3","src":"/content-plugins/org.ekstep.video-1.3/manifest.json","type":"json"},{"id":385467069,"src":"/assets/public//content/do_31270089678451507213885/artifact/ice_screenshot_20190217-164338_1550402439889.png","assetId":"do_31270089678451507213885","type":"image","preload":false},{"id":"QuizImage","src":"/content-plugins/org.ekstep.questionset-1.0/editor/assets/quizimage.png","assetId":"QuizImage","type":"image","preload":true},{"id":"org.ekstep.questionset.audioicon","src":"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png","assetId":"org.ekstep.questionset.audioicon","type":"image","preload":true},{"id":"org.ekstep.questionset.default-imgageicon","src":"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png","assetId":"org.ekstep.questionset.default-imgageicon","type":"image","preload":true},{"id":"8dbbc49b-6776-458c-b211-c1a260437421","src":"https://ntpproductionall.blob.core.windows.net/ntp-content-production/content/do_3128517134878638081614/artifact/ms-new-bit-1.mp4","assetId":"8dbbc49b-6776-458c-b211-c1a260437421","type":"video"}]},"plugin-manifest":{"plugin":[{"id":"org.ekstep.navigation","ver":"1.0","type":"plugin","depends":""},{"id":"org.ekstep.questionunit","ver":"1.0","type":"plugin","depends":""},{"id":"org.ekstep.questionunit.mcq","ver":"1.1","type":"plugin","depends":"org.ekstep.questionunit"},{"id":"org.ekstep.questionset.quiz","ver":"1.0","type":"plugin","depends":""},{"id":"org.ekstep.iterator","ver":"1.0","type":"plugin","depends":""},{"id":"org.ekstep.questionset","ver":"1.0","type":"plugin","depends":"org.ekstep.questionset.quiz,org.ekstep.iterator"},{"id":"org.ekstep.video","ver":"1.3","type":"widget","depends":""}]},"compatibilityVersion":4}} | [{"src":"https://ntpproductionall.blob.core.windows.net/ntp-content-production/content/do_3128517134878638081614/artifact/ms-new-bit-1.mp4","id":"8dbbc49b-6776-458c-b211-c1a260437421","type":"video"}] | {"theme":{"id":"theme","version":"1.0","startStage":"b8b47094-1d69-43a1-9c88-c02e760996c5","stage":[{"x":0,"y":0,"w":100,"h":100,"id":"b8b47094-1d69-43a1-9c88-c02e760996c5","rotate":null,"config":{"__cdata":"{\"opacity\":100,\"strokeWidth\":1,\"stroke\":\"rgba(255, 255, 255, 0)\",\"autoplay\":false,\"visible\":true,\"color\":\"#FFFFFF\",\"genieControls\":false,\"instructions\":\"\"}"},"param":[{"name":"next","value":"85c23e06-51c9-416b-ac9b-b9d5386a37ee"}],"manifest":{"media":[{"assetId":"385467069"},{"assetId":"QuizImage"},{"assetId":"org.ekstep.questionset.audioicon"},{"assetId":"org.ekstep.questionset.default-imgageicon"}]},"org.ekstep.questionset":[{"x":9,"y":6,"w":80,"h":85,"rotate":0,"z-index":0,"id":"b27c19a3-768b-4b07-9809-00d7a1e0e5c6","data":{"__cdata":"[{\"code\":\"NA\",\"isShuffleOption\":false,\"body\":\"{\\\"data\\\":{\\\"plugin\\\":{\\\"id\\\":\\\"org.ekstep.questionunit.mcq\\\",\\\"version\\\":\\\"1.1\\\",\\\"templateId\\\":\\\"horizontalMCQ\\\"},\\\"data\\\":{\\\"question\\\":{\\\"text\\\":\\\"

Q)జిప్సమ్ మరియు ప్లాస్టర్ ఆఫ్ పారిస్ ల నందు ఉండే నీటి అణువులలో తేడా ఎంత?

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"hint\\\":\\\"\\\"},\\\"options\\\":[{\\\"text\\\":\\\"

A)3/2

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"hint\\\":\\\"\\\",\\\"isCorrect\\\":true,\\\"$$hashKey\\\":\\\"object:1034\\\"},{\\\"text\\\":\\\"

B)1/2

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"hint\\\":\\\"\\\",\\\"isCorrect\\\":false,\\\"$$hashKey\\\":\\\"object:1035\\\"},{\\\"text\\\":\\\"

C)2

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"isCorrect\\\":false,\\\"$$hashKey\\\":\\\"object:1045\\\"},{\\\"text\\\":\\\"

D)5/2

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"isCorrect\\\":false,\\\"$$hashKey\\\":\\\"object:1050\\\"}],\\\"questionCount\\\":0,\\\"media\\\":[{\\\"id\\\":385467069,\\\"src\\\":\\\"/assets/public//content/do_31270089678451507213885/artifact/ice_screenshot_20190217-164338_1550402439889.png\\\",\\\"assetId\\\":\\\"do_31270089678451507213885\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":false},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true}]},\\\"config\\\":{\\\"metadata\\\":{\\\"data\\\":{\\\"plugin\\\":{\\\"id\\\":\\\"org.ekstep.questionunit.mcq\\\",\\\"version\\\":\\\"1.1\\\",\\\"templateId\\\":\\\"horizontalMCQ\\\"},\\\"data\\\":{\\\"question\\\":{\\\"text\\\":\\\"

Q)జిప్సమ్ మరియు ప్లాస్టర్ ఆఫ్ పారిస్ ల నందు ఉండే నీటి అణువులలో తేడా ఎంత?

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"hint\\\":\\\"\\\"},\\\"options\\\":[{\\\"text\\\":\\\"

A)3/2

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"hint\\\":\\\"\\\",\\\"isCorrect\\\":true,\\\"$$hashKey\\\":\\\"object:1034\\\"},{\\\"text\\\":\\\"

B)1/2

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"hint\\\":\\\"\\\",\\\"isCorrect\\\":false,\\\"$$hashKey\\\":\\\"object:1035\\\"},{\\\"text\\\":\\\"

C)2

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"isCorrect\\\":false,\\\"$$hashKey\\\":\\\"object:1045\\\"},{\\\"text\\\":\\\"

D)5/2

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"isCorrect\\\":false,\\\"$$hashKey\\\":\\\"object:1050\\\"}],\\\"questionCount\\\":0,\\\"media\\\":[{\\\"id\\\":385467069,\\\"src\\\":\\\"/assets/public//content/do_31270089678451507213885/artifact/ice_screenshot_20190217-164338_1550402439889.png\\\",\\\"assetId\\\":\\\"do_31270089678451507213885\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":false},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true}]},\\\"config\\\":{\\\"metadata\\\":{\\\"data\\\":{\\\"plugin\\\":{\\\"id\\\":\\\"org.ekstep.questionunit.mcq\\\",\\\"version\\\":\\\"1.1\\\",\\\"templateId\\\":\\\"horizontalMCQ\\\"},\\\"data\\\":{\\\"question\\\":{\\\"text\\\":\\\"

Q)The difference of the molecules of water in gypsum and plaster       

\\\\n\\\\n

  of Paris is

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"hint\\\":\\\"\\\"},\\\"options\\\":[{\\\"text\\\":\\\"

A)3/2

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"hint\\\":\\\"\\\",\\\"isCorrect\\\":true,\\\"$$hashKey\\\":\\\"object:1034\\\"},{\\\"text\\\":\\\"

B)1/2

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"hint\\\":\\\"\\\",\\\"isCorrect\\\":false,\\\"$$hashKey\\\":\\\"object:1035\\\"},{\\\"text\\\":\\\"

C)2

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"isCorrect\\\":false,\\\"$$hashKey\\\":\\\"object:1045\\\"},{\\\"text\\\":\\\"

D)5/2

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"isCorrect\\\":false,\\\"$$hashKey\\\":\\\"object:1050\\\"}],\\\"questionCount\\\":0,\\\"media\\\":[{\\\"id\\\":385467069,\\\"src\\\":\\\"/assets/public//content/do_31270089678451507213885/artifact/ice_screenshot_20190217-164338_1550402439889.png\\\",\\\"assetId\\\":\\\"do_31270089678451507213885\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":false},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true}]},\\\"config\\\":{\\\"metadata\\\":{\\\"data\\\":{\\\"plugin\\\":{\\\"id\\\":\\\"org.ekstep.questionunit.mcq\\\",\\\"version\\\":\\\"1.1\\\",\\\"templateId\\\":\\\"horizontalMCQ\\\"},\\\"data\\\":{\\\"question\\\":{\\\"text\\\":\\\"

Q) కార్బన్ ఎలక్ట్రాన్ విన్యాసాన్ని ఈ క్రింది విధంగా రాయరాదని సాత్విక చెప్పింది. ఎందువలన?

\\\\n\\\",\\\"image\\\":\\\"/assets/public//content/do_31270089678451507213885/artifact/ice_screenshot_20190217-164338_1550402439889.png\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"hint\\\":\\\"\\\"},\\\"options\\\":[{\\\"text\\\":\\\"

A)ఆఫ్  బౌ నియమాన్ని పాటించుట లేదు

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"hint\\\":\\\"\\\",\\\"isCorrect\\\":false,\\\"$$hashKey\\\":\\\"object:1034\\\"},{\\\"text\\\":\\\"

B)హుండు నియమాన్ని పాటించుట లేదు

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"hint\\\":\\\"\\\",\\\"isCorrect\\\":true,\\\"$$hashKey\\\":\\\"object:1035\\\"},{\\\"text\\\":\\\"

C)పౌలీవర్జన నియమాన్ని పాటించుట లేదు

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"isCorrect\\\":false,\\\"$$hashKey\\\":\\\"object:1045\\\"},{\\\"text\\\":\\\"

D)పై వన్నీ సరైనవే

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"isCorrect\\\":false,\\\"$$hashKey\\\":\\\"object:1050\\\"}],\\\"questionCount\\\":0,\\\"media\\\":[{\\\"id\\\":385467069,\\\"src\\\":\\\"/assets/public//content/do_31270089678451507213885/artifact/ice_screenshot_20190217-164338_1550402439889.png\\\",\\\"assetId\\\":\\\"do_31270089678451507213885\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":false},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true}]},\\\"config\\\":{\\\"metadata\\\":{\\\"data\\\":{\\\"plugin\\\":{\\\"id\\\":\\\"org.ekstep.questionunit.mcq\\\",\\\"version\\\":\\\"1.1\\\",\\\"templateId\\\":\\\"horizontalMCQ\\\"},\\\"data\\\":{\\\"question\\\":{\\\"text\\\":\\\"

Q) Satwika told that the electronic configuration of carbon cannot be written like this……why?

\\\\n\\\\n

Be cause this electronic configuration does not obey

\\\\n\\\",\\\"image\\\":\\\"/assets/public//content/do_31270089678451507213885/artifact/ice_screenshot_20190217-164338_1550402439889.png\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"hint\\\":\\\"\\\"},\\\"options\\\":[{\\\"text\\\":\\\"

A) aufbau principle  

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"hint\\\":\\\"\\\",\\\"isCorrect\\\":false,\\\"$$hashKey\\\":\\\"object:1034\\\"},{\\\"text\\\":\\\"

B) Hund”s rule

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"hint\\\":\\\"\\\",\\\"isCorrect\\\":true,\\\"$$hashKey\\\":\\\"object:1035\\\"},{\\\"text\\\":\\\"

C) Paul”s exclusion princple 

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"isCorrect\\\":false,\\\"$$hashKey\\\":\\\"object:1045\\\"},{\\\"text\\\":\\\"

D) All the above

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"isCorrect\\\":false,\\\"$$hashKey\\\":\\\"object:1050\\\"}],\\\"questionCount\\\":0,\\\"media\\\":[{\\\"id\\\":385467069,\\\"src\\\":\\\"/assets/public//content/do_31270089678451507213885/artifact/ice_screenshot_20190217-164338_1550402439889.png\\\",\\\"assetId\\\":\\\"do_31270089678451507213885\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":false},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true}]},\\\"config\\\":{\\\"metadata\\\":{\\\"data\\\":{\\\"plugin\\\":{\\\"id\\\":\\\"org.ekstep.questionunit.mcq\\\",\\\"version\\\":\\\"1.1\\\",\\\"templateId\\\":\\\"horizontalMCQ\\\"},\\\"data\\\":{\\\"question\\\":{\\\"text\\\":\\\"

Q) Satwika told that the electronic configuration of carbon cannot be written like this……why?

\\\\n\\\\n

Be cause this electronic configuration does not obey

\\\\n\\\",\\\"image\\\":\\\"/assets/public//content/do_31270089678451507213885/artifact/ice_screenshot_20190217-164338_1550402439889.png\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"hint\\\":\\\"\\\"},\\\"options\\\":[{\\\"text\\\":\\\"

A) aufbau principle  

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"hint\\\":\\\"\\\",\\\"isCorrect\\\":false,\\\"$$hashKey\\\":\\\"object:1034\\\"},{\\\"text\\\":\\\"

B) Hund”s rule

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"hint\\\":\\\"\\\",\\\"isCorrect\\\":true,\\\"$$hashKey\\\":\\\"object:1035\\\"},{\\\"text\\\":\\\"

C) Paul”s exclusion princple 

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"isCorrect\\\":false,\\\"$$hashKey\\\":\\\"object:1045\\\"},{\\\"text\\\":\\\"

D) All the above

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"isCorrect\\\":false,\\\"$$hashKey\\\":\\\"object:1050\\\"}],\\\"questionCount\\\":0,\\\"media\\\":[]},\\\"config\\\":{\\\"metadata\\\":{\\\"data\\\":{\\\"plugin\\\":{\\\"id\\\":\\\"org.ekstep.questionunit.mcq\\\",\\\"version\\\":\\\"1.1\\\",\\\"templateId\\\":\\\"horizontalMCQ\\\"},\\\"data\\\":{\\\"question\\\":{\\\"text\\\":\\\"

Q) ఒక మూలకం పరమాణువ సంఖ్య 20 అయినా ఆవర్తన పట్టికలో ఆ మూలక స్థానం

\\\\n\\\\n
    \\\\n\\\\t
  1. 4 వ పీరియడ్ మరియు 2 వ గ్రూప్  B) 1 వ పీరియడ్ మరియు 2 వ గ్రూప్ C) 3 వ పీరియడ్ మరియు 13 వ గ్రూప్
  2. \\\\n
\\\\n\\\\n

D) 2 వ పీరియడ్ మరియు 2 వ గ్రూప్

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"hint\\\":\\\"\\\"},\\\"options\\\":[{\\\"text\\\":\\\"

A) 4 వ పీరియడ్ మరియు 2 వ గ్రూప్  

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"hint\\\":\\\"\\\",\\\"isCorrect\\\":true,\\\"$$hashKey\\\":\\\"object:1034\\\"},{\\\"text\\\":\\\"

B) 1 వ పీరియడ్ మరియు 2 వ గ్రూప్

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"hint\\\":\\\"\\\",\\\"isCorrect\\\":false,\\\"$$hashKey\\\":\\\"object:1035\\\"},{\\\"text\\\":\\\"

C) 3 వ పీరియడ్ మరియు 13 వ గ్రూప్

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"isCorrect\\\":false,\\\"$$hashKey\\\":\\\"object:1045\\\"},{\\\"text\\\":\\\"

D) 2 వ పీరియడ్ మరియు 2 వ గ్రూప్

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"isCorrect\\\":false,\\\"$$hashKey\\\":\\\"object:1050\\\"}],\\\"questionCount\\\":0,\\\"media\\\":[]},\\\"config\\\":{\\\"metadata\\\":{\\\"data\\\":{\\\"plugin\\\":{\\\"id\\\":\\\"org.ekstep.questionunit.mcq\\\",\\\"version\\\":\\\"1.1\\\",\\\"templateId\\\":\\\"horizontalMCQ\\\"},\\\"data\\\":{\\\"question\\\":{\\\"text\\\":\\\"

Q) If an atomic number of an element is 20 then what is the position of that element in a modern periodic table

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"hint\\\":\\\"\\\"},\\\"options\\\":[{\\\"text\\\":\\\"

A) 4th period and 2nd group  

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"hint\\\":\\\"\\\",\\\"isCorrect\\\":true,\\\"$$hashKey\\\":\\\"object:1034\\\"},{\\\"text\\\":\\\"

B) 1st period and 2nd group

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"hint\\\":\\\"\\\",\\\"isCorrect\\\":false,\\\"$$hashKey\\\":\\\"object:1035\\\"},{\\\"text\\\":\\\"

C) 3rd period and 13th group

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"isCorrect\\\":false,\\\"$$hashKey\\\":\\\"object:1045\\\"},{\\\"text\\\":\\\"

 D) 2nd period and 2nd group

\\\\n\\\\n

 

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"isCorrect\\\":false,\\\"$$hashKey\\\":\\\"object:1050\\\"}],\\\"questionCount\\\":0,\\\"media\\\":[]},\\\"config\\\":{\\\"metadata\\\":{\\\"data\\\":{\\\"plugin\\\":{\\\"id\\\":\\\"org.ekstep.questionunit.mcq\\\",\\\"version\\\":\\\"1.1\\\",\\\"templateId\\\":\\\"horizontalMCQ\\\"},\\\"data\\\":{\\\"question\\\":{\\\"text\\\":\\\"

Q) In a Doberiner triad the atomic weight of first and third elements are 32 and 125 , then what is the atomic weight of second one

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"hint\\\":\\\"\\\"},\\\"options\\\":[{\\\"text\\\":\\\"

A) 78.5

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"hint\\\":\\\"\\\",\\\"isCorrect\\\":true,\\\"$$hashKey\\\":\\\"object:1034\\\"},{\\\"text\\\":\\\"

B)80 

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"hint\\\":\\\"\\\",\\\"isCorrect\\\":false,\\\"$$hashKey\\\":\\\"object:1035\\\"},{\\\"text\\\":\\\"

C)23

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"isCorrect\\\":false,\\\"$$hashKey\\\":\\\"object:1045\\\"},{\\\"text\\\":\\\"

D)60

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"isCorrect\\\":false,\\\"$$hashKey\\\":\\\"object:1050\\\"}],\\\"questionCount\\\":0,\\\"media\\\":[]},\\\"config\\\":{\\\"metadata\\\":{\\\"data\\\":{\\\"plugin\\\":{\\\"id\\\":\\\"org.ekstep.questionunit.mcq\\\",\\\"version\\\":\\\"1.1\\\",\\\"templateId\\\":\\\"horizontalMCQ\\\"},\\\"data\\\":{\\\"question\\\":{\\\"text\\\":\\\"

Q) Find the frequency of a radiowave of wave lenth 100 metres?

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"hint\\\":\\\"\\\"},\\\"options\\\":[{\\\"text\\\":\\\"

A) 3 HZ  

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"hint\\\":\\\"\\\",\\\"isCorrect\\\":false,\\\"$$hashKey\\\":\\\"object:1034\\\"},{\\\"text\\\":\\\"

B)3 KHZ  

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"hint\\\":\\\"\\\",\\\"isCorrect\\\":false,\\\"$$hashKey\\\":\\\"object:1035\\\"},{\\\"text\\\":\\\"

C)3 MHZ 

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"isCorrect\\\":true,\\\"$$hashKey\\\":\\\"object:1045\\\"},{\\\"text\\\":\\\"

D)30 MHZ

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"isCorrect\\\":false,\\\"$$hashKey\\\":\\\"object:1050\\\"}],\\\"questionCount\\\":0,\\\"media\\\":[]},\\\"config\\\":{\\\"metadata\\\":{\\\"data\\\":{\\\"plugin\\\":{\\\"id\\\":\\\"org.ekstep.questionunit.mcq\\\",\\\"version\\\":\\\"1.1\\\",\\\"templateId\\\":\\\"horizontalMCQ\\\"},\\\"data\\\":{\\\"question\\\":{\\\"text\\\":\\\"

 Q .When l=2 , what is the maximum value of ml

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"hint\\\":\\\"\\\"},\\\"options\\\":[{\\\"text\\\":\\\"

A) 2

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"hint\\\":\\\"\\\",\\\"isCorrect\\\":true,\\\"$$hashKey\\\":\\\"object:1034\\\"},{\\\"text\\\":\\\"

B)-2

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"hint\\\":\\\"\\\",\\\"isCorrect\\\":false,\\\"$$hashKey\\\":\\\"object:1035\\\"},{\\\"text\\\":\\\"

C)2(2)+1

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"isCorrect\\\":false,\\\"$$hashKey\\\":\\\"object:1045\\\"},{\\\"text\\\":\\\"

D)0

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"isCorrect\\\":false,\\\"$$hashKey\\\":\\\"object:1050\\\"}],\\\"questionCount\\\":0,\\\"media\\\":[]},\\\"config\\\":{\\\"metadata\\\":{\\\"data\\\":{\\\"plugin\\\":{\\\"id\\\":\\\"org.ekstep.questionunit.mcq\\\",\\\"version\\\":\\\"1.1\\\",\\\"templateId\\\":\\\"horizontalMCQ\\\"},\\\"data\\\":{\\\"question\\\":{\\\"text\\\":\\\"

1) l=2 అయినపుడు  యొక్క mlగరిష్ట విలువ ఎంత?

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"hint\\\":\\\"\\\"},\\\"options\\\":[{\\\"text\\\":\\\"

A) 2

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"hint\\\":\\\"\\\",\\\"isCorrect\\\":true,\\\"$$hashKey\\\":\\\"object:1034\\\"},{\\\"text\\\":\\\"

B)-2

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"hint\\\":\\\"\\\",\\\"isCorrect\\\":false,\\\"$$hashKey\\\":\\\"object:1035\\\"},{\\\"text\\\":\\\"

C)2(2)+1

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"isCorrect\\\":false,\\\"$$hashKey\\\":\\\"object:1045\\\"},{\\\"text\\\":\\\"

D)0

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"isCorrect\\\":false,\\\"$$hashKey\\\":\\\"object:1050\\\"}],\\\"questionCount\\\":0,\\\"media\\\":[]},\\\"config\\\":{\\\"metadata\\\":{\\\"data\\\":{\\\"plugin\\\":{\\\"id\\\":\\\"org.ekstep.questionunit.mcq\\\",\\\"version\\\":\\\"1.1\\\",\\\"templateId\\\":\\\"horizontalMCQ\\\"},\\\"data\\\":{\\\"question\\\":{\\\"text\\\":\\\"

1) l=2 అయినపుడు  యొక్క mlగరిష్ట విలువ ఎంత?

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"hint\\\":\\\"\\\"},\\\"options\\\":[{\\\"text\\\":\\\"

A) 2

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"hint\\\":\\\"\\\",\\\"isCorrect\\\":false,\\\"$$hashKey\\\":\\\"object:1034\\\"},{\\\"text\\\":\\\"

B)-2

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"hint\\\":\\\"\\\",\\\"isCorrect\\\":false,\\\"$$hashKey\\\":\\\"object:1035\\\"},{\\\"text\\\":\\\"

C)2(2)+1

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"isCorrect\\\":true,\\\"$$hashKey\\\":\\\"object:1045\\\"},{\\\"text\\\":\\\"

D)0

\\\\n\\\",\\\"image\\\":\\\"\\\",\\\"audio\\\":\\\"\\\",\\\"audioName\\\":\\\"\\\",\\\"isCorrect\\\":false,\\\"$$hashKey\\\":\\\"object:1050\\\"}],\\\"questionCount\\\":0,\\\"media\\\":[]},\\\"config\\\":{\\\"metadata\\\":{\\\"max_score\\\":1,\\\"isShuffleOption\\\":false,\\\"isPartialScore\\\":true,\\\"templateType\\\":\\\"Horizontal\\\",\\\"name\\\":\\\"\\\\n\\\\tఈ క్రింది వాటిలో విశిష్టోష్ణానికి ప్రమాణం\\\\n\\\\n\\\",\\\"title\\\":\\\"Copy of - \\\\n\\\\tఈ క్రింది వాటిలో విశిష్టోష్ణానికి ప్రమాణం\\\\n\\\\n\\\",\\\"board\\\":\\\"State (Andhra Pradesh)\\\",\\\"topic\\\":[],\\\"medium\\\":\\\"Telugu\\\",\\\"gradeLevel\\\":[\\\"Class 10\\\"],\\\"subject\\\":\\\"Physical Science\\\",\\\"qlevel\\\":\\\"EASY\\\",\\\"description\\\":\\\"10 th ps bits\\\",\\\"category\\\":\\\"MCQ\\\",\\\"topicData\\\":\\\"(0) topics selected\\\"},\\\"max_time\\\":0,\\\"max_score\\\":1,\\\"partial_scoring\\\":true,\\\"layout\\\":\\\"Horizontal\\\",\\\"isShuffleOption\\\":false,\\\"questionCount\\\":1},\\\"media\\\":[]},\\\"medium\\\":\\\"Telugu\\\",\\\"questionTitle\\\":\\\"Copy of - \\\\n\\\\tఈ క్రింది వాటిలో విశిష్టోష్ణానికి ప్రమాణం\\\\n\\\\n\\\",\\\"qlevel\\\":\\\"MEDIUM\\\",\\\"subject\\\":\\\"Physical Science\\\",\\\"board\\\":\\\"State (Andhra Pradesh)\\\",\\\"templateType\\\":\\\"Horizontal\\\",\\\"isPartialScore\\\":true,\\\"gradeLevel\\\":[\\\"Class 10\\\"],\\\"isShuffleOption\\\":false,\\\"topic\\\":[\\\"Structure Of Atom\\\"],\\\"questionDesc\\\":\\\"10 th ps bits\\\",\\\"max_score\\\":1,\\\"name\\\":\\\"Examprep_10tm_ps_cha8_Q1\\\",\\\"title\\\":\\\"Copy of - Copy of - Copy of - Examprep_10tm_ps_cha8_Q1\\\",\\\"topicData\\\":\\\"(0) topics selected\\\",\\\"description\\\":\\\"10 PS BITS\\\",\\\"category\\\":\\\"MCQ\\\"},\\\"max_time\\\":0,\\\"max_score\\\":1,\\\"partial_scoring\\\":true,\\\"layout\\\":\\\"Horizontal\\\",\\\"isShuffleOption\\\":false,\\\"questionCount\\\":1},\\\"media\\\":[]},\\\"medium\\\":\\\"Telugu\\\",\\\"questionTitle\\\":\\\"Copy of - Examprep_10tm_ps_cha8_Q1\\\",\\\"qlevel\\\":\\\"MEDIUM\\\",\\\"subject\\\":\\\"Physical Science\\\",\\\"board\\\":\\\"State (Andhra Pradesh)\\\",\\\"templateType\\\":\\\"Horizontal\\\",\\\"isPartialScore\\\":true,\\\"gradeLevel\\\":[\\\"Class 10\\\"],\\\"isShuffleOption\\\":false,\\\"topic\\\":[\\\"Structure Of Atom\\\"],\\\"questionDesc\\\":\\\"10 PS BITS\\\",\\\"max_score\\\":1,\\\"name\\\":\\\"Examprep_10tm_ps_cha8_Q1\\\",\\\"title\\\":\\\"Copy of - Examprep_10tm_ps_cha8_Q1\\\",\\\"topicData\\\":\\\"(1) topics selected\\\",\\\"category\\\":\\\"MCQ\\\"},\\\"max_time\\\":0,\\\"max_score\\\":1,\\\"partial_scoring\\\":true,\\\"layout\\\":\\\"Horizontal\\\",\\\"isShuffleOption\\\":false,\\\"questionCount\\\":1},\\\"media\\\":[]},\\\"medium\\\":\\\"Telugu\\\",\\\"questionTitle\\\":\\\"Copy of - Examprep_10tm_ps_cha8_Q1\\\",\\\"qlevel\\\":\\\"DIFFICULT\\\",\\\"subject\\\":\\\"Physical Science\\\",\\\"board\\\":\\\"State (Andhra Pradesh)\\\",\\\"templateType\\\":\\\"Horizontal\\\",\\\"isPartialScore\\\":true,\\\"gradeLevel\\\":[\\\"Class 10\\\"],\\\"isShuffleOption\\\":false,\\\"topic\\\":[\\\"Structure Of Atom\\\"],\\\"max_score\\\":1,\\\"name\\\":\\\"Examprep_10em_ps_cha8_Q1\\\",\\\"title\\\":\\\"Copy of - Examprep_10em_ps_cha8_Q1\\\",\\\"topicData\\\":\\\"(1) topics selected\\\",\\\"description\\\":\\\"10 ps bits\\\",\\\"category\\\":\\\"MCQ\\\"},\\\"max_time\\\":0,\\\"max_score\\\":1,\\\"partial_scoring\\\":true,\\\"layout\\\":\\\"Horizontal\\\",\\\"isShuffleOption\\\":false,\\\"questionCount\\\":1},\\\"media\\\":[]},\\\"medium\\\":\\\"English\\\",\\\"questionTitle\\\":\\\"Copy of - Examprep_10em_ps_cha8_Q1\\\",\\\"qlevel\\\":\\\"DIFFICULT\\\",\\\"subject\\\":\\\"Physical Science\\\",\\\"board\\\":\\\"State (Andhra Pradesh)\\\",\\\"templateType\\\":\\\"Horizontal\\\",\\\"isPartialScore\\\":true,\\\"gradeLevel\\\":[\\\"Class 10\\\"],\\\"isShuffleOption\\\":false,\\\"topic\\\":[],\\\"questionDesc\\\":\\\"10 ps bits\\\",\\\"max_score\\\":1,\\\"name\\\":\\\"Examprep_10em_ps_cha8_Q2\\\",\\\"title\\\":\\\"Copy of - Examprep_10em_ps_cha8_Q2\\\",\\\"topicData\\\":\\\"(1) topics selected\\\",\\\"description\\\":\\\"10 PS BITS\\\",\\\"category\\\":\\\"MCQ\\\"},\\\"max_time\\\":0,\\\"max_score\\\":1,\\\"partial_scoring\\\":true,\\\"layout\\\":\\\"Horizontal\\\",\\\"isShuffleOption\\\":false,\\\"questionCount\\\":1},\\\"media\\\":[]},\\\"medium\\\":\\\"English\\\",\\\"questionTitle\\\":\\\"Copy of - Examprep_10em_ps_cha8_Q2\\\",\\\"qlevel\\\":\\\"DIFFICULT\\\",\\\"subject\\\":\\\"Physical Science\\\",\\\"board\\\":\\\"State (Andhra Pradesh)\\\",\\\"templateType\\\":\\\"Horizontal\\\",\\\"isPartialScore\\\":true,\\\"gradeLevel\\\":[\\\"Class 10\\\"],\\\"isShuffleOption\\\":false,\\\"topic\\\":[\\\"Classification Of Elements\\\"],\\\"questionDesc\\\":\\\"10 PS BITS\\\",\\\"max_score\\\":1,\\\"name\\\":\\\"Examprep_10em_ps_cha8_Q2\\\",\\\"title\\\":\\\"Copy of - Examprep_10em_ps_cha8_Q2\\\",\\\"topicData\\\":\\\"(0) topics selected\\\",\\\"category\\\":\\\"MCQ\\\"},\\\"max_time\\\":0,\\\"max_score\\\":1,\\\"partial_scoring\\\":true,\\\"layout\\\":\\\"Horizontal\\\",\\\"isShuffleOption\\\":false,\\\"questionCount\\\":1},\\\"media\\\":[]},\\\"medium\\\":\\\"English\\\",\\\"questionTitle\\\":\\\"Copy of - Examprep_10em_ps_cha8_Q2\\\",\\\"qlevel\\\":\\\"DIFFICULT\\\",\\\"subject\\\":\\\"Physical Science\\\",\\\"board\\\":\\\"State (Andhra Pradesh)\\\",\\\"templateType\\\":\\\"Horizontal\\\",\\\"isPartialScore\\\":true,\\\"gradeLevel\\\":[\\\"Class 10\\\"],\\\"isShuffleOption\\\":false,\\\"topic\\\":[\\\"Classification Of Elements\\\"],\\\"max_score\\\":1,\\\"name\\\":\\\"Copy of - Examprep_10em_ps_cha8_Q2\\\",\\\"title\\\":\\\"Copy of - Copy of - Examprep_10em_ps_cha8_Q2\\\",\\\"topicData\\\":\\\"(1) topics selected\\\",\\\"description\\\":\\\"10 ps bits\\\",\\\"category\\\":\\\"MCQ\\\"},\\\"max_time\\\":0,\\\"max_score\\\":1,\\\"partial_scoring\\\":true,\\\"layout\\\":\\\"Horizontal\\\",\\\"isShuffleOption\\\":false,\\\"questionCount\\\":1},\\\"media\\\":[]},\\\"medium\\\":\\\"Telugu\\\",\\\"questionTitle\\\":\\\"Copy of - Copy of - Examprep_10em_ps_cha8_Q2\\\",\\\"qlevel\\\":\\\"DIFFICULT\\\",\\\"subject\\\":\\\"Physical Science\\\",\\\"board\\\":\\\"State (Andhra Pradesh)\\\",\\\"templateType\\\":\\\"Horizontal\\\",\\\"isPartialScore\\\":true,\\\"gradeLevel\\\":[\\\"Class 10\\\"],\\\"isShuffleOption\\\":false,\\\"topic\\\":[\\\"Classification Of Elements\\\"],\\\"questionDesc\\\":\\\"10 ps bits\\\",\\\"max_score\\\":1,\\\"name\\\":\\\"Examprep_10tm_ps_cha8_Q2\\\",\\\"title\\\":\\\"Copy of - Copy of - Examprep_10tm_ps_cha8_Q2\\\",\\\"topicData\\\":\\\"(1) topics selected\\\",\\\"description\\\":\\\"10 PS BITS\\\",\\\"category\\\":\\\"MCQ\\\"},\\\"max_time\\\":0,\\\"max_score\\\":1,\\\"partial_scoring\\\":true,\\\"layout\\\":\\\"Horizontal\\\",\\\"isShuffleOption\\\":false,\\\"questionCount\\\":1},\\\"media\\\":[]},\\\"medium\\\":\\\"Telugu\\\",\\\"questionTitle\\\":\\\"Copy of - Examprep_10tm_ps_cha8_Q2\\\",\\\"qlevel\\\":\\\"MEDIUM\\\",\\\"subject\\\":\\\"Physical Science\\\",\\\"board\\\":\\\"State (Andhra Pradesh)\\\",\\\"templateType\\\":\\\"Horizontal\\\",\\\"isPartialScore\\\":true,\\\"gradeLevel\\\":[\\\"Class 10\\\"],\\\"isShuffleOption\\\":false,\\\"topic\\\":[\\\"Classification Of Elements\\\"],\\\"questionDesc\\\":\\\"10 PS BITS\\\",\\\"max_score\\\":1,\\\"name\\\":\\\"Examprep_10em ps_cha4 Q3\\\",\\\"title\\\":\\\"Examprep_10em ps_cha4 Q3\\\",\\\"topicData\\\":\\\"(1) topics selected\\\",\\\"description\\\":\\\"10 ps bits\\\",\\\"category\\\":\\\"MCQ\\\"},\\\"max_time\\\":0,\\\"max_score\\\":1,\\\"partial_scoring\\\":true,\\\"layout\\\":\\\"Horizontal\\\",\\\"isShuffleOption\\\":false,\\\"questionCount\\\":1},\\\"media\\\":[{\\\"id\\\":385467069,\\\"src\\\":\\\"/assets/public//content/do_31270089678451507213885/artifact/ice_screenshot_20190217-164338_1550402439889.png\\\",\\\"assetId\\\":\\\"do_31270089678451507213885\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":false},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true}]},\\\"medium\\\":\\\"Telugu\\\",\\\"questionTitle\\\":\\\"Examprep_10em ps_cha4 Q3\\\",\\\"qlevel\\\":\\\"MEDIUM\\\",\\\"subject\\\":\\\"Physical Science\\\",\\\"board\\\":\\\"State (Andhra Pradesh)\\\",\\\"templateType\\\":\\\"Horizontal\\\",\\\"isPartialScore\\\":true,\\\"gradeLevel\\\":[\\\"Class 10\\\"],\\\"isShuffleOption\\\":false,\\\"topic\\\":[\\\"Classification Of Elements\\\"],\\\"questionDesc\\\":\\\"10 ps bits\\\",\\\"max_score\\\":1,\\\"name\\\":\\\"Examprep_10em ps_cha8- Q3\\\",\\\"title\\\":\\\"Examprep_10em ps_cha8- Q3\\\",\\\"topicData\\\":\\\"(1) topics selected\\\",\\\"category\\\":\\\"MCQ\\\"},\\\"max_time\\\":0,\\\"max_score\\\":1,\\\"partial_scoring\\\":true,\\\"layout\\\":\\\"Horizontal\\\",\\\"isShuffleOption\\\":false,\\\"questionCount\\\":1},\\\"media\\\":[{\\\"id\\\":385467069,\\\"src\\\":\\\"/assets/public//content/do_31270089678451507213885/artifact/ice_screenshot_20190217-164338_1550402439889.png\\\",\\\"assetId\\\":\\\"do_31270089678451507213885\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":false},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true}]},\\\"medium\\\":\\\"Telugu\\\",\\\"questionTitle\\\":\\\"Examprep_10em ps_cha8- Q3\\\",\\\"qlevel\\\":\\\"DIFFICULT\\\",\\\"subject\\\":\\\"Physical Science\\\",\\\"board\\\":\\\"State (Andhra Pradesh)\\\",\\\"templateType\\\":\\\"Horizontal\\\",\\\"isPartialScore\\\":true,\\\"gradeLevel\\\":[\\\"Class 10\\\"],\\\"isShuffleOption\\\":false,\\\"topic\\\":[\\\"Classification Of Elements\\\"],\\\"max_score\\\":1,\\\"name\\\":\\\"Examprep_10tm ps_cha8- Q3\\\",\\\"title\\\":\\\"Copy of - Examprep_10tm ps_cha8- Q3\\\",\\\"topicData\\\":\\\"(1) topics selected\\\",\\\"description\\\":\\\"10 ps bits\\\",\\\"category\\\":\\\"MCQ\\\"},\\\"max_time\\\":0,\\\"max_score\\\":1,\\\"partial_scoring\\\":true,\\\"layout\\\":\\\"Horizontal\\\",\\\"isShuffleOption\\\":false,\\\"questionCount\\\":1},\\\"media\\\":[{\\\"id\\\":385467069,\\\"src\\\":\\\"/assets/public//content/do_31270089678451507213885/artifact/ice_screenshot_20190217-164338_1550402439889.png\\\",\\\"assetId\\\":\\\"do_31270089678451507213885\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":false},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true}]},\\\"medium\\\":\\\"English\\\",\\\"questionTitle\\\":\\\"Copy of - Examprep_10tm ps_cha8- Q3\\\",\\\"qlevel\\\":\\\"MEDIUM\\\",\\\"subject\\\":\\\"Physical Science\\\",\\\"board\\\":\\\"State (Andhra Pradesh)\\\",\\\"templateType\\\":\\\"Horizontal\\\",\\\"isPartialScore\\\":true,\\\"gradeLevel\\\":[\\\"Class 10\\\"],\\\"isShuffleOption\\\":false,\\\"topic\\\":[],\\\"questionDesc\\\":\\\"10 ps bits\\\",\\\"max_score\\\":1,\\\"name\\\":\\\"Examprep_10em ps_cha8- Q5\\\",\\\"title\\\":\\\"Examprep_10em ps_cha8- Q5\\\",\\\"topicData\\\":\\\"(1) topics selected\\\",\\\"description\\\":\\\"10 PS BITS\\\",\\\"category\\\":\\\"MCQ\\\"},\\\"max_time\\\":0,\\\"max_score\\\":1,\\\"partial_scoring\\\":true,\\\"layout\\\":\\\"Horizontal\\\",\\\"isShuffleOption\\\":false,\\\"questionCount\\\":1},\\\"media\\\":[{\\\"id\\\":385467069,\\\"src\\\":\\\"/assets/public//content/do_31270089678451507213885/artifact/ice_screenshot_20190217-164338_1550402439889.png\\\",\\\"assetId\\\":\\\"do_31270089678451507213885\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":false},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true}]},\\\"medium\\\":\\\"English\\\",\\\"questionTitle\\\":\\\"Examprep_10em ps_cha8- Q5\\\",\\\"qlevel\\\":\\\"MEDIUM\\\",\\\"subject\\\":\\\"Physical Science\\\",\\\"board\\\":\\\"State (Andhra Pradesh)\\\",\\\"templateType\\\":\\\"Horizontal\\\",\\\"isPartialScore\\\":true,\\\"gradeLevel\\\":[\\\"Class 10\\\"],\\\"isShuffleOption\\\":false,\\\"topic\\\":[\\\"Structure Of Atom\\\"],\\\"questionDesc\\\":\\\"10 PS BITS\\\",\\\"max_score\\\":1,\\\"name\\\":\\\"Examprep_10em ps_cha8- Q5\\\",\\\"title\\\":\\\"Examprep_10em ps_cha8- Q5\\\",\\\"description\\\":\\\"10 PS BITS\\\",\\\"category\\\":\\\"MCQ\\\"},\\\"max_time\\\":0,\\\"max_score\\\":1,\\\"partial_scoring\\\":true,\\\"layout\\\":\\\"Horizontal\\\",\\\"isShuffleOption\\\":false,\\\"questionCount\\\":1},\\\"media\\\":[{\\\"id\\\":385467069,\\\"src\\\":\\\"/assets/public//content/do_31270089678451507213885/artifact/ice_screenshot_20190217-164338_1550402439889.png\\\",\\\"assetId\\\":\\\"do_31270089678451507213885\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":false},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.audioicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true},{\\\"id\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"src\\\":\\\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\\\",\\\"assetId\\\":\\\"org.ekstep.questionset.default-imgageicon\\\",\\\"type\\\":\\\"image\\\",\\\"preload\\\":true}]}}\",\"itemType\":\"UNIT\",\"version\":2,\"category\":\"MCQ\",\"createdBy\":\"c252c844-4178-498f-9006-d63540319254\",\"channel\":\"0123207707019919361056\",\"type\":\"mcq\",\"template\":\"NA\",\"template_id\":\"NA\",\"framework\":\"ap_k-12_1\",\"data\":{\"plugin\":{\"id\":\"org.ekstep.questionunit.mcq\",\"version\":\"1.1\",\"templateId\":\"horizontalMCQ\"},\"data\":{\"question\":{\"text\":\"

Q)జిప్సమ్ మరియు ప్లాస్టర్ ఆఫ్ పారిస్ ల నందు ఉండే నీటి అణువులలో తేడా ఎంత?

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\"},\"options\":[{\"text\":\"

A)3/2

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\",\"isCorrect\":true,\"$$hashKey\":\"object:1034\"},{\"text\":\"

B)1/2

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1035\"},{\"text\":\"

C)2

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1045\"},{\"text\":\"

D)5/2

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1050\"}],\"questionCount\":0,\"media\":[{\"id\":385467069,\"src\":\"/assets/public//content/do_31270089678451507213885/artifact/ice_screenshot_20190217-164338_1550402439889.png\",\"assetId\":\"do_31270089678451507213885\",\"type\":\"image\",\"preload\":false},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true}]},\"config\":{\"metadata\":{\"data\":{\"plugin\":{\"id\":\"org.ekstep.questionunit.mcq\",\"version\":\"1.1\",\"templateId\":\"horizontalMCQ\"},\"data\":{\"question\":{\"text\":\"

Q)The difference of the molecules of water in gypsum and plaster       

\\n\\n

  of Paris is

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\"},\"options\":[{\"text\":\"

A)3/2

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\",\"isCorrect\":true,\"$$hashKey\":\"object:1034\"},{\"text\":\"

B)1/2

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1035\"},{\"text\":\"

C)2

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1045\"},{\"text\":\"

D)5/2

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1050\"}],\"questionCount\":0,\"media\":[{\"id\":385467069,\"src\":\"/assets/public//content/do_31270089678451507213885/artifact/ice_screenshot_20190217-164338_1550402439889.png\",\"assetId\":\"do_31270089678451507213885\",\"type\":\"image\",\"preload\":false},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true}]},\"config\":{\"metadata\":{\"data\":{\"plugin\":{\"id\":\"org.ekstep.questionunit.mcq\",\"version\":\"1.1\",\"templateId\":\"horizontalMCQ\"},\"data\":{\"question\":{\"text\":\"

Q) కార్బన్ ఎలక్ట్రాన్ విన్యాసాన్ని ఈ క్రింది విధంగా రాయరాదని సాత్విక చెప్పింది. ఎందువలన?

\\n\",\"image\":\"/assets/public//content/do_31270089678451507213885/artifact/ice_screenshot_20190217-164338_1550402439889.png\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\"},\"options\":[{\"text\":\"

A)ఆఫ్  బౌ నియమాన్ని పాటించుట లేదు

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1034\"},{\"text\":\"

B)హుండు నియమాన్ని పాటించుట లేదు

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\",\"isCorrect\":true,\"$$hashKey\":\"object:1035\"},{\"text\":\"

C)పౌలీవర్జన నియమాన్ని పాటించుట లేదు

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1045\"},{\"text\":\"

D)పై వన్నీ సరైనవే

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1050\"}],\"questionCount\":0,\"media\":[{\"id\":385467069,\"src\":\"/assets/public//content/do_31270089678451507213885/artifact/ice_screenshot_20190217-164338_1550402439889.png\",\"assetId\":\"do_31270089678451507213885\",\"type\":\"image\",\"preload\":false},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true}]},\"config\":{\"metadata\":{\"data\":{\"plugin\":{\"id\":\"org.ekstep.questionunit.mcq\",\"version\":\"1.1\",\"templateId\":\"horizontalMCQ\"},\"data\":{\"question\":{\"text\":\"

Q) Satwika told that the electronic configuration of carbon cannot be written like this……why?

\\n\\n

Be cause this electronic configuration does not obey

\\n\",\"image\":\"/assets/public//content/do_31270089678451507213885/artifact/ice_screenshot_20190217-164338_1550402439889.png\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\"},\"options\":[{\"text\":\"

A) aufbau principle  

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1034\"},{\"text\":\"

B) Hund”s rule

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\",\"isCorrect\":true,\"$$hashKey\":\"object:1035\"},{\"text\":\"

C) Paul”s exclusion princple 

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1045\"},{\"text\":\"

D) All the above

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1050\"}],\"questionCount\":0,\"media\":[{\"id\":385467069,\"src\":\"/assets/public//content/do_31270089678451507213885/artifact/ice_screenshot_20190217-164338_1550402439889.png\",\"assetId\":\"do_31270089678451507213885\",\"type\":\"image\",\"preload\":false},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true}]},\"config\":{\"metadata\":{\"data\":{\"plugin\":{\"id\":\"org.ekstep.questionunit.mcq\",\"version\":\"1.1\",\"templateId\":\"horizontalMCQ\"},\"data\":{\"question\":{\"text\":\"

Q) Satwika told that the electronic configuration of carbon cannot be written like this……why?

\\n\\n

Be cause this electronic configuration does not obey

\\n\",\"image\":\"/assets/public//content/do_31270089678451507213885/artifact/ice_screenshot_20190217-164338_1550402439889.png\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\"},\"options\":[{\"text\":\"

A) aufbau principle  

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1034\"},{\"text\":\"

B) Hund”s rule

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\",\"isCorrect\":true,\"$$hashKey\":\"object:1035\"},{\"text\":\"

C) Paul”s exclusion princple 

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1045\"},{\"text\":\"

D) All the above

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1050\"}],\"questionCount\":0,\"media\":[]},\"config\":{\"metadata\":{\"data\":{\"plugin\":{\"id\":\"org.ekstep.questionunit.mcq\",\"version\":\"1.1\",\"templateId\":\"horizontalMCQ\"},\"data\":{\"question\":{\"text\":\"

Q) ఒక మూలకం పరమాణువ సంఖ్య 20 అయినా ఆవర్తన పట్టికలో ఆ మూలక స్థానం

\\n\\n
    \\n\\t
  1. 4 వ పీరియడ్ మరియు 2 వ గ్రూప్  B) 1 వ పీరియడ్ మరియు 2 వ గ్రూప్ C) 3 వ పీరియడ్ మరియు 13 వ గ్రూప్
  2. \\n
\\n\\n

D) 2 వ పీరియడ్ మరియు 2 వ గ్రూప్

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\"},\"options\":[{\"text\":\"

A) 4 వ పీరియడ్ మరియు 2 వ గ్రూప్  

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\",\"isCorrect\":true,\"$$hashKey\":\"object:1034\"},{\"text\":\"

B) 1 వ పీరియడ్ మరియు 2 వ గ్రూప్

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1035\"},{\"text\":\"

C) 3 వ పీరియడ్ మరియు 13 వ గ్రూప్

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1045\"},{\"text\":\"

D) 2 వ పీరియడ్ మరియు 2 వ గ్రూప్

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1050\"}],\"questionCount\":0,\"media\":[]},\"config\":{\"metadata\":{\"data\":{\"plugin\":{\"id\":\"org.ekstep.questionunit.mcq\",\"version\":\"1.1\",\"templateId\":\"horizontalMCQ\"},\"data\":{\"question\":{\"text\":\"

Q) If an atomic number of an element is 20 then what is the position of that element in a modern periodic table

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\"},\"options\":[{\"text\":\"

A) 4th period and 2nd group  

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\",\"isCorrect\":true,\"$$hashKey\":\"object:1034\"},{\"text\":\"

B) 1st period and 2nd group

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1035\"},{\"text\":\"

C) 3rd period and 13th group

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1045\"},{\"text\":\"

 D) 2nd period and 2nd group

\\n\\n

 

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1050\"}],\"questionCount\":0,\"media\":[]},\"config\":{\"metadata\":{\"data\":{\"plugin\":{\"id\":\"org.ekstep.questionunit.mcq\",\"version\":\"1.1\",\"templateId\":\"horizontalMCQ\"},\"data\":{\"question\":{\"text\":\"

Q) In a Doberiner triad the atomic weight of first and third elements are 32 and 125 , then what is the atomic weight of second one

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\"},\"options\":[{\"text\":\"

A) 78.5

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\",\"isCorrect\":true,\"$$hashKey\":\"object:1034\"},{\"text\":\"

B)80 

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1035\"},{\"text\":\"

C)23

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1045\"},{\"text\":\"

D)60

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1050\"}],\"questionCount\":0,\"media\":[]},\"config\":{\"metadata\":{\"data\":{\"plugin\":{\"id\":\"org.ekstep.questionunit.mcq\",\"version\":\"1.1\",\"templateId\":\"horizontalMCQ\"},\"data\":{\"question\":{\"text\":\"

Q) Find the frequency of a radiowave of wave lenth 100 metres?

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\"},\"options\":[{\"text\":\"

A) 3 HZ  

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1034\"},{\"text\":\"

B)3 KHZ  

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1035\"},{\"text\":\"

C)3 MHZ 

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"isCorrect\":true,\"$$hashKey\":\"object:1045\"},{\"text\":\"

D)30 MHZ

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1050\"}],\"questionCount\":0,\"media\":[]},\"config\":{\"metadata\":{\"data\":{\"plugin\":{\"id\":\"org.ekstep.questionunit.mcq\",\"version\":\"1.1\",\"templateId\":\"horizontalMCQ\"},\"data\":{\"question\":{\"text\":\"

 Q .When l=2 , what is the maximum value of ml

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\"},\"options\":[{\"text\":\"

A) 2

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\",\"isCorrect\":true,\"$$hashKey\":\"object:1034\"},{\"text\":\"

B)-2

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1035\"},{\"text\":\"

C)2(2)+1

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1045\"},{\"text\":\"

D)0

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1050\"}],\"questionCount\":0,\"media\":[]},\"config\":{\"metadata\":{\"data\":{\"plugin\":{\"id\":\"org.ekstep.questionunit.mcq\",\"version\":\"1.1\",\"templateId\":\"horizontalMCQ\"},\"data\":{\"question\":{\"text\":\"

1) l=2 అయినపుడు  యొక్క mlగరిష్ట విలువ ఎంత?

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\"},\"options\":[{\"text\":\"

A) 2

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\",\"isCorrect\":true,\"$$hashKey\":\"object:1034\"},{\"text\":\"

B)-2

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1035\"},{\"text\":\"

C)2(2)+1

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1045\"},{\"text\":\"

D)0

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1050\"}],\"questionCount\":0,\"media\":[]},\"config\":{\"metadata\":{\"data\":{\"plugin\":{\"id\":\"org.ekstep.questionunit.mcq\",\"version\":\"1.1\",\"templateId\":\"horizontalMCQ\"},\"data\":{\"question\":{\"text\":\"

1) l=2 అయినపుడు  యొక్క mlగరిష్ట విలువ ఎంత?

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\"},\"options\":[{\"text\":\"

A) 2

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1034\"},{\"text\":\"

B)-2

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1035\"},{\"text\":\"

C)2(2)+1

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"isCorrect\":true,\"$$hashKey\":\"object:1045\"},{\"text\":\"

D)0

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1050\"}],\"questionCount\":0,\"media\":[]},\"config\":{\"metadata\":{\"max_score\":1,\"isShuffleOption\":false,\"isPartialScore\":true,\"templateType\":\"Horizontal\",\"name\":\"\\n\\tఈ క్రింది వాటిలో విశిష్టోష్ణానికి ప్రమాణం\\n\\n\",\"title\":\"Copy of - \\n\\tఈ క్రింది వాటిలో విశిష్టోష్ణానికి ప్రమాణం\\n\\n\",\"board\":\"State (Andhra Pradesh)\",\"topic\":[],\"medium\":\"Telugu\",\"gradeLevel\":[\"Class 10\"],\"subject\":\"Physical Science\",\"qlevel\":\"EASY\",\"description\":\"10 th ps bits\",\"category\":\"MCQ\",\"topicData\":\"(0) topics selected\"},\"max_time\":0,\"max_score\":1,\"partial_scoring\":true,\"layout\":\"Horizontal\",\"isShuffleOption\":false,\"questionCount\":1},\"media\":[]},\"medium\":\"Telugu\",\"questionTitle\":\"Copy of - \\n\\tఈ క్రింది వాటిలో విశిష్టోష్ణానికి ప్రమాణం\\n\\n\",\"qlevel\":\"MEDIUM\",\"subject\":\"Physical Science\",\"board\":\"State (Andhra Pradesh)\",\"templateType\":\"Horizontal\",\"isPartialScore\":true,\"gradeLevel\":[\"Class 10\"],\"isShuffleOption\":false,\"topic\":[\"Structure Of Atom\"],\"questionDesc\":\"10 th ps bits\",\"max_score\":1,\"name\":\"Examprep_10tm_ps_cha8_Q1\",\"title\":\"Copy of - Copy of - Copy of - Examprep_10tm_ps_cha8_Q1\",\"topicData\":\"(0) topics selected\",\"description\":\"10 PS BITS\",\"category\":\"MCQ\"},\"max_time\":0,\"max_score\":1,\"partial_scoring\":true,\"layout\":\"Horizontal\",\"isShuffleOption\":false,\"questionCount\":1},\"media\":[]},\"medium\":\"Telugu\",\"questionTitle\":\"Copy of - Examprep_10tm_ps_cha8_Q1\",\"qlevel\":\"MEDIUM\",\"subject\":\"Physical Science\",\"board\":\"State (Andhra Pradesh)\",\"templateType\":\"Horizontal\",\"isPartialScore\":true,\"gradeLevel\":[\"Class 10\"],\"isShuffleOption\":false,\"topic\":[\"Structure Of Atom\"],\"questionDesc\":\"10 PS BITS\",\"max_score\":1,\"name\":\"Examprep_10tm_ps_cha8_Q1\",\"title\":\"Copy of - Examprep_10tm_ps_cha8_Q1\",\"topicData\":\"(1) topics selected\",\"category\":\"MCQ\"},\"max_time\":0,\"max_score\":1,\"partial_scoring\":true,\"layout\":\"Horizontal\",\"isShuffleOption\":false,\"questionCount\":1},\"media\":[]},\"medium\":\"Telugu\",\"questionTitle\":\"Copy of - Examprep_10tm_ps_cha8_Q1\",\"qlevel\":\"DIFFICULT\",\"subject\":\"Physical Science\",\"board\":\"State (Andhra Pradesh)\",\"templateType\":\"Horizontal\",\"isPartialScore\":true,\"gradeLevel\":[\"Class 10\"],\"isShuffleOption\":false,\"topic\":[\"Structure Of Atom\"],\"max_score\":1,\"name\":\"Examprep_10em_ps_cha8_Q1\",\"title\":\"Copy of - Examprep_10em_ps_cha8_Q1\",\"topicData\":\"(1) topics selected\",\"description\":\"10 ps bits\",\"category\":\"MCQ\"},\"max_time\":0,\"max_score\":1,\"partial_scoring\":true,\"layout\":\"Horizontal\",\"isShuffleOption\":false,\"questionCount\":1},\"media\":[]},\"medium\":\"English\",\"questionTitle\":\"Copy of - Examprep_10em_ps_cha8_Q1\",\"qlevel\":\"DIFFICULT\",\"subject\":\"Physical Science\",\"board\":\"State (Andhra Pradesh)\",\"templateType\":\"Horizontal\",\"isPartialScore\":true,\"gradeLevel\":[\"Class 10\"],\"isShuffleOption\":false,\"topic\":[],\"questionDesc\":\"10 ps bits\",\"max_score\":1,\"name\":\"Examprep_10em_ps_cha8_Q2\",\"title\":\"Copy of - Examprep_10em_ps_cha8_Q2\",\"topicData\":\"(1) topics selected\",\"description\":\"10 PS BITS\",\"category\":\"MCQ\"},\"max_time\":0,\"max_score\":1,\"partial_scoring\":true,\"layout\":\"Horizontal\",\"isShuffleOption\":false,\"questionCount\":1},\"media\":[]},\"medium\":\"English\",\"questionTitle\":\"Copy of - Examprep_10em_ps_cha8_Q2\",\"qlevel\":\"DIFFICULT\",\"subject\":\"Physical Science\",\"board\":\"State (Andhra Pradesh)\",\"templateType\":\"Horizontal\",\"isPartialScore\":true,\"gradeLevel\":[\"Class 10\"],\"isShuffleOption\":false,\"topic\":[\"Classification Of Elements\"],\"questionDesc\":\"10 PS BITS\",\"max_score\":1,\"name\":\"Examprep_10em_ps_cha8_Q2\",\"title\":\"Copy of - Examprep_10em_ps_cha8_Q2\",\"topicData\":\"(0) topics selected\",\"category\":\"MCQ\"},\"max_time\":0,\"max_score\":1,\"partial_scoring\":true,\"layout\":\"Horizontal\",\"isShuffleOption\":false,\"questionCount\":1},\"media\":[]},\"medium\":\"English\",\"questionTitle\":\"Copy of - Examprep_10em_ps_cha8_Q2\",\"qlevel\":\"DIFFICULT\",\"subject\":\"Physical Science\",\"board\":\"State (Andhra Pradesh)\",\"templateType\":\"Horizontal\",\"isPartialScore\":true,\"gradeLevel\":[\"Class 10\"],\"isShuffleOption\":false,\"topic\":[\"Classification Of Elements\"],\"max_score\":1,\"name\":\"Copy of - Examprep_10em_ps_cha8_Q2\",\"title\":\"Copy of - Copy of - Examprep_10em_ps_cha8_Q2\",\"topicData\":\"(1) topics selected\",\"description\":\"10 ps bits\",\"category\":\"MCQ\"},\"max_time\":0,\"max_score\":1,\"partial_scoring\":true,\"layout\":\"Horizontal\",\"isShuffleOption\":false,\"questionCount\":1},\"media\":[]},\"medium\":\"Telugu\",\"questionTitle\":\"Copy of - Copy of - Examprep_10em_ps_cha8_Q2\",\"qlevel\":\"DIFFICULT\",\"subject\":\"Physical Science\",\"board\":\"State (Andhra Pradesh)\",\"templateType\":\"Horizontal\",\"isPartialScore\":true,\"gradeLevel\":[\"Class 10\"],\"isShuffleOption\":false,\"topic\":[\"Classification Of Elements\"],\"questionDesc\":\"10 ps bits\",\"max_score\":1,\"name\":\"Examprep_10tm_ps_cha8_Q2\",\"title\":\"Copy of - Copy of - Examprep_10tm_ps_cha8_Q2\",\"topicData\":\"(1) topics selected\",\"description\":\"10 PS BITS\",\"category\":\"MCQ\"},\"max_time\":0,\"max_score\":1,\"partial_scoring\":true,\"layout\":\"Horizontal\",\"isShuffleOption\":false,\"questionCount\":1},\"media\":[]},\"medium\":\"Telugu\",\"questionTitle\":\"Copy of - Examprep_10tm_ps_cha8_Q2\",\"qlevel\":\"MEDIUM\",\"subject\":\"Physical Science\",\"board\":\"State (Andhra Pradesh)\",\"templateType\":\"Horizontal\",\"isPartialScore\":true,\"gradeLevel\":[\"Class 10\"],\"isShuffleOption\":false,\"topic\":[\"Classification Of Elements\"],\"questionDesc\":\"10 PS BITS\",\"max_score\":1,\"name\":\"Examprep_10em ps_cha4 Q3\",\"title\":\"Examprep_10em ps_cha4 Q3\",\"topicData\":\"(1) topics selected\",\"description\":\"10 ps bits\",\"category\":\"MCQ\"},\"max_time\":0,\"max_score\":1,\"partial_scoring\":true,\"layout\":\"Horizontal\",\"isShuffleOption\":false,\"questionCount\":1},\"media\":[{\"id\":385467069,\"src\":\"/assets/public//content/do_31270089678451507213885/artifact/ice_screenshot_20190217-164338_1550402439889.png\",\"assetId\":\"do_31270089678451507213885\",\"type\":\"image\",\"preload\":false},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true}]},\"medium\":\"Telugu\",\"questionTitle\":\"Examprep_10em ps_cha4 Q3\",\"qlevel\":\"MEDIUM\",\"subject\":\"Physical Science\",\"board\":\"State (Andhra Pradesh)\",\"templateType\":\"Horizontal\",\"isPartialScore\":true,\"gradeLevel\":[\"Class 10\"],\"isShuffleOption\":false,\"topic\":[\"Classification Of Elements\"],\"questionDesc\":\"10 ps bits\",\"max_score\":1,\"name\":\"Examprep_10em ps_cha8- Q3\",\"title\":\"Examprep_10em ps_cha8- Q3\",\"topicData\":\"(1) topics selected\",\"category\":\"MCQ\"},\"max_time\":0,\"max_score\":1,\"partial_scoring\":true,\"layout\":\"Horizontal\",\"isShuffleOption\":false,\"questionCount\":1},\"media\":[{\"id\":385467069,\"src\":\"/assets/public//content/do_31270089678451507213885/artifact/ice_screenshot_20190217-164338_1550402439889.png\",\"assetId\":\"do_31270089678451507213885\",\"type\":\"image\",\"preload\":false},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true}]},\"medium\":\"Telugu\",\"questionTitle\":\"Examprep_10em ps_cha8- Q3\",\"qlevel\":\"DIFFICULT\",\"subject\":\"Physical Science\",\"board\":\"State (Andhra Pradesh)\",\"templateType\":\"Horizontal\",\"isPartialScore\":true,\"gradeLevel\":[\"Class 10\"],\"isShuffleOption\":false,\"topic\":[\"Classification Of Elements\"],\"max_score\":1,\"name\":\"Examprep_10tm ps_cha8- Q3\",\"title\":\"Copy of - Examprep_10tm ps_cha8- Q3\",\"topicData\":\"(1) topics selected\",\"description\":\"10 ps bits\",\"category\":\"MCQ\"},\"max_time\":0,\"max_score\":1,\"partial_scoring\":true,\"layout\":\"Horizontal\",\"isShuffleOption\":false,\"questionCount\":1},\"media\":[{\"id\":385467069,\"src\":\"/assets/public//content/do_31270089678451507213885/artifact/ice_screenshot_20190217-164338_1550402439889.png\",\"assetId\":\"do_31270089678451507213885\",\"type\":\"image\",\"preload\":false},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true}]},\"medium\":\"English\",\"questionTitle\":\"Copy of - Examprep_10tm ps_cha8- Q3\",\"qlevel\":\"MEDIUM\",\"subject\":\"Physical Science\",\"board\":\"State (Andhra Pradesh)\",\"templateType\":\"Horizontal\",\"isPartialScore\":true,\"gradeLevel\":[\"Class 10\"],\"isShuffleOption\":false,\"topic\":[],\"questionDesc\":\"10 ps bits\",\"max_score\":1,\"name\":\"Examprep_10em ps_cha8- Q5\",\"title\":\"Examprep_10em ps_cha8- Q5\",\"topicData\":\"(1) topics selected\",\"description\":\"10 PS BITS\",\"category\":\"MCQ\"},\"max_time\":0,\"max_score\":1,\"partial_scoring\":true,\"layout\":\"Horizontal\",\"isShuffleOption\":false,\"questionCount\":1},\"media\":[{\"id\":385467069,\"src\":\"/assets/public//content/do_31270089678451507213885/artifact/ice_screenshot_20190217-164338_1550402439889.png\",\"assetId\":\"do_31270089678451507213885\",\"type\":\"image\",\"preload\":false},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true}]},\"medium\":\"English\",\"questionTitle\":\"Examprep_10em ps_cha8- Q5\",\"qlevel\":\"MEDIUM\",\"subject\":\"Physical Science\",\"board\":\"State (Andhra Pradesh)\",\"templateType\":\"Horizontal\",\"isPartialScore\":true,\"gradeLevel\":[\"Class 10\"],\"topic\":[\"Structure Of Atom\"],\"questionDesc\":\"10 PS BITS\",\"max_score\":1,\"name\":\"Examprep_10em ps_cha8- Q5\",\"title\":\"Examprep_10em ps_cha8- Q5\",\"description\":\"10 PS BITS\",\"options\":[{\"answer\":true,\"value\":{\"type\":\"text\",\"asset\":\"1\"}}],\"identifier\":\"do_31270597409132544015699\",\"isSelected\":true,\"$$hashKey\":\"object:1142\"}]"},"config":{"__cdata":"{\"title\":\"10 PS BITS\",\"max_score\":1,\"allow_skip\":true,\"show_feedback\":true,\"shuffle_questions\":false,\"shuffle_options\":false,\"total_items\":1,\"btn_edit\":\"Edit\"}"},"org.ekstep.question":[{"id":"6d5e4460-7393-49e9-b158-430441c0e6c0","type":"mcq","pluginId":"org.ekstep.questionunit.mcq","pluginVer":"1.1","templateId":"horizontalMCQ","data":{"__cdata":"{\"question\":{\"text\":\"

Q)జిప్సమ్ మరియు ప్లాస్టర్ ఆఫ్ పారిస్ ల నందు ఉండే నీటి అణువులలో తేడా ఎంత?

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\"},\"options\":[{\"text\":\"

A)3/2

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\",\"isCorrect\":true,\"$$hashKey\":\"object:1034\"},{\"text\":\"

B)1/2

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1035\"},{\"text\":\"

C)2

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1045\"},{\"text\":\"

D)5/2

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1050\"}],\"questionCount\":0,\"media\":[{\"id\":385467069,\"src\":\"/assets/public//content/do_31270089678451507213885/artifact/ice_screenshot_20190217-164338_1550402439889.png\",\"assetId\":\"do_31270089678451507213885\",\"type\":\"image\",\"preload\":false},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true}]}"},"config":{"__cdata":"{\"metadata\":{\"data\":{\"plugin\":{\"id\":\"org.ekstep.questionunit.mcq\",\"version\":\"1.1\",\"templateId\":\"horizontalMCQ\"},\"data\":{\"question\":{\"text\":\"

Q)జిప్సమ్ మరియు ప్లాస్టర్ ఆఫ్ పారిస్ ల నందు ఉండే నీటి అణువులలో తేడా ఎంత?

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\"},\"options\":[{\"text\":\"

A)3/2

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\",\"isCorrect\":true,\"$$hashKey\":\"object:1034\"},{\"text\":\"

B)1/2

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1035\"},{\"text\":\"

C)2

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1045\"},{\"text\":\"

D)5/2

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1050\"}],\"questionCount\":0,\"media\":[{\"id\":385467069,\"src\":\"/assets/public//content/do_31270089678451507213885/artifact/ice_screenshot_20190217-164338_1550402439889.png\",\"assetId\":\"do_31270089678451507213885\",\"type\":\"image\",\"preload\":false},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true}]},\"config\":{\"metadata\":{\"data\":{\"plugin\":{\"id\":\"org.ekstep.questionunit.mcq\",\"version\":\"1.1\",\"templateId\":\"horizontalMCQ\"},\"data\":{\"question\":{\"text\":\"

Q)The difference of the molecules of water in gypsum and plaster       

\\n\\n

  of Paris is

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\"},\"options\":[{\"text\":\"

A)3/2

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\",\"isCorrect\":true,\"$$hashKey\":\"object:1034\"},{\"text\":\"

B)1/2

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1035\"},{\"text\":\"

C)2

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1045\"},{\"text\":\"

D)5/2

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1050\"}],\"questionCount\":0,\"media\":[{\"id\":385467069,\"src\":\"/assets/public//content/do_31270089678451507213885/artifact/ice_screenshot_20190217-164338_1550402439889.png\",\"assetId\":\"do_31270089678451507213885\",\"type\":\"image\",\"preload\":false},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true}]},\"config\":{\"metadata\":{\"data\":{\"plugin\":{\"id\":\"org.ekstep.questionunit.mcq\",\"version\":\"1.1\",\"templateId\":\"horizontalMCQ\"},\"data\":{\"question\":{\"text\":\"

Q) కార్బన్ ఎలక్ట్రాన్ విన్యాసాన్ని ఈ క్రింది విధంగా రాయరాదని సాత్విక చెప్పింది. ఎందువలన?

\\n\",\"image\":\"/assets/public//content/do_31270089678451507213885/artifact/ice_screenshot_20190217-164338_1550402439889.png\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\"},\"options\":[{\"text\":\"

A)ఆఫ్  బౌ నియమాన్ని పాటించుట లేదు

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1034\"},{\"text\":\"

B)హుండు నియమాన్ని పాటించుట లేదు

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\",\"isCorrect\":true,\"$$hashKey\":\"object:1035\"},{\"text\":\"

C)పౌలీవర్జన నియమాన్ని పాటించుట లేదు

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1045\"},{\"text\":\"

D)పై వన్నీ సరైనవే

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1050\"}],\"questionCount\":0,\"media\":[{\"id\":385467069,\"src\":\"/assets/public//content/do_31270089678451507213885/artifact/ice_screenshot_20190217-164338_1550402439889.png\",\"assetId\":\"do_31270089678451507213885\",\"type\":\"image\",\"preload\":false},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true}]},\"config\":{\"metadata\":{\"data\":{\"plugin\":{\"id\":\"org.ekstep.questionunit.mcq\",\"version\":\"1.1\",\"templateId\":\"horizontalMCQ\"},\"data\":{\"question\":{\"text\":\"

Q) Satwika told that the electronic configuration of carbon cannot be written like this……why?

\\n\\n

Be cause this electronic configuration does not obey

\\n\",\"image\":\"/assets/public//content/do_31270089678451507213885/artifact/ice_screenshot_20190217-164338_1550402439889.png\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\"},\"options\":[{\"text\":\"

A) aufbau principle  

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1034\"},{\"text\":\"

B) Hund”s rule

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\",\"isCorrect\":true,\"$$hashKey\":\"object:1035\"},{\"text\":\"

C) Paul”s exclusion princple 

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1045\"},{\"text\":\"

D) All the above

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1050\"}],\"questionCount\":0,\"media\":[{\"id\":385467069,\"src\":\"/assets/public//content/do_31270089678451507213885/artifact/ice_screenshot_20190217-164338_1550402439889.png\",\"assetId\":\"do_31270089678451507213885\",\"type\":\"image\",\"preload\":false},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true}]},\"config\":{\"metadata\":{\"data\":{\"plugin\":{\"id\":\"org.ekstep.questionunit.mcq\",\"version\":\"1.1\",\"templateId\":\"horizontalMCQ\"},\"data\":{\"question\":{\"text\":\"

Q) Satwika told that the electronic configuration of carbon cannot be written like this……why?

\\n\\n

Be cause this electronic configuration does not obey

\\n\",\"image\":\"/assets/public//content/do_31270089678451507213885/artifact/ice_screenshot_20190217-164338_1550402439889.png\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\"},\"options\":[{\"text\":\"

A) aufbau principle  

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1034\"},{\"text\":\"

B) Hund”s rule

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\",\"isCorrect\":true,\"$$hashKey\":\"object:1035\"},{\"text\":\"

C) Paul”s exclusion princple 

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1045\"},{\"text\":\"

D) All the above

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1050\"}],\"questionCount\":0,\"media\":[]},\"config\":{\"metadata\":{\"data\":{\"plugin\":{\"id\":\"org.ekstep.questionunit.mcq\",\"version\":\"1.1\",\"templateId\":\"horizontalMCQ\"},\"data\":{\"question\":{\"text\":\"

Q) ఒక మూలకం పరమాణువ సంఖ్య 20 అయినా ఆవర్తన పట్టికలో ఆ మూలక స్థానం

\\n\\n
    \\n\\t
  1. 4 వ పీరియడ్ మరియు 2 వ గ్రూప్  B) 1 వ పీరియడ్ మరియు 2 వ గ్రూప్ C) 3 వ పీరియడ్ మరియు 13 వ గ్రూప్
  2. \\n
\\n\\n

D) 2 వ పీరియడ్ మరియు 2 వ గ్రూప్

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\"},\"options\":[{\"text\":\"

A) 4 వ పీరియడ్ మరియు 2 వ గ్రూప్  

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\",\"isCorrect\":true,\"$$hashKey\":\"object:1034\"},{\"text\":\"

B) 1 వ పీరియడ్ మరియు 2 వ గ్రూప్

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1035\"},{\"text\":\"

C) 3 వ పీరియడ్ మరియు 13 వ గ్రూప్

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1045\"},{\"text\":\"

D) 2 వ పీరియడ్ మరియు 2 వ గ్రూప్

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1050\"}],\"questionCount\":0,\"media\":[]},\"config\":{\"metadata\":{\"data\":{\"plugin\":{\"id\":\"org.ekstep.questionunit.mcq\",\"version\":\"1.1\",\"templateId\":\"horizontalMCQ\"},\"data\":{\"question\":{\"text\":\"

Q) If an atomic number of an element is 20 then what is the position of that element in a modern periodic table

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\"},\"options\":[{\"text\":\"

A) 4th period and 2nd group  

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\",\"isCorrect\":true,\"$$hashKey\":\"object:1034\"},{\"text\":\"

B) 1st period and 2nd group

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1035\"},{\"text\":\"

C) 3rd period and 13th group

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1045\"},{\"text\":\"

 D) 2nd period and 2nd group

\\n\\n

 

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1050\"}],\"questionCount\":0,\"media\":[]},\"config\":{\"metadata\":{\"data\":{\"plugin\":{\"id\":\"org.ekstep.questionunit.mcq\",\"version\":\"1.1\",\"templateId\":\"horizontalMCQ\"},\"data\":{\"question\":{\"text\":\"

Q) In a Doberiner triad the atomic weight of first and third elements are 32 and 125 , then what is the atomic weight of second one

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\"},\"options\":[{\"text\":\"

A) 78.5

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\",\"isCorrect\":true,\"$$hashKey\":\"object:1034\"},{\"text\":\"

B)80 

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1035\"},{\"text\":\"

C)23

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1045\"},{\"text\":\"

D)60

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1050\"}],\"questionCount\":0,\"media\":[]},\"config\":{\"metadata\":{\"data\":{\"plugin\":{\"id\":\"org.ekstep.questionunit.mcq\",\"version\":\"1.1\",\"templateId\":\"horizontalMCQ\"},\"data\":{\"question\":{\"text\":\"

Q) Find the frequency of a radiowave of wave lenth 100 metres?

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\"},\"options\":[{\"text\":\"

A) 3 HZ  

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1034\"},{\"text\":\"

B)3 KHZ  

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1035\"},{\"text\":\"

C)3 MHZ 

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"isCorrect\":true,\"$$hashKey\":\"object:1045\"},{\"text\":\"

D)30 MHZ

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1050\"}],\"questionCount\":0,\"media\":[]},\"config\":{\"metadata\":{\"data\":{\"plugin\":{\"id\":\"org.ekstep.questionunit.mcq\",\"version\":\"1.1\",\"templateId\":\"horizontalMCQ\"},\"data\":{\"question\":{\"text\":\"

 Q .When l=2 , what is the maximum value of ml

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\"},\"options\":[{\"text\":\"

A) 2

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\",\"isCorrect\":true,\"$$hashKey\":\"object:1034\"},{\"text\":\"

B)-2

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1035\"},{\"text\":\"

C)2(2)+1

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1045\"},{\"text\":\"

D)0

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1050\"}],\"questionCount\":0,\"media\":[]},\"config\":{\"metadata\":{\"data\":{\"plugin\":{\"id\":\"org.ekstep.questionunit.mcq\",\"version\":\"1.1\",\"templateId\":\"horizontalMCQ\"},\"data\":{\"question\":{\"text\":\"

1) l=2 అయినపుడు  యొక్క mlగరిష్ట విలువ ఎంత?

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\"},\"options\":[{\"text\":\"

A) 2

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\",\"isCorrect\":true,\"$$hashKey\":\"object:1034\"},{\"text\":\"

B)-2

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1035\"},{\"text\":\"

C)2(2)+1

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1045\"},{\"text\":\"

D)0

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1050\"}],\"questionCount\":0,\"media\":[]},\"config\":{\"metadata\":{\"data\":{\"plugin\":{\"id\":\"org.ekstep.questionunit.mcq\",\"version\":\"1.1\",\"templateId\":\"horizontalMCQ\"},\"data\":{\"question\":{\"text\":\"

1) l=2 అయినపుడు  యొక్క mlగరిష్ట విలువ ఎంత?

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\"},\"options\":[{\"text\":\"

A) 2

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1034\"},{\"text\":\"

B)-2

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"hint\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1035\"},{\"text\":\"

C)2(2)+1

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"isCorrect\":true,\"$$hashKey\":\"object:1045\"},{\"text\":\"

D)0

\\n\",\"image\":\"\",\"audio\":\"\",\"audioName\":\"\",\"isCorrect\":false,\"$$hashKey\":\"object:1050\"}],\"questionCount\":0,\"media\":[]},\"config\":{\"metadata\":{\"max_score\":1,\"isShuffleOption\":false,\"isPartialScore\":true,\"templateType\":\"Horizontal\",\"name\":\"\\n\\tఈ క్రింది వాటిలో విశిష్టోష్ణానికి ప్రమాణం\\n\\n\",\"title\":\"Copy of - \\n\\tఈ క్రింది వాటిలో విశిష్టోష్ణానికి ప్రమాణం\\n\\n\",\"board\":\"State (Andhra Pradesh)\",\"topic\":[],\"medium\":\"Telugu\",\"gradeLevel\":[\"Class 10\"],\"subject\":\"Physical Science\",\"qlevel\":\"EASY\",\"description\":\"10 th ps bits\",\"category\":\"MCQ\",\"topicData\":\"(0) topics selected\"},\"max_time\":0,\"max_score\":1,\"partial_scoring\":true,\"layout\":\"Horizontal\",\"isShuffleOption\":false,\"questionCount\":1},\"media\":[]},\"medium\":\"Telugu\",\"questionTitle\":\"Copy of - \\n\\tఈ క్రింది వాటిలో విశిష్టోష్ణానికి ప్రమాణం\\n\\n\",\"qlevel\":\"MEDIUM\",\"subject\":\"Physical Science\",\"board\":\"State (Andhra Pradesh)\",\"templateType\":\"Horizontal\",\"isPartialScore\":true,\"gradeLevel\":[\"Class 10\"],\"isShuffleOption\":false,\"topic\":[\"Structure Of Atom\"],\"questionDesc\":\"10 th ps bits\",\"max_score\":1,\"name\":\"Examprep_10tm_ps_cha8_Q1\",\"title\":\"Copy of - Copy of - Copy of - Examprep_10tm_ps_cha8_Q1\",\"topicData\":\"(0) topics selected\",\"description\":\"10 PS BITS\",\"category\":\"MCQ\"},\"max_time\":0,\"max_score\":1,\"partial_scoring\":true,\"layout\":\"Horizontal\",\"isShuffleOption\":false,\"questionCount\":1},\"media\":[]},\"medium\":\"Telugu\",\"questionTitle\":\"Copy of - Examprep_10tm_ps_cha8_Q1\",\"qlevel\":\"MEDIUM\",\"subject\":\"Physical Science\",\"board\":\"State (Andhra Pradesh)\",\"templateType\":\"Horizontal\",\"isPartialScore\":true,\"gradeLevel\":[\"Class 10\"],\"isShuffleOption\":false,\"topic\":[\"Structure Of Atom\"],\"questionDesc\":\"10 PS BITS\",\"max_score\":1,\"name\":\"Examprep_10tm_ps_cha8_Q1\",\"title\":\"Copy of - Examprep_10tm_ps_cha8_Q1\",\"topicData\":\"(1) topics selected\",\"category\":\"MCQ\"},\"max_time\":0,\"max_score\":1,\"partial_scoring\":true,\"layout\":\"Horizontal\",\"isShuffleOption\":false,\"questionCount\":1},\"media\":[]},\"medium\":\"Telugu\",\"questionTitle\":\"Copy of - Examprep_10tm_ps_cha8_Q1\",\"qlevel\":\"DIFFICULT\",\"subject\":\"Physical Science\",\"board\":\"State (Andhra Pradesh)\",\"templateType\":\"Horizontal\",\"isPartialScore\":true,\"gradeLevel\":[\"Class 10\"],\"isShuffleOption\":false,\"topic\":[\"Structure Of Atom\"],\"max_score\":1,\"name\":\"Examprep_10em_ps_cha8_Q1\",\"title\":\"Copy of - Examprep_10em_ps_cha8_Q1\",\"topicData\":\"(1) topics selected\",\"description\":\"10 ps bits\",\"category\":\"MCQ\"},\"max_time\":0,\"max_score\":1,\"partial_scoring\":true,\"layout\":\"Horizontal\",\"isShuffleOption\":false,\"questionCount\":1},\"media\":[]},\"medium\":\"English\",\"questionTitle\":\"Copy of - Examprep_10em_ps_cha8_Q1\",\"qlevel\":\"DIFFICULT\",\"subject\":\"Physical Science\",\"board\":\"State (Andhra Pradesh)\",\"templateType\":\"Horizontal\",\"isPartialScore\":true,\"gradeLevel\":[\"Class 10\"],\"isShuffleOption\":false,\"topic\":[],\"questionDesc\":\"10 ps bits\",\"max_score\":1,\"name\":\"Examprep_10em_ps_cha8_Q2\",\"title\":\"Copy of - Examprep_10em_ps_cha8_Q2\",\"topicData\":\"(1) topics selected\",\"description\":\"10 PS BITS\",\"category\":\"MCQ\"},\"max_time\":0,\"max_score\":1,\"partial_scoring\":true,\"layout\":\"Horizontal\",\"isShuffleOption\":false,\"questionCount\":1},\"media\":[]},\"medium\":\"English\",\"questionTitle\":\"Copy of - Examprep_10em_ps_cha8_Q2\",\"qlevel\":\"DIFFICULT\",\"subject\":\"Physical Science\",\"board\":\"State (Andhra Pradesh)\",\"templateType\":\"Horizontal\",\"isPartialScore\":true,\"gradeLevel\":[\"Class 10\"],\"isShuffleOption\":false,\"topic\":[\"Classification Of Elements\"],\"questionDesc\":\"10 PS BITS\",\"max_score\":1,\"name\":\"Examprep_10em_ps_cha8_Q2\",\"title\":\"Copy of - Examprep_10em_ps_cha8_Q2\",\"topicData\":\"(0) topics selected\",\"category\":\"MCQ\"},\"max_time\":0,\"max_score\":1,\"partial_scoring\":true,\"layout\":\"Horizontal\",\"isShuffleOption\":false,\"questionCount\":1},\"media\":[]},\"medium\":\"English\",\"questionTitle\":\"Copy of - Examprep_10em_ps_cha8_Q2\",\"qlevel\":\"DIFFICULT\",\"subject\":\"Physical Science\",\"board\":\"State (Andhra Pradesh)\",\"templateType\":\"Horizontal\",\"isPartialScore\":true,\"gradeLevel\":[\"Class 10\"],\"isShuffleOption\":false,\"topic\":[\"Classification Of Elements\"],\"max_score\":1,\"name\":\"Copy of - Examprep_10em_ps_cha8_Q2\",\"title\":\"Copy of - Copy of - Examprep_10em_ps_cha8_Q2\",\"topicData\":\"(1) topics selected\",\"description\":\"10 ps bits\",\"category\":\"MCQ\"},\"max_time\":0,\"max_score\":1,\"partial_scoring\":true,\"layout\":\"Horizontal\",\"isShuffleOption\":false,\"questionCount\":1},\"media\":[]},\"medium\":\"Telugu\",\"questionTitle\":\"Copy of - Copy of - Examprep_10em_ps_cha8_Q2\",\"qlevel\":\"DIFFICULT\",\"subject\":\"Physical Science\",\"board\":\"State (Andhra Pradesh)\",\"templateType\":\"Horizontal\",\"isPartialScore\":true,\"gradeLevel\":[\"Class 10\"],\"isShuffleOption\":false,\"topic\":[\"Classification Of Elements\"],\"questionDesc\":\"10 ps bits\",\"max_score\":1,\"name\":\"Examprep_10tm_ps_cha8_Q2\",\"title\":\"Copy of - Copy of - Examprep_10tm_ps_cha8_Q2\",\"topicData\":\"(1) topics selected\",\"description\":\"10 PS BITS\",\"category\":\"MCQ\"},\"max_time\":0,\"max_score\":1,\"partial_scoring\":true,\"layout\":\"Horizontal\",\"isShuffleOption\":false,\"questionCount\":1},\"media\":[]},\"medium\":\"Telugu\",\"questionTitle\":\"Copy of - Examprep_10tm_ps_cha8_Q2\",\"qlevel\":\"MEDIUM\",\"subject\":\"Physical Science\",\"board\":\"State (Andhra Pradesh)\",\"templateType\":\"Horizontal\",\"isPartialScore\":true,\"gradeLevel\":[\"Class 10\"],\"isShuffleOption\":false,\"topic\":[\"Classification Of Elements\"],\"questionDesc\":\"10 PS BITS\",\"max_score\":1,\"name\":\"Examprep_10em ps_cha4 Q3\",\"title\":\"Examprep_10em ps_cha4 Q3\",\"topicData\":\"(1) topics selected\",\"description\":\"10 ps bits\",\"category\":\"MCQ\"},\"max_time\":0,\"max_score\":1,\"partial_scoring\":true,\"layout\":\"Horizontal\",\"isShuffleOption\":false,\"questionCount\":1},\"media\":[{\"id\":385467069,\"src\":\"/assets/public//content/do_31270089678451507213885/artifact/ice_screenshot_20190217-164338_1550402439889.png\",\"assetId\":\"do_31270089678451507213885\",\"type\":\"image\",\"preload\":false},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true}]},\"medium\":\"Telugu\",\"questionTitle\":\"Examprep_10em ps_cha4 Q3\",\"qlevel\":\"MEDIUM\",\"subject\":\"Physical Science\",\"board\":\"State (Andhra Pradesh)\",\"templateType\":\"Horizontal\",\"isPartialScore\":true,\"gradeLevel\":[\"Class 10\"],\"isShuffleOption\":false,\"topic\":[\"Classification Of Elements\"],\"questionDesc\":\"10 ps bits\",\"max_score\":1,\"name\":\"Examprep_10em ps_cha8- Q3\",\"title\":\"Examprep_10em ps_cha8- Q3\",\"topicData\":\"(1) topics selected\",\"category\":\"MCQ\"},\"max_time\":0,\"max_score\":1,\"partial_scoring\":true,\"layout\":\"Horizontal\",\"isShuffleOption\":false,\"questionCount\":1},\"media\":[{\"id\":385467069,\"src\":\"/assets/public//content/do_31270089678451507213885/artifact/ice_screenshot_20190217-164338_1550402439889.png\",\"assetId\":\"do_31270089678451507213885\",\"type\":\"image\",\"preload\":false},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true}]},\"medium\":\"Telugu\",\"questionTitle\":\"Examprep_10em ps_cha8- Q3\",\"qlevel\":\"DIFFICULT\",\"subject\":\"Physical Science\",\"board\":\"State (Andhra Pradesh)\",\"templateType\":\"Horizontal\",\"isPartialScore\":true,\"gradeLevel\":[\"Class 10\"],\"isShuffleOption\":false,\"topic\":[\"Classification Of Elements\"],\"max_score\":1,\"name\":\"Examprep_10tm ps_cha8- Q3\",\"title\":\"Copy of - Examprep_10tm ps_cha8- Q3\",\"topicData\":\"(1) topics selected\",\"description\":\"10 ps bits\",\"category\":\"MCQ\"},\"max_time\":0,\"max_score\":1,\"partial_scoring\":true,\"layout\":\"Horizontal\",\"isShuffleOption\":false,\"questionCount\":1},\"media\":[{\"id\":385467069,\"src\":\"/assets/public//content/do_31270089678451507213885/artifact/ice_screenshot_20190217-164338_1550402439889.png\",\"assetId\":\"do_31270089678451507213885\",\"type\":\"image\",\"preload\":false},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true}]},\"medium\":\"English\",\"questionTitle\":\"Copy of - Examprep_10tm ps_cha8- Q3\",\"qlevel\":\"MEDIUM\",\"subject\":\"Physical Science\",\"board\":\"State (Andhra Pradesh)\",\"templateType\":\"Horizontal\",\"isPartialScore\":true,\"gradeLevel\":[\"Class 10\"],\"isShuffleOption\":false,\"topic\":[],\"questionDesc\":\"10 ps bits\",\"max_score\":1,\"name\":\"Examprep_10em ps_cha8- Q5\",\"title\":\"Examprep_10em ps_cha8- Q5\",\"topicData\":\"(1) topics selected\",\"description\":\"10 PS BITS\",\"category\":\"MCQ\"},\"max_time\":0,\"max_score\":1,\"partial_scoring\":true,\"layout\":\"Horizontal\",\"isShuffleOption\":false,\"questionCount\":1},\"media\":[{\"id\":385467069,\"src\":\"/assets/public//content/do_31270089678451507213885/artifact/ice_screenshot_20190217-164338_1550402439889.png\",\"assetId\":\"do_31270089678451507213885\",\"type\":\"image\",\"preload\":false},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.audioicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png\",\"assetId\":\"org.ekstep.questionset.audioicon\",\"type\":\"image\",\"preload\":true},{\"id\":\"org.ekstep.questionset.default-imgageicon\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png\",\"assetId\":\"org.ekstep.questionset.default-imgageicon\",\"type\":\"image\",\"preload\":true}]},\"medium\":\"English\",\"questionTitle\":\"Examprep_10em ps_cha8- Q5\",\"qlevel\":\"MEDIUM\",\"subject\":\"Physical Science\",\"board\":\"State (Andhra Pradesh)\",\"templateType\":\"Horizontal\",\"isPartialScore\":true,\"gradeLevel\":[\"Class 10\"],\"isShuffleOption\":false,\"topic\":[\"Structure Of Atom\"],\"questionDesc\":\"10 PS BITS\",\"max_score\":1,\"name\":\"Examprep_10em ps_cha8- Q5\",\"title\":\"Examprep_10em ps_cha8- Q5\",\"description\":\"10 PS BITS\",\"category\":\"MCQ\"},\"max_time\":0,\"max_score\":1,\"partial_scoring\":true,\"layout\":\"Horizontal\",\"isShuffleOption\":false,\"questionCount\":1}"},"w":80,"h":85,"x":9,"y":6}]}]},{"x":0,"y":0,"w":100,"h":100,"id":"85c23e06-51c9-416b-ac9b-b9d5386a37ee","rotate":null,"config":{"__cdata":"{\"opacity\":100,\"strokeWidth\":1,\"stroke\":\"rgba(255, 255, 255, 0)\",\"autoplay\":false,\"visible\":true,\"color\":\"#FFFFFF\",\"genieControls\":false,\"instructions\":\"\"}"},"param":[{"name":"previous","value":"b8b47094-1d69-43a1-9c88-c02e760996c5"}],"manifest":{"media":[{"assetId":"8dbbc49b-6776-458c-b211-c1a260437421"}]},"org.ekstep.video":[{"y":7.9,"x":10.97,"w":78.4,"h":79.51,"rotate":0,"z-index":0,"id":"8dbbc49b-6776-458c-b211-c1a260437421","config":{"__cdata":"{\"autoplay\":true,\"controls\":true,\"muted\":false,\"visible\":true,\"url\":\"https://drive.google.com/uc?export=download&id=1evKCELQOUYdT4unEdUdpemdS18vu4euZ\"}"}}]}],"manifest":{"media":[{"id":"2381510b-4d49-47d5-b765-c130233028dc","plugin":"org.ekstep.navigation","ver":"1.0","src":"/content-plugins/org.ekstep.navigation-1.0/renderer/controller/navigation_ctrl.js","type":"js"},{"id":"e34e434b-c4de-4907-b025-56c156957112","plugin":"org.ekstep.navigation","ver":"1.0","src":"/content-plugins/org.ekstep.navigation-1.0/renderer/templates/navigation.html","type":"js"},{"id":"org.ekstep.navigation","plugin":"org.ekstep.navigation","ver":"1.0","src":"/content-plugins/org.ekstep.navigation-1.0/renderer/plugin.js","type":"plugin"},{"id":"org.ekstep.navigation_manifest","plugin":"org.ekstep.navigation","ver":"1.0","src":"/content-plugins/org.ekstep.navigation-1.0/manifest.json","type":"json"},{"id":"org.ekstep.questionunit.renderer.audioicon","plugin":"org.ekstep.questionunit","ver":"1.0","src":"/content-plugins/org.ekstep.questionunit-1.0/renderer/assets/audio-icon.png","type":"image"},{"id":"org.ekstep.questionunit.renderer.downarrow","plugin":"org.ekstep.questionunit","ver":"1.0","src":"/content-plugins/org.ekstep.questionunit-1.0/renderer/assets/down_arrow.png","type":"image"},{"id":"aa57e9c4-e978-4ab1-be74-09a657aa092c","plugin":"org.ekstep.questionunit","ver":"1.0","src":"/content-plugins/org.ekstep.questionunit-1.0/renderer/components/js/components.js","type":"js"},{"id":"07bac729-0b71-42d1-b4e3-d5398474d0d0","plugin":"org.ekstep.questionunit","ver":"1.0","src":"/content-plugins/org.ekstep.questionunit-1.0/renderer/components/css/components.css","type":"css"},{"id":"6b74a0ea-d9dc-4ae9-9134-abe3124260da","plugin":"org.ekstep.questionunit","ver":"1.0","src":"/content-plugins/org.ekstep.questionunit-1.0/renderer/libs/katex/katex.min.js","type":"js"},{"id":"d5b4d576-7477-490b-9713-d9857b5beab1","plugin":"org.ekstep.questionunit","ver":"1.0","src":"/content-plugins/org.ekstep.questionunit-1.0/renderer/libs/katex/katex.min.css","type":"css"},{"id":"8aa00822-5d00-400c-bdbd-9847737db6a5","plugin":"org.ekstep.questionunit","ver":"1.0","src":"/content-plugins/org.ekstep.questionunit-1.0/renderer/libs/katex/fonts/katex_main-bold.ttf","type":"js"},{"id":"bdace240-7a3b-4b7b-b9d1-564747334578","plugin":"org.ekstep.questionunit","ver":"1.0","src":"/content-plugins/org.ekstep.questionunit-1.0/renderer/libs/katex/fonts/katex_main-bolditalic.ttf","type":"js"},{"id":"363edb27-4782-4ee5-b7c7-cb8e85e29d52","plugin":"org.ekstep.questionunit","ver":"1.0","src":"/content-plugins/org.ekstep.questionunit-1.0/renderer/libs/katex/fonts/katex_main-italic.ttf","type":"js"},{"id":"939ed1b9-7e48-4c1d-aeee-831d44698d6c","plugin":"org.ekstep.questionunit","ver":"1.0","src":"/content-plugins/org.ekstep.questionunit-1.0/renderer/libs/katex/fonts/katex_main-regular.ttf","type":"js"},{"id":"9e8dca64-6a00-4dde-b840-3ee96d50a2b8","plugin":"org.ekstep.questionunit","ver":"1.0","src":"/content-plugins/org.ekstep.questionunit-1.0/renderer/libs/katex/fonts/katex_math-bolditalic.ttf","type":"js"},{"id":"d7135270-d403-48ce-be16-c449d26e6da7","plugin":"org.ekstep.questionunit","ver":"1.0","src":"/content-plugins/org.ekstep.questionunit-1.0/renderer/libs/katex/fonts/katex_math-italic.ttf","type":"js"},{"id":"3d410ae3-3072-43d8-b452-9620eb5585ed","plugin":"org.ekstep.questionunit","ver":"1.0","src":"/content-plugins/org.ekstep.questionunit-1.0/renderer/libs/katex/fonts/katex_math-regular.ttf","type":"js"},{"id":"5447cb80-7597-411c-90dc-13a0c02cc863","plugin":"org.ekstep.questionunit","ver":"1.0","src":"/content-plugins/org.ekstep.questionunit-1.0/renderer/libs/katex/fonts/katex_size1-regular.ttf","type":"js"},{"id":"08358247-a9e0-4cb3-8bb3-9b97b7d6174e","plugin":"org.ekstep.questionunit","ver":"1.0","src":"/content-plugins/org.ekstep.questionunit-1.0/renderer/libs/katex/fonts/katex_size2-regular.ttf","type":"js"},{"id":"03556441-a8cd-45a6-93b7-6e98743490b1","plugin":"org.ekstep.questionunit","ver":"1.0","src":"/content-plugins/org.ekstep.questionunit-1.0/renderer/libs/katex/fonts/katex_size3-regular.ttf","type":"js"},{"id":"4713a36c-49dc-4b66-a387-79e04c050598","plugin":"org.ekstep.questionunit","ver":"1.0","src":"/content-plugins/org.ekstep.questionunit-1.0/renderer/libs/katex/fonts/katex_size4-regular.ttf","type":"js"},{"id":"org.ekstep.questionunit","plugin":"org.ekstep.questionunit","ver":"1.0","src":"/content-plugins/org.ekstep.questionunit-1.0/renderer/plugin.js","type":"plugin"},{"id":"org.ekstep.questionunit_manifest","plugin":"org.ekstep.questionunit","ver":"1.0","src":"/content-plugins/org.ekstep.questionunit-1.0/manifest.json","type":"json"},{"id":"b6a388cc-6d04-4cb1-9ca0-5919a75ddb9c","plugin":"org.ekstep.questionunit.mcq","ver":"1.1","src":"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/styles/style.css","type":"css"},{"id":"97b66a43-6b00-4753-aebc-d2b270e1c702","plugin":"org.ekstep.questionunit.mcq","ver":"1.1","src":"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/styles/horizontal_and_vertical.css","type":"css"},{"id":"367e9c5d-aa82-436c-9767-31a4e3d89356","plugin":"org.ekstep.questionunit.mcq","ver":"1.1","src":"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/template/mcq-layouts.js","type":"js"},{"id":"95e6a633-711c-4e51-ae0a-4207874b6be9","plugin":"org.ekstep.questionunit.mcq","ver":"1.1","src":"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/template/template_controller.js","type":"js"},{"id":"28903dd9-4012-4866-9d1c-061a53db19a3","plugin":"org.ekstep.questionunit.mcq","ver":"1.1","src":"/content-plugins/org.ekstep.questionunit.mcq-1.1//renderer/assets/tick_icon.png","type":"image"},{"id":"008a394a-399c-4010-9b83-a727abe607c0","plugin":"org.ekstep.questionunit.mcq","ver":"1.1","src":"/content-plugins/org.ekstep.questionunit.mcq-1.1//renderer/assets/audio-icon2.png","type":"image"},{"id":"030d52c7-669a-49e7-ad10-2632bdb949e8","plugin":"org.ekstep.questionunit.mcq","ver":"1.1","src":"/content-plugins/org.ekstep.questionunit.mcq-1.1//renderer/assets/music-blue.png","type":"image"},{"id":"org.ekstep.questionunit.mcq","plugin":"org.ekstep.questionunit.mcq","ver":"1.1","src":"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/plugin.js","type":"plugin"},{"id":"org.ekstep.questionunit.mcq_manifest","plugin":"org.ekstep.questionunit.mcq","ver":"1.1","src":"/content-plugins/org.ekstep.questionunit.mcq-1.1/manifest.json","type":"json"},{"id":"org.ekstep.questionset.quiz","plugin":"org.ekstep.questionset.quiz","ver":"1.0","src":"/content-plugins/org.ekstep.questionset.quiz-1.0/renderer/plugin.js","type":"plugin"},{"id":"org.ekstep.questionset.quiz_manifest","plugin":"org.ekstep.questionset.quiz","ver":"1.0","src":"/content-plugins/org.ekstep.questionset.quiz-1.0/manifest.json","type":"json"},{"id":"org.ekstep.iterator","plugin":"org.ekstep.iterator","ver":"1.0","src":"/content-plugins/org.ekstep.iterator-1.0/renderer/plugin.js","type":"plugin"},{"id":"org.ekstep.iterator_manifest","plugin":"org.ekstep.iterator","ver":"1.0","src":"/content-plugins/org.ekstep.iterator-1.0/manifest.json","type":"json"},{"id":"ec943bc6-5684-483c-95a0-db88990c2ea6","plugin":"org.ekstep.questionset","ver":"1.0","src":"/content-plugins/org.ekstep.questionset-1.0/renderer/utils/telemetry_logger.js","type":"js"},{"id":"87862711-b63c-4140-a355-dd4cd5d336b4","plugin":"org.ekstep.questionset","ver":"1.0","src":"/content-plugins/org.ekstep.questionset-1.0/renderer/utils/html_audio_plugin.js","type":"js"},{"id":"4b619cde-e3da-429c-bc05-b329365bc9b0","plugin":"org.ekstep.questionset","ver":"1.0","src":"/content-plugins/org.ekstep.questionset-1.0/renderer/utils/qs_feedback_popup.js","type":"js"},{"id":"org.ekstep.questionset","plugin":"org.ekstep.questionset","ver":"1.0","src":"/content-plugins/org.ekstep.questionset-1.0/renderer/plugin.js","type":"plugin"},{"id":"org.ekstep.questionset_manifest","plugin":"org.ekstep.questionset","ver":"1.0","src":"/content-plugins/org.ekstep.questionset-1.0/manifest.json","type":"json"},{"id":"2caa1e61-bc5a-4104-a74c-65dcda49a148","plugin":"org.ekstep.video","ver":"1.3","src":"/content-plugins/org.ekstep.video-1.3/renderer/libs/video.js","type":"js"},{"id":"7a98d0c5-d4c0-4eda-bd52-5c793ab29aab","plugin":"org.ekstep.video","ver":"1.3","src":"/content-plugins/org.ekstep.video-1.3/renderer/libs/videoyoutube.js","type":"js"},{"id":"026257be-900a-4929-ae15-8ba4632d1225","plugin":"org.ekstep.video","ver":"1.3","src":"/content-plugins/org.ekstep.video-1.3/renderer/libs/videojs.css","type":"css"},{"id":"org.ekstep.video","plugin":"org.ekstep.video","ver":"1.3","src":"/content-plugins/org.ekstep.video-1.3/renderer/videoplugin.js","type":"plugin"},{"id":"org.ekstep.video_manifest","plugin":"org.ekstep.video","ver":"1.3","src":"/content-plugins/org.ekstep.video-1.3/manifest.json","type":"json"},{"id":385467069,"src":"/assets/public//content/do_31270089678451507213885/artifact/ice_screenshot_20190217-164338_1550402439889.png","assetId":"do_31270089678451507213885","type":"image","preload":false},{"id":"QuizImage","src":"/content-plugins/org.ekstep.questionset-1.0/editor/assets/quizimage.png","assetId":"QuizImage","type":"image","preload":true},{"id":"org.ekstep.questionset.audioicon","src":"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio.png","assetId":"org.ekstep.questionset.audioicon","type":"image","preload":true},{"id":"org.ekstep.questionset.default-imgageicon","src":"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/default-image.png","assetId":"org.ekstep.questionset.default-imgageicon","type":"image","preload":true},{"id":"8dbbc49b-6776-458c-b211-c1a260437421","src":"https://drive.google.com/uc?export=download&id=1evKCELQOUYdT4unEdUdpemdS18vu4euZ","assetId":"8dbbc49b-6776-458c-b211-c1a260437421","type":"video"}]},"plugin-manifest":{"plugin":[{"id":"org.ekstep.navigation","ver":"1.0","type":"plugin","depends":""},{"id":"org.ekstep.questionunit","ver":"1.0","type":"plugin","depends":""},{"id":"org.ekstep.questionunit.mcq","ver":"1.1","type":"plugin","depends":"org.ekstep.questionunit"},{"id":"org.ekstep.questionset.quiz","ver":"1.0","type":"plugin","depends":""},{"id":"org.ekstep.iterator","ver":"1.0","type":"plugin","depends":""},{"id":"org.ekstep.questionset","ver":"1.0","type":"plugin","depends":"org.ekstep.questionset.quiz,org.ekstep.iterator"},{"id":"org.ekstep.video","ver":"1.3","type":"widget","depends":""}]},"compatibilityVersion":4}}')); + + + +INSERT INTO hierarchy_store.content_hierarchy(identifier, hierarchy, relational_metadata) VALUES ('do_21351537329604198413739', '{"copyright":"tn","lastStatusChangedOn":"2022-04-13T04:57:21.808+0000","originData":"{\"name\":\"4.8 upcoming batch course\",\"copyType\":\"deep\",\"license\":\"CC BY 4.0\",\"organisation\":[\"Tamil Nadu\",\"MPPS HANUMANNAHALLI\"],\"author\":\"newtncc guy\"}","publish_type":"public","author":"newtncc guy","se_mediumIds":["tn_k-12_5_medium_english","tn_k-12_5_medium_tamil"],"organisation":["Tamil Nadu","MPPS HANUMANNAHALLI"],"children":[{"lastStatusChangedOn":"2022-04-13T04:57:24.497+0000","parent":"do_21351537329604198413739","children":[{"copyright":"Tamil Nadu, PUPS, REDDIYARPATTI","lastStatusChangedOn":"2021-10-29T07:21:31.809+0000","parent":"do_21351537331811942413741","licenseterms":"By creating any type of content (resources, books, courses etc.) on DIKSHA, you consent to publish it under the Creative Commons License Framework. Please choose the applicable creative commons license you wish to apply to your content.","organisation":["Tamil Nadu","PUPS, REDDIYARPATTI"],"mediaType":"content","name":"AK course assess","discussionForum":"{\"enabled\":\"No\"}","createdOn":"2021-10-29T07:18:02.486+0000","createdFor":["01269878797503692810","012741962549116928720"],"channel":"01269878797503692810","assets":[],"plugins":"[{\"identifier\":\"org.ekstep.stage\",\"semanticVersion\":\"1.0\"},{\"identifier\":\"org.ekstep.questionset\",\"semanticVersion\":\"1.0\"},{\"identifier\":\"org.ekstep.navigation\",\"semanticVersion\":\"1.0\"},{\"identifier\":\"org.ekstep.questionset.quiz\",\"semanticVersion\":\"1.0\"},{\"identifier\":\"org.ekstep.iterator\",\"semanticVersion\":\"1.0\"},{\"identifier\":\"org.ekstep.questionunit\",\"semanticVersion\":\"1.2\"},{\"identifier\":\"org.ekstep.keyboard\",\"semanticVersion\":\"1.1\"},{\"identifier\":\"org.ekstep.questionunit.ftb\",\"semanticVersion\":\"1.1\"},{\"identifier\":\"org.ekstep.questionunit.mcq\",\"semanticVersion\":\"1.3\"},{\"identifier\":\"org.ekstep.questionunit.mtf\",\"semanticVersion\":\"1.2\"},{\"identifier\":\"org.ekstep.summary\",\"semanticVersion\":\"1.0\"}]","lastUpdatedOn":"2021-10-29T07:21:31.809+0000","subject":["Skills","Environmental Science"],"size":585405,"editorState":"{\"plugin\":{\"noOfExtPlugins\":12,\"extPlugins\":[{\"plugin\":\"org.ekstep.contenteditorfunctions\",\"version\":\"1.2\"},{\"plugin\":\"org.ekstep.keyboardshortcuts\",\"version\":\"1.0\"},{\"plugin\":\"org.ekstep.richtext\",\"version\":\"1.0\"},{\"plugin\":\"org.ekstep.iterator\",\"version\":\"1.0\"},{\"plugin\":\"org.ekstep.navigation\",\"version\":\"1.0\"},{\"plugin\":\"org.ekstep.reviewercomments\",\"version\":\"1.0\"},{\"plugin\":\"org.ekstep.questionunit.mtf\",\"version\":\"1.2\"},{\"plugin\":\"org.ekstep.questionunit.mcq\",\"version\":\"1.3\"},{\"plugin\":\"org.ekstep.keyboard\",\"version\":\"1.1\"},{\"plugin\":\"org.ekstep.questionunit.reorder\",\"version\":\"1.1\"},{\"plugin\":\"org.ekstep.questionunit.sequence\",\"version\":\"1.1\"},{\"plugin\":\"org.ekstep.questionunit.ftb\",\"version\":\"1.1\"}]},\"stage\":{\"noOfStages\":1,\"currentStage\":\"ce1a09a4-7553-416d-9791-a1c343676425\",\"selectedPluginObject\":\"922b926e-1104-439c-ad35-06301265ea6d\"},\"sidebar\":{\"selectedMenu\":\"settings\"}}","se_topics":[],"streamingUrl":"https://sunbirdstagingpublic.blob.core.windows.net/sunbird-content-staging/content/ecml/do_21339794950117785611-latest","identifier":"do_21339794950117785611","description":"Enter description for Assessment","gradeLevel":["Class 1"],"ownershipType":["createdBy"],"compatibilityLevel":5,"audience":["Student"],"se_boards":["State (Tamil Nadu)"],"os":["All"],"cloudStorageKey":"content/do_21339794950117785611/artifact/1635492085307_do_21339794950117785611.zip","primaryCategory":"Course Assessment","appIcon":"https://sunbirdstagingpublic.blob.core.windows.net/sunbird-content-staging/content/do_21339794950117785611/artifact/do_2133016737715240961616_1623739474410_pexels-photo-594364.thumb.jpeg","se_mediums":["English"],"downloadUrl":"https://sunbirdstagingpublic.blob.core.windows.net/sunbird-content-staging/content/do_21339794950117785611/ak-course-assess_1635492091534_do_21339794950117785611_1.ecar","se_subjects":["Skills","Environmental Science"],"lockKey":"6c7cb7b8-cf43-423b-b146-bbdb21809574","medium":["English"],"framework":"tn_k-12_5","displayScore":true,"posterImage":"https://sunbirdstagingpublic.blob.core.windows.net/sunbird-content-staging/content/do_2133016737715240961616/artifact/do_2133016737715240961616_1623739474410_pexels-photo-594364.jpeg","creator":"newtncc","versionKey":"1635492303529","mimeType":"application/vnd.ekstep.ecml-archive","code":"org.sunbird.l1vyPd","license":"CC BY 4.0","maxAttempts":2,"version":2,"prevStatus":"Review","contentType":"SelfAssess","prevState":"Review","language":["English"],"board":"State (Tamil Nadu)","totalQuestions":5,"lastPublishedOn":"2021-10-29T07:21:27.739+0000","totalScore":5,"objectType":"Content","lastUpdatedBy":"deec6352-0f62-4306-9818-aba349a0e0f8","status":"Live","pragma":[],"createdBy":"deec6352-0f62-4306-9818-aba349a0e0f8","dialcodeRequired":"No","lastSubmittedOn":"2021-10-29T07:25:03.508+0000","interceptionPoints":"{}","idealScreenSize":"normal","contentEncoding":"gzip","depth":2,"consumerId":"cb069f8d-e4e1-46c5-831f-d4a83b323ada","lastPublishedBy":"91a81041-bbbd-4bd7-947f-09f9e469213c","topic":[],"se_gradeLevels":["Class 1"],"osId":"org.ekstep.quiz.app","copyrightYear":2021,"se_FWIds":["tn_k-12_5"],"contentDisposition":"inline","additionalCategories":["Classroom Teaching Video"],"previewUrl":"https://sunbirdstagingpublic.blob.core.windows.net/sunbird-content-staging/content/ecml/do_21339794950117785611-latest","artifactUrl":"https://sunbirdstagingpublic.blob.core.windows.net/sunbird-content-staging/content/do_21339794950117785611/artifact/1635492085307_do_21339794950117785611.zip","visibility":"Default","credentials":"{\"enabled\":\"No\"}","variants":"{\"full\":{\"ecarUrl\":\"https://sunbirdstagingpublic.blob.core.windows.net/sunbird-content-staging/content/do_21339794950117785611/ak-course-assess_1635492091534_do_21339794950117785611_1.ecar\",\"size\":\"587280\"},\"spine\":{\"ecarUrl\":\"https://sunbirdstagingpublic.blob.core.windows.net/sunbird-content-staging/content/do_21339794950117785611/ak-course-assess_1635492091732_do_21339794950117785611_1_SPINE.ecar\",\"size\":\"13396\"}}","index":1,"pkgVersion":1,"idealScreenDensity":"hdpi"},{"copyright":"NCERT","lastStatusChangedOn":"2020-10-14T05:10:21.536+0000","parent":"do_21351537331811942413741","licenseterms":"By creating any type of content (resources, books, courses etc.) on DIKSHA, you consent to publish it under the Creative Commons License Framework. Please choose the applicable creative commons license you wish to apply to your content.","organisation":["NCERT"],"mediaType":"content","name":"UT Self assess1`","createdOn":"2020-10-14T05:03:28.570+0000","createdFor":["01283607456185548825093"],"channel":"01283607456185548825093","assets":[],"plugins":"[{\"identifier\":\"org.ekstep.stage\",\"semanticVersion\":\"1.0\"},{\"identifier\":\"org.ekstep.questionset\",\"semanticVersion\":\"1.0\"},{\"identifier\":\"org.ekstep.navigation\",\"semanticVersion\":\"1.0\"},{\"identifier\":\"org.ekstep.questionset.quiz\",\"semanticVersion\":\"1.0\"},{\"identifier\":\"org.ekstep.iterator\",\"semanticVersion\":\"1.0\"},{\"identifier\":\"org.ekstep.questionunit\",\"semanticVersion\":\"1.2\"},{\"identifier\":\"org.ekstep.keyboard\",\"semanticVersion\":\"1.1\"},{\"identifier\":\"org.ekstep.questionunit.ftb\",\"semanticVersion\":\"1.1\"},{\"identifier\":\"org.ekstep.questionunit.mcq\",\"semanticVersion\":\"1.3\"},{\"identifier\":\"org.ekstep.questionunit.mtf\",\"semanticVersion\":\"1.2\"},{\"identifier\":\"org.ekstep.summary\",\"semanticVersion\":\"1.0\"}]","lastUpdatedOn":"2020-10-14T05:10:17.434+0000","subject":["Mathematics"],"size":219320.0,"editorState":"{\"plugin\":{\"noOfExtPlugins\":12,\"extPlugins\":[{\"plugin\":\"org.ekstep.contenteditorfunctions\",\"version\":\"1.2\"},{\"plugin\":\"org.ekstep.keyboardshortcuts\",\"version\":\"1.0\"},{\"plugin\":\"org.ekstep.richtext\",\"version\":\"1.0\"},{\"plugin\":\"org.ekstep.iterator\",\"version\":\"1.0\"},{\"plugin\":\"org.ekstep.navigation\",\"version\":\"1.0\"},{\"plugin\":\"org.ekstep.reviewercomments\",\"version\":\"1.0\"},{\"plugin\":\"org.ekstep.questionunit.mtf\",\"version\":\"1.2\"},{\"plugin\":\"org.ekstep.questionunit.mcq\",\"version\":\"1.3\"},{\"plugin\":\"org.ekstep.keyboard\",\"version\":\"1.1\"},{\"plugin\":\"org.ekstep.questionunit.reorder\",\"version\":\"1.1\"},{\"plugin\":\"org.ekstep.questionunit.sequence\",\"version\":\"1.1\"},{\"plugin\":\"org.ekstep.questionunit.ftb\",\"version\":\"1.1\"}]},\"stage\":{\"noOfStages\":1,\"currentStage\":\"73920173-9880-48bc-a29d-2ae495c2d81c\",\"selectedPluginObject\":\"450c8034-83e1-4e56-9c26-b790621b693e\"},\"sidebar\":{\"selectedMenu\":\"settings\"}}","streamingUrl":"https://sunbirdstagingpublic.blob.core.windows.net/sunbird-content-staging/content/ecml/do_2131289236157972481377-latest","identifier":"do_2131289236157972481377","description":"Enter description for Assessment","gradeLevel":["Class 2"],"ownershipType":["createdBy"],"compatibilityLevel":2,"audience":["Student"],"os":["All"],"primaryCategory":"Explanation Content","appIcon":"https://sunbirdstagingpublic.blob.core.windows.net/sunbird-content-staging/content/do_2131289236157972481377/artifact/634x951-potrait_1577356719873.thumb.jpeg","SYS_INTERNAL_LAST_UPDATED_ON":"2020-10-14T05:10:21.544+0000","downloadUrl":"https://sunbirdstagingpublic.blob.core.windows.net/sunbird-content-staging/ecar_files/do_2131289236157972481377/ut-self-assess1_1602652220063_do_2131289236157972481377_1.0.ecar","lockKey":"6baae97d-931e-4933-9dbf-8674bd6c48d9","medium":["English"],"framework":"ncert_k-12","posterImage":"https://sunbirdstagingpublic.blob.core.windows.net/sunbird-content-staging/content/do_212921706247528448196/artifact/634x951-potrait_1577356719873.jpeg","creator":"Creator","versionKey":"1602652217434","mimeType":"application/vnd.ekstep.ecml-archive","code":"org.sunbird.RgaKqV","license":"CC BY 4.0","version":2,"prevStatus":"Processing","contentType":"PracticeResource","prevState":"Review","language":["English"],"totalQuestions":3,"lastPublishedOn":"2020-10-14T05:10:20.063+0000","totalScore":3,"objectType":"Content","lastUpdatedBy":"3f0825d1-3543-4354-a5ef-5aed4fbe2af9","status":"Live","pragma":[],"createdBy":"3f0825d1-3543-4354-a5ef-5aed4fbe2af9","dialcodeRequired":"No","lastSubmittedOn":"2020-10-14T05:06:55.143+0000","idealScreenSize":"normal","contentEncoding":"gzip","depth":2,"consumerId":"2eaff3db-cdd1-42e5-a611-bebbf906e6cf","lastPublishedBy":"fc87d47a-dce0-46a6-98b7-b530965fd392","osId":"org.ekstep.quiz.app","copyrightYear":2020,"appId":"preprod.diksha.portal","s3Key":"ecar_files/do_2131289236157972481377/ut-self-assess1_1602652220063_do_2131289236157972481377_1.0.ecar","contentDisposition":"inline","previewUrl":"https://sunbirdstagingpublic.blob.core.windows.net/sunbird-content-staging/content/ecml/do_2131289236157972481377-latest","artifactUrl":"https://sunbirdstagingpublic.blob.core.windows.net/sunbird-content-staging/content/do_2131289236157972481377/artifact/1602652219910_do_2131289236157972481377.zip","visibility":"Default","credentials":"{\"enabled\":\"No\"}","variants":"{\"spine\":{\"ecarUrl\":\"https://sunbirdstagingpublic.blob.core.windows.net/sunbird-content-staging/ecar_files/do_2131289236157972481377/ut-self-assess1_1602652220191_do_2131289236157972481377_1.0_spine.ecar\",\"size\":29949.0}}","index":2,"pkgVersion":1.0,"idealScreenDensity":"hdpi"}],"mediaType":"content","name":"1","discussionForum":{"enabled":"No"},"createdOn":"2022-04-13T04:57:24.497+0000","channel":"01269878797503692810","generateDIALCodes":"No","lastUpdatedOn":"2022-04-13T04:58:05.342+0000","identifier":"do_21351537331811942413741","ownershipType":["createdBy"],"compatibilityLevel":1,"audience":["Student"],"trackable":{"enabled":"No","autoBatch":"No"},"os":["All"],"primaryCategory":"Course Unit","languageCode":["en"],"downloadUrl":"https://sunbirdstagingpublic.blob.core.windows.net/sunbird-content-staging/content/do_21351537329604198413739/4.8.1-rc-end-to-end-verification_1649825926657_do_21351537329604198413739_1_SPINE.ecar","attributions":[],"versionKey":"1649825844497","mimeType":"application/vnd.ekstep.content-collection","code":"31d50c12-10ed-4de6-9758-7f4f5dbbadc2","license":"CC BY 4.0","leafNodes":["do_21339794950117785611","do_2131289236157972481377"],"version":2,"contentType":"CourseUnit","language":["English"],"lastPublishedOn":"2022-04-13T04:58:46.167+0000","objectType":"Collection","status":"Live","idealScreenSize":"normal","contentEncoding":"gzip","leafNodesCount":2,"depth":1,"osId":"org.ekstep.launcher","contentDisposition":"inline","visibility":"Parent","credentials":{"enabled":"No"},"variants":"{\"spine\":{\"ecarUrl\":\"https://sunbirdstagingpublic.blob.core.windows.net/sunbird-content-staging/content/do_21351537329604198413739/4.8.1-rc-end-to-end-verification_1649825926657_do_21351537329604198413739_1_SPINE.ecar\",\"size\":\"42727\"},\"online\":{\"ecarUrl\":\"https://sunbirdstagingpublic.blob.core.windows.net/sunbird-content-staging/content/do_21351537329604198413739/4.8.1-rc-end-to-end-verification_1649825926838_do_21351537329604198413739_1_ONLINE.ecar\",\"size\":\"7931\"}}","index":1,"pkgVersion":1,"idealScreenDensity":"hdpi"}],"body":null,"mediaType":"content","name":" 4.8.1 RC end to end verification ","toc_url":"https://sunbirdstagingpublic.blob.core.windows.net/sunbird-content-staging/content/do_21351537329604198413739/artifact/do_21351537329604198413739_toc.json","batches":[{"createdFor":["01269878797503692810","01275630321992499239077"],"endDate":null,"name":"2","batchId":"01350077393890508864","enrollmentType":"open","enrollmentEndDate":null,"startDate":"2022-03-23","status":1},{"createdFor":["01269878797503692810","01275630321992499239077"],"endDate":"2022-07-31","name":"upcoming ","batchId":"013514875102142464293","enrollmentType":"open","enrollmentEndDate":"2022-07-30","startDate":"2022-05-07","status":0},{"createdFor":["01269878797503692810","01275630321992499239077"],"endDate":null,"name":"4.8.1 End to end","batchId":"013515380194869248299","enrollmentType":"open","enrollmentEndDate":null,"startDate":"2022-04-13","status":1}],"discussionForum":{"enabled":"Yes"},"createdOn":"2022-04-13T04:57:21.808+0000","createdFor":["01269878797503692810","01275630321992499239077"],"channel":"01269878797503692810","generateDIALCodes":"No","lastUpdatedOn":"2022-04-13T04:58:05.342+0000","subject":["Accounting And Auditing"],"size":42727,"publishError":null,"targetMediumIds":["tn_k-12_5_medium_english","tn_k-12_5_medium_tamil"],"identifier":"do_21351537329604198413739","se_gradeLevelIds":["tn_k-12_5_gradelevel_class2"],"description":"Enter description for Course","resourceType":"Course","ownershipType":["createdBy"],"compatibilityLevel":4,"targetBoardIds":["tn_k-12_5_board_statetamilnadu"],"audience":["Student"],"trackable":{"enabled":"Yes","autoBatch":"No"},"se_boards":["State (Tamil Nadu)"],"os":["All"],"primaryCategory":"Course","languageCode":["en"],"se_mediums":["English","Tamil"],"se_subjectIds":["tn_k-12_5_subject_accountingandauditing","tn_k-12_5_subject_mathematics"],"downloadUrl":"https://sunbirdstagingpublic.blob.core.windows.net/sunbird-content-staging/content/do_21351537329604198413739/4.8.1-rc-end-to-end-verification_1649825926657_do_21351537329604198413739_1_SPINE.ecar","se_subjects":["Accounting And Auditing"],"lockKey":"63d3921f-defc-4838-b465-6ca222f5d180","attributions":[],"framework":"tn_k-12_5","creator":"Guest name changed","totalCompressedSize":804725,"versionKey":"1649825885342","mimeType":"application/vnd.ekstep.content-collection","sYS_INTERNAL_LAST_UPDATED_ON":"2022-04-13T04:58:46.656+0000","code":"org.sunbird.nldxzf.copy.copy","se_boardIds":["tn_k-12_5_board_statetamilnadu"],"license":"CC BY 4.0","leafNodes":["do_21339794950117785611","do_2131289236157972481377"],"version":2,"contentType":"Course","language":["English"],"lastPublishedOn":"2022-04-13T04:58:46.167+0000","contentTypesCount":"{\"PracticeResource\":1,\"SelfAssess\":1,\"CourseUnit\":1}","objectType":"content","origin":"do_21351486882984755213610","subjectIds":["tn_k-12_5_subject_accountingandauditing"],"status":"Live","targetFWIds":["tn_k-12_5"],"createdBy":"fca2925f-1eee-4654-9177-fece3fd6afc9","dialcodeRequired":"No","lastSubmittedOn":"2022-04-13T04:58:05.031+0000","keywords":["sandhya"],"dialcodes":null,"userConsent":"Yes","idealScreenSize":"normal","contentEncoding":"gzip","leafNodesCount":2,"depth":0,"consumerId":"cb069f8d-e4e1-46c5-831f-d4a83b323ada","lastPublishedBy":"08631a74-4b94-4cf7-a818-831135248a4a","flagReasons":null,"targetSubjectIds":["tn_k-12_5_subject_mathematics"],"mimeTypesCount":"{\"application/vnd.ekstep.ecml-archive\":2,\"application/vnd.ekstep.content-collection\":1}","se_gradeLevels":["Class 2"],"osId":"org.ekstep.quiz.app","copyrightYear":2022,"se_FWIds":["tn_k-12_5"],"appId":"staging.sunbird.portal","s3Key":"content/do_21351537329604198413739/artifact/do_21351537329604198413739_toc.json","contentDisposition":"inline","additionalCategories":["Lesson Plan","Textbook"],"childNodes":["do_21339794950117785611","do_21351537331811942413741","do_2131289236157972481377"],"visibility":"Default","credentials":{"enabled":"Yes"},"targetGradeLevelIds":["tn_k-12_5_gradelevel_class2"],"variants":"{\"spine\":{\"ecarUrl\":\"https://sunbirdstagingpublic.blob.core.windows.net/sunbird-content-staging/content/do_21351537329604198413739/4.8.1-rc-end-to-end-verification_1649825926657_do_21351537329604198413739_1_SPINE.ecar\",\"size\":\"42727\"},\"online\":{\"ecarUrl\":\"https://sunbirdstagingpublic.blob.core.windows.net/sunbird-content-staging/content/do_21351537329604198413739/4.8.1-rc-end-to-end-verification_1649825926838_do_21351537329604198413739_1_ONLINE.ecar\",\"size\":\"7931\"}}","pkgVersion":1,"idealScreenDensity":"hdpi","reservedDialcodes":{"J3U7Y2":0}}', +''); \ No newline at end of file diff --git a/csp-migrator/src/test/scala/org.sunbird.job.cspmigrator.spec/CSPMigratorSpec.scala b/csp-migrator/src/test/scala/org.sunbird.job.cspmigrator.spec/CSPMigratorSpec.scala new file mode 100644 index 000000000..584444c70 --- /dev/null +++ b/csp-migrator/src/test/scala/org.sunbird.job.cspmigrator.spec/CSPMigratorSpec.scala @@ -0,0 +1,288 @@ +package org.sunbird.job.cspmigrator.spec + +import com.typesafe.config.{Config, ConfigFactory} +import org.cassandraunit.CQLDataLoader +import org.cassandraunit.dataset.cql.FileCQLDataSet +import org.cassandraunit.utils.EmbeddedCassandraServerHelper +import org.mockito.ArgumentMatchers.{any, anyBoolean, anyString} +import org.mockito.Mockito +import org.mockito.Mockito.when +import org.scalatest.{BeforeAndAfterAll, FlatSpec, Matchers} +import org.scalatestplus.mockito.MockitoSugar +import org.sunbird.job.cspmigrator.helpers.{CSPCassandraMigrator, CSPNeo4jMigrator} +import org.sunbird.job.cspmigrator.task.CSPMigratorConfig +import org.sunbird.job.util._ + +import java.io.File +import scala.collection.JavaConverters._ + +class CSPMigratorSpec extends FlatSpec with BeforeAndAfterAll with Matchers with MockitoSugar { + + val config: Config = ConfigFactory.load("test.conf").withFallback(ConfigFactory.systemEnvironment()) + val jobConfig: CSPMigratorConfig = new CSPMigratorConfig(config) + implicit val mockNeo4JUtil: Neo4JUtil = mock[Neo4JUtil](Mockito.withSettings().serializable()) + val mockHttpUtil: HttpUtil = mock[HttpUtil](Mockito.withSettings().serializable()) + var cassandraUtil: CassandraUtil = _ + implicit val mockCloudUtil: CloudStorageUtil = mock[CloudStorageUtil](Mockito.withSettings().serializable()) + + override protected def beforeAll(): Unit = { + super.beforeAll() + EmbeddedCassandraServerHelper.startEmbeddedCassandra(80000L) + cassandraUtil = new CassandraUtil(jobConfig.cassandraHost, jobConfig.cassandraPort, jobConfig) + val session = cassandraUtil.session + val dataLoader = new CQLDataLoader(session) + dataLoader.load(new FileCQLDataSet(getClass.getResource("/test.cql").getPath, true, true)) + } + + override protected def afterAll(): Unit = { + super.afterAll() + try { + EmbeddedCassandraServerHelper.cleanEmbeddedCassandra() + } catch { + case ex: Exception => ex.printStackTrace() + } + } + + "ECML Body" should " get updated with migrate data in cassandra database" in { + + val cspneo4jMigrator = new TestCSPNeo4jMigrator() + val fieldsToMigrate: List[String] = jobConfig.getConfig.getStringList("neo4j_fields_to_migrate.content").asScala.toList + + val objectMetadata = Map[String, AnyRef]("ownershipType" -> Array("createdFor"), "previewUrl" -> "https://ntpproductionall.blob.core.windows.net/ntp-content-production/content/ecml/do_31270597860728832015700-latest", + "keywords" -> Array("10 PS BITS"), + "channel" -> "0123207707019919361056", + "downloadUrl" -> "https://ntpproductionall.blob.core.windows.net/ntp-content-production/ecar_files/do_31270597860728832015700/examprep_10tm_ps_cha-4-q5_1551025589101_do_31270597860728832015700_1.0.ecar", + "mimeType" -> "application/vnd.ekstep.ecml-archive", + "variants" -> Map[String,AnyRef]( + "spine" -> Map[String,AnyRef]( + "ecarUrl" -> "https://ntpproductionall.blob.core.windows.net/ntp-content-production/ecar_files/do_31270597860728832015700/examprep_10tm_ps_cha-4-q5_1551025590393_do_31270597860728832015700_1.0_spine.ecar", + "size" -> 8820.asInstanceOf[Number] + )), + "editorState" -> "{\"plugin\":{\"noOfExtPlugins\":14,\"extPlugins\":[{\"plugin\":\"org.ekstep.contenteditorfunctions\",\"version\":\"1.2\"},{\"plugin\":\"org.ekstep.keyboardshortcuts\",\"version\":\"1.0\"},{\"plugin\":\"org.ekstep.richtext\",\"version\":\"1.0\"},{\"plugin\":\"org.ekstep.iterator\",\"version\":\"1.0\"},{\"plugin\":\"org.ekstep.navigation\",\"version\":\"1.0\"},{\"plugin\":\"org.ekstep.mathtext\",\"version\":\"1.0\"},{\"plugin\":\"org.ekstep.libs.ckeditor\",\"version\":\"1.0\"},{\"plugin\":\"org.ekstep.questionunit\",\"version\":\"1.0\"},{\"plugin\":\"org.ekstep.keyboard\",\"version\":\"1.0\"},{\"plugin\":\"org.ekstep.questionunit.mcq\",\"version\":\"1.1\"},{\"plugin\":\"org.ekstep.questionunit.mtf\",\"version\":\"1.1\"},{\"plugin\":\"org.ekstep.questionunit.reorder\",\"version\":\"1.0\"},{\"plugin\":\"org.ekstep.questionunit.sequence\",\"version\":\"1.0\"},{\"plugin\":\"org.ekstep.questionunit.ftb\",\"version\":\"1.0\"}]},\"stage\":{\"noOfStages\":2,\"currentStage\":\"b8b47094-1d69-43a1-9c88-c02e760996c5\"},\"sidebar\":{\"selectedMenu\":\"settings\"}}", + "objectType" -> "Content", + "appIcon" -> "https://upload.wikimedia.org/wikipedia/en/c/c2/CC_201_01R_SDT_2009.jpg", + "primaryCategory" -> "Learning Resource", + "artifactUrl" -> "https://ntpproductionall.blob.core.windows.net/ntp-content-production/content/do_31270597860728832015700/artifact/1551025588108_do_31270597860728832015700.zip", + "contentType" -> "Resource", + "identifier" -> "do_31270597860728832015700", + "visibility" -> "Default", + "author" -> "APEKX", + "lastPublishedBy" -> "8587c52d-755b-4473-8d47-4b47f81eb56b", + "version" -> 2.asInstanceOf[Number], + "license" -> "CC BY 4.0", + "prevState" -> "Review", + "size" -> 9312393.asInstanceOf[Number], + "lastPublishedOn" -> "2019-02-24T16 ->26 ->29.101+0000", + "name" -> "\tExamprep_10tm_ps_cha 4-Q5", + "status" -> "Live", + "totalQuestions" -> 1.asInstanceOf[Number], + "code" -> "org.sunbird.zRuXiC", + "description" -> "10 PS BITS", + "streamingUrl" -> "https://ntpproductionall.blob.core.windows.net/ntp-content-production/content/ecml/do_31270597860728832015700-latest", + "posterImage" -> "https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_31270069910858956813856/artifact/10-ps-tm_1550378309450.png", + "idealScreenSize" -> "normal", + "createdOn" -> "2019-02-24T15:39:39.209+0000", + "copyrightYear" -> 2019.asInstanceOf[Number], + "contentDisposition" -> "inline", + "lastUpdatedOn" -> "2019-02-24T16:26:24.241+0000", + "dialcodeRequired" -> "No", + "owner" -> "IT_CELL CSE_AP AMARAVATI", + "creator" -> "MALLIMOGGALA SUBBAYYA", + "totalScore" -> 1.asInstanceOf[Number], + "pkgVersion" -> 1.asInstanceOf[Number], + "versionKey" -> "1551025584241", + "idealScreenDensity" -> "hdpi", + "framework" -> "ap_k-12_1", + "s3Key" -> "ecar_files/do_31270597860728832015700/examprep_10tm_ps_cha-4-q5_1551025589101_do_31270597860728832015700_1.0.ecar", + "lastSubmittedOn" -> "2019-02-24T15 ->44:04.741+0000", + "compatibilityLevel" -> 4.asInstanceOf[Number], + "ownedBy" -> "01232154589404364810952", + "board" -> "State (Andhra Pradesh)", + "resourceType" -> "Learn" + ) + + when(mockCloudUtil.uploadFile(anyString(),any[File](),Option(anyBoolean()),anyString())).thenReturn(Array.empty[String]) + val cspCassandraMigrator = new TestCSPCassandraMigrator() + + cspCassandraMigrator.process(objectMetadata, jobConfig, mockHttpUtil, cassandraUtil, mockCloudUtil) + + when(mockHttpUtil.getSize(anyString(), any())).thenReturn(200) + + val migratedMetadata = cspneo4jMigrator.process(objectMetadata, jobConfig, mockHttpUtil, mockCloudUtil) + fieldsToMigrate.map(migrateField => { + jobConfig.keyValueMigrateStrings.keySet().toArray().map(key => { + assert(migratedMetadata.getOrElse(migrateField, "") == null || !migratedMetadata.getOrElse(migrateField, "").asInstanceOf[String].contains(key)) + }) + }) + } + + "Collection Hierarchy" should " get updated with migrate data in cassandra database" in { + val cspMigrator = new TestCSPNeo4jMigrator() + + val objectMetadata = Map[String, AnyRef]( + "ownershipType" -> Array( + "createdBy" + ), + "copyright" -> "tn", + "se_gradeLevelIds" -> Array( + "tn_k-12_5_gradelevel_class2" + ), + "keywords" -> Array( + "sandhya" + ), + "subject" -> Array( + "Accounting And Auditing" + ), + "targetMediumIds" -> Array( + "tn_k-12_5_medium_english", + "tn_k-12_5_medium_tamil" + ), + "channel" -> "01269878797503692810", + "downloadUrl" -> "https://sunbirdstagingpublic.blob.core.windows.net/sunbird-content-staging/content/do_21351537329604198413739/4.8.1-rc-end-to-end-verification_1649825926657_do_21351537329604198413739_1_SPINE.ecar", + "mimeType" -> "application/vnd.ekstep.content-collection", + "variants" -> Map[String, AnyRef] ( + "spine" -> Map[String, AnyRef] ( + "ecarUrl" -> "https://sunbirdstagingpublic.blob.core.windows.net/sunbird-content-staging/content/do_21351537329604198413739/4.8.1-rc-end-to-end-verification_1649825926657_do_21351537329604198413739_1_SPINE.ecar", + "size" -> "42727" + ), + "online" -> Map[String, AnyRef] ( + "ecarUrl" -> "https://sunbirdstagingpublic.blob.core.windows.net/sunbird-content-staging/content/do_21351537329604198413739/4.8.1-rc-end-to-end-verification_1649825926838_do_21351537329604198413739_1_ONLINE.ecar", + "size" -> "7931" + ) + ), + "leafNodes" -> Array( + "do_21339794950117785611", + "do_2131289236157972481377" + ), + "targetGradeLevelIds" -> Array( + "tn_k-12_5_gradelevel_class2" + ), + "objectType" -> "Content", + "se_mediums" -> Array( + "English", + "Tamil" + ), + "primaryCategory" -> "Course", + "appId" -> "staging.sunbird.portal", + "contentEncoding" -> "gzip", + "lockKey" -> "63d3921f-defc-4838-b465-6ca222f5d180", + "generateDIALCodes" -> "No", + "totalCompressedSize" -> 804725.asInstanceOf[Number], + "mimeTypesCount" -> "{\"application/vnd.ekstep.ecml-archive\":2,\"application/vnd.ekstep.content-collection\":1}", + "sYS_INTERNAL_LAST_UPDATED_ON" -> "2022-04-13T05:00:21.380+0000", + "contentType" -> "Course", + "se_gradeLevels" -> Array( + "Class 2" + ), + "identifier" -> "do_21351537329604198413739", + "se_boardIds" -> Array( + "tn_k-12_5_board_statetamilnadu" + ), + "subjectIds" -> Array( + "tn_k-12_5_subject_accountingandauditing" + ), + "toc_url" -> "https://sunbirdstagingpublic.blob.core.windows.net/sunbird-content-staging/content/do_21351537329604198413739/artifact/do_21351537329604198413739_toc.json", + "visibility" -> "Default", + "contentTypesCount" -> "{\"PracticeResource\":1,\"SelfAssess\":1,\"CourseUnit\":1}", + "author" -> "newtncc guy", + "consumerId" -> "cb069f8d-e4e1-46c5-831f-d4a83b323ada", + "childNodes" -> Array( + "do_21339794950117785611", + "do_21351537331811942413741", + "do_2131289236157972481377" + ), + "discussionForum" -> { + "enabled" -> "Yes" + }, + "mediaType" -> "content", + "osId" -> "org.ekstep.quiz.app", + "lastPublishedBy" -> "08631a74-4b94-4cf7-a818-831135248a4a", + "version" -> 2.asInstanceOf[Number], + "se_subjects" -> Array( + "Accounting And Auditing" + ), + "license" -> "CC BY 4.0", + "prevState" -> "Review", + "size" -> 42727.asInstanceOf[Number], + "lastPublishedOn" -> "2022-04-13T04:58:46.167+0000", + "name" -> " 4.8.1 RC end to end verification ", + "targetBoardIds" -> Array( + "tn_k-12_5_board_statetamilnadu" + ), + "status" -> "Live", + "code" -> "org.sunbird.nldxzf.copy.copy", + "credentials" -> { + "enabled" -> "Yes" + }, + "prevStatus" -> "Processing", + "origin" -> "do_21351486882984755213610", + "description" -> "Enter description for Course", + "idealScreenSize" -> "normal", + "createdOn" -> "2022-04-13T04:57:21.808+0000", + "reservedDialcodes" -> Map[String, AnyRef] ( + "J3U7Y2" -> 0.asInstanceOf[Number] + ), + "se_boards" -> Array( + "State (Tamil Nadu)" + ), + "targetSubjectIds" -> Array( + "tn_k-12_5_subject_mathematics" + ), + "se_mediumIds" -> Array( + "tn_k-12_5_medium_english", + "tn_k-12_5_medium_tamil" + ), + "copyrightYear" -> 2022.asInstanceOf[Number], + "contentDisposition" -> "inline", + "additionalCategories" -> Array( + "Lesson Plan", + "Textbook" + ), + "dialcodeRequired" -> "No", + "lastStatusChangedOn" -> "2022-04-13T04:58:46.915+0000", + "createdFor" -> Array( + "01269878797503692810", + "01275630321992499239077" + ), + "creator" -> "Guest name changed", + "se_subjectIds" -> Array( + "tn_k-12_5_subject_accountingandauditing", + "tn_k-12_5_subject_mathematics" + ), + "se_FWIds" -> Array( + "tn_k-12_5" + ), + "targetFWIds" -> Array( + "tn_k-12_5" + ), + "pkgVersion" -> 1.asInstanceOf[Number], + "versionKey" -> "1649825885342", + "idealScreenDensity" -> "hdpi", + "framework" -> "tn_k-12_5", + "dialcodes" -> Array("J3U7Y2"), + "depth" -> 0.asInstanceOf[Number], + "s3Key" -> "content/do_21351537329604198413739/artifact/do_21351537329604198413739_toc.json", + "lastSubmittedOn" -> "2022-04-13T04:58:05.031+0000", + "createdBy" -> "fca2925f-1eee-4654-9177-fece3fd6afc9", + "compatibilityLevel" -> 4.asInstanceOf[Number], + "leafNodesCount" -> 2.asInstanceOf[Number], + "userConsent" -> "Yes", + "resourceType" -> "Course" + ) + + when(mockCloudUtil.uploadFile(anyString(),any[File](),Option(anyBoolean()),anyString())).thenReturn(Array.empty[String]) + when(mockHttpUtil.getSize(anyString(), any())).thenReturn(200) + val fieldsToMigrate: List[String] = jobConfig.getConfig.getStringList("neo4j_fields_to_migrate.collection").asScala.toList + val migratedMetadata = cspMigrator.process(objectMetadata, jobConfig, mockHttpUtil, mockCloudUtil) + fieldsToMigrate.map(migrateField => { + jobConfig.keyValueMigrateStrings.keySet().toArray().map(key => { + assert(!migratedMetadata.getOrElse(migrateField, "").asInstanceOf[String].contains(key)) + }) + }) + } + +} + +class TestCSPNeo4jMigrator extends CSPNeo4jMigrator {} + +class TestCSPCassandraMigrator extends CSPCassandraMigrator {} diff --git a/csp-migrator/src/test/scala/org.sunbird.job.cspmigrator.spec/URLExtractorSpec.scala b/csp-migrator/src/test/scala/org.sunbird.job.cspmigrator.spec/URLExtractorSpec.scala new file mode 100644 index 000000000..4c8fdabd3 --- /dev/null +++ b/csp-migrator/src/test/scala/org.sunbird.job.cspmigrator.spec/URLExtractorSpec.scala @@ -0,0 +1,30 @@ +package org.sunbird.job.cspmigrator.spec + +import org.scalatest.{BeforeAndAfterAll, FlatSpec, Matchers} +import org.scalatestplus.mockito.MockitoSugar +import org.sunbird.job.cspmigrator.helpers.URLExtractor + +class URLExtractorSpec extends FlatSpec with BeforeAndAfterAll with Matchers with MockitoSugar { + + + override protected def beforeAll(): Unit = { + super.beforeAll() + } + + override protected def afterAll(): Unit = { + super.afterAll() + + } + + "Extractor" should "extract all URLs from the ECML body" in { + val body: String = "{\"theme\":{\"id\":\"theme\",\"version\":\"1.0\",\"startStage\":\"c3e9064a-9470-40b8-a38a-cd2286a01eeb\",\"stage\":[{\"x\":0,\"y\":0,\"w\":100,\"h\":100,\"id\":\"c3e9064a-9470-40b8-a38a-cd2286a01eeb\",\"rotate\":null,\"config\":{\"__cdata\":\"{\\\"opacity\\\":100,\\\"strokeWidth\\\":1,\\\"stroke\\\":\\\"rgba(255, 255, 255, 0)\\\",\\\"autoplay\\\":false,\\\"visible\\\":true,\\\"color\\\":\\\"#FFFFFF\\\",\\\"instructions\\\":\\\"\\\",\\\"genieControls\\\":false}\"},\"manifest\":{\"media\":[{\"assetId\":\"do_213613202536579072134\"}]},\"image\":[{\"asset\":\"do_213613202536579072134\",\"x\":20,\"y\":20,\"w\":49.72,\"h\":63.31,\"rotate\":0,\"z-index\":0,\"id\":\"ea1dc233-9cbb-4617-b137-be4d8c8c6150\",\"config\":{\"__cdata\":\"{\\\"opacity\\\":100,\\\"strokeWidth\\\":1,\\\"stroke\\\":\\\"rgba(255, 255, 255, 0)\\\",\\\"autoplay\\\":false,\\\"visible\\\":true}\"}}]}],\"manifest\":{\"media\":[{\"id\":\"2288347e-475b-4e93-a72e-e780977e7c13\",\"plugin\":\"org.ekstep.navigation\",\"ver\":\"1.0\",\"src\":\"/content-plugins/org.ekstep.navigation-1.0/renderer/controller/navigation_ctrl.js\",\"type\":\"js\"},{\"id\":\"14164b50-a751-4d44-bae1-8a4d5016266a\",\"plugin\":\"org.ekstep.navigation\",\"ver\":\"1.0\",\"src\":\"/content-plugins/org.ekstep.navigation-1.0/renderer/templates/navigation.html\",\"type\":\"js\"},{\"id\":\"org.ekstep.navigation\",\"plugin\":\"org.ekstep.navigation\",\"ver\":\"1.0\",\"src\":\"/content-plugins/org.ekstep.navigation-1.0/renderer/plugin.js\",\"type\":\"plugin\"},{\"id\":\"org.ekstep.navigation_manifest\",\"plugin\":\"org.ekstep.navigation\",\"ver\":\"1.0\",\"src\":\"/content-plugins/org.ekstep.navigation-1.0/manifest.json\",\"type\":\"json\"},{\"id\":\"do_213613202536579072134\",\"src\":\"https://drive.google.com/uc?export=download&id=1wygNKORDwe6rJ7MTW_ohbUVX4Qw4nWa0\",\"type\":\"image\"}]},\"plugin-manifest\":{\"plugin\":[{\"id\":\"org.ekstep.navigation\",\"ver\":\"1.0\",\"type\":\"plugin\",\"depends\":\"\"}]},\"compatibilityVersion\":2}}" + val extractor = new TestURLExtractor() + val extractedURLs: List[String] = extractor.extractUrls(body) + assert(extractedURLs.nonEmpty) + } +} + + +class TestURLExtractor extends URLExtractor {} + + diff --git a/dialcode-context-updater/src/test/scala/org/sunbird/job/dialcodecontextupdater/spec/helper/DialcodeContextUpdaterSpec.scala b/dialcode-context-updater/src/test/scala/org/sunbird/job/dialcodecontextupdater/spec/helper/DialcodeContextUpdaterSpec.scala index 216409d1d..c3f388f30 100644 --- a/dialcode-context-updater/src/test/scala/org/sunbird/job/dialcodecontextupdater/spec/helper/DialcodeContextUpdaterSpec.scala +++ b/dialcode-context-updater/src/test/scala/org/sunbird/job/dialcodecontextupdater/spec/helper/DialcodeContextUpdaterSpec.scala @@ -37,7 +37,7 @@ class DialcodeContextUpdaterSpec extends BaseTestSpec { override protected def beforeAll(): Unit = { BaseMetricsReporter.gaugeMetrics.clear() EmbeddedCassandraServerHelper.startEmbeddedCassandra(80000L) - cassandraUtil = new CassandraUtil(jobConfig.cassandraHost, jobConfig.cassandraPort) + cassandraUtil = new CassandraUtil(jobConfig.cassandraHost, jobConfig.cassandraPort, jobConfig) val session = cassandraUtil.session val dataLoader = new CQLDataLoader(session) dataLoader.load(new FileCQLDataSet(getClass.getResource("/test.cql").getPath, true, true)) diff --git a/dialcode-context-updater/src/test/scala/org/sunbird/job/dialcodecontextupdater/spec/task/DialcodeContextUpdaterStreamTaskSpec.scala b/dialcode-context-updater/src/test/scala/org/sunbird/job/dialcodecontextupdater/spec/task/DialcodeContextUpdaterStreamTaskSpec.scala index ffa0b782d..1dc119f80 100644 --- a/dialcode-context-updater/src/test/scala/org/sunbird/job/dialcodecontextupdater/spec/task/DialcodeContextUpdaterStreamTaskSpec.scala +++ b/dialcode-context-updater/src/test/scala/org/sunbird/job/dialcodecontextupdater/spec/task/DialcodeContextUpdaterStreamTaskSpec.scala @@ -47,7 +47,7 @@ class DialcodeContextUpdaterStreamTaskSpec extends BaseTestSpec { override protected def beforeAll(): Unit = { BaseMetricsReporter.gaugeMetrics.clear() EmbeddedCassandraServerHelper.startEmbeddedCassandra(80000L) - cassandraUtil = new CassandraUtil(jobConfig.cassandraHost, jobConfig.cassandraPort) + cassandraUtil = new CassandraUtil(jobConfig.cassandraHost, jobConfig.cassandraPort, jobConfig) val session = cassandraUtil.session val dataLoader = new CQLDataLoader(session) dataLoader.load(new FileCQLDataSet(getClass.getResource("/test.cql").getPath, true, true)) diff --git a/enrolment-reconciliation/README.md b/enrolment-reconciliation/README.md deleted file mode 100644 index 77f41643d..000000000 --- a/enrolment-reconciliation/README.md +++ /dev/null @@ -1,66 +0,0 @@ -# Enrolment Reconciliation - -Enrolment Reconciliation is used to compute the progress for unit level and course level for each batch and user and updates to database. - -## Getting Started - -These instructions will get you a copy of the project up and running on your local machine for development and testing purposes. See deployment for notes on how to deploy the project on a yarn or kubernetes. -Design wiki link: https://project-sunbird.atlassian.net/wiki/spaces/SBDES/pages/2275672087/User+enrolment+progress+sync+-+SB-23493 -### Prerequisites - -1. Download flink-1.13.6-scala_2.12 from [apache-flink-downloads](https://www.apache.org/dyn/closer.lua/flink/flink-1.13.6/flink-1.13.6-bin-scala_2.12.tgz). -2. Download [hadoop dependencies](https://repo.maven.apache.org/maven2/org/apache/flink/flink-shaded-hadoop-2-uber/2.8.3-10.0/flink-shaded-hadoop-2-uber-2.8.3-10.0.jar) (only for running on Yarn). Copy the hadoop dependency jar under lib folder of the flink download. -3. export HADOOP_CLASSPATH=`/hadoop classpath` either in .bashrc or current execution shell. -4. Docker installed. -5. A running yarn cluster or a kubernetes cluster. - -### Build - -mvn clean install - -## Deployment - -### Yarn - -Flink requires memory to be allocated for both job-manager and task manager. -yjm parameter assigns job-manager memory and -ytm assigns task-manager memory. - -``` -./bin/flink run -m yarn-cluster -p 2 -yjm 1024m -ytm 1024m /activity-aggregate-updater/target/activity-aggregate-updater-0.0.1.jar -``` - -### Kubernetes - -``` -# Create a single node cluster -k3d create --server-arg --no-deploy --server-arg traefik --name flink-cluster --image rancher/k3s:v1.0.0 -# Export the single node cluster into KUBECONFIG in the current shell or in ~/.bashrc. -export KUBECONFIG="$(k3d get-kubeconfig --name='flink-cluster')" - -# Only for Mac OSX -# /usr/local/bin/kubectl -> /Applications/Docker.app/Contents/Resources/bin/kubectl -rm /usr/local/bin/kubectl -brew link --overwrite kubernetes-cli - -# Create a configmap using the flink-configuration-configmap.yaml -kubectl create -f knowledge-platform-job/kubernetes/flink-configuration-configmap.yaml - -# Create pods for jobmanager-service, job-manager and task-manager using the yaml files -kubectl create -f knowledge-platform-job/kubernetes/jobmanager-service.yaml -kubectl create -f knowledge-platform-job/kubernetes/jobmanager-deployment.yaml -kubectl create -f knowledge-platform-job/kubernetes/taskmanager-deployment.yaml - -# Create a port-forwarding for accessing the job-manager UI on localhost:8081 -kubectl port-forward deployment/flink-jobmanager 8081:8081 - -# Submit the job to the Kubernetes single node cluster flink-cluster -./bin/flink run -m localhost:8081 /activity-aggregate-updater/target/activity-aggregate-updater-0.0.1.jar - -# Commands to delete the pods created in the cluster -kubectl delete deployment/flink-jobmanager -kubectl delete deployment/flink-taskmanager -kubectl delete service/flink-jobmanager -kubectl delete configmaps/flink-config - -# Command to stop the single-node cluster -k3d stop --name="flink-cluster" -``` diff --git a/enrolment-reconciliation/src/main/resources/enrolment-reconciliation.conf b/enrolment-reconciliation/src/main/resources/enrolment-reconciliation.conf deleted file mode 100644 index df7fde33f..000000000 --- a/enrolment-reconciliation/src/main/resources/enrolment-reconciliation.conf +++ /dev/null @@ -1,43 +0,0 @@ -include "base-config.conf" - -kafka { - input.topic = "sunbirddev.batch.enrolment.sync.request" - output.audit.topic = "sunbirddev.telemetry.raw" - output.failed.topic = "sunbirddev.enrolment.reconciliation.failed" - output.certissue.topic = "sunbirddev.issue.certificate.request" - groupId = "sunbirddev-enrolment-reconciliation-group" -} - -task { - window.shards = 1 - consumer.parallelism = 1 - enrolment.reconciliation.parallelism = 1 - enrolment.complete.parallelism = 1 -} - -lms-cassandra { - keyspace = "sunbird_courses" - consumption.table = "user_content_consumption" - user_activity_agg.table = "user_activity_agg" - user_enrolments.table = "user_enrolments" -} - -redis { - database { - relationCache.id = 10 - } -} - -threshold.batch.write.size = 10 - -activity { - module.aggs.enabled = true - filter.processed.enrolments = true - collection.status.cache.expiry = 3600 -} - -service { - search { - basePath = "http://11.2.6.6/search" - } -} \ No newline at end of file diff --git a/enrolment-reconciliation/src/main/scala/org/sunbird/job/recounciliation/domain/Event.scala b/enrolment-reconciliation/src/main/scala/org/sunbird/job/recounciliation/domain/Event.scala deleted file mode 100644 index 9bc293064..000000000 --- a/enrolment-reconciliation/src/main/scala/org/sunbird/job/recounciliation/domain/Event.scala +++ /dev/null @@ -1,28 +0,0 @@ -package org.sunbird.job.recounciliation.domain -import org.apache.commons.lang3.StringUtils -import org.sunbird.job.domain.reader.JobRequest - -import java.util - -class Event(eventMap: java.util.Map[String, Any], partition: Int, offset: Long) extends JobRequest(eventMap, partition, offset) { - - val jobName = "EnrolmentReconciliation" - - def eData: Map[String, AnyRef] = readOrDefault("edata", new util.HashMap[String, AnyRef]()).asInstanceOf[Map[String, AnyRef]] - - def action: String = readOrDefault[String]("edata.action", "") - - def courseId: String = readOrDefault[String]("edata.courseId", "") - - def batchId: String = readOrDefault[String]("edata.batchId", "") - - def userId: String = readOrDefault[String]("edata.userId", "") - - def eType: String = readOrDefault[String]("edata.action", "") - - def isValidEvent(supportedEventType: String): Boolean = { - - StringUtils.equalsIgnoreCase(eType, supportedEventType) - } - -} \ No newline at end of file diff --git a/enrolment-reconciliation/src/main/scala/org/sunbird/job/recounciliation/domain/Models.scala b/enrolment-reconciliation/src/main/scala/org/sunbird/job/recounciliation/domain/Models.scala deleted file mode 100644 index e2b0d2b8e..000000000 --- a/enrolment-reconciliation/src/main/scala/org/sunbird/job/recounciliation/domain/Models.scala +++ /dev/null @@ -1,51 +0,0 @@ -package org.sunbird.job.recounciliation.domain - -import java.util -import java.util.{Date, UUID} -import scala.collection.JavaConverters._ - - -case class ActorObject(id: String, `type`: String = "User") - -case class EventContext(channel: String = "in.sunbird", - env: String = "Course", - sid: String = UUID.randomUUID().toString, - did: String = UUID.randomUUID().toString, - pdata: util.Map[String, String] = Map("ver" -> "3.0", "id" -> "org.sunbird.learning.platform", "pid" -> "course-progress-updater").asJava, - cdata: Array[util.Map[String, String]]) - - -case class EventData(props: Array[String], `type`: String) - -case class EventObject(id: String, `type`: String, rollup: util.Map[String, String]) - -case class TelemetryEvent(actor: ActorObject, - eid: String = "AUDIT", - edata: EventData, - ver: String = "3.0", - syncts: Long = System.currentTimeMillis(), - ets: Long = System.currentTimeMillis(), - context: EventContext = EventContext( - cdata = Array[util.Map[String, String]]() - ), - mid: String = s"LP.AUDIT.${UUID.randomUUID().toString}", - `object`: EventObject, - tags: util.List[AnyRef] = new util.ArrayList[AnyRef]() - ) - -case class ContentStatus(contentId: String, status: Int = 0, completedCount: Int = 0, viewCount: Int = 1, fromInput: Boolean = true, eventsFor: List[String] = List()) - -case class UserContentConsumption(userId: String, batchId: String, courseId: String, contents: Map[String, ContentStatus]) - -case class UserActivityAgg(activity_type: String, - user_id: String, - activity_id: String, - context_id: String, - aggregates: Map[String, Double], - agg_last_updated: Map[String, Long] - ) - -case class CollectionProgress(userId: String, batchId: String, courseId: String, progress: Int, completedOn: Date, contentStatus: Map[String, Int], inputContents: List[String], completed: Boolean = false) - -case class UserEnrolmentAgg(activityAgg: UserActivityAgg, collectionProgress: Option[CollectionProgress] = None) - diff --git a/enrolment-reconciliation/src/main/scala/org/sunbird/job/recounciliation/functions/EnrolmentReconciliationFn.scala b/enrolment-reconciliation/src/main/scala/org/sunbird/job/recounciliation/functions/EnrolmentReconciliationFn.scala deleted file mode 100644 index 7e6be4977..000000000 --- a/enrolment-reconciliation/src/main/scala/org/sunbird/job/recounciliation/functions/EnrolmentReconciliationFn.scala +++ /dev/null @@ -1,321 +0,0 @@ -package org.sunbird.job.recounciliation.functions - -import com.datastax.driver.core.Row -import com.datastax.driver.core.querybuilder.{QueryBuilder, Select, Update} -import com.google.gson.Gson -import com.google.gson.reflect.TypeToken -import com.twitter.storehaus.cache.TTLCache -import com.twitter.util.Duration -import org.apache.commons.collections.CollectionUtils -import org.apache.commons.lang3.StringUtils -import org.apache.flink.api.common.typeinfo.TypeInformation -import org.apache.flink.configuration.Configuration -import org.apache.flink.streaming.api.functions.ProcessFunction -import org.slf4j.LoggerFactory -import org.sunbird.job.cache.{DataCache, RedisConnect} -import org.sunbird.job.recounciliation.domain._ -import org.sunbird.job.recounciliation.task.EnrolmentReconciliationConfig -import org.sunbird.job.util.{CassandraUtil, HttpUtil} -import org.sunbird.job.{BaseProcessFunction, Metrics} - -import java.lang.reflect.Type -import java.util.concurrent.TimeUnit -import scala.collection.JavaConverters._ - - -class EnrolmentReconciliationFn(config: EnrolmentReconciliationConfig, httpUtil: HttpUtil, @transient var cassandraUtil: CassandraUtil = null) - (implicit val stringTypeInfo: TypeInformation[String]) - extends BaseProcessFunction[Event, String](config) { - - private[this] val logger = LoggerFactory.getLogger(classOf[EnrolmentReconciliationFn]) - val mapType: Type = new TypeToken[java.util.Map[String, AnyRef]]() {}.getType - private var cache: DataCache = _ - private var collectionStatusCache: TTLCache[String, String] = _ - lazy private val gson = new Gson() - - - override def open(parameters: Configuration): Unit = { - super.open(parameters) - cassandraUtil = new CassandraUtil(config.dbHost, config.dbPort) - cache = new DataCache(config, new RedisConnect(config), config.nodeStore, List()) - cache.init() - collectionStatusCache = TTLCache[String, String](Duration.apply(config.statusCacheExpirySec, TimeUnit.SECONDS)) - - } - - override def close(): Unit = { - cassandraUtil.close() - cache.close() - super.close() - } - - override def metricsList(): List[String] = { - List(config.totalEventCount, - config.failedEventCount, - config.dbUpdateCount, - config.dbReadCount, - config.cacheHitCount, - config.cacheMissCount, - config.retiredCCEventsCount, - config.skipEventsCount - ) - } - - - override def processElement(event: Event, context: ProcessFunction[Event, String]#Context, metrics: Metrics): Unit = { - metrics.incCounter(config.totalEventCount) - if (event.isValidEvent(config.supportedEventType)) { - // Fetch the content status from the table in batch format - val dbUserConsumption: List[UserContentConsumption] = getContentStatusFromDB( - Map("courseId" -> event.courseId, "userId" -> event.userId, "batchId" -> event.batchId), metrics) - - val courseAggregations = dbUserConsumption.flatMap{ userConsumption => - // Course Level Agg using the merged data of ContentConsumption per user, course and batch. - val optCourseAgg = courseActivityAgg(userConsumption, context)(metrics) - val courseAggs = if (optCourseAgg.nonEmpty) List(optCourseAgg.get) else List() - - // Identify the children of the course (only collections) for which aggregates computation required. - // Computation of aggregates using leafNodes (of the specific collection) and user completed contents. - // Here computing only "completedCount" aggregate. - if (config.moduleAggEnabled) { - val courseChildrenAggs = courseChildrenActivityAgg(userConsumption)(metrics) - courseAggs ++ courseChildrenAggs - } else courseAggs - } - // Saving all queries for course and it's children (only collection) aggregates. - val aggQueries = courseAggregations.map(agg => getUserAggQuery(agg.activityAgg)) - updateDB(config.thresholdBatchWriteSize, aggQueries)(metrics) - - // Saving enrolment completion data. - val collectionProgressList = courseAggregations.filter(agg => agg.collectionProgress.nonEmpty).map(agg => agg.collectionProgress.get) - val collectionProgressUpdateList = collectionProgressList.filter(progress => !progress.completed) - context.output(config.collectionUpdateOutputTag, collectionProgressUpdateList) - - val collectionProgressCompleteList = collectionProgressList.filter(progress => progress.completed) - context.output(config.collectionCompleteOutputTag, collectionProgressCompleteList) - - } else { - metrics.incCounter(config.skipEventsCount) - logger.info("Event skipped as it is invalid ", event) - } - - } - - def getUserAggQuery(progress: UserActivityAgg): - Update.Where = { - QueryBuilder.update(config.dbKeyspace, config.dbUserActivityAggTable) - .`with`(QueryBuilder.putAll("aggregates", progress.aggregates.asJava)) - .and(QueryBuilder.putAll("agg_last_updated", progress.agg_last_updated.asJava)) - .where(QueryBuilder.eq("activity_id", progress.activity_id)) - .and(QueryBuilder.eq("activity_type", progress.activity_type)) - .and(QueryBuilder.eq("context_id", progress.context_id)) - .and(QueryBuilder.eq("user_id", progress.user_id)) - } - - def getEnrolmentUpdateQuery(enrolment: CollectionProgress): Update.Where = { - logger.info("Enrolment updated for userId: " + enrolment.userId + " batchId: " + enrolment.batchId) - QueryBuilder.update(config.dbKeyspace, config.dbUserEnrolmentsTable) - .`with`(QueryBuilder.set("status", 1)) - .and(QueryBuilder.set("progress", enrolment.progress)) - .and(QueryBuilder.set("contentstatus", enrolment.contentStatus.asJava)) - .and(QueryBuilder.set("datetime", System.currentTimeMillis)) - .where(QueryBuilder.eq("userid", enrolment.userId)) - .and(QueryBuilder.eq("courseid", enrolment.courseId)) - .and(QueryBuilder.eq("batchid", enrolment.batchId)) - } - - def getCollectionStatus(collectionId: String): String = { - val cacheStatus = collectionStatusCache.getNonExpired(collectionId).getOrElse("") - if (StringUtils.isEmpty(cacheStatus)) { - val dbStatus = getDBStatus(collectionId) - collectionStatusCache.putClocked(collectionId, dbStatus) - dbStatus - } else cacheStatus - } - - def getDBStatus(collectionId: String): String = { - val requestBody = - s"""{ - | "request": { - | "filters": { - | "objectType": "Collection", - | "identifier": "$collectionId", - | "status": ["Live", "Unlisted", "Retired"] - | }, - | "fields": ["status"] - | } - |}""".stripMargin - - val response = httpUtil.post(config.searchAPIURL, requestBody) - if (response.status == 200) { - val responseBody = gson.fromJson(response.body, classOf[java.util.Map[String, AnyRef]]) - val result = responseBody.getOrDefault("result", new java.util.HashMap[String, AnyRef]()).asInstanceOf[java.util.Map[String, AnyRef]] - val count = result.getOrDefault("count", 0.asInstanceOf[Number]).asInstanceOf[Number].intValue() - if (count > 0) { - val list = result.getOrDefault("content", new java.util.ArrayList[java.util.Map[String, AnyRef]]()).asInstanceOf[java.util.List[java.util.Map[String, AnyRef]]] - list.asScala.head.get("status").asInstanceOf[String] - } else throw new Exception(s"There are no published or retired collection with id: $collectionId") - } else { - logger.error("search-service error: " + response.body) - throw new Exception("search-service not returning error:" + response.status) - } - } - - def getContentStatusFromDB(eDataBatch: Map[String, String], metrics: Metrics): List[UserContentConsumption] = { - val primaryFields = Map( - config.userId.toLowerCase() -> eDataBatch.getOrElse("userId", ""), - config.batchId.toLowerCase -> eDataBatch.getOrElse("batchId", ""), - config.courseId.toLowerCase -> eDataBatch.getOrElse("courseId", "") - ) - - val records = Option(readFromDB(primaryFields, config.dbKeyspace, config.dbUserContentConsumptionTable, metrics)) - val contentConsumption = records.map(record => record.groupBy(col => Map(config.batchId -> col.getObject(config.batchId.toLowerCase()).asInstanceOf[String], config.userId -> col.getObject(config.userId.toLowerCase()).asInstanceOf[String], config.courseId -> col.getObject(config.courseId.toLowerCase()).asInstanceOf[String]))) - .map(groupedRecords => groupedRecords.map(entry => { - val identifierMap = entry._1 - val consumptionList = entry._2.flatMap(row => Map(row.getObject(config.contentId.toLowerCase()).asInstanceOf[String] -> Map(config.status -> row.getObject(config.status), config.viewcount -> row.getObject(config.viewcount), config.completedcount -> row.getObject(config.completedcount)))) - .map(entry => { - val contentStatus = entry._2.filter(x => x._2 != null) - val contentId = entry._1 - val status = contentStatus.getOrElse(config.status, 1).asInstanceOf[Number].intValue() - val viewCount = contentStatus.getOrElse(config.viewcount, 0).asInstanceOf[Number].intValue() - val completedCount = contentStatus.getOrElse(config.completedcount, 0).asInstanceOf[Number].intValue() - (contentId, ContentStatus(contentId, status, completedCount, viewCount, false)) - }).toMap - - val userId = identifierMap(config.userId) - val batchId = identifierMap(config.batchId) - val courseId = identifierMap(config.courseId) - - UserContentConsumption(userId, batchId, courseId, consumptionList) - })).getOrElse(List[UserContentConsumption]()).toList - contentConsumption - } - - def readFromDB(columns: Map[String, AnyRef], keySpace: String, table: String, metrics: Metrics): List[Row] = { - val selectWhere: Select.Where = QueryBuilder.select().all() - .from(keySpace, table). - where() - columns.map(col => { - col._2 match { - case value: List[Any] => - selectWhere.and(QueryBuilder.in(col._1, value.asJava)) - case _ => - selectWhere.and(QueryBuilder.eq(col._1, col._2)) - } - }) - metrics.incCounter(config.dbReadCount) - cassandraUtil.find(selectWhere.toString).asScala.toList - - } - - def getUCKey(userConsumption: UserContentConsumption): String = { - userConsumption.userId + ":" + userConsumption.courseId + ":" + userConsumption.batchId - } - - /** - * Course Level Agg using the merged data of ContentConsumption per user, course and batch. - */ - def courseActivityAgg(userConsumption: UserContentConsumption, context: ProcessFunction[Event, String]#Context)(implicit metrics: Metrics): Option[UserEnrolmentAgg] = { - val courseId = userConsumption.courseId - val userId = userConsumption.userId - val contextId = "cb:" + userConsumption.batchId - val key = s"$courseId:$courseId:${config.leafNodes}" - val leafNodes = readFromCache(key, metrics).distinct - if (leafNodes.isEmpty) { - logger.error(s"leaf nodes are not available for: $key") - context.output(config.failedEventOutputTag, gson.toJson(userConsumption)) - val status = getCollectionStatus(courseId) - if (StringUtils.equals("Retired", status)) { - metrics.incCounter(config.retiredCCEventsCount) - println(s"contents consumed from a retired collection: $courseId") - logger.warn(s"contents consumed from a retired collection: $courseId") - None - } else { - metrics.incCounter(config.failedEventCount) - val message = s"leaf nodes are not available for a published collection: $courseId" - logger.error(message) - throw new Exception(message) - } - } else { - val completedCount = leafNodes.intersect(userConsumption.contents.filter(cc => cc._2.status == 2).map(cc => cc._2.contentId).toList.distinct).size - val contentStatus = userConsumption.contents.map(cc => (cc._2.contentId, cc._2.status)).toMap - val inputContents = userConsumption.contents.filter(cc => cc._2.fromInput).keys.toList - val collectionProgress = if (completedCount >= leafNodes.size) { - Option(CollectionProgress(userId, userConsumption.batchId, courseId, completedCount, new java.util.Date(), contentStatus, inputContents, true)) - } else { - Option(CollectionProgress(userId, userConsumption.batchId, courseId, completedCount, null, contentStatus, inputContents)) - } - Option(UserEnrolmentAgg(UserActivityAgg("Course", userId, courseId, contextId, Map("completedCount" -> completedCount.toDouble), Map("completedCount" -> System.currentTimeMillis())), collectionProgress)) - } - } - - def readFromCache(key: String, metrics: Metrics): List[String] = { - metrics.incCounter(config.cacheHitCount) - val list = cache.getKeyMembers(key) - if (CollectionUtils.isEmpty(list)) { - metrics.incCounter(config.cacheMissCount) - logger.info("Redis cache (smembers) not available for key: " + key) - } - list.asScala.toList - } - - /** - * Identified the children of the course (only collections) for which aggregates computation required. - * Computation of aggregates using leafNodes (of the specific collection) and user completed contents. - * Here computing only "completedCount" aggregate. - */ - def courseChildrenActivityAgg(userConsumption: UserContentConsumption)(implicit metrics: Metrics): List[UserEnrolmentAgg] = { - val courseId = userConsumption.courseId - val userId = userConsumption.userId - val contextId = "cb:" + userConsumption.batchId - - // These are the child collections which require computation of aggregates - for this user. - val ancestors = userConsumption.contents.mapValues(content => { - val contentId = content.contentId - readFromCache(key = s"$courseId:$contentId:${config.ancestors}", metrics) - }).values.flatten.filter(a => !StringUtils.equals(a, courseId)).toList.distinct - - // LeafNodes of the identified child collections - for this user. - val collectionsWithLeafNodes = ancestors.map(unitId => { - (unitId, readFromCache(key = s"$courseId:$unitId:${config.leafNodes}", metrics).distinct) - }).toMap - - // Content completed - By this user. - val userCompletedContents = userConsumption.contents.filter(cc => cc._2.status == 2).map(cc => cc._2.contentId).toList.distinct - - // Child Collection UserAggregate list - for this user. - collectionsWithLeafNodes.map(e => { - val collectionId = e._1 - val leafNodes = e._2 - val completedCount = leafNodes.intersect(userCompletedContents).size - /* TODO - List - TODO 1. Generalise activityType from "Course" to "Collection". - TODO 2.Identify how to generate start and end event for CourseUnit. - */ - val activityAgg = UserActivityAgg("Course", userId, collectionId, contextId, Map("completedCount" -> completedCount), Map("completedCount" -> System.currentTimeMillis())) - UserEnrolmentAgg(activityAgg, None) - }).toList - } - - /** - * Method to update the specific table in a batch format. - */ - def updateDB(batchSize: Int, queriesList: List[Update.Where])(implicit metrics: Metrics): Unit = { - val groupedQueries = queriesList.grouped(batchSize).toList - groupedQueries.foreach(queries => { - val cqlBatch = QueryBuilder.batch() - queries.map(query => cqlBatch.add(query)) - val result = cassandraUtil.upsert(cqlBatch.toString) - if (result) { - metrics.incCounter(config.dbUpdateCount) - } else { - val msg = "Database update has failed: " + cqlBatch.toString - logger.error(msg) - throw new Exception(msg) - } - }) - } - -} - - diff --git a/enrolment-reconciliation/src/main/scala/org/sunbird/job/recounciliation/functions/ProgressCompleteFunction.scala b/enrolment-reconciliation/src/main/scala/org/sunbird/job/recounciliation/functions/ProgressCompleteFunction.scala deleted file mode 100644 index 5649a92ce..000000000 --- a/enrolment-reconciliation/src/main/scala/org/sunbird/job/recounciliation/functions/ProgressCompleteFunction.scala +++ /dev/null @@ -1,119 +0,0 @@ -package org.sunbird.job.recounciliation.functions - -import com.datastax.driver.core.querybuilder.{QueryBuilder, Select, Update} -import com.google.gson.Gson -import org.apache.flink.api.common.typeinfo.TypeInformation -import org.apache.flink.configuration.Configuration -import org.apache.flink.streaming.api.functions.ProcessFunction -import org.slf4j.LoggerFactory -import org.sunbird.job.recounciliation.domain.{ActorObject, CollectionProgress, EventContext, EventData, EventObject, TelemetryEvent} -import org.sunbird.job.recounciliation.task.EnrolmentReconciliationConfig -import org.sunbird.job.util.CassandraUtil -import org.sunbird.job.{BaseProcessFunction, Metrics} - -import java.util.UUID -import scala.collection.JavaConverters._ - -class ProgressCompleteFunction(config: EnrolmentReconciliationConfig)(implicit val enrolmentCompleteTypeInfo: TypeInformation[List[CollectionProgress]], val stringTypeInfo: TypeInformation[String], @transient var cassandraUtil: CassandraUtil = null) - extends BaseProcessFunction[List[CollectionProgress], String](config) { - - private[this] val logger = LoggerFactory.getLogger(classOf[ProgressCompleteFunction]) - lazy private val gson = new Gson() - - override def open(parameters: Configuration): Unit = { - super.open(parameters) - cassandraUtil = new CassandraUtil(config.dbHost, config.dbPort) - } - - override def close(): Unit = { - cassandraUtil.close() - super.close() - } - - override def processElement(events: List[CollectionProgress], context: ProcessFunction[List[CollectionProgress], String]#Context, metrics: Metrics): Unit = { - val pendingEnrolments = if (config.filterCompletedEnrolments) events.filter {p => - val row = getEnrolment(p.userId, p.courseId, p.batchId)(metrics) - (row != null && row.getInt("status") != 2) - } else events - - val enrolmentQueries = pendingEnrolments.map(enrolmentComplete => getEnrolmentCompleteQuery(enrolmentComplete)) - updateDB(config.thresholdBatchWriteSize, enrolmentQueries)(metrics) - pendingEnrolments.foreach(e => { - createIssueCertEvent(e, context)(metrics) - generateAuditEvent(e, context)(metrics) - }) - } - - override def metricsList(): List[String] = { - List(config.dbReadCount, config.dbUpdateCount, config.enrolmentCompleteCount, config.certIssueEventsCount) - } - - def generateAuditEvent(data: CollectionProgress, context: ProcessFunction[List[CollectionProgress], String]#Context)(implicit metrics: Metrics) = { - val auditEvent = TelemetryEvent( - actor = ActorObject(id = data.userId), - edata = EventData(props = Array("status", "completedon"), `type` = "enrol-complete"), // action values are "start", "complete". - context = EventContext(cdata = Array(Map("type" -> config.courseBatch, "id" -> data.batchId).asJava, Map("type" -> "Course", "id" -> data.courseId).asJava)), - `object` = EventObject(id = data.userId, `type` = "User", rollup = Map[String, String]("l1" -> data.courseId).asJava) - ) - context.output(config.auditEventOutputTag, gson.toJson(auditEvent)) - - } - - def getEnrolment(userId: String, courseId: String, batchId: String)(implicit metrics: Metrics) = { - val selectWhere: Select.Where = QueryBuilder.select().all() - .from(config.dbKeyspace, config.dbUserEnrolmentsTable). - where() - selectWhere.and(QueryBuilder.eq("userid", userId)) - .and(QueryBuilder.eq("courseid", courseId)) - .and(QueryBuilder.eq("batchid", batchId)) - metrics.incCounter(config.dbReadCount) - cassandraUtil.findOne(selectWhere.toString) - } - - def getEnrolmentCompleteQuery(enrolment: CollectionProgress): Update.Where = { - logger.info("Enrolment completed for userId: " + enrolment.userId + " batchId: " + enrolment.batchId) - QueryBuilder.update(config.dbKeyspace, config.dbUserEnrolmentsTable) - .`with`(QueryBuilder.set("status", 2)) - .and(QueryBuilder.set("completedon", enrolment.completedOn)) - .and(QueryBuilder.set("progress", enrolment.progress)) - .and(QueryBuilder.set("contentstatus", enrolment.contentStatus.asJava)) - .and(QueryBuilder.set("datetime", System.currentTimeMillis)) - .where(QueryBuilder.eq("userid", enrolment.userId)) - .and(QueryBuilder.eq("courseid", enrolment.courseId)) - .and(QueryBuilder.eq("batchid", enrolment.batchId)) - } - - /** - * Method to update the specific table in a batch format. - */ - def updateDB(batchSize: Int, queriesList: List[Update.Where])(implicit metrics: Metrics): Unit = { - val groupedQueries = queriesList.grouped(batchSize).toList - groupedQueries.foreach(queries => { - val cqlBatch = QueryBuilder.batch() - queries.map(query => cqlBatch.add(query)) - val result = cassandraUtil.upsert(cqlBatch.toString) - if (result) { - metrics.incCounter(config.dbUpdateCount) - metrics.incCounter(config.enrolmentCompleteCount) - } else { - val msg = "Database update has failed" + cqlBatch.toString - logger.error(msg) - throw new Exception(msg) - } - }) - } - - /** - * Generation of Certificate Issue event for the enrolment completed users to validate and generate certificate. - * @param enrolment - * @param context - * @param metrics - */ - def createIssueCertEvent(enrolment: CollectionProgress, context: ProcessFunction[List[CollectionProgress], String]#Context)(implicit metrics: Metrics): Unit = { - val ets = System.currentTimeMillis - val mid = s"""LP.${ets}.${UUID.randomUUID}""" - val event = s"""{"eid": "BE_JOB_REQUEST","ets": ${ets},"mid": "${mid}","actor": {"id": "Course Certificate Generator","type": "System"},"context": {"pdata": {"ver": "1.0","id": "org.sunbird.platform"}},"object": {"id": "${enrolment.batchId}_${enrolment.courseId}","type": "CourseCertificateGeneration"},"edata": {"userIds": ["${enrolment.userId}"],"action": "issue-certificate","iteration": 1, "trigger": "auto-issue","batchId": "${enrolment.batchId}","reIssue": false,"courseId": "${enrolment.courseId}"}}""" - context.output(config.certIssueOutputTag, event) - metrics.incCounter(config.certIssueEventsCount) - } -} diff --git a/enrolment-reconciliation/src/main/scala/org/sunbird/job/recounciliation/functions/ProgressUpdateFunction.scala b/enrolment-reconciliation/src/main/scala/org/sunbird/job/recounciliation/functions/ProgressUpdateFunction.scala deleted file mode 100644 index d893edac2..000000000 --- a/enrolment-reconciliation/src/main/scala/org/sunbird/job/recounciliation/functions/ProgressUpdateFunction.scala +++ /dev/null @@ -1,85 +0,0 @@ -package org.sunbird.job.recounciliation.functions - -import com.datastax.driver.core.querybuilder.{QueryBuilder, Select, Update} -import org.apache.flink.api.common.typeinfo.TypeInformation -import org.apache.flink.configuration.Configuration -import org.apache.flink.streaming.api.functions.ProcessFunction -import org.slf4j.LoggerFactory -import org.sunbird.job.recounciliation.domain.CollectionProgress -import org.sunbird.job.recounciliation.task.EnrolmentReconciliationConfig -import org.sunbird.job.util.CassandraUtil -import org.sunbird.job.{BaseProcessFunction, Metrics} - -import scala.collection.JavaConverters._ - -class ProgressUpdateFunction(config: EnrolmentReconciliationConfig)(implicit val enrolmentCompleteTypeInfo: TypeInformation[List[CollectionProgress]], val stringTypeInfo: TypeInformation[String], @transient var cassandraUtil: CassandraUtil = null) - extends BaseProcessFunction[List[CollectionProgress], String](config) { - - private[this] val logger = LoggerFactory.getLogger(classOf[ProgressUpdateFunction]) - - override def open(parameters: Configuration): Unit = { - super.open(parameters) - cassandraUtil = new CassandraUtil(config.dbHost, config.dbPort) - } - - override def close(): Unit = { - cassandraUtil.close() - super.close() - } - - override def processElement(events: List[CollectionProgress], context: ProcessFunction[List[CollectionProgress], String]#Context, metrics: Metrics): Unit = { - val pendingEnrolments = if (config.filterCompletedEnrolments) events.filter { p => - val row = getEnrolment(p.userId, p.courseId, p.batchId)(metrics) - (row != null && row.getInt("status") != 2) - } else events - val enrolmentQueries = pendingEnrolments.map(collectionProgress => getEnrolmentUpdateQuery(collectionProgress)) - updateDB(config.thresholdBatchWriteSize, enrolmentQueries)(metrics) - // Create and update the checksum to DeDup store for the input events. - } - - override def metricsList(): List[String] = { - List(config.dbReadCount, config.dbUpdateCount) - } - - def getEnrolment(userId: String, courseId: String, batchId: String)(implicit metrics: Metrics) = { - val selectWhere: Select.Where = QueryBuilder.select().all() - .from(config.dbKeyspace, config.dbUserEnrolmentsTable). - where() - selectWhere.and(QueryBuilder.eq("userid", userId)) - .and(QueryBuilder.eq("courseid", courseId)) - .and(QueryBuilder.eq("batchid", batchId)) - metrics.incCounter(config.dbReadCount) - cassandraUtil.findOne(selectWhere.toString) - } - - def getEnrolmentUpdateQuery(enrolment: CollectionProgress): Update.Where = { - logger.info("Enrolment updated for userId: " + enrolment.userId + " batchId: " + enrolment.batchId) - QueryBuilder.update(config.dbKeyspace, config.dbUserEnrolmentsTable) - .`with`(QueryBuilder.set("status", 1)) - .and(QueryBuilder.set("progress", enrolment.progress)) - .and(QueryBuilder.set("contentstatus", enrolment.contentStatus.asJava)) - .and(QueryBuilder.set("datetime", System.currentTimeMillis)) - .where(QueryBuilder.eq("userid", enrolment.userId)) - .and(QueryBuilder.eq("courseid", enrolment.courseId)) - .and(QueryBuilder.eq("batchid", enrolment.batchId)) - } - - /** - * Method to update the specific table in a batch format. - */ - def updateDB(batchSize: Int, queriesList: List[Update.Where])(implicit metrics: Metrics): Unit = { - val groupedQueries = queriesList.grouped(batchSize).toList - groupedQueries.foreach(queries => { - val cqlBatch = QueryBuilder.batch() - queries.map(query => cqlBatch.add(query)) - val result = cassandraUtil.upsert(cqlBatch.toString) - if (result) { - metrics.incCounter(config.dbUpdateCount) - } else { - val msg = "Database update has failed" + cqlBatch.toString - logger.error(msg) - throw new Exception(msg) - } - }) - } -} diff --git a/enrolment-reconciliation/src/main/scala/org/sunbird/job/recounciliation/task/EnrolmentReconciliationConfig.scala b/enrolment-reconciliation/src/main/scala/org/sunbird/job/recounciliation/task/EnrolmentReconciliationConfig.scala deleted file mode 100644 index 8b44a4cf0..000000000 --- a/enrolment-reconciliation/src/main/scala/org/sunbird/job/recounciliation/task/EnrolmentReconciliationConfig.scala +++ /dev/null @@ -1,118 +0,0 @@ -package org.sunbird.job.recounciliation.task - -import com.typesafe.config.Config -import org.apache.flink.api.common.typeinfo.TypeInformation -import org.apache.flink.api.java.typeutils.TypeExtractor -import org.apache.flink.streaming.api.scala.OutputTag -import org.sunbird.job.BaseJobConfig -import org.sunbird.job.recounciliation.domain.CollectionProgress - -import java.util - -class EnrolmentReconciliationConfig(override val config: Config) extends BaseJobConfig(config, "enrolment-reconciliation") { - - implicit val mapTypeInfo: TypeInformation[util.Map[String, AnyRef]] = TypeExtractor.getForClass(classOf[util.Map[String, AnyRef]]) - implicit val stringTypeInfo: TypeInformation[String] = TypeExtractor.getForClass(classOf[String]) - implicit val enrolmentCompleteTypeInfo: TypeInformation[List[CollectionProgress]] = TypeExtractor.getForClass(classOf[List[CollectionProgress]]) - - // Kafka Topics Configuration - val kafkaInputTopic: String = config.getString("kafka.input.topic") - val kafkaAuditEventTopic: String = config.getString("kafka.output.audit.topic") - val kafkaFailedEventTopic: String = config.getString("kafka.output.failed.topic") - val kafkaCertIssueTopic: String = config.getString("kafka.output.certissue.topic") - - override val kafkaConsumerParallelism: Int = config.getInt("task.consumer.parallelism") - val enrolmentReconciliationParallelism: Int = config.getInt("task.enrolment.reconciliation.parallelism") - val enrolmentCompleteParallelism: Int = config.getInt("task.enrolment.complete.parallelism") - - // Metric List - val totalEventCount = "total-events-count" - val failedEventCount = "failed-events-count" - val dbUpdateCount = "db-update-count" - val dbReadCount = "db-read-count" - val cacheHitCount = "cache-hit-count" - val cacheMissCount = "cache-miss-count" - val skipEventsCount = "skipped-events-count" - val certIssueEventsCount = "cert-issue-events-count" - val processedEnrolmentCount = "processed-enrolment-count" - val enrolmentCompleteCount = "enrolment-complete-count" - val retiredCCEventsCount = "retired-consumption-events-count" - - // Consumers - val enrolmentReconciliationConsumer = "enrolment-reconciliation-consumer" - - - // Producers - val enrolmentReconciliationProducer = "enrolment-reconciliation-audit-events-sink" - val enrolmentCompleteEventProducer = "enrolment-complete-audit-sink" - val enrolmentReconciliationFailedEventProducer = "enrolment-reconciliation-failed-sink" - val certIssueEventProducer = "certificate-issue-event-producer" - - val dbHost: String = config.getString("lms-cassandra.host") - val dbPort: Int = config.getInt("lms-cassandra.port") - val dbUserContentConsumptionTable: String = config.getString("lms-cassandra.consumption.table") - val dbUserActivityAggTable: String = config.getString("lms-cassandra.user_activity_agg.table") - val dbUserEnrolmentsTable: String = config.getString("lms-cassandra.user_enrolments.table") - val dbKeyspace: String = config.getString("lms-cassandra.keyspace") - - // Redis Configurations - val nodeStore: Int = config.getInt("redis.database.relationCache.id") // Both LeafNodes And Ancestor nodes - - val supportedEventType:String = "user-enrolment-sync" - - // Other services configuration - val searchServiceBasePath: String = config.getString("service.search.basePath") - val searchAPIURL = searchServiceBasePath + "/v3/search" - - val auditEventOutputTagName = "audit-events" - val auditEventOutputTag: OutputTag[String] = OutputTag[String](auditEventOutputTagName) - val failedEventOutputTagName = "failed-events" - val failedEventOutputTag: OutputTag[String] = OutputTag[String](failedEventOutputTagName) - val collectionCompleteOutputTagName = "collection-progress-complete-events" - val collectionCompleteOutputTag: OutputTag[List[CollectionProgress]] = OutputTag[List[CollectionProgress]](collectionCompleteOutputTagName) - val collectionUpdateOutputTagName = "collection-progress-update-events" - val collectionUpdateOutputTag: OutputTag[List[CollectionProgress]] = OutputTag[List[CollectionProgress]](collectionUpdateOutputTagName) - val certIssueOutputTagName = "certificate-issue-events" - val certIssueOutputTag: OutputTag[String] = OutputTag[String](certIssueOutputTagName) - - // Job specific configurations - val thresholdBatchWriteSize: Int = config.getInt("threshold.batch.write.size") - val moduleAggEnabled: Boolean = config.getBoolean("activity.module.aggs.enabled") - val statusCacheExpirySec: Int = config.getInt("activity.collection.status.cache.expiry") - val filterCompletedEnrolments: Boolean = if (config.hasPath("activity.filter.processed.enrolments")) config.getBoolean("activity.filter.processed.enrolments") else true - - // constants - val activityType = "activity_type" - val activityId = "activity_id" - val contextId = "context_id" - val activityUser = "user_id" - val aggLastUpdated = "agg_last_updated" - val agg = "agg" - val courseId = "courseId" - val batchId = "batchId" - val contentId = "contentId" - val progress = "progress" - val contents = "contents" - val contentStatus = "contentStatus" - val userId = "userId" - val status = "status" - val unitActivityType = "course-unit" - val courseActivityType = "course" - val leafNodes = "leafnodes" - val ancestors = "ancestors" - val viewcount = "viewcount" - val completedcount = "completedcount" - val complete = "complete" - val eData = "edata" - val action = "action" - val batchEnrolmentUpdateCode = "batch-enrolment-update" - val routerFn = "RouterFn" - val consumptionDeDupFn= "consumption-dedup-process" - val activityAggregateUpdaterFn = "activity-aggregate-updater-fn" - val partition = "partition" - val courseBatch = "CourseBatch" - val collectionProgressUpdateFn = "progress-update-process" - val collectionCompleteFn = "collection-completion-process" - - -} diff --git a/enrolment-reconciliation/src/main/scala/org/sunbird/job/recounciliation/task/EnrolmentReconciliationStreamTask.scala b/enrolment-reconciliation/src/main/scala/org/sunbird/job/recounciliation/task/EnrolmentReconciliationStreamTask.scala deleted file mode 100644 index a6db61e70..000000000 --- a/enrolment-reconciliation/src/main/scala/org/sunbird/job/recounciliation/task/EnrolmentReconciliationStreamTask.scala +++ /dev/null @@ -1,68 +0,0 @@ -package org.sunbird.job.recounciliation.task - -import com.typesafe.config.ConfigFactory -import org.apache.flink.api.common.typeinfo.TypeInformation -import org.apache.flink.api.java.typeutils.TypeExtractor -import org.apache.flink.api.java.utils.ParameterTool -import org.apache.flink.streaming.api.scala.StreamExecutionEnvironment -import org.sunbird.job.connector.FlinkKafkaConnector -import org.sunbird.job.recounciliation.domain.{CollectionProgress, Event} -import org.sunbird.job.recounciliation.functions.{EnrolmentReconciliationFn, ProgressCompleteFunction, ProgressUpdateFunction} -import org.sunbird.job.util.{FlinkUtil, HttpUtil} - -import java.io.File -import java.util - - -class EnrolmentReconciliationStreamTask(config: EnrolmentReconciliationConfig, kafkaConnector: FlinkKafkaConnector, httpUtil: HttpUtil) { - def process(): Unit = { - implicit val env: StreamExecutionEnvironment = FlinkUtil.getExecutionContext(config) - implicit val mapTypeInfo: TypeInformation[util.Map[String, AnyRef]] = TypeExtractor.getForClass(classOf[util.Map[String, AnyRef]]) - implicit val stringTypeInfo: TypeInformation[String] = TypeExtractor.getForClass(classOf[String]) - implicit val enrolmentCompleteTypeInfo: TypeInformation[List[CollectionProgress]] = TypeExtractor.getForClass(classOf[List[CollectionProgress]]) - implicit val eventTypeInfo: TypeInformation[Event] = TypeExtractor.getForClass(classOf[Event]) - val source = kafkaConnector.kafkaJobRequestSource[Event](config.kafkaInputTopic) - - val progressStream = env.addSource(source).name(config.enrolmentReconciliationConsumer) - .uid(config.enrolmentReconciliationConsumer).setParallelism(config.kafkaConsumerParallelism) - .rebalance - .process(new EnrolmentReconciliationFn(config, httpUtil)) - .name("enrolment-reconciliation").uid("enrolment-reconciliation") - .setParallelism(config.enrolmentReconciliationParallelism) - - progressStream.getSideOutput(config.auditEventOutputTag).addSink(kafkaConnector.kafkaStringSink(config.kafkaAuditEventTopic)) - .name(config.enrolmentReconciliationProducer).uid(config.enrolmentReconciliationProducer) - progressStream.getSideOutput(config.failedEventOutputTag).addSink(kafkaConnector.kafkaStringSink(config.kafkaFailedEventTopic)) - .name(config.enrolmentReconciliationFailedEventProducer).uid(config.enrolmentReconciliationFailedEventProducer) - - // TODO: set separate parallelism for below task. - progressStream.getSideOutput(config.collectionUpdateOutputTag).process(new ProgressUpdateFunction(config)) - .name(config.collectionProgressUpdateFn).uid(config.collectionProgressUpdateFn).setParallelism(config.enrolmentCompleteParallelism) - val enrolmentCompleteStream = progressStream.getSideOutput(config.collectionCompleteOutputTag).process(new ProgressCompleteFunction(config)) - .name(config.collectionCompleteFn).uid(config.collectionCompleteFn).setParallelism(config.enrolmentCompleteParallelism) - - enrolmentCompleteStream.getSideOutput(config.certIssueOutputTag).addSink(kafkaConnector.kafkaStringSink(config.kafkaCertIssueTopic)) - .name(config.certIssueEventProducer).uid(config.certIssueEventProducer) - enrolmentCompleteStream.getSideOutput(config.auditEventOutputTag).addSink(kafkaConnector.kafkaStringSink(config.kafkaAuditEventTopic)) - .name(config.enrolmentCompleteEventProducer).uid(config.enrolmentCompleteEventProducer) - - env.execute(config.jobName) - } -} - -// $COVERAGE-OFF$ Disabling scoverage as the below code can only be invoked within flink cluster -object EnrolmentReconciliationStreamTask { - - def main(args: Array[String]): Unit = { - val configFilePath = Option(ParameterTool.fromArgs(args).get("config.file.path")) - val config = configFilePath.map { - path => ConfigFactory.parseFile(new File(path)).resolve() - }.getOrElse(ConfigFactory.load("enrolment-reconciliation.conf").withFallback(ConfigFactory.systemEnvironment())) - val enrolmentReconciliationConfig = new EnrolmentReconciliationConfig(config) - val kafkaUtil = new FlinkKafkaConnector(enrolmentReconciliationConfig) - val task = new EnrolmentReconciliationStreamTask(enrolmentReconciliationConfig, kafkaUtil, new HttpUtil) - task.process() - } -} - -// $COVERAGE-ON$ diff --git a/enrolment-reconciliation/src/test/resources/test.conf b/enrolment-reconciliation/src/test/resources/test.conf deleted file mode 100644 index 27c3a79ab..000000000 --- a/enrolment-reconciliation/src/test/resources/test.conf +++ /dev/null @@ -1,54 +0,0 @@ -include "base-test.conf" - -kafka { - input.topic = "sunbirddev.batch.enrolment.sync.request" - output.audit.topic = "sunbirddev.telemetry.raw" - output.failed.topic = "sunbirddev.enrolment.reconciliation.failed" - output.certissue.topic = "sunbirddev.issue.certificate.request" - groupId = "sunbirddev-enrolment-reconciliation-group" -} - -task { - window.shards = 1 - consumer.parallelism = 1 - dedup.parallelism = 1 - enrolment.reconciliation.parallelism = 1 - enrolment.complete.parallelism = 1 -} - -lms-cassandra { - keyspace = "sunbird_courses" - consumption.table = "user_content_consumption" - user_activity_agg.table = "user_activity_agg" - user_enrolments.table = "user_enrolments" -} - -redis { - database { - relationCache.id = 10 - } -} - -dedup-redis { - host = localhost - port = 6379 - database.index = 3 - database.expiry = 604800 -} - -threshold.batch.read.interval = 60 // In sec -threshold.batch.read.size = 1000 -threshold.batch.write.size = 10 - -activity { - module.aggs.enabled = true - input.dedup.enabled = true - filter.processed.enrolments = true - collection.status.cache.expiry = 3600 -} - -service { - search { - basePath = "http://localhost:9000" - } -} \ No newline at end of file diff --git a/enrolment-reconciliation/src/test/resources/test.cql b/enrolment-reconciliation/src/test/resources/test.cql deleted file mode 100644 index 5fff30d90..000000000 --- a/enrolment-reconciliation/src/test/resources/test.cql +++ /dev/null @@ -1,79 +0,0 @@ -CREATE KEYSPACE sunbird_courses with replication = {'class':'SimpleStrategy','replication_factor':1}; - -CREATE TABLE sunbird_courses.user_content_consumption ( - userid text, - courseid text, - batchid text, - contentid text, - completedcount int, - datetime timestamp, - lastaccesstime text, - lastcompletedtime text, - lastupdatedtime text, - progress int, - status int, - viewcount int, - PRIMARY KEY (userid, courseid, batchid, contentid) -) WITH CLUSTERING ORDER BY (courseid ASC, batchid ASC, contentid ASC); - -// EVENT_1 Testcase data -INSERT INTO sunbird_courses.user_content_consumption(userid, contentid, batchid,courseid,progress,status,viewcount,completedcount) VALUES ('8454cb21-3ce9-4e30-85b5-fade097880d8','do_11260735471149056012299','0126083288437637121','do_1127212344324751361295',100, 2, 3,1) ; -INSERT INTO sunbird_courses.user_content_consumption(userid, contentid, batchid,courseid,progress,status,viewcount,completedcount) VALUES ('8454cb21-3ce9-4e30-85b5-fade097880d8','do_11260735471149056012300','0126083288437637121','do_1127212344324751361295',100, 2, 2,2) ; -INSERT INTO sunbird_courses.user_content_consumption(userid, contentid, batchid,courseid,progress,status,viewcount,completedcount) VALUES ('8454cb21-3ce9-4e30-85b5-fade097880d8','do_11260735471149056012301','0126083288437637121','do_1127212344324751361295',0, 1,0,1) ; - -//Event_2 Testcase Data -INSERT INTO sunbird_courses.user_content_consumption(userid, contentid, batchid,courseid,progress,status,viewcount,completedcount) VALUES ('user001','do_R1','Batch1','course001',100, 2,1,0) ; -INSERT INTO sunbird_courses.user_content_consumption(userid, contentid, batchid,courseid,progress,status,viewcount,completedcount) VALUES ('user001','do_R2','Batch1','course001',100, 2,1,0) ; -INSERT INTO sunbird_courses.user_content_consumption(userid, contentid, batchid,courseid,progress,status,viewcount,completedcount) VALUES ('user001','do_R3','Batch1','course001',100, 2,1,0) ; - - - -CREATE TABLE IF NOT EXISTS sunbird_courses.user_activity_agg ( - activity_id text, - user_id text, - activity_type text, - context_id text, - agg Map, - aggregates Map, - agg_last_updated Map, - PRIMARY KEY ((activity_type, activity_id), context_id, user_id) -); - -CREATE TABLE sunbird_courses.user_enrolments ( - userid text, - courseid text, - batchid text, - active boolean, - addedby text, - certificates list>>, - completedon timestamp, - completionpercentage int, - contentstatus map, - datetime timestamp, - enrolleddate text, - issued_certificates list>>, - lastreadcontentid text, - lastreadcontentstatus int, - progress int, - status int, - PRIMARY KEY (userid, courseid, batchid) -) WITH CLUSTERING ORDER BY (courseid ASC, batchid ASC) - AND bloom_filter_fp_chance = 0.01 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} - AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 - AND default_time_to_live = 0 - AND gc_grace_seconds = 864000 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 0 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; -CREATE INDEX inx_ues_status ON sunbird_courses.user_enrolments (status); -CREATE INDEX inx_ues_certs ON sunbird_courses.user_enrolments (values(certificates)); - - -INSERT INTO sunbird_courses.user_enrolments(userid, courseid, batchid, status) VALUES ('user001', 'course001', 'Batch1', 1) diff --git a/enrolment-reconciliation/src/test/scala/org/sunbird/job/fixture/EventFixture.scala b/enrolment-reconciliation/src/test/scala/org/sunbird/job/fixture/EventFixture.scala deleted file mode 100644 index be565d6e1..000000000 --- a/enrolment-reconciliation/src/test/scala/org/sunbird/job/fixture/EventFixture.scala +++ /dev/null @@ -1,194 +0,0 @@ -package org.sunbird.job.fixture - -object EventFixture { - - /** - * case-1. Inserting a first course which is having 3 leaf nodes in the redis database and cassandra database - * does not contains this course id. - * courseId = - * ============BE_JOB_REQUEST_CONTENTS========== - * do_1127212344324751361295 - course - * do_course_unit1 - unit1 - * do_11260735471149056012299 - resource - * do_course_unit2 - unit2 - * do_11260735471149056012300 - resource - * do_course_unit3 - unit3 - * do_11260735471149056012301 - resource - * do_11260735471149056012300 -resource - * - * - * ============== content status in the event ====== - * do_11260735471149056012299 - 2 - * do_11260735471149056012301 - 1 - * do_11260735471149056012300 - 1 - * ============== content status in the database(content-consumption) - * do_11260735471149056012299 - 2 - * do_11260735471149056012301 - 1 - * do_11260735471149056012300 - 2 - * - * ============== Computation ============== - * unit level computation - * do_11260735471149056012299:ansestor -> do_course_unit1, do_1127212344324751361295 - * do_course_unit1:do_1127212344324751361295:leafnodes: do_11260735471149056012299 - * lefNodesSize = 1, completed = 1 - * 1/1 = 100% - * - * do_11260735471149056012301:ansestor -> do_course_unit3,do_1127212344324751361295 - * do_course_unit3:do_1127212344324751361295:leafNodes -> do_11260735471149056012301,do_11260735471149056012300 - * leafNodesSize = 2,completed 1 - * 1/2 = 50% - * - * do_11260735471149056012300:ansestor -> do_course_unit3,do_course_unit2,do_1127212344324751361295 - * do_course_unit3:do_1127212344324751361295:leafNodes -> do_11260735471149056012301,do_11260735471149056012300 - * leafNodesSize = 2, completed = 1 - * 1/2 = 50% - * - * do_course_unit2:do_1127212344324751361295:leafNodes -> do_11260735471149056012300 - * leafNodesSize = 1, completed = 1 - * 1/1 = 100% - * - * course level progress computation - * do_1127212344324751361295:leafNodes = do_11260735471149056012299, do_11260735471149056012301, do_11260735471149056012300 - */ - - val EVENT_1: String = - """ - |{"eid":"BE_JOB_REQUEST","ets":1563788371969,"mid":"LMS.1563788371969.590c5fa0-0ce8-46ed-bf6c-681c0a1fdac8","actor":{"type":"System","id":"Course Batch Updater"},"context":{"pdata":{"ver":"1.0","id":"org.sunbird.platform"}},"object":{"type":"CourseBatchEnrolment","id":"0126083288437637121_8454cb21-3ce9-4e30-85b5-fade097880d8"},"edata":{"action":"user-enrolment-sync","iteration":1,"batchId":"0126083288437637121","userId":"8454cb21-3ce9-4e30-85b5-fade097880d8","courseId":"do_1127212344324751361295"}} - |""".stripMargin - - val courseLeafNodes = Map("do_1127212344324751361295:do_1127212344324751361295:leafnodes" -> List("do_11260735471149056012299", "do_11260735471149056012300", "do_11260735471149056012301")) - val unitLeafNodes_1 = Map("do_1127212344324751361295:do_course_unit1:leafnodes" -> List("do_11260735471149056012299")) - val unitLeafNodes_2 = Map("do_1127212344324751361295:do_course_unit2:leafnodes" -> List("do_11260735471149056012300")) - val unitLeafNodes_3 = Map("do_1127212344324751361295:do_course_unit3:leafnodes" -> List("do_11260735471149056012301", "do_11260735471149056012300")) - - val ancestorsResource_1 = Map("do_1127212344324751361295:do_11260735471149056012299:ancestors" -> List("do_course_unit1", "do_1127212344324751361295")) - val ancestorsResource_2 = Map("do_1127212344324751361295:do_11260735471149056012300:ancestors" -> List("do_course_unit2", "do_course_unit3", "do_1127212344324751361295")) - val ancestorsResource_3 = Map("do_1127212344324751361295:do_11260735471149056012301:ancestors" -> List("do_course_unit3", "do_1127212344324751361295")) - - val CASE_1:Map[String, AnyRef] = Map("event" -> EVENT_1, "cacheData" -> List(courseLeafNodes, - unitLeafNodes_1, unitLeafNodes_2,unitLeafNodes_3, ancestorsResource_1,ancestorsResource_2,ancestorsResource_3)) - - - - val EVENT_2: String = - """ - |{"eid":"BE_JOB_REQUEST","ets":1563788371969,"mid":"LMS.1563788371969.590c5fa0-0ce8-46ed-bf6c-681c0a1fdac8","actor":{"type":"System","id":"Course Batch Updater"},"context":{"pdata":{"ver":"1.0","id":"org.sunbird.platform"}},"object":{"type":"CourseBatchEnrolment","id":"0126083288437637121_8454cb21-3ce9-4e30-85b5-fade097880d8"},"edata":{"contents":[{"contentId":"do_R1","status":2},{"contentId":"do_R2","status":1},{"contentId":"do_R3","status":2}],"action":"batch-enrolment-update","iteration":1,"batchId":"Batch1","userId":"user001","courseId":"course001"}} - |""".stripMargin - - /** **** course structure **** - * - * case2: When all resource progress is 2 in the content-consumption table - * - * course001 - course - * unit1 - unit1 - * do_R1 - Resource - * do_R3 - Resource - * unit2 - Unit2 - * do_R2 - Resource - * do_R3 - Resource - * - * ============== content status in the event ====== - * do_R1 - 1 - * do_R3 - 1 - * do_R2 - 1 - * ============== content status in the database(content-consumption) - * do_R1 - 2 - * do_R2 - 2 - * do_R3 - 2 - * - * //Unit Level - * course001:do_R1:ansestor => unit1,course001 - * unit1:leafNodes -> do_R1,do_R3 - * output:leafNodesSize = 2, completed=2 - * course001:do_R2:ansestor => unit2,course001 - * unit2:leafNodes -> do_R2,do_R3 - * output:leafNodesSize = 2, completed = 2 - * course001:do_R3:ansestor => unit1, unit2,course001 - * unit1:leafNodes -> do_R1,do_R3 - * unit2:leafNodes -> do_R2,do_R3 - * output:leafNodes=2, completed=2 - * // CourseLevel - * output:LeafNodes =3, Completed =3 - * - * - */ - val e2_courseLeafNodes = Map("course001:course001:leafnodes" -> List("do_R1", "do_R3", "do_R2")) - val e2_unitLeafNodes_1 = Map("course001:unit1:leafnodes" -> List("do_R1", "do_R3")) - val e2_unitLeafNodes_2 = Map("course001:unit2:leafnodes" -> List("do_R2", "do_R3")) - - val e2_ancestorsResource_1 = Map("course001:do_R1:ancestors" -> List("unit1", "course001")) - val e2_ancestorsResource_2 = Map("course001:do_R3:ancestors" -> List("unit1", "unit2", "course001")) - val e2_ancestorsResource_3 = Map("course001:do_R2:ancestors" -> List("unit2", "course001")) - - val CASE_2:Map[String, AnyRef] = Map("event" -> EVENT_2, "cacheData" -> List(e2_courseLeafNodes, - e2_unitLeafNodes_1, e2_unitLeafNodes_2,e2_ancestorsResource_1, e2_ancestorsResource_2,e2_ancestorsResource_3)) - - /** * - * - * Case3: When resource data is not available in the content_consumption table and user_activity_agg - * - * C11 - course - * unit11 - unit1 - * R11 - Resource - * R22 - Resource - * unit22 - Unit2 - * R11 - Resource - * - * ============== content status in the event ====== - * R11 - 2 - * R22 - 2 - * ============== content status in the database(content-consumption) - * Data is not available - * - * //Unit Level - * C11:R11:ansestor => unit11,unit22,C11 - * unit11:leafNodes -> R11,R22 - * output:leafNodesSize = 2, completed=2 - * unit11:leafNodes -> R11 - * output:leafNodesSize = 1, completed=1 - * C11:R22:ansestor => unit11,C11 - * unit11:leafNodes -> R11,R22 - * output:leafNodesSize = 2, completed = 2 - * // CourseLevel - * output:LeafNodes =2, Completed =2 - * - */ - - val e3_courseLeafNodes = Map("C11:C11:leafnodes" -> List("R11", "R22")) - val e3_unitLeafNodes_1 = Map("C11:unit11:leafnodes" -> List("R11", "R22")) - val e3_unitLeafNodes_2 = Map("C11:unit22:leafnodes" -> List("R11")) - - val e3_ancestorsResource_1 = Map("C11:R11:ancestors" -> List("unit11", "C11")) - val e3_ancestorsResource_2 = Map("C11:R11:ancestors" -> List("unit22", "C11")) - val e3_ancestorsResource_3 = Map("C11:R22:ancestors" -> List("unit11", "C11")) - - val EVENT_3: String = - """ - |{"eid":"BE_JOB_REQUEST","ets":1563788371969,"mid":"LMS.1563788371969.590c5fa0-0ce8-46ed-bf6c-681c0a1fdac8","actor":{"type":"System","id":"Course Batch Updater"},"context":{"pdata":{"ver":"1.0","id":"org.sunbird.platform"}},"object":{"type":"CourseBatchEnrolment","id":"0126083288437637121_8454cb21-3ce9-4e30-85b5-fade097880d8"},"edata":{"contents":[{"contentId":"R11","status":2},{"contentId":"R22","status":2}],"action":"batch-enrolment-update","iteration":1,"batchId":"B11","userId":"U11","courseId":"C11"}} - |""".stripMargin - - val CASE_3:Map[String, AnyRef] = Map("event" -> EVENT_3, "cacheData" -> List(e3_courseLeafNodes, e3_unitLeafNodes_1, e3_unitLeafNodes_2,e3_ancestorsResource_1, e3_ancestorsResource_2, e3_ancestorsResource_3) ) - - val EVENT_4: String = - """ - |{"eid":"BE_JOB_REQUEST","ets":1563788371969,"mid":"LMS.1563788371969.590c5fa0-0ce8-46ed-bf6c-681c0a1fdac8","actor":{"type":"System","id":"Course Batch Updater"},"context":{"pdata":{"ver":"1.0","id":"org.sunbird.platform"}},"object":{"type":"CourseBatchEnrolment","id":"0126083288437637121_8454cb21-3ce9-4e30-85b5-fade097880d8"},"edata":{"contents":[{"contentId":"do_11260735471149056012299","status":2},{"contentId":"do_11260735471149056012300","status":1},{"contentId":"do_11260735471149056012301","status":1}],"action":"batch-enrolment-update","iteration":1,"batchId":"0126083288437637121","userId":"8454cb21-3ce9-4e30-85b5-fade097880d8","courseId":"do_1127212344324751361295"}} - |""".stripMargin - - val EVENT_5: String = - """ - |{"eid":"BE_JOB_REQUEST","ets":1563788371969,"mid":"LMS.1563788371969.590c5fa0-0ce8-46ed-bf6c-681c0a1fdac8","actor":{"type":"System","id":"Course Batch Updater"},"context":{"pdata":{"ver":"1.0","id":"org.sunbird.platform"}},"object":{"type":"CourseBatchEnrolment","id":"0126083288437637121_8454cb21-3ce9-4e30-85b5-fade097880d8"},"edata":{"contents":[{"contentId":"do_11260735471149056012299","status":2},{"contentId":"do_11260735471149056012300","status":1},{"contentId":"do_11260735471149056012301","status":1}],"action":"batch-update","iteration":1,"batchId":"0126083288437637121","userId":"8454cb21-3ce9-4e30-85b5-fade097880d8","courseId":"do_1127212344324751361295"}} - |""".stripMargin - - val CC_EVENT1: String = - """ - |{"eid":"BE_JOB_REQUEST","ets":1563788371969,"mid":"LMS.1563788371969.590c5fa0-0ce8-46ed-bf6c-681c0a1fdac81","actor":{"type":"System","id":"Course Batch Updater"},"context":{"pdata":{"ver":"1.0","id":"org.sunbird.platform"}},"object":{"type":"CourseBatchEnrolment","id":"0126083288437637121_8454cb21-3ce9-4e30-85b5-fade097880d8"},"edata":{"contents":[{"contentId":"do_R1","status":2}],"action":"batch-enrolment-update","iteration":1,"batchId":"Batch1","userId":"user001","courseId":"course001"}} - |""".stripMargin - val CC_EVENT2: String = - """ - |{"eid":"BE_JOB_REQUEST","ets":1563788371969,"mid":"LMS.1563788371969.590c5fa0-0ce8-46ed-bf6c-681c0a1fdac82","actor":{"type":"System","id":"Course Batch Updater"},"context":{"pdata":{"ver":"1.0","id":"org.sunbird.platform"}},"object":{"type":"CourseBatchEnrolment","id":"0126083288437637121_8454cb21-3ce9-4e30-85b5-fade097880d8"},"edata":{"contents":[{"contentId":"do_R2","status":2}],"action":"batch-enrolment-update","iteration":1,"batchId":"Batch1","userId":"user001","courseId":"course001"}} - |""".stripMargin - val CC_EVENT3: String = - """ - |{"eid":"BE_JOB_REQUEST","ets":1563788371969,"mid":"LMS.1563788371969.590c5fa0-0ce8-46ed-bf6c-681c0a1fdac83","actor":{"type":"System","id":"Course Batch Updater"},"context":{"pdata":{"ver":"1.0","id":"org.sunbird.platform"}},"object":{"type":"CourseBatchEnrolment","id":"0126083288437637121_8454cb21-3ce9-4e30-85b5-fade097880d8"},"edata":{"contents":[{"contentId":"do_R3","status":2}],"action":"batch-enrolment-update","iteration":1,"batchId":"Batch1","userId":"user001","courseId":"course001"}} - |""".stripMargin -} \ No newline at end of file diff --git a/enrolment-reconciliation/src/test/scala/org/sunbird/job/spec/BaseActivityAggregateTestSpec.scala b/enrolment-reconciliation/src/test/scala/org/sunbird/job/spec/BaseActivityAggregateTestSpec.scala deleted file mode 100644 index 967e7d37a..000000000 --- a/enrolment-reconciliation/src/test/scala/org/sunbird/job/spec/BaseActivityAggregateTestSpec.scala +++ /dev/null @@ -1,59 +0,0 @@ -package org.sunbird.job.spec - -import org.apache.flink.streaming.api.functions.sink.SinkFunction - -import java.util - - -class AuditEventSink extends SinkFunction[String] { - - override def invoke(value: String): Unit = { - synchronized { - AuditEventSink.values.add(value) - } - } -} - -object AuditEventSink { - val values: util.List[String] = new util.ArrayList() -} - -class FailedEventSink extends SinkFunction[String] { - - override def invoke(value: String): Unit = { - synchronized { - FailedEventSink.values.add(value) - } - } -} - -object FailedEventSink { - val values: util.List[String] = new util.ArrayList() -} - -class SuccessEvent extends SinkFunction[String] { - - override def invoke(value: String): Unit = { - synchronized { - SuccessEventSink.values.add(value) - } - } -} - -object SuccessEventSink { - val values: util.List[String] = new util.ArrayList() -} - - -class CertificateIssuedEventsSink extends SinkFunction[String] { - - override def invoke(value: String): Unit = { - synchronized { - CertificateIssuedEvents.values.add(value) - } - } -} - -object CertificateIssuedEvents { - val values: util.List[String] = new util.ArrayList() -} diff --git a/enrolment-reconciliation/src/test/scala/org/sunbird/job/spec/EnrolmentReconciliationStreamTaskSpec.scala b/enrolment-reconciliation/src/test/scala/org/sunbird/job/spec/EnrolmentReconciliationStreamTaskSpec.scala deleted file mode 100644 index 75ea7a0df..000000000 --- a/enrolment-reconciliation/src/test/scala/org/sunbird/job/spec/EnrolmentReconciliationStreamTaskSpec.scala +++ /dev/null @@ -1,121 +0,0 @@ -package org.sunbird.job.spec - -import com.google.gson.Gson -import com.typesafe.config.{Config, ConfigFactory} -import org.apache.flink.api.common.typeinfo.TypeInformation -import org.apache.flink.api.java.typeutils.TypeExtractor -import org.apache.flink.runtime.testutils.MiniClusterResourceConfiguration -import org.apache.flink.streaming.api.functions.source.SourceFunction -import org.apache.flink.streaming.api.functions.source.SourceFunction.SourceContext -import org.apache.flink.test.util.MiniClusterWithClientResource -import org.cassandraunit.CQLDataLoader -import org.cassandraunit.dataset.cql.FileCQLDataSet -import org.cassandraunit.utils.EmbeddedCassandraServerHelper -import org.mockito.Mockito -import org.mockito.Mockito._ -import org.sunbird.job.cache.RedisConnect -import org.sunbird.job.connector.FlinkKafkaConnector -import org.sunbird.job.fixture.EventFixture -import org.sunbird.job.recounciliation.domain.Event -import org.sunbird.job.recounciliation.task.{EnrolmentReconciliationConfig, EnrolmentReconciliationStreamTask} -import org.sunbird.job.util.{CassandraUtil, HttpUtil, JSONUtil} -import org.sunbird.spec.{BaseMetricsReporter, BaseTestSpec} -import redis.clients.jedis.Jedis -import redis.embedded.RedisServer - -import java.util -import scala.collection.JavaConverters._ - -class EnrolmentReconciliationStreamTaskSpec extends BaseTestSpec { - - implicit val mapTypeInfo: TypeInformation[java.util.Map[String, AnyRef]] = TypeExtractor.getForClass(classOf[java.util.Map[String, AnyRef]]) - - val flinkCluster = new MiniClusterWithClientResource(new MiniClusterResourceConfiguration.Builder() - .setConfiguration(testConfiguration()) - .setNumberSlotsPerTaskManager(1) - .setNumberTaskManagers(1) - .build) - - var redisServer: RedisServer = _ - redisServer = new RedisServer(6340) - redisServer.start() - var jedis: Jedis = _ - - val mockKafkaUtil: FlinkKafkaConnector = mock[FlinkKafkaConnector](Mockito.withSettings().serializable()) - val gson = new Gson() - val config: Config = ConfigFactory.load("test.conf") - val jobConfig: EnrolmentReconciliationConfig = new EnrolmentReconciliationConfig(config) - - - var cassandraUtil: CassandraUtil = _ - - override protected def beforeAll(): Unit = { - super.beforeAll() - val redisConnect = new RedisConnect(jobConfig) - jedis = redisConnect.getConnection(jobConfig.nodeStore) - EmbeddedCassandraServerHelper.startEmbeddedCassandra(80000L) - cassandraUtil = new CassandraUtil(jobConfig.dbHost, jobConfig.dbPort) - val session = cassandraUtil.session - - val dataLoader = new CQLDataLoader(session) - dataLoader.load(new FileCQLDataSet(getClass.getResource("/test.cql").getPath, true, true)) - // Clear the metrics - BaseMetricsReporter.gaugeMetrics.clear() - jedis.flushDB() - flinkCluster.before() - - updateRedis(jedis, EventFixture.CASE_1.asInstanceOf[Map[String, AnyRef]]) - updateRedis(jedis, EventFixture.CASE_2.asInstanceOf[Map[String, AnyRef]]) - updateRedis(jedis, EventFixture.CASE_3.asInstanceOf[Map[String, AnyRef]]) - } - - override protected def afterAll(): Unit = { - super.afterAll() - try { - EmbeddedCassandraServerHelper.cleanEmbeddedCassandra() - redisServer.stop() - } catch { - case ex: Exception => ex.printStackTrace() - } - flinkCluster.after() - } - - def initialize() { - when(mockKafkaUtil.kafkaJobRequestSource[Event](jobConfig.kafkaInputTopic)).thenReturn(new EnrolmentReconciliationEventSource) - when(mockKafkaUtil.kafkaStringSink(jobConfig.kafkaAuditEventTopic)).thenReturn(new AuditEventSink) - when(mockKafkaUtil.kafkaStringSink(jobConfig.kafkaFailedEventTopic)).thenReturn(new FailedEventSink) - when(mockKafkaUtil.kafkaStringSink(jobConfig.kafkaCertIssueTopic)).thenReturn(new CertificateIssuedEventsSink) - } - - - "EnrolmentReConciliation " should "validate metrics" in { - initialize() - new EnrolmentReconciliationStreamTask(jobConfig, mockKafkaUtil, httpUtil = new HttpUtil).process() - BaseMetricsReporter.gaugeMetrics(s"${jobConfig.jobName}.${jobConfig.totalEventCount}").getValue() should be(2) - BaseMetricsReporter.gaugeMetrics(s"${jobConfig.jobName}.${jobConfig.dbUpdateCount}").getValue() should be(1) - BaseMetricsReporter.gaugeMetrics(s"${jobConfig.jobName}.${jobConfig.skipEventsCount}").getValue() should be(1) - } - - def updateRedis(jedis: Jedis, testData: Map[String, AnyRef]) { - testData.get("cacheData").map(data => { - data.asInstanceOf[List[Map[String, AnyRef]]].map(cacheData => { - cacheData.map(x => { - x._2.asInstanceOf[List[String]].foreach(d => { - jedis.sadd(x._1, d) - }) - }) - }) - }) - } - -} - -class EnrolmentReconciliationEventSource extends SourceFunction[Event] { - override def run(ctx: SourceContext[Event]): Unit = { - ctx.collect(new Event(JSONUtil.deserialize[util.Map[String, Any]](EventFixture.EVENT_1), 0, 10)) - ctx.collect(new Event(JSONUtil.deserialize[util.Map[String, Any]](EventFixture.EVENT_2), 0, 11)) - } - override def cancel(): Unit = {} - -} - diff --git a/jobs-core/pom.xml b/jobs-core/pom.xml index 241787931..e2f3d7c3b 100644 --- a/jobs-core/pom.xml +++ b/jobs-core/pom.xml @@ -79,9 +79,9 @@ 0.1.1 - org.sunbird - cloud-store-sdk_2.12 - 1.4.3 + ${CLOUD_STORE_GROUP_ID} + ${CLOUD_STORE_ARTIFACT_ID} + ${CLOUD_STORE_VERSION} log4j diff --git a/jobs-core/src/main/scala/org/sunbird/job/BaseProcessFunction.scala b/jobs-core/src/main/scala/org/sunbird/job/BaseProcessFunction.scala index 3e5beed00..c175fe4ed 100644 --- a/jobs-core/src/main/scala/org/sunbird/job/BaseProcessFunction.scala +++ b/jobs-core/src/main/scala/org/sunbird/job/BaseProcessFunction.scala @@ -1,7 +1,5 @@ package org.sunbird.job -import java.util.concurrent.ConcurrentHashMap -import java.util.concurrent.atomic.AtomicLong import org.apache.flink.api.scala.metrics.ScalaGauge import org.apache.flink.configuration.Configuration import org.apache.flink.streaming.api.functions.{KeyedProcessFunction, ProcessFunction} @@ -9,6 +7,9 @@ import org.apache.flink.streaming.api.scala.function.ProcessWindowFunction import org.apache.flink.streaming.api.windowing.windows.{GlobalWindow, TimeWindow} import org.apache.flink.util.Collector +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.atomic.AtomicLong + case class Metrics(metrics: ConcurrentHashMap[String, AtomicLong]) { def incCounter(metric: String): Unit = { metrics.get(metric).getAndIncrement() @@ -101,12 +102,17 @@ abstract class BaseProcessKeyedFunction[K, T, R](config: BaseJobConfig) extends override def open(parameters: Configuration): Unit = { metricsList().map { metric => getRuntimeContext.getMetricGroup.addGroup(config.jobName) - .gauge[Long, ScalaGauge[Long]](metric, ScalaGauge[Long]( () => metrics.getAndReset(metric) )) + .gauge[Long, ScalaGauge[Long]](metric, ScalaGauge[Long](() => metrics.getAndReset(metric))) } + open(parameters, metrics) } + def open(parameters: Configuration, metrics: Metrics): Unit = {} + def processElement(event: T, context: KeyedProcessFunction[K, T, R]#Context, metrics: Metrics): Unit + def onTimer(timestamp: Long, ctx: KeyedProcessFunction[K, T, R]#OnTimerContext, metrics: Metrics): Unit = {} + def metricsList(): List[String] override def processElement(event: T, context: KeyedProcessFunction[K, T, R]#Context, out: Collector[R]): Unit = { diff --git a/jobs-core/src/main/scala/org/sunbird/job/util/CSPMetaUtil.scala b/jobs-core/src/main/scala/org/sunbird/job/util/CSPMetaUtil.scala new file mode 100644 index 000000000..a34653c32 --- /dev/null +++ b/jobs-core/src/main/scala/org/sunbird/job/util/CSPMetaUtil.scala @@ -0,0 +1,131 @@ +package org.sunbird.job.util + +import java.util + +import org.apache.commons.collections.MapUtils +import org.apache.commons.lang3.StringUtils +import org.slf4j.LoggerFactory +import org.sunbird.job.BaseJobConfig + +import scala.collection.JavaConverters._ + +object CSPMetaUtil { + + private[this] val logger = LoggerFactory.getLogger(classOf[CSPMetaUtil]) + + def updateAbsolutePath(data: util.Map[String, AnyRef])(implicit config: BaseJobConfig): util.Map[String, AnyRef] = { + logger.info("CSPMetaUtil ::: updateAbsolutePath ::: data before url replace :: " + data) + val relativePathPrefix: String = config.getString("cloudstorage.relative_path_prefix", "") + val cspMeta: util.List[String] = config.config.getStringList("cloudstorage.metadata.list") + val absolutePath = config.getString("cloudstorage.read_base_path", "") + java.io.File.separator + config.getString("cloud_storage_container", "") + val result = if (MapUtils.isNotEmpty(data)) { + val updatedMeta: util.Map[String, AnyRef] = data.asScala.map(x => if (cspMeta.contains(x._1)) (x._1, x._2.asInstanceOf[String].replace(relativePathPrefix, absolutePath)) else (x._1, x._2)).toMap.asJava + updatedMeta + } else data + logger.info("CSPMetaUtil ::: updateAbsolutePath ::: data after url replace :: " + result) + result + } + + def updateAbsolutePath(data: util.List[util.Map[String, AnyRef]])(implicit config: BaseJobConfig): util.List[util.Map[String, AnyRef]] = { + logger.info("CSPMetaUtil ::: updateAbsolutePath util.List[util.Map[String, AnyRef]] ::: data before url replace :: " + data) + val relativePathPrefix: String = config.getString("cloudstorage.relative_path_prefix", "") + val cspMeta: util.List[String] = config.config.getStringList("cloudstorage.metadata.list") + val absolutePath: String = config.getString("cloudstorage.read_base_path", "") + java.io.File.separator + config.getString("cloud_storage_container", "") + val result = data.asScala.toList.map(meta => { + if (MapUtils.isNotEmpty(meta)) { + val updatedMeta: util.Map[String, AnyRef] = meta.asScala.map(x => if (cspMeta.contains(x._1)) (x._1, getBasePath(x._1, x._2, Array(relativePathPrefix), Array(absolutePath))) else (x._1, x._2)).toMap.asJava + updatedMeta + } else meta + }).asJava + logger.info("CSPMetaUtil ::: updateAbsolutePath util.List[util.Map[String, AnyRef]] ::: data after url replace :: " + result) + result + } + + def updateAbsolutePath(data: String)(implicit config: BaseJobConfig): String = { + logger.info("CSPMetaUtil ::: updateAbsolutePath String ::: data before url replace :: " + data) + val relativePathPrefix: String = config.getString("cloudstorage.relative_path_prefix", "") + val absolutePath = config.getString("cloudstorage.read_base_path", "") + java.io.File.separator + config.getString("cloud_storage_container", "") + val result = if (StringUtils.isNotEmpty(data)) { + val updatedData: String = data.replaceAll(relativePathPrefix, absolutePath) + updatedData + } else data + logger.info("CSPMetaUtil ::: updateAbsolutePath String ::: data after url replace :: " + result) + result + } + + def updateRelativePath(data: util.Map[String, AnyRef])(implicit config: BaseJobConfig): util.Map[String, AnyRef] = { + logger.info("CSPMetaUtil ::: updateRelativePath util.Map[String, AnyRef] ::: data before url replace :: " + data) + val relativePathPrefix: String = config.getString("cloudstorage.relative_path_prefix", "") + val cspMeta: util.List[String] = config.config.getStringList("cloudstorage.metadata.list") + val validCSPSource: List[String] = config.config.getStringList("cloudstorage.write_base_path").asScala.toList + val basePaths: Array[String] = validCSPSource.map(source => source + java.io.File.separator + config.getString("cloud_storage_container", "")).toArray + val repArray = getReplacementData(basePaths, relativePathPrefix) + val result = if (MapUtils.isNotEmpty(data)) { + val updatedMeta: util.Map[String, AnyRef] = data.asScala.map(x => if (cspMeta.contains(x._1)) (x._1, getBasePath(x._1, x._2, basePaths, repArray)) else (x._1, x._2)).toMap.asJava + updatedMeta + } else data + logger.info("CSPMetaUtil ::: updateRelativePath util.Map[String, AnyRef] ::: data after url replace :: " + result) + result + } + + def updateRelativePath(query: String)(implicit config: BaseJobConfig): String = { + logger.info("CSPMetaUtil ::: updateRelativePath ::: query before url replace :: " + query) + val relativePathPrefix: String = config.getString("cloudstorage.relative_path_prefix", "") + val validCSPSource: List[String] = config.config.getStringList("cloudstorage.write_base_path").asScala.toList + val paths: Array[String] = validCSPSource.map(s => s + java.io.File.separator + config.getString("cloud_storage_container", "")).toArray + val repArray = getReplacementData(paths, relativePathPrefix) + val result = StringUtils.replaceEach(query, paths, repArray) + logger.info("CSPMetaUtil ::: updateRelativePath ::: query after url replace :: " + result) + result + } + + def updateCloudPath(objList: List[Map[String, AnyRef]])(implicit config: BaseJobConfig): List[Map[String, AnyRef]] = { + logger.info("CSPMetaUtil ::: updateCloudPath List[Map[String, AnyRef]] ::: data before url replace :: " + objList) + val cspMeta: util.List[String] = config.config.getStringList("cloudstorage.metadata.list") + val validCSPSource: List[String] = config.config.getStringList("cloudstorage.write_base_path").asScala.toList + val paths: Array[String] = validCSPSource.map(s => s + java.io.File.separator + config.getString("cloud_storage_container", "")).toArray + val newCloudPath: String = config.getString("cloudstorage.read_base_path", "") + java.io.File.separator + config.getString("cloud_storage_container", "") + val repArray = getReplacementData(paths, newCloudPath) + val result = objList.map(data => { + if (null != data && data.nonEmpty) { + data.map(x => if (cspMeta.contains(x._1)) (x._1, getBasePath(x._1, x._2, paths, repArray)) else (x._1, x._2)).toMap + } else data + }) + logger.info("CSPMetaUtil ::: updateCloudPath List[Map[String, AnyRef]] ::: data after url replace :: " + result) + result + } + + private def getBasePath(key: String, value: AnyRef, oldPath: Array[String], newPath: Array[String])(implicit config: BaseJobConfig): AnyRef = { + logger.info(s"CSPMetaUtil ::: getBasePath ::: Updating Path for Key : ${key} & Value : ${value}") + val res = if (null != value) { + value match { + case x: String => if (StringUtils.isNotBlank(x)) StringUtils.replaceEach(x, oldPath, newPath) else x + case y: Map[String, AnyRef] => { + val dStr = ScalaJsonUtil.serialize(y) + val result = StringUtils.replaceEach(dStr, oldPath, newPath) + val output: Map[String, AnyRef] = ScalaJsonUtil.deserialize[Map[String, AnyRef]](result) + output + } + case z: util.Map[String, AnyRef] => { + val dStr = ScalaJsonUtil.serialize(z) + val result = StringUtils.replaceEach(dStr, oldPath, newPath) + val output: util.Map[String, AnyRef] = ScalaJsonUtil.deserialize[util.Map[String, AnyRef]](result) + output + } + } + } else value + logger.info(s"CSPMetaUtil ::: getBasePath ::: Updated Path for Key : ${key} & Updated Value is : ${res}") + res + } + + def getReplacementData(oldPath: Array[String], repStr: String): Array[String] = { + val repArray = new Array[String](oldPath.length) + for (i <- oldPath.indices) { + repArray(i) = repStr + } + repArray + } + +} + +class CSPMetaUtil {} \ No newline at end of file diff --git a/jobs-core/src/main/scala/org/sunbird/job/util/CassandraUtil.scala b/jobs-core/src/main/scala/org/sunbird/job/util/CassandraUtil.scala index d0ff7651f..1ddadcb80 100644 --- a/jobs-core/src/main/scala/org/sunbird/job/util/CassandraUtil.scala +++ b/jobs-core/src/main/scala/org/sunbird/job/util/CassandraUtil.scala @@ -3,13 +3,16 @@ package org.sunbird.job.util import com.datastax.driver.core._ import com.datastax.driver.core.exceptions.DriverException import org.slf4j.LoggerFactory +import org.sunbird.job.BaseJobConfig import java.util -class CassandraUtil(host: String, port: Int) { +class CassandraUtil(host: String, port: Int, config: BaseJobConfig) extends Serializable { private[this] val logger = LoggerFactory.getLogger("CassandraUtil") + val isrRelativePathEnabled = config.getBoolean("cloudstorage.metadata.replace_absolute_path", false) + val cluster = { Cluster.builder() .addContactPoints(host) @@ -22,7 +25,8 @@ class CassandraUtil(host: String, port: Int) { def findOne(query: String): Row = { try { val rs: ResultSet = session.execute(query) - rs.one + val result = rs.one + result } catch { case ex: DriverException => logger.error(s"findOne - Error while executing query $query :: ", ex) @@ -43,7 +47,10 @@ class CassandraUtil(host: String, port: Int) { } def upsert(query: String): Boolean = { - val rs: ResultSet = session.execute(query) + logger.info("cassandra util ::: query:: " + query) + val updatedQuery = if (isrRelativePathEnabled) CSPMetaUtil.updateRelativePath(query)(config) else query + logger.info("updated query ::: " + updatedQuery) + val rs: ResultSet = session.execute(updatedQuery) rs.wasApplied } diff --git a/jobs-core/src/main/scala/org/sunbird/job/util/CloudStorageUtil.scala b/jobs-core/src/main/scala/org/sunbird/job/util/CloudStorageUtil.scala index 5397eddcf..83d9fca2f 100644 --- a/jobs-core/src/main/scala/org/sunbird/job/util/CloudStorageUtil.scala +++ b/jobs-core/src/main/scala/org/sunbird/job/util/CloudStorageUtil.scala @@ -16,30 +16,21 @@ class CloudStorageUtil(config: BaseJobConfig) extends Serializable { @throws[Exception] def getService: BaseStorageService = { if (null == storageService) { - if (StringUtils.equalsIgnoreCase(cloudStorageType, "azure")) { - val azureStorageKey = config.getString("azure_storage_key", "") - val azureStorageSecret = config.getString("azure_storage_secret", "") - storageService = StorageServiceFactory.getStorageService(StorageConfig(cloudStorageType, azureStorageKey, azureStorageSecret)) - } else if (StringUtils.equalsIgnoreCase(cloudStorageType, "aws")) { - val awsStorageKey = config.getString("aws_storage_key", "") - val awsStorageSecret = config.getString("aws_storage_secret", "") - storageService = StorageServiceFactory.getStorageService(StorageConfig(cloudStorageType, awsStorageKey, awsStorageSecret)) - } else if (StringUtils.equalsIgnoreCase(cloudStorageType, "gcloud")) { - val storageKey = config.getString("gcloud_client_key", "") - val storageSecret = config.getString("gcloud_private_secret", "") - storageService = StorageServiceFactory.getStorageService(StorageConfig(cloudStorageType, storageKey, storageSecret)) - } else throw new Exception("Error while initialising cloud storage: " + cloudStorageType) + val storageKey = config.getString("cloud_storage_key", "") + val storageSecret = config.getString("cloud_storage_secret", "") + val endPoint = config.getString("cloud_storage_endpoint", "") + // TODO: endPoint defined to support "cephs3". Make code changes after cloud-store-sdk 2.11 support it. + val storageEndPoint = if (StringUtils.isNotBlank(endPoint)) Option(endPoint) else None + storageService = StorageServiceFactory.getStorageService(StorageConfig(cloudStorageType, storageKey, storageSecret,storageEndPoint)) } storageService } def getContainerName: String = { - cloudStorageType match { - case "azure" => config.getString("azure_storage_container", "") - case "aws" => config.getString("aws_storage_container", "") - case "gcloud" => config.getString("gcloud_storage_bucket", "") - case _ => throw new Exception("Container name not configured.") - } + if (StringUtils.isBlank(config.getString("cloud_storage_container", ""))) + throw new Exception("Container name not configured.") + else + config.getString("cloud_storage_container", "") } def uploadFile(folderName: String, file: File, slug: Option[Boolean] = Option(true), container: String = container): Array[String] = { diff --git a/jobs-core/src/main/scala/org/sunbird/job/util/Neo4JUtil.scala b/jobs-core/src/main/scala/org/sunbird/job/util/Neo4JUtil.scala index eefcc01ea..43a1d0c1c 100644 --- a/jobs-core/src/main/scala/org/sunbird/job/util/Neo4JUtil.scala +++ b/jobs-core/src/main/scala/org/sunbird/job/util/Neo4JUtil.scala @@ -2,16 +2,20 @@ package org.sunbird.job.util import java.util +import org.apache.commons.lang3.StringUtils import org.neo4j.driver.v1.{Config, GraphDatabase} import org.slf4j.LoggerFactory +import org.sunbird.job.BaseJobConfig + import scala.collection.JavaConverters._ -class Neo4JUtil(routePath: String, graphId: String) { +class Neo4JUtil(routePath: String, graphId: String, config: BaseJobConfig) extends Serializable { private[this] val logger = LoggerFactory.getLogger(classOf[Neo4JUtil]) val maxIdleSession = 20 val driver = GraphDatabase.driver(routePath, getConfig) + val isrRelativePathEnabled = config.getBoolean("cloudstorage.metadata.replace_absolute_path", false) def getConfig: Config = { val config = Config.build @@ -35,18 +39,20 @@ class Neo4JUtil(routePath: String, graphId: String) { val session = driver.session() val query = s"""MATCH (n:${graphId}{IL_UNIQUE_ID:"${identifier}"}) return n;""" val statementResult = session.run(query) - if (statementResult.hasNext) - statementResult.single().get("n").asMap() - else null + if (statementResult.hasNext) { + val data = statementResult.single().get("n").asMap() + if(isrRelativePathEnabled) CSPMetaUtil.updateAbsolutePath(data)(config) else data + } else null } def getNodePropertiesWithObjectType(objectType: String): util.List[util.Map[String, AnyRef]] = { val session = driver.session() val query = s"""MATCH (n:${graphId}) where n.IL_FUNC_OBJECT_TYPE = "${objectType}" AND n.IL_SYS_NODE_TYPE="DATA_NODE" return n;""" val statementResult = session.run(query) - if (statementResult.hasNext) - statementResult.list().asScala.toList.map(record => record.get("n").asMap()).asJava - else null + if (statementResult.hasNext){ + val data = statementResult.list().asScala.toList.map(record => record.get("n").asMap()).asJava + if(isrRelativePathEnabled) CSPMetaUtil.updateAbsolutePath(data)(config) else data + } else null } def getNodesName(identifiers: List[String]): Map[String, String] = { @@ -63,17 +69,19 @@ class Neo4JUtil(routePath: String, graphId: String) { def updateNodeProperty(identifier: String, key: String, value: String): Unit = { val query = s"""MATCH (n:$graphId {IL_UNIQUE_ID:"$identifier"}) SET n.$key=$value return n;""" - logger.info("Query: " + query) + val updatedQuery = if(isrRelativePathEnabled) CSPMetaUtil.updateRelativePath(query)(config) else query + logger.info("Query: " + updatedQuery) val session = driver.session() - val result = session.run(query) + val result = session.run(updatedQuery) if (result.hasNext) logger.info("Successfully Updated node with identifier: $identifier") else throw new Exception(s"Unable to update the node with identifier: $identifier") } def executeQuery(query: String) = { + val updatedQuery = if(isrRelativePathEnabled) CSPMetaUtil.updateRelativePath(query)(config) else query val session = driver.session() - session.run(query) + session.run(updatedQuery) } //Return a map of id and node @@ -82,10 +90,11 @@ class Neo4JUtil(routePath: String, graphId: String) { } def updateNode(identifier: String, metadata: Map[String, AnyRef]): Unit = { + val updatedMetadata = if(isrRelativePathEnabled) CSPMetaUtil.updateRelativePath(metadata.asJava)(config) else metadata.asJava val query = s"""MATCH (n:$graphId {IL_UNIQUE_ID:"$identifier"}) SET n = """ + "$properties return n;" logger.info(s"Query for updating metadata for identifier : ${identifier} is : ${query}") val session = driver.session() - val properties: java.util.Map[String, AnyRef] = Map[String, AnyRef]("properties" -> metadata.asJava).asJava + val properties: java.util.Map[String, AnyRef] = Map[String, AnyRef]("properties" -> updatedMetadata).asJava val result = session.run(query, properties) if (result.hasNext) logger.info(s"Successfully Updated node with identifier: $identifier") diff --git a/jobs-core/src/test/scala/org/sunbird/spec/BaseProcessFunctionTestSpec.scala b/jobs-core/src/test/scala/org/sunbird/spec/BaseProcessFunctionTestSpec.scala index 849274b85..b6a559b69 100644 --- a/jobs-core/src/test/scala/org/sunbird/spec/BaseProcessFunctionTestSpec.scala +++ b/jobs-core/src/test/scala/org/sunbird/spec/BaseProcessFunctionTestSpec.scala @@ -109,7 +109,7 @@ class BaseProcessFunctionTestSpec extends BaseSpec with Matchers { topics.foreach(createCustomTopic(_)) } - "Validation of SerDe" should "validate serialization and deserialization of Map, String and Event schema" in { + ignore should "validate serialization and deserialization of Map, String and Event schema" in { implicit val env: StreamExecutionEnvironment = FlinkUtil.getExecutionContext(bsConfig) diff --git a/jobs-core/src/test/scala/org/sunbird/spec/BaseSpec.scala b/jobs-core/src/test/scala/org/sunbird/spec/BaseSpec.scala index 65c056063..640813f36 100644 --- a/jobs-core/src/test/scala/org/sunbird/spec/BaseSpec.scala +++ b/jobs-core/src/test/scala/org/sunbird/spec/BaseSpec.scala @@ -1,6 +1,6 @@ package org.sunbird.spec -import com.opentable.db.postgres.embedded.EmbeddedPostgres +// import com.opentable.db.postgres.embedded.EmbeddedPostgres import org.scalatest.{BeforeAndAfterAll, FlatSpec} import redis.embedded.RedisServer @@ -11,7 +11,7 @@ class BaseSpec extends FlatSpec with BeforeAndAfterAll { super.beforeAll() redisServer = new RedisServer(6340) redisServer.start() - EmbeddedPostgres.builder.setPort(5430).start() // Use the same port 5430 which is defined in the base-test.conf + // EmbeddedPostgres.builder.setPort(5430).start() // Use the same port 5430 which is defined in the base-test.conf } override protected def afterAll(): Unit = { diff --git a/jobs-core/src/test/scala/org/sunbird/spec/FileUtilsSpec.scala b/jobs-core/src/test/scala/org/sunbird/spec/FileUtilsSpec.scala index de4a7682d..bdddcbe37 100644 --- a/jobs-core/src/test/scala/org/sunbird/spec/FileUtilsSpec.scala +++ b/jobs-core/src/test/scala/org/sunbird/spec/FileUtilsSpec.scala @@ -12,7 +12,7 @@ class FileUtilsSpec extends FlatSpec with Matchers { result.nonEmpty shouldBe (true) } - "downloadFile " should " download the media source file starting with http or https " in { + ignore should " download the media source file starting with http or https " in { val fileUrl: String = "https://preprodall.blob.core.windows.net/ntp-content-preprod/content/do_21273718766395392014320/artifact/book-image_1554832478631.jpg" val downloadedFile: File = FileUtils.downloadFile(fileUrl, "/tmp/contentBundle") assert(downloadedFile.exists()) diff --git a/jobs-core/src/test/scala/org/sunbird/spec/HTTPUtilSpec.scala b/jobs-core/src/test/scala/org/sunbird/spec/HTTPUtilSpec.scala index 861f7d663..e9bd6e2b6 100644 --- a/jobs-core/src/test/scala/org/sunbird/spec/HTTPUtilSpec.scala +++ b/jobs-core/src/test/scala/org/sunbird/spec/HTTPUtilSpec.scala @@ -55,8 +55,8 @@ class HTTPUtilSpec extends FlatSpec with Matchers { } } - "downloadFile" should "download file with lower case name" in { - val fileUrl = "https://file-examples.com/wp-content/uploads/2017/04/file_example_MP4_480_1_5MG.mp4" + ignore should "download file with lower case name" in { + val fileUrl = "https://sunbirddevbbpublic.blob.core.windows.net//content/assets/do_21369942869119795219/kors-smaapn.mp4" val httpUtil = new HttpUtil val downloadPath = "/tmp/content" + File.separator + "_temp_" + System.currentTimeMillis val downloadedFile = httpUtil.downloadFile(fileUrl, downloadPath) diff --git a/jobs-distribution/Dockerfile b/jobs-distribution/Dockerfile index 4fbb6b576..7eec73ac4 100644 --- a/jobs-distribution/Dockerfile +++ b/jobs-distribution/Dockerfile @@ -7,6 +7,9 @@ RUN apt-get install -y imagemagick COPY target/jobs-distribution-1.0.tar.gz /tmp USER flink RUN tar -xvf /tmp/jobs-distribution-1.0.tar.gz -C $FLINK_HOME/lib/ +RUN mkdir $FLINK_HOME/plugins/s3-fs-presto +RUN cp $FLINK_HOME/opt/flink-s3-fs-presto-1.13.5.jar $FLINK_HOME/plugins/s3-fs-presto/ +RUN cp $FLINK_HOME/opt/flink-s3-fs-presto-1.13.5.jar $FLINK_HOME/lib/flink-aaa-s3-fs-presto-1.13.5.jar USER root RUN rm -f /tmp/jobs-distribution-1.0.tar.gz USER flink diff --git a/jobs-distribution/pom.xml b/jobs-distribution/pom.xml index 9a5cc2171..865c8041a 100644 --- a/jobs-distribution/pom.xml +++ b/jobs-distribution/pom.xml @@ -20,18 +20,18 @@ ${flink.version} jar - - org.sunbird - activity-aggregate-updater - 1.0.0 - jar - - - org.sunbird - relation-cache-updater - 1.0.0 - jar - + + + + + + + + + + + + org.sunbird post-publish-processor @@ -44,12 +44,12 @@ 1.0.0 jar - - org.sunbird - questionset-publish - 1.0.0 - jar - + + + + + + org.sunbird content-publish @@ -62,69 +62,93 @@ 1.0.0 jar + + + + + + + + + + + + + + + + + + org.sunbird - enrolment-reconciliation + asset-enrichment 1.0.0 jar org.sunbird - collection-cert-pre-processor + audit-history-indexer 1.0.0 jar org.sunbird - collection-certificate-generator + auto-creator-v2 1.0.0 jar org.sunbird - asset-enrichment + content-auto-creator 1.0.0 jar + + + + + + org.sunbird - audit-history-indexer + qrcode-image-generator 1.0.0 jar org.sunbird - auto-creator-v2 + audit-event-generator 1.0.0 jar org.sunbird - content-auto-creator + dialcode-context-updater 1.0.0 jar org.sunbird - metrics-data-transformer + csp-migrator 1.0.0 jar org.sunbird - qrcode-image-generator + live-node-publisher 1.0.0 jar org.sunbird - audit-event-generator + live-video-stream-generator 1.0.0 jar org.sunbird - dialcode-context-updater + cassandra-data-migration 1.0.0 jar diff --git a/kubernets/pipelines/build/Jenkinsfile b/kubernets/pipelines/build/Jenkinsfile index 106194310..388228182 100644 --- a/kubernets/pipelines/build/Jenkinsfile +++ b/kubernets/pipelines/build/Jenkinsfile @@ -20,12 +20,15 @@ node('build-slave') { commit_hash = sh(script: 'git rev-parse --short HEAD', returnStdout: true).trim() build_tag = sh(script: "echo " + params.github_release_tag.split('/')[-1] + "_" + commit_hash + "_" + env.BUILD_NUMBER, returnStdout: true).trim() echo "build_tag: " + build_tag + cloud_store_group_id = params.CLOUD_STORE_GROUP_ID + cloud_store_artifact_id = params.CLOUD_STORE_ARTIFACT_ID + cloud_store_version = params.CLOUD_STORE_VERSION stage('Build') { env.NODE_ENV = "build" print "Environment will be : ${env.NODE_ENV}" sh '/opt/apache-maven-3.6.3/bin/mvn3.6 -v' - sh '/opt/apache-maven-3.6.3/bin/mvn3.6 clean install -DskipTests' + sh '/opt/apache-maven-3.6.3/bin/mvn3.6 clean install -DskipTests -DCLOUD_STORE_GROUP_ID=' + cloud_store_group_id + ' -DCLOUD_STORE_ARTIFACT_ID=' + cloud_store_artifact_id + ' -DCLOUD_STORE_VERSION=' + cloud_store_version } diff --git a/relation-cache-updater/README.md b/live-video-stream-generator/README.md similarity index 84% rename from relation-cache-updater/README.md rename to live-video-stream-generator/README.md index 570ba9024..eb8941ff7 100644 --- a/relation-cache-updater/README.md +++ b/live-video-stream-generator/README.md @@ -1,11 +1,10 @@ -# Relation Cache Updater +# Video Stream Generator -Relation Cache updater job is write the content leafnodes and ancestors nodes +Live Video Stream Generator job is to generate the stream url for uploaded video (mp4/webm) files - this is a duplicate job of Video Stream Generator with difference in Input topic. Job was created to support CSP migration activity, ## Getting Started These instructions will get you a copy of the project up and running on your local machine for development and testing purposes. See deployment for notes on how to deploy the project on a yarn or kubernetes. -Design wiki link: https://project-sunbird.atlassian.net/wiki/spaces/SBDES/pages/1493041222/Courses+Infra+-+Design ### Prerequisites 1. Download flink-1.13.6-scala_2.12 from [apache-flink-downloads](https://www.apache.org/dyn/closer.lua/flink/flink-1.13.6/flink-1.13.6-bin-scala_2.12.tgz). @@ -25,7 +24,7 @@ mvn clean install Flink requires memory to be allocated for both job-manager and task manager. -yjm parameter assigns job-manager memory and -ytm assigns task-manager memory. ``` -./bin/flink run -m yarn-cluster -p 2 -yjm 1024m -ytm 1024m /relation-cache-updater/target/relation-cache-updater-0.0.1.jar +./bin/flink run -m yarn-cluster -p 2 -yjm 1024m -ytm 1024m /video-stream-generator/target/video-stream-generator-0.0.1.jar ``` ### Kubernetes @@ -53,7 +52,7 @@ kubectl create -f knowledge-platform-job/kubernetes/taskmanager-deployment.yaml kubectl port-forward deployment/flink-jobmanager 8081:8081 # Submit the job to the Kubernetes single node cluster flink-cluster -./bin/flink run -m localhost:8081 /relation-cache-updater/target/relation-cache-updater-0.0.1.jar +./bin/flink run -m localhost:8081 /video-stream-generator/target/video-stream-generator-0.0.1.jar # Commands to delete the pods created in the cluster kubectl delete deployment/flink-jobmanager diff --git a/enrolment-reconciliation/pom.xml b/live-video-stream-generator/pom.xml similarity index 90% rename from enrolment-reconciliation/pom.xml rename to live-video-stream-generator/pom.xml index 4e3713e1d..a79b8883d 100644 --- a/enrolment-reconciliation/pom.xml +++ b/live-video-stream-generator/pom.xml @@ -9,12 +9,12 @@ knowledge-platform-jobs 1.0 - enrolment-reconciliation + live-video-stream-generator 1.0.0 jar - enrolment-reconciliation + live-video-stream-generator - Enrolment Reconciliation + Video stream URL generator Flink Job @@ -29,11 +29,6 @@ ${flink.version} provided - - com.twitter - storehaus-cache_${scala.version} - 0.15.0 - org.sunbird jobs-core @@ -77,12 +72,6 @@ 3.11.2.0 test - - it.ozimov - embedded-redis - 0.7.1 - test - org.scalatest scalatest_${scala.version} @@ -98,8 +87,8 @@ - src/main/scala - src/test/scala + src/main/scala + src/test/scala org.apache.maven.plugins @@ -109,7 +98,6 @@ 11 - org.apache.maven.plugins maven-shade-plugin @@ -142,7 +130,7 @@ - org.sunbird.job.recounciliation.task.EnrolmentReconciliationStreamTask + org.sunbird.job.livevideostream.task.LiveVideoStreamGeneratorStreamTask ${project.build.directory}/surefire-reports . - relation-cache-updater-testsuite.txt + live-video-stream-generator-testsuite.txt diff --git a/live-video-stream-generator/src/main/resources/live-video-stream-generator.conf b/live-video-stream-generator/src/main/resources/live-video-stream-generator.conf new file mode 100644 index 000000000..5346dcaf9 --- /dev/null +++ b/live-video-stream-generator/src/main/resources/live-video-stream-generator.conf @@ -0,0 +1,94 @@ +include "base-config.conf" + +kafka { + input.topic = "sunbirddev.live.video.stream.request" + groupId = "sunbirddev-live-video-stream-generator-group" +} + +task { + consumer.parallelism = 1 + parallelism = 1 + timer.duration = 60 + max.retries = 10 +} + +lms-cassandra { + keyspace = "sunbirddev_platform_db" + table = "job_request" +} + +service { + content { + basePath = "http://11.2.6.6/content" + } +} + +# Azure Media Service Config +azure { + location = "centralindia" + tenant = "tenant" + subscription_id = "subscription id " + + login { + endpoint="https://login.microsoftonline.com" + } + + api { + endpoint="https://management.azure.com" + version = "2018-07-01" + } + + account_name = "account name" + resource_group_name = "group name" + + transform { + default = "media_transform_default" + hls = "media_transform_hls" + } + + stream { + base_url = "https://sunbirdspikemedia-inct.streaming.media.azure.net" + endpoint_name = "default" + protocol = "Hls" + policy_name = "Predefined_ClearStreamingOnly" + } + + token { + client_key = "client key" + client_secret = "client secret" + } +} + +azure_tenant="tenant" +azure_subscription_id="subscription id" +azure_account_name="account name" +azure_resource_group_name="group name" +azure_token_client_key="client key" +azure_token_client_secret="client secret" + +# CSP Name. e.g: aws or azure +media_service_type="aws" + +#AWS Elemental Media Convert Config +aws { + region="ap-south-1" + content_bucket_name="awsmedia-spike" + token { + access_key="access key" + access_secret="access secret" + } + api { + endpoint="API Endpoint for media convert" + version="2017-08-29" + } + service { + name="mediaconvert" + queue="Media Convert Queue Id" + role="Media Convert Role Name" + } + stream { + protocol="Hls" + } +} + +media_service_job_success_status=["FINISHED", "COMPLETE"] diff --git a/activity-aggregate-updater/src/main/resources/log4j.properties b/live-video-stream-generator/src/main/resources/log4j.properties similarity index 90% rename from activity-aggregate-updater/src/main/resources/log4j.properties rename to live-video-stream-generator/src/main/resources/log4j.properties index cacd49c79..04cc82649 100644 --- a/activity-aggregate-updater/src/main/resources/log4j.properties +++ b/live-video-stream-generator/src/main/resources/log4j.properties @@ -1,6 +1,6 @@ # log4j.appender.file=org.apache.log4j.FileAppender log4j.appender.file=org.apache.log4j.RollingFileAppender -log4j.appender.file.file=course-metrics-updater.log +log4j.appender.file.file=live-video-stream-generator.log log4j.appender.file.append=true log4j.appender.file.layout=org.apache.log4j.PatternLayout log4j.appender.file.MaxFileSize=256KB diff --git a/live-video-stream-generator/src/main/scala/org/sunbird/job/livevideostream/domain/Event.scala b/live-video-stream-generator/src/main/scala/org/sunbird/job/livevideostream/domain/Event.scala new file mode 100644 index 000000000..f0d8ede3e --- /dev/null +++ b/live-video-stream-generator/src/main/scala/org/sunbird/job/livevideostream/domain/Event.scala @@ -0,0 +1,37 @@ +package org.sunbird.job.livevideostream.domain + +import org.apache.commons.lang3.StringUtils +import org.sunbird.job.domain.reader.JobRequest + +import java.util + +class Event(eventMap: java.util.Map[String, Any], partition: Int, offset: Long) extends JobRequest(eventMap, partition, offset) { + + private val jobName = "LiveVideoStreamGenerator" + + def action: String = readOrDefault[String]("edata.action", "") + + def mimeType: String = readOrDefault[String]("edata.mimeType", "") + + def channel: String = readOrDefault[String]("context.channel", "") + + def eid: String = readOrDefault[String]("eid", "") + + def artifactUrl: String = readOrDefault[String]("edata.artifactUrl", "") + + def identifier: String = readOrDefault[String]("edata.identifier", "") + + def eData: Map[String, AnyRef] = readOrDefault("edata", new util.HashMap[String, AnyRef]()).asInstanceOf[Map[String, AnyRef]] + + def isValid: Boolean = { + StringUtils.isNotBlank(artifactUrl) && + StringUtils.isNotBlank(mimeType) && + ( + StringUtils.equalsIgnoreCase(mimeType, "video/mp4") || + StringUtils.equalsIgnoreCase(mimeType, "video/webm") + ) && + StringUtils.isNotBlank(identifier) && + StringUtils.isNotBlank(channel) + } + +} diff --git a/live-video-stream-generator/src/main/scala/org/sunbird/job/livevideostream/exception/MediaServiceException.scala b/live-video-stream-generator/src/main/scala/org/sunbird/job/livevideostream/exception/MediaServiceException.scala new file mode 100644 index 000000000..906ec883b --- /dev/null +++ b/live-video-stream-generator/src/main/scala/org/sunbird/job/livevideostream/exception/MediaServiceException.scala @@ -0,0 +1,3 @@ +package org.sunbird.job.livevideostream.exception + +class MediaServiceException(var errorCode: String = null, msg: String, ex: Exception = null) extends Exception(msg, ex) diff --git a/live-video-stream-generator/src/main/scala/org/sunbird/job/livevideostream/functions/LiveVideoStreamGenerator.scala b/live-video-stream-generator/src/main/scala/org/sunbird/job/livevideostream/functions/LiveVideoStreamGenerator.scala new file mode 100644 index 000000000..247b9b267 --- /dev/null +++ b/live-video-stream-generator/src/main/scala/org/sunbird/job/livevideostream/functions/LiveVideoStreamGenerator.scala @@ -0,0 +1,91 @@ +package org.sunbird.job.livevideostream.functions + +import org.apache.flink.api.common.typeinfo.TypeInformation +import org.apache.flink.configuration.Configuration +import org.apache.flink.streaming.api.functions.KeyedProcessFunction +import org.slf4j.LoggerFactory +import org.sunbird.job.exception.InvalidEventException +import org.sunbird.job.livevideostream.domain.Event +import org.sunbird.job.livevideostream.service.LiveVideoStreamService +import org.sunbird.job.livevideostream.task.LiveVideoStreamGeneratorConfig +import org.sunbird.job.util.HttpUtil +import org.sunbird.job.{BaseProcessKeyedFunction, Metrics} + +import java.util + +class LiveVideoStreamGenerator(config: LiveVideoStreamGeneratorConfig, httpUtil: HttpUtil) + (implicit mapTypeInfo: TypeInformation[util.Map[String, AnyRef]], + stringTypeInfo: TypeInformation[String]) + extends BaseProcessKeyedFunction[String, Event, Event](config) { + + implicit lazy val videoStreamConfig: LiveVideoStreamGeneratorConfig = config + private[this] lazy val logger = LoggerFactory.getLogger(classOf[LiveVideoStreamGenerator]) + private var videoStreamService: LiveVideoStreamService = _ + private lazy val timerDurationInMS: Long = config.timerDuration * 1000 + private var nextTimerTimestamp = 0L + + override def metricsList(): List[String] = { + List(config.totalEventsCount, config.skippedEventCount, config.successEventCount, config.failedEventCount, config.retryEventCount) + } + + override def open(parameters: Configuration, metrics: Metrics): Unit = { + videoStreamService = new LiveVideoStreamService()(config, httpUtil) + val processing = videoStreamService.readFromDB(Map("status" -> "PROCESSING")) ++ videoStreamService.readFromDB(Map("status" -> "FAILED", "iteration" -> Map("type" -> "lte", "value" -> 10))) + if (processing.nonEmpty) { + logger.info("Requests in queue to validate update the status: " + processing.size) + videoStreamService.processJobRequest(metrics) + } else logger.info("open() ==> There are no video streaming requests in queue to validate update the status.") + } + + override def close(): Unit = { + videoStreamService.closeConnection() + super.close() + } + + @throws(classOf[InvalidEventException]) + override def processElement(event: Event, + context: KeyedProcessFunction[String, Event, Event]#Context, + metrics: Metrics): Unit = { + try { + metrics.incCounter(config.totalEventsCount) + if (event.isValid) { + val eData = event.eData ++ Map("channel" -> event.channel) + videoStreamService.submitJobRequest(eData) + logger.info("Streaming job submitted for " + event.identifier + " with url: " + event.artifactUrl) + registerTimer(context) + } else metrics.incCounter(config.skippedEventCount) + } catch { + case ex: Exception => + metrics.incCounter(config.failedEventCount) + throw new InvalidEventException(ex.getMessage, Map("partition" -> event.partition, "offset" -> event.offset), ex) + } + } + + override def onTimer(timestamp: Long, ctx: KeyedProcessFunction[String, Event, Event]#OnTimerContext, metrics: Metrics): Unit = { + unregisterTimer() + val processing = videoStreamService.readFromDB(Map("status" -> "PROCESSING")) ++ videoStreamService.readFromDB(Map("status" -> "FAILED", "iteration" -> Map("type" -> "lte", "value" -> 10))) + if (processing.nonEmpty) { + logger.info("Requests in queue to validate update the status: " + processing.size) + videoStreamService.processJobRequest(metrics) + registerTimer(ctx) + } else { + logger.info("There are no video streaming requests in queue to validate update the status.") + } + } + + private def registerTimer(context: KeyedProcessFunction[String, Event, Event]#Context): Unit = { + if (nextTimerTimestamp == 0L) { + val nextTrigger = context.timestamp() + timerDurationInMS + context.timerService().registerProcessingTimeTimer(nextTrigger) + nextTimerTimestamp = nextTrigger + logger.info("Timer registered to execute at " + nextTimerTimestamp) + } else { + logger.info("Timer already exists at: " + nextTimerTimestamp) + } + } + + private def unregisterTimer(): Unit = { + nextTimerTimestamp = 0L + } + +} diff --git a/live-video-stream-generator/src/main/scala/org/sunbird/job/livevideostream/helpers/AwsRequestBody.scala b/live-video-stream-generator/src/main/scala/org/sunbird/job/livevideostream/helpers/AwsRequestBody.scala new file mode 100644 index 000000000..c0f43059c --- /dev/null +++ b/live-video-stream-generator/src/main/scala/org/sunbird/job/livevideostream/helpers/AwsRequestBody.scala @@ -0,0 +1,6 @@ +package org.sunbird.job.livevideostream.helpers + +object AwsRequestBody { + + val submit_hls_job = "{\"Queue\": \"queueId\",\"role\":\"mediaRole\",\"Settings\": {\"OutputGroups\": [{\"CustomName\": \"hls\",\"Name\": \"Apple HLS\",\"Outputs\": [{\"ContainerSettings\": {\"Container\": \"M3U8\",\"M3u8Settings\": {\"AudioFramesPerPes\": 4,\"PcrControl\": \"PCR_EVERY_PES_PACKET\",\"PmtPid\": 480,\"PrivateMetadataPid\": 503,\"ProgramNumber\": 1,\"PatInterval\": 0,\"PmtInterval\": 0,\"Scte35Source\": \"NONE\",\"NielsenId3\": \"NONE\",\"TimedMetadata\": \"NONE\",\"VideoPid\": 481,\"AudioPids\": [482,483,484,485,486,487,488,489,490,491,492]}},\"VideoDescription\": {\"Width\": 256,\"ScalingBehavior\": \"DEFAULT\",\"Height\": 144,\"TimecodeInsertion\": \"DISABLED\",\"AntiAlias\": \"ENABLED\",\"Sharpness\": 50,\"CodecSettings\": {\"Codec\": \"H_264\",\"H264Settings\": {\"InterlaceMode\": \"PROGRESSIVE\",\"NumberReferenceFrames\": 3,\"Syntax\": \"DEFAULT\",\"Softness\": 0,\"GopClosedCadence\": 1,\"GopSize\": 90,\"Slices\": 1,\"GopBReference\": \"DISABLED\",\"SlowPal\": \"DISABLED\",\"SpatialAdaptiveQuantization\": \"ENABLED\",\"TemporalAdaptiveQuantization\": \"ENABLED\",\"FlickerAdaptiveQuantization\": \"DISABLED\",\"EntropyEncoding\": \"CABAC\",\"Bitrate\": 109000,\"FramerateControl\": \"INITIALIZE_FROM_SOURCE\",\"RateControlMode\": \"CBR\",\"CodecProfile\": \"MAIN\",\"Telecine\": \"NONE\",\"MinIInterval\": 0,\"AdaptiveQuantization\": \"HIGH\",\"CodecLevel\": \"AUTO\",\"FieldEncoding\": \"PAFF\",\"SceneChangeDetect\": \"ENABLED\",\"QualityTuningLevel\": \"SINGLE_PASS\",\"FramerateConversionAlgorithm\": \"DUPLICATE_DROP\",\"UnregisteredSeiTimecode\": \"DISABLED\",\"GopSizeUnits\": \"FRAMES\",\"ParControl\": \"INITIALIZE_FROM_SOURCE\",\"NumberBFramesBetweenReferenceFrames\": 2,\"RepeatPps\": \"DISABLED\",\"DynamicSubGop\": \"STATIC\"}},\"AfdSignaling\": \"NONE\",\"DropFrameTimecode\": \"ENABLED\",\"RespondToAfd\": \"NONE\",\"ColorMetadata\": \"INSERT\"},\"AudioDescriptions\": [{\"AudioTypeControl\": \"FOLLOW_INPUT\",\"CodecSettings\": {\"Codec\": \"AAC\",\"AacSettings\": {\"AudioDescriptionBroadcasterMix\": \"NORMAL\",\"Bitrate\": 96000,\"RateControlMode\": \"CBR\",\"CodecProfile\": \"LC\",\"CodingMode\": \"CODING_MODE_2_0\",\"RawFormat\": \"NONE\",\"SampleRate\": 48000,\"Specification\": \"MPEG4\"}},\"LanguageCodeControl\": \"FOLLOW_INPUT\"}],\"OutputSettings\": {\"HlsSettings\": {\"AudioGroupId\": \"program_audio\",\"SegmentModifier\": \"$dt$\",\"IFrameOnlyManifest\": \"EXCLUDE\"}},\"NameModifier\": \"_144\"},{\"ContainerSettings\": {\"Container\": \"M3U8\",\"M3u8Settings\": {\"AudioFramesPerPes\": 4,\"PcrControl\": \"PCR_EVERY_PES_PACKET\",\"PmtPid\": 480,\"PrivateMetadataPid\": 503,\"ProgramNumber\": 1,\"PatInterval\": 0,\"PmtInterval\": 0,\"Scte35Source\": \"NONE\",\"Scte35Pid\": 500,\"NielsenId3\": \"NONE\",\"TimedMetadata\": \"NONE\",\"TimedMetadataPid\": 502,\"VideoPid\": 481,\"AudioPids\": [482,483,484,485,486,487,488,489,490,491,492]}},\"VideoDescription\": {\"Width\": 640,\"ScalingBehavior\": \"DEFAULT\",\"Height\": 360,\"TimecodeInsertion\": \"DISABLED\",\"AntiAlias\": \"ENABLED\",\"Sharpness\": 50,\"CodecSettings\": {\"Codec\": \"H_264\",\"H264Settings\": {\"InterlaceMode\": \"PROGRESSIVE\",\"NumberReferenceFrames\": 3,\"Syntax\": \"DEFAULT\",\"Softness\": 0,\"GopClosedCadence\": 1,\"GopSize\": 90,\"Slices\": 1,\"GopBReference\": \"DISABLED\",\"SlowPal\": \"DISABLED\",\"SpatialAdaptiveQuantization\": \"ENABLED\",\"TemporalAdaptiveQuantization\": \"ENABLED\",\"FlickerAdaptiveQuantization\": \"DISABLED\",\"EntropyEncoding\": \"CABAC\",\"Bitrate\": 525000,\"FramerateControl\": \"INITIALIZE_FROM_SOURCE\",\"RateControlMode\": \"CBR\",\"CodecProfile\": \"MAIN\",\"Telecine\": \"NONE\",\"MinIInterval\": 0,\"AdaptiveQuantization\": \"HIGH\",\"CodecLevel\": \"AUTO\",\"FieldEncoding\": \"PAFF\",\"SceneChangeDetect\": \"ENABLED\",\"QualityTuningLevel\": \"SINGLE_PASS\",\"FramerateConversionAlgorithm\": \"DUPLICATE_DROP\",\"UnregisteredSeiTimecode\": \"DISABLED\",\"GopSizeUnits\": \"FRAMES\",\"ParControl\": \"INITIALIZE_FROM_SOURCE\",\"NumberBFramesBetweenReferenceFrames\": 2,\"RepeatPps\": \"DISABLED\",\"DynamicSubGop\": \"STATIC\"}},\"AfdSignaling\": \"NONE\",\"DropFrameTimecode\": \"ENABLED\",\"RespondToAfd\": \"NONE\",\"ColorMetadata\": \"INSERT\"},\"AudioDescriptions\": [{\"AudioTypeControl\": \"FOLLOW_INPUT\",\"CodecSettings\": {\"Codec\": \"AAC\",\"AacSettings\": {\"AudioDescriptionBroadcasterMix\": \"NORMAL\",\"Bitrate\": 96000,\"RateControlMode\": \"CBR\",\"CodecProfile\": \"LC\",\"CodingMode\": \"CODING_MODE_2_0\",\"RawFormat\": \"NONE\",\"SampleRate\": 48000,\"Specification\": \"MPEG4\"}},\"LanguageCodeControl\": \"FOLLOW_INPUT\"}],\"OutputSettings\": {\"HlsSettings\": {\"AudioGroupId\": \"program_audio\",\"SegmentModifier\": \"$dt$\",\"IFrameOnlyManifest\": \"EXCLUDE\"}},\"NameModifier\": \"_360\"},{\"ContainerSettings\": {\"Container\": \"M3U8\",\"M3u8Settings\": {\"AudioFramesPerPes\": 4,\"PcrControl\": \"PCR_EVERY_PES_PACKET\",\"PmtPid\": 480,\"PrivateMetadataPid\": 503,\"ProgramNumber\": 1,\"PatInterval\": 0,\"PmtInterval\": 0,\"Scte35Source\": \"NONE\",\"Scte35Pid\": 500,\"NielsenId3\": \"NONE\",\"TimedMetadata\": \"NONE\",\"TimedMetadataPid\": 502,\"VideoPid\": 481,\"AudioPids\": [482,483,484,485,486,487,488,489,490,491,492]}},\"VideoDescription\": {\"Width\": 1280,\"ScalingBehavior\": \"DEFAULT\",\"Height\": 720,\"TimecodeInsertion\": \"DISABLED\",\"AntiAlias\": \"ENABLED\",\"Sharpness\": 50,\"CodecSettings\": {\"Codec\": \"H_264\",\"H264Settings\": {\"InterlaceMode\": \"PROGRESSIVE\",\"NumberReferenceFrames\": 3,\"Syntax\": \"DEFAULT\",\"Softness\": 0,\"GopClosedCadence\": 1,\"GopSize\": 90,\"Slices\": 1,\"GopBReference\": \"DISABLED\",\"SlowPal\": \"DISABLED\",\"SpatialAdaptiveQuantization\": \"ENABLED\",\"TemporalAdaptiveQuantization\": \"ENABLED\",\"FlickerAdaptiveQuantization\": \"DISABLED\",\"EntropyEncoding\": \"CABAC\",\"Bitrate\": 1378000,\"FramerateControl\": \"INITIALIZE_FROM_SOURCE\",\"RateControlMode\": \"CBR\",\"CodecProfile\": \"MAIN\",\"Telecine\": \"NONE\",\"MinIInterval\": 0,\"AdaptiveQuantization\": \"HIGH\",\"CodecLevel\": \"AUTO\",\"FieldEncoding\": \"PAFF\",\"SceneChangeDetect\": \"ENABLED\",\"QualityTuningLevel\": \"SINGLE_PASS\",\"FramerateConversionAlgorithm\": \"DUPLICATE_DROP\",\"UnregisteredSeiTimecode\": \"DISABLED\",\"GopSizeUnits\": \"FRAMES\",\"ParControl\": \"INITIALIZE_FROM_SOURCE\",\"NumberBFramesBetweenReferenceFrames\": 2,\"RepeatPps\": \"DISABLED\",\"DynamicSubGop\": \"STATIC\"}},\"AfdSignaling\": \"NONE\",\"DropFrameTimecode\": \"ENABLED\",\"RespondToAfd\": \"NONE\",\"ColorMetadata\": \"INSERT\"},\"AudioDescriptions\": [{\"AudioTypeControl\": \"FOLLOW_INPUT\",\"CodecSettings\": {\"Codec\": \"AAC\",\"AacSettings\": {\"AudioDescriptionBroadcasterMix\": \"NORMAL\",\"Bitrate\": 96000,\"RateControlMode\": \"CBR\",\"CodecProfile\": \"LC\",\"CodingMode\": \"CODING_MODE_2_0\",\"RawFormat\": \"NONE\",\"SampleRate\": 48000,\"Specification\": \"MPEG4\"}},\"LanguageCodeControl\": \"FOLLOW_INPUT\"}],\"OutputSettings\": {\"HlsSettings\": {\"AudioGroupId\": \"program_audio\",\"IFrameOnlyManifest\": \"EXCLUDE\"}},\"NameModifier\": \"_720\"}],\"OutputGroupSettings\": {\"Type\": \"HLS_GROUP_SETTINGS\",\"HlsGroupSettings\": {\"ManifestDurationFormat\": \"INTEGER\",\"SegmentLength\": 10,\"TimedMetadataId3Period\": 10,\"CaptionLanguageSetting\": \"OMIT\",\"TimedMetadataId3Frame\": \"PRIV\",\"CodecSpecification\": \"RFC_4281\",\"OutputSelection\": \"MANIFESTS_AND_SEGMENTS\",\"ProgramDateTimePeriod\": 600,\"MinSegmentLength\": 0,\"MinFinalSegmentLength\": 0,\"DirectoryStructure\": \"SINGLE_DIRECTORY\",\"ProgramDateTime\": \"EXCLUDE\",\"SegmentControl\": \"SEGMENTED_FILES\",\"ManifestCompression\": \"NONE\",\"ClientCache\": \"ENABLED\",\"StreamInfResolution\": \"INCLUDE\",\"Destination\": \"outputLocation\"}}}],\"AdAvailOffset\": 0,\"Inputs\": [{\"AudioSelectors\": {\"Audio Selector 1\": {\"Offset\": 0,\"DefaultSelection\": \"DEFAULT\",\"ProgramSelection\": 1}},\"VideoSelector\": {\"ColorSpace\": \"FOLLOW\"},\"FilterEnable\": \"AUTO\",\"PsiControl\": \"USE_PSI\",\"FilterStrength\": 0,\"DeblockFilter\": \"DISABLED\",\"DenoiseFilter\": \"DISABLED\",\"TimecodeSource\": \"EMBEDDED\",\"FileInput\": \"inputVideoFile\"}]}}" +} diff --git a/live-video-stream-generator/src/main/scala/org/sunbird/job/livevideostream/helpers/AwsResult.scala b/live-video-stream-generator/src/main/scala/org/sunbird/job/livevideostream/helpers/AwsResult.scala new file mode 100644 index 000000000..63854ec26 --- /dev/null +++ b/live-video-stream-generator/src/main/scala/org/sunbird/job/livevideostream/helpers/AwsResult.scala @@ -0,0 +1,54 @@ +package org.sunbird.job.livevideostream.helpers + +import java.text.SimpleDateFormat +import java.util.{Date, TimeZone} + +import org.apache.commons.lang3.StringUtils + +import scala.collection.immutable.HashMap + +object AwsResult extends Result { + + val formatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") + formatter.setTimeZone(TimeZone.getTimeZone("UTC")) + + override def getSubmitJobResult(response: MediaResponse): Map[String, AnyRef] = { + val job: Map[String, AnyRef] = response.result.getOrElse("job", Map).asInstanceOf[Map[String, AnyRef]] + val timing: Map[String, AnyRef] = job.getOrElse("timing", Map).asInstanceOf[Map[String, AnyRef]] + HashMap[String, AnyRef]( + "job" -> HashMap[String, AnyRef]( + "id" -> job.getOrElse("id", "").toString, + "status" -> job.getOrElse("status", "").toString.toUpperCase(), + "submittedOn" -> formatter.format(new Date(timing.getOrElse("submitTime", "").toString.toLong*1000)) + ) + ) + } + + override def getJobResult(response: MediaResponse): Map[String, AnyRef] = { + val job: Map[String, AnyRef] = response.result.getOrElse("job", Map).asInstanceOf[Map[String, AnyRef]] + val timing: Map[String, AnyRef] = job.getOrElse("timing", Map).asInstanceOf[Map[String, AnyRef]] + + HashMap[String, AnyRef]( + "job" -> HashMap[String, AnyRef]( + "id" -> job.getOrElse("id", "").toString, + "status" -> job.getOrElse("status", "").toString.toUpperCase(), + "submittedOn" -> formatter.format(new Date(timing.getOrElse("submitTime", "").toString.toLong*1000)), + "lastModifiedOn" -> { if(StringUtils.isNotBlank(timing.getOrElse("finishTime", "").toString)){formatter.format(new Date(timing.getOrElse("finishTime", "").toString.toLong*1000))} else {""}}, + "error" -> { + if (StringUtils.equalsIgnoreCase(job.getOrElse("status", "").toString.toUpperCase(), "ERROR")) { + HashMap[String, String]( + "errorCode" -> job.getOrElse("errorCode", "").toString, + "errorMessage" -> job.getOrElse("errorMessage", "").toString + ) + }else{ + null + } + } + ) + ) + } + + override def getCancelJobResult(response: MediaResponse): Map[String, AnyRef] = ??? + + override def getListJobResult(response: MediaResponse): Map[String, AnyRef] = ??? +} diff --git a/live-video-stream-generator/src/main/scala/org/sunbird/job/livevideostream/helpers/AwsSignUtils.scala b/live-video-stream-generator/src/main/scala/org/sunbird/job/livevideostream/helpers/AwsSignUtils.scala new file mode 100644 index 000000000..f5628f50c --- /dev/null +++ b/live-video-stream-generator/src/main/scala/org/sunbird/job/livevideostream/helpers/AwsSignUtils.scala @@ -0,0 +1,133 @@ +package org.sunbird.job.livevideostream.helpers + +import java.text.SimpleDateFormat +import java.util.{Date, TimeZone} + +import javax.crypto.Mac +import javax.crypto.spec.SecretKeySpec +import javax.xml.bind.DatatypeConverter +import org.apache.commons.codec.binary.Hex +import org.apache.commons.codec.digest.DigestUtils +import org.apache.commons.lang3.StringUtils +import org.sunbird.job.livevideostream.task.LiveVideoStreamGeneratorConfig + +object AwsSignUtils { + + val dateFormat = new SimpleDateFormat("yyyyMMdd") + dateFormat.setTimeZone(TimeZone.getTimeZone("UTC")) + + def getSiginingkey()(implicit config: LiveVideoStreamGeneratorConfig): Array[Byte] = { + val date = dateFormat.format(new Date()).getBytes("UTF8") + val kSecret = ("AWS4" + config.getConfig("aws.token.access_secret")).getBytes("UTF8") + val kDate = HmacSHA256(date, kSecret) + val kRegion = HmacSHA256(config.getConfig("aws.region").getBytes("UTF8"), kDate) + val kService = HmacSHA256(config.getConfig("aws.service.name").getBytes("UTF8"), kRegion) + val kSigning = HmacSHA256("aws4_request".getBytes("UTF8"), kService) + kSigning + } + + def getStringToSign(httpMethod: String, url: String, headers: Map[String, String], payload: String)(implicit config: LiveVideoStreamGeneratorConfig): String = { + val canonicalUri = getCanonicalUri(url) + val canonicalQueryString = getCanonicalQueryString(url) + val hashedPayload = getHashedPayload(payload) + val canonicalHeaders = getCanonicalHeaders(headers, hashedPayload) + val signedHeaders = getSignedHeaders(headers.keySet) + + val canonicalRequest = httpMethod + "\n" + canonicalUri + "\n" + canonicalQueryString + "\n" + canonicalHeaders + "\n" + signedHeaders + "\n" + hashedPayload + + val timeStampISO8601Format = headers.get("x-amz-date").get + val scope = dateFormat.format(new Date()) + "/" + config.getConfig("aws.region") + "/" + config.getConfig("aws.service.name") + "/aws4_request" + + val stringToSign = "AWS4-HMAC-SHA256" + "\n" + timeStampISO8601Format + "\n" + scope + "\n" + sha256Hash(canonicalRequest) + + stringToSign + } + + def generateToken(httpMethod: String, url: String, headers: Map[String, String], payload: String)(implicit config: LiveVideoStreamGeneratorConfig): String = { + val signature = new String(Hex.encodeHex(HmacSHA256(getStringToSign(httpMethod, url, headers, payload).getBytes("UTF-8"), getSiginingkey()))) + + "AWS4-HMAC-SHA256 Credential=" + config.getConfig("aws.token.access_key") + "/" + dateFormat.format(new Date()) + "/" + config.getConfig("aws.region") + "/" + config.getConfig("aws.service.name") + "/aws4_request,SignedHeaders=" + getSignedHeaders(headers.keySet) + ",Signature=" + signature + } + + + @throws[Exception] + def HmacSHA256(data: Array[Byte], key: Array[Byte]) = { + val algorithm: String = "HMacSha256" + val mac: Mac = Mac.getInstance(algorithm) + mac.init(new SecretKeySpec(key, algorithm)) + mac.doFinal(data) + } + + def uriEncode(input: Array[Char], encodeSlash: Boolean): String = { + val result: StringBuilder = new StringBuilder() + var i: Int = 0 + for (i <- 0 to input.length()) { + val ch = input.charAt(i) + if ((ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9') || ch == '_' || ch == '-' || ch == '~' || ch == '.') { + result.append(ch) + } else if (ch == '/') { + if (encodeSlash) result.append("%2F") else result.append(ch) + } else { + result.append(Integer.toHexString((ch.asInstanceOf[Integer]))) + } + } + result.toString() + } + + def sha256Hash(input: String): String = { + DigestUtils.sha256Hex(input) + } + + def getCanonicalUri(url: String)(implicit config: LiveVideoStreamGeneratorConfig): String = { + val version = config.getConfig("aws.api.version") + var uri = url.split(version)(1) + if (StringUtils.isBlank(uri)) "" else { + uri = "/" + version + uri; uri + } + } + + def getCanonicalQueryString(url: String): String = { + var result: String = "" + if (url.split("\\?").length > 1) { + val queryString: String = url.split("\\?")(1) + if (StringUtils.isNotBlank(queryString)) { + for (param <- queryString.split("\\&")) { + if (param.split("=").length == 2) + result += uriEncode(param.split("=")(0).toCharArray, false) + "=" + uriEncode(param.split("=")(1).toCharArray, false) + "&" + else + result += uriEncode(param.split("=")(0).toCharArray, false) + "&" + } + result = result.substring(0, result.length - 2) + } + result + } else result + } + + def getCanonicalHeaders(headers: Map[String, String], hashedPayload: String): String = { + var result: String = "" + headers.foreach(header => { + result += header._1.toLowerCase + ":" + header._2.trim + "\n" + }) + result + } + + def getSignedHeaders(keySet: scala.collection.Set[String]): String = { + var result: String = "" + keySet.foreach(key => { + result += key.toLowerCase + ";" + }) + result.substring(0, result.length - 1) + } + + def getHashedPayload(payload: String): String = { + if (StringUtils.isNotBlank(payload)) { + sha256Hash(payload) + } else { + sha256Hash("") + } + } + + def stringtoHex(str: String): String = { + DatatypeConverter.printHexBinary(str.getBytes("UTF8")) + } +} diff --git a/live-video-stream-generator/src/main/scala/org/sunbird/job/livevideostream/helpers/AzureRequestBody.scala b/live-video-stream-generator/src/main/scala/org/sunbird/job/livevideostream/helpers/AzureRequestBody.scala new file mode 100644 index 000000000..675efb889 --- /dev/null +++ b/live-video-stream-generator/src/main/scala/org/sunbird/job/livevideostream/helpers/AzureRequestBody.scala @@ -0,0 +1,8 @@ +package org.sunbird.job.livevideostream.helpers + +object AzureRequestBody { + + val create_asset = " {\"properties\": {\"description\": \"assetDescription\",\"alternateId\" : \"assetId\"}}" + val submit_job = "{\"properties\": {\"input\": {\"@odata.type\": \"#Microsoft.Media.JobInputHttp\",\"baseUri\": \"baseInputUrl\",\"files\": [\"inputVideoFile\"]},\"outputs\": [{\"@odata.type\": \"#Microsoft.Media.JobOutputAsset\",\"assetName\": \"assetId\"}]}}" + val create_stream_locator="{\"properties\":{\"assetName\": \"assetId\",\"streamingPolicyName\": \"policyName\"}}" +} diff --git a/live-video-stream-generator/src/main/scala/org/sunbird/job/livevideostream/helpers/AzureResult.scala b/live-video-stream-generator/src/main/scala/org/sunbird/job/livevideostream/helpers/AzureResult.scala new file mode 100644 index 000000000..3ab1227f6 --- /dev/null +++ b/live-video-stream-generator/src/main/scala/org/sunbird/job/livevideostream/helpers/AzureResult.scala @@ -0,0 +1,56 @@ +package org.sunbird.job.livevideostream.helpers + +import org.apache.commons.lang3.StringUtils + +import scala.collection.immutable.HashMap + +object AzureResult extends Result { + + override def getSubmitJobResult(response: MediaResponse): Map[String, AnyRef] = { + val result = response.result + val output: Map[String, AnyRef] = result.getOrElse("properties", Map).asInstanceOf[Map[String, AnyRef]].getOrElse("outputs", List).asInstanceOf[List[Map[String, AnyRef]]].head + HashMap[String, AnyRef]( + "job" -> HashMap[String, AnyRef]( + "id" -> result.getOrElse("name", "").toString, + "status" -> output.getOrElse("state", "").toString.toUpperCase(), + "submittedOn" -> result.getOrElse("properties", Map).asInstanceOf[Map[String, AnyRef]].getOrElse("created", "").toString, + "lastModifiedOn" -> result.getOrElse("properties", Map).asInstanceOf[Map[String, AnyRef]].getOrElse("lastModified", "").toString + ) + ) + + } + + override def getJobResult(response: MediaResponse): Map[String, AnyRef] = { + val result = response.result + val output: Map[String, AnyRef] = result.getOrElse("properties", Map).asInstanceOf[Map[String, AnyRef]].getOrElse("outputs", List).asInstanceOf[List[Map[String, AnyRef]]].head + + HashMap[String, AnyRef]( + "job" -> HashMap[String, AnyRef]( + "id" -> result.getOrElse("name", "").toString, + "status" -> output.getOrElse("state", "").toString.toUpperCase(), + "submittedOn" -> result.getOrElse("properties", Map).asInstanceOf[Map[String, AnyRef]].getOrElse("created", "").toString, + "lastModifiedOn" -> result.getOrElse("properties", Map).asInstanceOf[Map[String, AnyRef]].getOrElse("lastModified", "").toString, + "error" -> { + if (StringUtils.equalsIgnoreCase(output.getOrElse("state", "").toString.toUpperCase(),"ERROR")) { + val errorMap: Map[String, AnyRef] = output.getOrElse("error", Map).asInstanceOf[Map[String, AnyRef]] + Map[String, String]( + "errorCode" -> errorMap.getOrElse("code", "").toString, + "errorMessage" -> errorMap.getOrElse("details", List).asInstanceOf[List[Map[String, AnyRef]]].head.getOrElse("message", "").toString + ) + } else { + null + } + } + ) + ) + } + + override def getCancelJobResult(response: MediaResponse): Map[String, AnyRef] = { + null + } + + override def getListJobResult(response: MediaResponse): Map[String, AnyRef] = { + null + } + +} diff --git a/live-video-stream-generator/src/main/scala/org/sunbird/job/livevideostream/helpers/CaseClasses.scala b/live-video-stream-generator/src/main/scala/org/sunbird/job/livevideostream/helpers/CaseClasses.scala new file mode 100644 index 000000000..d1a6f73b8 --- /dev/null +++ b/live-video-stream-generator/src/main/scala/org/sunbird/job/livevideostream/helpers/CaseClasses.scala @@ -0,0 +1,24 @@ +package org.sunbird.job.livevideostream.helpers + +import org.joda.time.DateTime +import scala.collection.immutable.HashMap + + +case class MediaRequest(id: String, params: Map[String, AnyRef] = new HashMap[String, AnyRef], request: Map[String, AnyRef] = new HashMap[String, AnyRef]); + +case class MediaResponse(id: String, ts: String, params: Map[String, Any] = new HashMap[String, AnyRef], responseCode: String, result: Map[String, AnyRef] = new HashMap[String, AnyRef]) + +object ResponseCode extends Enumeration { + type Code = Value + val OK = Value(200) + val CLIENT_ERROR = Value(400) + val SERVER_ERROR = Value(500) + val RESOURCE_NOT_FOUND = Value(404) +} + +trait AlgoOutput extends AnyRef + +case class JobRequest(client_key: String, request_id: String, job_id: Option[String], status: String, request_data: String, iteration: Int, dt_job_submitted: Option[DateTime] = None, location: Option[String] = None, dt_file_created: Option[DateTime] = None, dt_first_event: Option[DateTime] = None, dt_last_event: Option[DateTime] = None, dt_expiration: Option[DateTime] = None, dt_job_processing: Option[DateTime] = None, dt_job_completed: Option[DateTime] = None, input_events: Option[Int] = None, output_events: Option[Int] = None, file_size: Option[Long] = None, latency: Option[Int] = None, execution_time: Option[Long] = None, err_message: Option[String] = None, stage: Option[String] = None, stage_status: Option[String] = None, job_name: Option[String] = None) extends AlgoOutput + +case class JobStage(request_id: String, client_key: String, stage: String, stage_status: String, status: String, err_message: String = "", dt_job_processing: Option[DateTime] = Option(new DateTime())) +case class StreamingStage(request_id: String, client_key: String, job_id: String, stage: String, stage_status: String, status: String, iteration: Int, err_message: String = "") \ No newline at end of file diff --git a/live-video-stream-generator/src/main/scala/org/sunbird/job/livevideostream/helpers/Response.scala b/live-video-stream-generator/src/main/scala/org/sunbird/job/livevideostream/helpers/Response.scala new file mode 100644 index 000000000..a64b01d48 --- /dev/null +++ b/live-video-stream-generator/src/main/scala/org/sunbird/job/livevideostream/helpers/Response.scala @@ -0,0 +1,65 @@ +package org.sunbird.job.livevideostream.helpers + +import java.util.UUID +import scala.collection.immutable.HashMap +import org.apache.commons.lang3.StringUtils +import org.sunbird.job.util.{HTTPResponse, JSONUtil} + + +object Response { + + lazy val MEDIA_SERVICE_TYPE = "" +// val MEDIA_SERVICE_TYPE = AppConfig.getConfig("media_service_type") + + def getResponse(response: HTTPResponse): MediaResponse = { + var result: Map[String, AnyRef] = new HashMap[String, AnyRef] + + try { + val body = response.body + if (StringUtils.isNotBlank(body)) + result = JSONUtil.deserialize[Map[String, AnyRef]](body) + } catch { + case e: UnsupportedOperationException => e.printStackTrace() + case e: Exception => e.printStackTrace() + } + + response.status match { + case 200 => getSuccessResponse(result) + case 201 => getSuccessResponse(result) + case 400 => getFailureResponse(result, "BAD_REQUEST", "Please Provide Correct Request Data.") + case 401 => getFailureResponse(result, "SERVER_ERROR", "Access Token Expired.") + case 404 => getFailureResponse(result, "RESOURCE_NOT_FOUND", "Resource Not Found.") + case 405 => getFailureResponse(result, "METHOD_NOT_ALLOWED", "Requested Operation Not Allowed.") + case 500 => getFailureResponse(result, "SERVER_ERROR", "Internal Server Error. Please Try Again Later!") + case _ => getFailureResponse(result, "SERVER_ERROR", "Internal Server Error. Please Try Again Later!") + } + + } + + def getSuccessResponse(result: Map[String, AnyRef]): MediaResponse = { + MediaResponse(UUID.randomUUID().toString, System.currentTimeMillis().toString, new HashMap[String, AnyRef], + ResponseCode.OK.toString, result) + } + + def getFailureResponse(result: Map[String, AnyRef], errorCode: String, errorMessage: String): MediaResponse = { + val respCode: String = errorCode match { + case "BAD_REQUEST" => ResponseCode.CLIENT_ERROR.toString + case "RESOURCE_NOT_FOUND" => ResponseCode.RESOURCE_NOT_FOUND.toString + case "METHOD_NOT_ALLOWED" => ResponseCode.CLIENT_ERROR.toString + case "SERVER_ERROR" => ResponseCode.SERVER_ERROR.toString + } + val params = HashMap[String, String]( + "err" -> errorCode, + "errMsg" -> errorMessage + ) + MediaResponse(UUID.randomUUID().toString, System.currentTimeMillis().toString, params, respCode, result) + } + + def getCancelJobResult(response: MediaResponse): Map[String, AnyRef] = { + null + } + + def getListJobResult(response: MediaResponse): Map[String, AnyRef] = { + null + } +} diff --git a/live-video-stream-generator/src/main/scala/org/sunbird/job/livevideostream/helpers/Result.scala b/live-video-stream-generator/src/main/scala/org/sunbird/job/livevideostream/helpers/Result.scala new file mode 100644 index 000000000..eba65d267 --- /dev/null +++ b/live-video-stream-generator/src/main/scala/org/sunbird/job/livevideostream/helpers/Result.scala @@ -0,0 +1,12 @@ +package org.sunbird.job.livevideostream.helpers + +trait Result { + + def getSubmitJobResult(response: MediaResponse): Map[String, AnyRef] + + def getJobResult(response: MediaResponse): Map[String, AnyRef] + + def getCancelJobResult(response: MediaResponse): Map[String, AnyRef] + + def getListJobResult(response: MediaResponse): Map[String, AnyRef] +} diff --git a/live-video-stream-generator/src/main/scala/org/sunbird/job/livevideostream/service/AwsMediaService.scala b/live-video-stream-generator/src/main/scala/org/sunbird/job/livevideostream/service/AwsMediaService.scala new file mode 100644 index 000000000..d6a46da53 --- /dev/null +++ b/live-video-stream-generator/src/main/scala/org/sunbird/job/livevideostream/service/AwsMediaService.scala @@ -0,0 +1,81 @@ +package org.sunbird.job.livevideostream.service + +import org.sunbird.job.livevideostream.exception.MediaServiceException +import org.sunbird.job.livevideostream.helpers.{AwsRequestBody, AwsSignUtils, MediaResponse, Response} +import org.sunbird.job.livevideostream.task.LiveVideoStreamGeneratorConfig + +import java.text.SimpleDateFormat +import java.util.{Date, TimeZone} +import org.sunbird.job.util.HttpUtil + +import scala.collection.immutable.HashMap +import scala.reflect.io.File + +abstract class AwsMediaService extends IMediaService { + + protected def getApiUrl(apiName: String)(implicit config: LiveVideoStreamGeneratorConfig, httpUtil: HttpUtil): String = { + val host: String = config.getConfig("aws.api.endpoint") + val apiVersion: String = config.getConfig("aws.api.version") + val baseUrl: String = host + File.separator + apiVersion + apiName.toLowerCase() match { + case "job" => baseUrl + "/jobs" + case _ => throw new MediaServiceException("ERR_INVALID_API_NAME", "Please Provide Valid AWS Media Service API Name") + } + } + + protected def getJobDetails(jobId: String)(implicit config: LiveVideoStreamGeneratorConfig, httpUtil: HttpUtil): MediaResponse = { + val url = getApiUrl("job") + "/" + jobId + val header = getDefaultHeader("GET", url, null) + Response.getResponse(httpUtil.get(url, header)) + } + + protected def prepareJobRequestBody(jobRequest: Map[String, AnyRef])(implicit config: LiveVideoStreamGeneratorConfig, httpUtil: HttpUtil): String = { + val queue = config.getConfig("aws.service.queue") + val role = config.getConfig("aws.service.role") + val streamType = config.getConfig("aws.stream.protocol").toLowerCase() + val artifactUrl = jobRequest.get("artifactUrl").mkString + val contentId = jobRequest.get("identifier").mkString + val pkgVersion = jobRequest.getOrElse("pkgVersion", "").toString + val inputFile = prepareInputUrl(artifactUrl) + val output = prepareOutputUrl(contentId, streamType, pkgVersion) + AwsRequestBody.submit_hls_job + .replace("queueId", queue) + .replace("mediaRole", role) + .replace("inputVideoFile", inputFile) + .replace("outputLocation", output) + } + + protected def prepareInputUrl(url: String)(implicit config: LiveVideoStreamGeneratorConfig, httpUtil: HttpUtil): String = { + val temp = url.split("content") + val bucket = config.getConfig("aws.content_bucket_name") + val separator = File.separator; + "s3:" + separator + separator + bucket + separator + "content" + temp(1) + } + + protected def prepareOutputUrl(contentId: String, streamType: String, pkgVersion: String)(implicit config: LiveVideoStreamGeneratorConfig): String = { + val bucket = config.getConfig("aws.content_bucket_name") + val output = streamType.toLowerCase + "_" + pkgVersion + val separator = File.separator; + "s3:" + separator + separator + bucket + separator + "content" + separator + contentId + separator + output + separator + } + + protected def getSignatureHeader()(implicit config: LiveVideoStreamGeneratorConfig): Map[String, String] = { + val formatter = new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'") + formatter.setTimeZone(TimeZone.getTimeZone("UTC")) + val date = formatter.format(new Date()) + val host: String = config.getConfig("aws.api.endpoint").replace("https://", "") + Map[String, String]("Content-Type" -> "application/json", "host" -> host, "x-amz-date" -> date) + } + + protected def getDefaultHeader(httpMethod: String, url: String, payload: String)(implicit config: LiveVideoStreamGeneratorConfig): Map[String, String] = { + val signHeader = getSignatureHeader + val authToken = AwsSignUtils.generateToken(httpMethod, url, signHeader, payload) + val host: String = config.getConfig("aws.api.endpoint").replace("https://", "") + HashMap[String, String]( + "Content-Type" -> "application/json", + "host" -> host, + "x-amz-date" -> signHeader.get("x-amz-date").mkString, + "Authorization" -> authToken + ) + } +} diff --git a/live-video-stream-generator/src/main/scala/org/sunbird/job/livevideostream/service/AzureMediaService.scala b/live-video-stream-generator/src/main/scala/org/sunbird/job/livevideostream/service/AzureMediaService.scala new file mode 100644 index 000000000..14e126712 --- /dev/null +++ b/live-video-stream-generator/src/main/scala/org/sunbird/job/livevideostream/service/AzureMediaService.scala @@ -0,0 +1,149 @@ +package org.sunbird.job.livevideostream.service + +import java.io.File +import org.apache.commons.lang3.StringUtils +import org.sunbird.job.livevideostream.exception.MediaServiceException +import org.sunbird.job.util.{HttpUtil, JSONUtil} +import org.sunbird.job.livevideostream.helpers.{AzureRequestBody, MediaResponse, Response} +import org.sunbird.job.livevideostream.task.LiveVideoStreamGeneratorConfig + +import scala.collection.immutable.HashMap + + +abstract class AzureMediaService extends IMediaService { + + private var API_ACCESS_TOKEN: String = "" + + private def getToken()(implicit config: LiveVideoStreamGeneratorConfig, httpUtil: HttpUtil): String = { + val tenant = config.getSystemConfig("azure.tenant") + val clientKey = config.getSystemConfig("azure.token.client_key") + val clientSecret = config.getSystemConfig("azure.token.client_secret") + val loginUrl = config.getConfig("azure.login.endpoint") + "/" + tenant + "/oauth2/token" + + val data = Map[String, String]( + "grant_type" -> "client_credentials", + "client_id" -> clientKey, + "client_secret" -> clientSecret, + "resource" -> "https://management.core.windows.net/" + ) + + val header = Map[String, String]( + "Content-Type" -> "application/x-www-form-urlencoded", + "Keep-Alive" -> "true" + ) + + val response:MediaResponse = Response.getResponse(httpUtil.post_map(loginUrl, data, header)) + if(response.responseCode == "OK"){ + response.result("access_token").asInstanceOf[String] + } else { + throw new Exception("Error while getting the azure access token::"+JSONUtil.serialize(response)) + } + } + + protected def getJobDetails(jobId: String)(implicit config: LiveVideoStreamGeneratorConfig, httpUtil: HttpUtil): MediaResponse = { + val url = getApiUrl("job").replace("jobIdentifier", jobId) + val response:MediaResponse = Response.getResponse(httpUtil.get(url, getDefaultHeader())) + if(response.responseCode == "OK"){ + response + } else { + throw new Exception("Error while getting the job detail::"+JSONUtil.serialize(response)) + } + } + + protected def createAsset(assetId: String, jobId: String)(implicit config: LiveVideoStreamGeneratorConfig, httpUtil: HttpUtil): MediaResponse = { + val url = getApiUrl("asset").replace("assetId", assetId) + val requestBody = AzureRequestBody.create_asset.replace("assetId", assetId) + .replace("assetDescription", "Output Asset for " + jobId) + val response:MediaResponse = Response.getResponse(httpUtil.put(url, requestBody, getDefaultHeader())) + if(response.responseCode == "OK"){ + response + } else { + throw new Exception("Error while creating asset::(assetId->"+assetId+", jobId->"+jobId+")::"+JSONUtil.serialize(response)) + } + } + + protected def createStreamingLocator(streamingLocatorName: String, assetName: String)(implicit config: LiveVideoStreamGeneratorConfig, httpUtil: HttpUtil): MediaResponse = { + val url = getApiUrl("stream_locator").replace("streamingLocatorName", streamingLocatorName) + val streamingPolicyName = config.getConfig("azure.stream.policy_name") + val reqBody = AzureRequestBody.create_stream_locator.replace("assetId", assetName).replace("policyName", streamingPolicyName) + Response.getResponse(httpUtil.put(url, reqBody, getDefaultHeader())) + } + + protected def getStreamingLocator(streamingLocatorName: String)(implicit config: LiveVideoStreamGeneratorConfig, httpUtil: HttpUtil): MediaResponse = { + val url = getApiUrl("stream_locator").replace("streamingLocatorName", streamingLocatorName) + val response:MediaResponse = Response.getResponse(httpUtil.get(url, getDefaultHeader())) + if(response.responseCode == "OK"){ + response + } else { + throw new Exception("Error while getStreamingLocator::(streamingLocatorName->" + streamingLocatorName + ")::"+JSONUtil.serialize(response)) + } + } + + protected def getStreamUrls(streamingLocatorName: String)(implicit config: LiveVideoStreamGeneratorConfig, httpUtil: HttpUtil): MediaResponse = { + val url = getApiUrl("list_paths").replace("streamingLocatorName", streamingLocatorName) + val response:MediaResponse = Response.getResponse(httpUtil.post(url, "{}", getDefaultHeader())) + if(response.responseCode == "OK"){ + response + } else { + throw new Exception("Error while getStreamUrls::(streamingLocatorName->" + streamingLocatorName + ")::"+JSONUtil.serialize(response)) + } + } + + protected def getApiUrl(apiName: String)(implicit config: LiveVideoStreamGeneratorConfig, httpUtil: HttpUtil): String = { + val subscriptionId: String = config.getSystemConfig("azure.subscription_id") + val resourceGroupName: String = config.getSystemConfig("azure.resource_group_name") + val accountName: String = config.getSystemConfig("azure.account_name") + val apiVersion: String = config.getConfig("azure.api.version") + val transformName: String = config.getConfig("azure.transform.default") + + val baseUrl: String = new StringBuilder().append(config.getConfig("azure.api.endpoint")+"/subscriptions/") + .append(subscriptionId) + .append("/resourceGroups/") + .append(resourceGroupName) + .append("/providers/Microsoft.Media/mediaServices/") + .append(accountName).mkString + + + apiName.toLowerCase() match { + case "asset" => baseUrl + "/assets/assetId?api-version=" + apiVersion + case "job" => baseUrl + "/transforms/" + transformName + "/jobs/jobIdentifier?api-version=" + apiVersion + case "stream_locator" => baseUrl + "/streamingLocators/streamingLocatorName?api-version=" + apiVersion + case "list_paths" => baseUrl + "/streamingLocators/streamingLocatorName/listPaths?api-version=" + apiVersion + case _ => throw new MediaServiceException("ERR_INVALID_API_NAME", "Please Provide Valid Media Service API Name") + } + } + + protected def getDefaultHeader()(implicit config: LiveVideoStreamGeneratorConfig, httpUtil: HttpUtil): Map[String, String] = { + val accessToken = if (StringUtils.isNotBlank(API_ACCESS_TOKEN)) API_ACCESS_TOKEN else getToken() + val authToken = "Bearer " + accessToken + HashMap[String, String]( + "Content-Type" -> "application/json", + "Accept" -> "application/json", + "Authorization" -> authToken + ) + } + + protected def prepareStreamingUrl(streamLocatorName: String, jobId: String)(implicit config: LiveVideoStreamGeneratorConfig, httpUtil: HttpUtil): Map[String, AnyRef] = { + val streamType = config.getConfig("azure.stream.protocol") + val streamHost = config.getConfig("azure.stream.base_url") + var url = "" + val listPathResponse = getStreamUrls(streamLocatorName) + if (listPathResponse.responseCode.equalsIgnoreCase("OK")) { + val urlList: List[Map[String, AnyRef]] = listPathResponse.result.getOrElse("streamingPaths", List).asInstanceOf[List[Map[String, AnyRef]]] + urlList.foreach(streamMap => { + if (StringUtils.equalsIgnoreCase(streamMap.getOrElse("streamingProtocol", null).toString, streamType)) { + url = streamMap("paths").asInstanceOf[List[String]].head + } + }) + val streamUrl = streamHost + url.replace("aapl", "aapl-v3") + HashMap[String, AnyRef]("streamUrl" -> streamUrl) + } else { + val getResponse: MediaResponse = getJobDetails(jobId) + val fileName: String = getResponse.result.getOrElse("properties", Map).asInstanceOf[Map[String, AnyRef]].getOrElse("input", Map).asInstanceOf[Map[String, AnyRef]].getOrElse("files", List).asInstanceOf[List[AnyRef]].head.toString + val getStreamResponse = getStreamingLocator(streamLocatorName); + val locatorId = getStreamResponse.result.getOrElse("properties", Map).asInstanceOf[Map[String, AnyRef]].getOrElse("streamingLocatorId", "").toString + val streamUrl = streamHost + File.separator + locatorId + File.separator + fileName.replace(".mp4", ".ism") + "/manifest(format=m3u8-aapl-v3)" + HashMap[String, AnyRef]("streamUrl" -> streamUrl) + } + } +} diff --git a/live-video-stream-generator/src/main/scala/org/sunbird/job/livevideostream/service/IMediaService.scala b/live-video-stream-generator/src/main/scala/org/sunbird/job/livevideostream/service/IMediaService.scala new file mode 100644 index 000000000..9370b9a7c --- /dev/null +++ b/live-video-stream-generator/src/main/scala/org/sunbird/job/livevideostream/service/IMediaService.scala @@ -0,0 +1,20 @@ +package org.sunbird.job.livevideostream.service + +import org.sunbird.job.livevideostream.task.LiveVideoStreamGeneratorConfig +import org.sunbird.job.util.HttpUtil +import org.sunbird.job.livevideostream.helpers.{MediaRequest, MediaResponse} + + +trait IMediaService { + + def submitJob(request: MediaRequest)(implicit config: LiveVideoStreamGeneratorConfig, httpUtil: HttpUtil): MediaResponse + + def getJob(jobId: String)(implicit config: LiveVideoStreamGeneratorConfig, httpUtil: HttpUtil): MediaResponse + + def getStreamingPaths(jobId: String)(implicit config: LiveVideoStreamGeneratorConfig, httpUtil: HttpUtil): MediaResponse + + def listJobs(listJobsRequest: MediaRequest): MediaResponse + + def cancelJob(cancelJobRequest: MediaRequest): MediaResponse + +} diff --git a/live-video-stream-generator/src/main/scala/org/sunbird/job/livevideostream/service/LiveVideoStreamService.scala b/live-video-stream-generator/src/main/scala/org/sunbird/job/livevideostream/service/LiveVideoStreamService.scala new file mode 100644 index 000000000..d4501506e --- /dev/null +++ b/live-video-stream-generator/src/main/scala/org/sunbird/job/livevideostream/service/LiveVideoStreamService.scala @@ -0,0 +1,208 @@ +package org.sunbird.job.livevideostream.service + +import java.util.UUID +import com.datastax.driver.core.querybuilder.{QueryBuilder, Select} +import org.joda.time.DateTime +import org.slf4j.LoggerFactory +import org.sunbird.job.Metrics +import org.sunbird.job.livevideostream.helpers.JobRequest +import org.sunbird.job.livevideostream.service.impl.MediaServiceFactory +import org.sunbird.job.livevideostream.task.LiveVideoStreamGeneratorConfig +import org.sunbird.job.util.{CassandraUtil, HTTPResponse, HttpUtil, JSONUtil} +import org.sunbird.job.livevideostream.helpers.{JobRequest, MediaRequest, MediaResponse, StreamingStage} + +import scala.collection.JavaConverters._ + +class LiveVideoStreamService(implicit config: LiveVideoStreamGeneratorConfig, httpUtil: HttpUtil) { + private[this] lazy val logger = LoggerFactory.getLogger(classOf[LiveVideoStreamService]) + private lazy val mediaService = MediaServiceFactory.getMediaService(config) + private lazy val dbKeyspace:String = config.dbKeyspace + private lazy val dbTable:String = config.dbTable + lazy val cassandraUtil:CassandraUtil = new CassandraUtil(config.lmsDbHost, config.lmsDbPort, config) + private lazy val clientKey:String = "SYSTEM_LP" + private lazy val SUBMITTED:String = "SUBMITTED" + private lazy val VIDEO_STREAMING:String = "VIDEO_STREAMING" + + def submitJobRequest(eData: Map[String, AnyRef]): Unit = { + val stageName = "STREAMING_JOB_SUBMISSION"; + val jobSubmitted = DateTime.now() + val requestId = UUID.randomUUID().toString + val jobRequest = JobRequest(clientKey, requestId, None, SUBMITTED, JSONUtil.serialize(eData), 0, Option(jobSubmitted), + Option(eData.getOrElse("artifactUrl", "").asInstanceOf[String]), None, None, None, None, None, + None, None, None, None, None, None, None, Option(stageName), Option(SUBMITTED), Option(VIDEO_STREAMING)) + + saveJobRequest(jobRequest) + submitStreamJob(jobRequest) + } + + def processJobRequest(metrics: Metrics): Unit = { + updateProcessingRequest(metrics) + resubmitFailedJob() + } + + def updateProcessingRequest(metrics: Metrics): Unit = { + val processingJobRequests = readFromDB(Map("status" -> "PROCESSING")) + val stageName = "STREAMING_JOB_COMPLETE" + + for (jobRequest <- processingJobRequests) { + val iteration = jobRequest.iteration + val streamStage = if (jobRequest.job_id != None) { + val mediaResponse:MediaResponse = mediaService.getJob(jobRequest.job_id.get) + logger.info("Get job details while saving: " + JSONUtil.serialize(mediaResponse.result)) + if(mediaResponse.responseCode.contentEquals("OK")) { + val job = mediaResponse.result.getOrElse("job", Map()).asInstanceOf[Map[String, AnyRef]] + val jobStatus = job.getOrElse("status","").asInstanceOf[String] + + if(config.jobStatus.contains(jobStatus)) { + val streamingUrl = mediaService.getStreamingPaths(jobRequest.job_id.get).result.getOrElse("streamUrl","").asInstanceOf[String] + val requestData = JSONUtil.deserialize[Map[String, AnyRef]](jobRequest.request_data) + val contentId = requestData.getOrElse("identifier", "").asInstanceOf[String] + val channel = requestData.getOrElse("channel", "").asInstanceOf[String] + + if(updatePreviewUrl(contentId, streamingUrl, channel)) { + StreamingStage(jobRequest.request_id, jobRequest.client_key, jobRequest.job_id.get, stageName, jobStatus, "FINISHED", iteration + 1); + } else { + // TODO:: Set job status to FAILED + null + } + } else if(jobStatus.equalsIgnoreCase("ERROR")){ + val errMessage = job.getOrElse("error", Map()).asInstanceOf[Map[String, AnyRef]].getOrElse("errorMessage", "No error message").asInstanceOf[String] + StreamingStage(jobRequest.request_id, jobRequest.client_key, jobRequest.job_id.get, stageName, jobStatus, "FAILED", iteration + 1, errMessage) + } else { + null + } + } else { + val errorMsg = mediaResponse.result.toString + StreamingStage(jobRequest.request_id, jobRequest.client_key, null, stageName, "FAILED", "FAILED", iteration + 1, errorMsg); + } + } else { + StreamingStage(jobRequest.request_id, jobRequest.client_key, null, stageName, "FAILED", "FAILED", iteration + 1, jobRequest.err_message.getOrElse("")); + } + + if (streamStage != null) { + val counter = if (streamStage.status.equals("FINISHED")) config.successEventCount else { + if (streamStage.iteration <= config.maxRetries) config.retryEventCount else config.failedEventCount + } + metrics.incCounter(counter) + updateJobRequestStage(streamStage) + } + } + } + + def resubmitFailedJob(): Unit = { + val failedJobRequests = readFromDB(Map("status" -> "FAILED", "iteration" -> Map("type"-> "lte", "value" -> config.maxRetries))).toArray + failedJobRequests.foreach { jobRequest => + submitStreamJob(jobRequest) + } + } + + def submitStreamJob(jobRequest: JobRequest): Unit = { + + val requestData = JSONUtil.deserialize[Map[String, AnyRef]](jobRequest.request_data) + val mediaRequest = MediaRequest(UUID.randomUUID().toString, null, requestData) + val response:MediaResponse = mediaService.submitJob(mediaRequest) + val stageName = "STREAMING_JOB_SUBMISSION" + var streamStage:Option[StreamingStage] = None + + if (response.responseCode.equals("OK")) { + val jobId = response.result.getOrElse("job", Map()).asInstanceOf[Map[String, AnyRef]].getOrElse("id","").asInstanceOf[String]; + val jobStatus = response.result.getOrElse("job", Map()).asInstanceOf[Map[String, AnyRef]].getOrElse("status","").asInstanceOf[String]; + streamStage = Option(StreamingStage(jobRequest.request_id, jobRequest.client_key, jobId, stageName, jobStatus, "PROCESSING", jobRequest.iteration + 1)) + } else { + val errorMsg = response.result.toString + + streamStage = Option(StreamingStage(jobRequest.request_id, jobRequest.client_key, null, stageName, "FAILED", "FAILED", jobRequest.iteration + 1, errorMsg)); + } + + updateJobRequestStage(streamStage.get); + } + + private def updatePreviewUrl(contentId: String, streamingUrl: String, channel: String): Boolean = { + if(streamingUrl.nonEmpty && contentId.nonEmpty) { + val requestBody = "{\"request\": {\"content\": {\"streamingUrl\":\""+ streamingUrl +"\", \"migrationVersion\":1.2}}}" + val url = config.lpURL + config.contentV4Update + contentId + val headers = Map[String, String]("X-Channel-Id" -> channel, "Content-Type"->"application/json") + val response:HTTPResponse = httpUtil.patch(url, requestBody, headers) + + if(response.status == 200){ + true + } else { + logger.error("Error while updating previewUrl for content : " + contentId + " :: "+response.body) +// throw new Exception("Error while updating previewUrl for content : " + contentId + " :: "+response.body) + false + } + } else { + false + } + } + + def readFromDB(columns: Map[String, AnyRef]): List[JobRequest] = { + val selectWhere: Select.Where = QueryBuilder.select().all() + .from(dbKeyspace, dbTable) + .allowFiltering() + .where() + + columns.map(col => { + col._2 match { + case value: List[Any] => + selectWhere.and(QueryBuilder.in(col._1, value.asJava)) + case value: Map[String, AnyRef] => + if (value("type") == "lte") { + selectWhere.and(QueryBuilder.lte(col._1, value("value"))) + } else { + selectWhere.and(QueryBuilder.gte(col._1, value("value"))) + } + case _ => + selectWhere.and(QueryBuilder.eq(col._1, col._2)) + } + }) + + selectWhere.and(QueryBuilder.eq("job_name", VIDEO_STREAMING)) + + val result = cassandraUtil.find(selectWhere.toString).asScala.toList.map { jr => + JobRequest(jr.getString("client_key"), jr.getString("request_id"), Option(jr.getString("job_id")), jr.getString("status"), jr.getString("request_data"), jr.getInt("iteration"), stage=Option(jr.getString("stage")), stage_status=Option(jr.getString("stage_status")),job_name=Option(jr.getString("job_name"))) + } + result + } + + def saveJobRequest(jobRequest: JobRequest): Boolean = { + val query = QueryBuilder.insertInto(dbKeyspace, dbTable) + .value("client_key", jobRequest.client_key) + .value("request_id", jobRequest.request_id) + .value("job_id", jobRequest.job_id.getOrElse("")) + .value("status", jobRequest.status) + .value("request_data", jobRequest.request_data) + .value("iteration", jobRequest.iteration) + .value("dt_job_submitted", setDateColumn(jobRequest.dt_job_submitted).get) + .value("location", jobRequest.location.get) + .value("stage", jobRequest.stage.get) + .value("stage_status", jobRequest.stage_status.get) + .value("job_name", jobRequest.job_name.get) + + val result = cassandraUtil.session.execute(query) + result.wasApplied() + } + + def updateJobRequestStage(streamStage: StreamingStage): Boolean = { + val query = QueryBuilder.update(dbKeyspace, dbTable) + .`with`(QueryBuilder.set("job_id", streamStage.job_id)) + .and(QueryBuilder.set("stage", streamStage.stage)) + .and(QueryBuilder.set("stage_status", streamStage.stage_status)) + .and(QueryBuilder.set("status", streamStage.status)) + .and(QueryBuilder.set("iteration", streamStage.iteration)) + .and(QueryBuilder.set("err_message", streamStage.err_message)) + .where(QueryBuilder.eq("request_id", streamStage.request_id)) + .and(QueryBuilder.eq("client_key", streamStage.client_key)) + + cassandraUtil.upsert(query.toString) + } + + def setDateColumn(date: Option[DateTime]): Option[Long] = { + val timestamp = date.orNull + if (null == timestamp) None else Option(timestamp.getMillis) + } + + def closeConnection(): Unit = { + cassandraUtil.close() + } +} diff --git a/live-video-stream-generator/src/main/scala/org/sunbird/job/livevideostream/service/impl/AwsMediaServiceImpl.scala b/live-video-stream-generator/src/main/scala/org/sunbird/job/livevideostream/service/impl/AwsMediaServiceImpl.scala new file mode 100644 index 000000000..cc4385a65 --- /dev/null +++ b/live-video-stream-generator/src/main/scala/org/sunbird/job/livevideostream/service/impl/AwsMediaServiceImpl.scala @@ -0,0 +1,47 @@ +package org.sunbird.job.livevideostream.service.impl + +import org.sunbird.job.livevideostream.helpers.{MediaRequest, MediaResponse} +import org.sunbird.job.util.HttpUtil +import org.sunbird.job.livevideostream.helpers.{AwsResult, MediaRequest, MediaResponse, Response} +import org.sunbird.job.livevideostream.service.AwsMediaService +import org.sunbird.job.livevideostream.task.LiveVideoStreamGeneratorConfig + +import scala.collection.immutable.HashMap + +object AwsMediaServiceImpl extends AwsMediaService { + + override def submitJob(request: MediaRequest)(implicit config: LiveVideoStreamGeneratorConfig, httpUtil: HttpUtil): MediaResponse = { + val url = getApiUrl("job") + val reqBody = prepareJobRequestBody(request.request) + val header = getDefaultHeader("POST", url, reqBody) + val response:MediaResponse = Response.getResponse(httpUtil.post(url, reqBody, header)) + if (response.responseCode == "OK") Response.getSuccessResponse(AwsResult.getSubmitJobResult(response)) else response + } + + override def getJob(jobId: String)(implicit config: LiveVideoStreamGeneratorConfig, httpUtil: HttpUtil): MediaResponse = { + val response = getJobDetails(jobId) + if (response.responseCode == "OK") Response.getSuccessResponse(AwsResult.getJobResult(response)) else response + } + + override def getStreamingPaths(jobId: String)(implicit config: LiveVideoStreamGeneratorConfig, httpUtil: HttpUtil): MediaResponse = { + val region = config.getConfig("aws.region"); + val getResponse = getJobDetails(jobId) + val inputs: List[Map[String, AnyRef]] = getResponse.result.getOrElse("job", Map).asInstanceOf[Map[String, AnyRef]].getOrElse("settings", Map).asInstanceOf[Map[String, AnyRef]].getOrElse("inputs", List).asInstanceOf[List[Map[String, AnyRef]]] + val input: String = inputs.head.getOrElse("fileInput", "").toString + val outputGroups: List[Map[String, AnyRef]] = getResponse.result.getOrElse("job", Map).asInstanceOf[Map[String, AnyRef]].getOrElse("settings", Map).asInstanceOf[Map[String, AnyRef]].getOrElse("outputGroups", List).asInstanceOf[List[Map[String, AnyRef]]] + val outputGroupSettings = outputGroups.head.getOrElse("outputGroupSettings", Map).asInstanceOf[Map[String, AnyRef]] + val destination = outputGroupSettings.getOrElse("hlsGroupSettings", Map).asInstanceOf[Map[String, AnyRef]].getOrElse("destination", "").asInstanceOf[String] + val temp = destination.split("_") + val output = config.getConfig("aws.stream.protocol").toLowerCase() + "_" + temp(temp.length-1).replace("/","").trim() + val host = "https://s3." + region + ".amazonaws.com" + val streamUrl: String = input.replace("s3:/", host) + .replace("artifact", output) + .replace(".mp4", ".m3u8") + .replace(".webm", ".m3u8") + Response.getSuccessResponse(HashMap[String, AnyRef]("streamUrl" -> streamUrl)) + } + + override def listJobs(listJobsRequest: MediaRequest): MediaResponse = ??? + + override def cancelJob(cancelJobRequest: MediaRequest): MediaResponse = ??? +} diff --git a/live-video-stream-generator/src/main/scala/org/sunbird/job/livevideostream/service/impl/AzureMediaServiceImpl.scala b/live-video-stream-generator/src/main/scala/org/sunbird/job/livevideostream/service/impl/AzureMediaServiceImpl.scala new file mode 100644 index 000000000..b93eaf020 --- /dev/null +++ b/live-video-stream-generator/src/main/scala/org/sunbird/job/livevideostream/service/impl/AzureMediaServiceImpl.scala @@ -0,0 +1,56 @@ +package org.sunbird.job.livevideostream.service.impl + +import org.sunbird.job.livevideostream.service.AzureMediaService +import org.sunbird.job.livevideostream.task.LiveVideoStreamGeneratorConfig +import org.sunbird.job.util.HttpUtil +import org.sunbird.job.livevideostream.helpers.{AzureRequestBody, AzureResult, MediaRequest, MediaResponse, Response} + +import scala.collection.immutable.HashMap + + +object AzureMediaServiceImpl extends AzureMediaService { + + override def submitJob(request: MediaRequest)(implicit config: LiveVideoStreamGeneratorConfig, httpUtil: HttpUtil): MediaResponse = { + val inputUrl = request.request.getOrElse("artifactUrl", "").toString + val contentId = request.request.get("identifier").mkString + val jobId = contentId + "_" + System.currentTimeMillis() + val temp = inputUrl.splitAt(inputUrl.lastIndexOf("/") + 1) + val assetId = "asset-" + jobId + + val createAssetResponse = createAsset(assetId, jobId) + + if (createAssetResponse.responseCode.equalsIgnoreCase("OK")) { + val apiUrl = getApiUrl("job").replace("jobIdentifier", jobId) + val reqBody = AzureRequestBody.submit_job.replace("assetId", assetId).replace("baseInputUrl", temp._1).replace("inputVideoFile", temp._2) + val response:MediaResponse = Response.getResponse(httpUtil.put(apiUrl, reqBody, getDefaultHeader())) + if (response.responseCode == "OK") Response.getSuccessResponse(AzureResult.getSubmitJobResult(response)) else response + } else { + Response.getFailureResponse(createAssetResponse.result, "SERVER_ERROR", "Output Asset [ " + assetId + " ] Creation Failed for Job : " + jobId) + } + } + + override def getJob(jobId: String)(implicit config: LiveVideoStreamGeneratorConfig, httpUtil: HttpUtil): MediaResponse = { + val response = getJobDetails(jobId) + if (response.responseCode == "OK") Response.getSuccessResponse(AzureResult.getJobResult(response)) else response + } + + override def getStreamingPaths(jobId: String)(implicit config: LiveVideoStreamGeneratorConfig, httpUtil: HttpUtil): MediaResponse = { + val streamLocatorName = "sl-" + jobId + val assetName = "asset-" + jobId + val locatorResponse = createStreamingLocator(streamLocatorName, assetName) + if (locatorResponse.responseCode == "OK" || locatorResponse.responseCode == "CLIENT_ERROR") { + Response.getSuccessResponse(prepareStreamingUrl(streamLocatorName, jobId)) + } else { + Response.getFailureResponse(new HashMap[String, AnyRef], "SERVER_ERROR", "Streaming Locator [" + streamLocatorName + "] Creation Failed for Job : " + jobId) + } + } + + override def listJobs(listJobsRequest: MediaRequest): MediaResponse = { + null + } + + override def cancelJob(cancelJobRequest: MediaRequest): MediaResponse = { + null + } + +} diff --git a/live-video-stream-generator/src/main/scala/org/sunbird/job/livevideostream/service/impl/MediaServiceFactory.scala b/live-video-stream-generator/src/main/scala/org/sunbird/job/livevideostream/service/impl/MediaServiceFactory.scala new file mode 100644 index 000000000..f6587a451 --- /dev/null +++ b/live-video-stream-generator/src/main/scala/org/sunbird/job/livevideostream/service/impl/MediaServiceFactory.scala @@ -0,0 +1,17 @@ +package org.sunbird.job.livevideostream.service.impl + +import org.sunbird.job.livevideostream.exception.MediaServiceException +import org.sunbird.job.livevideostream.service.IMediaService +import org.sunbird.job.livevideostream.task.LiveVideoStreamGeneratorConfig + +object MediaServiceFactory { + + def getMediaService(config: LiveVideoStreamGeneratorConfig): IMediaService = { + val serviceType: String = config.getConfig("media_service_type") + serviceType match { + case "azure" => AzureMediaServiceImpl + case "aws" => AwsMediaServiceImpl + case _ => throw new MediaServiceException("ERR_INVALID_SERVICE_TYPE", "Please Provide Valid Media Service Name") + } + } +} diff --git a/live-video-stream-generator/src/main/scala/org/sunbird/job/livevideostream/task/LiveVideoStreamGeneratorConfig.scala b/live-video-stream-generator/src/main/scala/org/sunbird/job/livevideostream/task/LiveVideoStreamGeneratorConfig.scala new file mode 100644 index 000000000..0e3827be9 --- /dev/null +++ b/live-video-stream-generator/src/main/scala/org/sunbird/job/livevideostream/task/LiveVideoStreamGeneratorConfig.scala @@ -0,0 +1,60 @@ +package org.sunbird.job.livevideostream.task + +import java.util + +import com.typesafe.config.Config +import org.apache.flink.api.common.typeinfo.TypeInformation +import org.apache.flink.api.java.typeutils.TypeExtractor +import org.sunbird.job.BaseJobConfig +import org.sunbird.job.livevideostream.exception.MediaServiceException + +class LiveVideoStreamGeneratorConfig(override val config: Config) extends BaseJobConfig(config, "video-stream-generator") { + + private val serialVersionUID = 2905979434303791379L + + implicit val mapTypeInfo: TypeInformation[util.Map[String, AnyRef]] = TypeExtractor.getForClass(classOf[util.Map[String, AnyRef]]) + implicit val stringTypeInfo: TypeInformation[String] = TypeExtractor.getForClass(classOf[String]) + + // Kafka Topics Configuration + val kafkaInputTopic: String = config.getString("kafka.input.topic") + override val kafkaConsumerParallelism: Int = config.getInt("task.consumer.parallelism") + override val parallelism: Int = config.getInt("task.parallelism") + + val timerDuration = config.getInt("task.timer.duration") // Timer duration in sec. + val maxRetries = if (config.hasPath("task.max.retries")) config.getInt("task.max.retries") else 10 + + // Metric List + val totalEventsCount = "total-events-count" + val successEventCount = "success-events-count" + val failedEventCount = "failed-events-count" + val retryEventCount = "retry-events-count" + val skippedEventCount = "skipped-events-count" + + // Consumers + val videoStreamConsumer = "video-streaming-consumer" + val videoStreamGeneratorFunction = "manage-streaming-jobs" + + // Cassandra Configurations + val dbTable: String = config.getString("lms-cassandra.table") + val dbKeyspace: String = config.getString("lms-cassandra.keyspace") + val hierarchyPrimaryKey: List[String] = List("identifier") + + // LP Configurations + val lpURL: String = config.getString("service.content.basePath") + val contentV4Update = "/content/v4/system/update/" + + val jobStatus:util.List[String] = if(config.hasPath("media_service_job_success_status")) config.getStringList("media_service_job_success_status") else util.Arrays.asList("FINISHED", "COMPLETE") + + def getConfig(key: String): String = { + if (config.hasPath(key)) + config.getString(key) + else throw new MediaServiceException("CONFIG_NOT_FOUND", "Configuration for key [" + key + "] Not Found.") + } + + def getSystemConfig(key: String): String = { + val sysKey=key.replaceAll("\\.","_") + if (config.hasPath(sysKey)) + config.getString(sysKey) + else throw new MediaServiceException("CONFIG_NOT_FOUND", "Configuration for key [" + sysKey + "] Not Found.") + } +} diff --git a/relation-cache-updater/src/main/scala/org/sunbird/job/relationcache/task/RelationCacheUpdaterStreamTask.scala b/live-video-stream-generator/src/main/scala/org/sunbird/job/livevideostream/task/LiveVideoStreamGeneratorStreamTask.scala similarity index 55% rename from relation-cache-updater/src/main/scala/org/sunbird/job/relationcache/task/RelationCacheUpdaterStreamTask.scala rename to live-video-stream-generator/src/main/scala/org/sunbird/job/livevideostream/task/LiveVideoStreamGeneratorStreamTask.scala index 7e4757476..3b60870d0 100644 --- a/relation-cache-updater/src/main/scala/org/sunbird/job/relationcache/task/RelationCacheUpdaterStreamTask.scala +++ b/live-video-stream-generator/src/main/scala/org/sunbird/job/livevideostream/task/LiveVideoStreamGeneratorStreamTask.scala @@ -1,4 +1,4 @@ -package org.sunbird.job.relationcache.task +package org.sunbird.job.livevideostream.task import java.io.File import java.util @@ -8,24 +8,26 @@ import org.apache.flink.api.java.typeutils.TypeExtractor import org.apache.flink.api.java.utils.ParameterTool import org.apache.flink.streaming.api.scala.StreamExecutionEnvironment import org.sunbird.job.connector.FlinkKafkaConnector -import org.sunbird.job.relationcache.domain.Event -import org.sunbird.job.relationcache.functions.RelationCacheUpdater -import org.sunbird.job.util.FlinkUtil +import org.sunbird.job.livevideostream.domain.Event +import org.sunbird.job.livevideostream.functions.LiveVideoStreamGenerator +import org.sunbird.job.util.{FlinkUtil, HttpUtil} -class RelationCacheUpdaterStreamTask(config: RelationCacheUpdaterConfig, kafkaConnector: FlinkKafkaConnector) { +class LiveVideoStreamGeneratorStreamTask(config: LiveVideoStreamGeneratorConfig, kafkaConnector: FlinkKafkaConnector, httpUtil: HttpUtil) { def process(): Unit = { implicit val env: StreamExecutionEnvironment = FlinkUtil.getExecutionContext(config) implicit val eventTypeInfo: TypeInformation[Event] = TypeExtractor.getForClass(classOf[Event]) implicit val mapTypeInfo: TypeInformation[util.Map[String, AnyRef]] = TypeExtractor.getForClass(classOf[util.Map[String, AnyRef]]) implicit val stringTypeInfo: TypeInformation[String] = TypeExtractor.getForClass(classOf[String]) - val source = kafkaConnector.kafkaJobRequestSource[Event](config.kafkaInputTopic) - env.addSource(source).name(config.relationCacheConsumer) - .uid(config.relationCacheConsumer).setParallelism(config.kafkaConsumerParallelism) + val source = kafkaConnector.kafkaJobRequestSource[Event](config.kafkaInputTopic) + env.addSource(source).name(config.videoStreamConsumer) + .uid(config.videoStreamConsumer).setParallelism(config.kafkaConsumerParallelism) .rebalance - .process(new RelationCacheUpdater(config)) - .name("relation-cache-updater").uid("relation-cache-updater") + .keyBy(_.identifier) + .process(new LiveVideoStreamGenerator(config, httpUtil)) + .name(config.videoStreamGeneratorFunction) + .uid(config.videoStreamGeneratorFunction) .setParallelism(config.parallelism) env.execute(config.jobName) @@ -33,16 +35,17 @@ class RelationCacheUpdaterStreamTask(config: RelationCacheUpdaterConfig, kafkaCo } // $COVERAGE-OFF$ Disabling scoverage as the below code can only be invoked within flink cluster -object RelationCacheUpdaterStreamTask { +object LiveVideoStreamGeneratorStreamTask { def main(args: Array[String]): Unit = { val configFilePath = Option(ParameterTool.fromArgs(args).get("config.file.path")) val config = configFilePath.map { path => ConfigFactory.parseFile(new File(path)).resolve() - }.getOrElse(ConfigFactory.load("relation-cache-updater.conf").withFallback(ConfigFactory.systemEnvironment())) - val cacheUpdaterConfig = new RelationCacheUpdaterConfig(config) - val kafkaUtil = new FlinkKafkaConnector(cacheUpdaterConfig) - val task = new RelationCacheUpdaterStreamTask(cacheUpdaterConfig, kafkaUtil) + }.getOrElse(ConfigFactory.load("live-video-stream-generator.conf").withFallback(ConfigFactory.systemEnvironment())) + val videoStreamConfig = new LiveVideoStreamGeneratorConfig(config) + val kafkaUtil = new FlinkKafkaConnector(videoStreamConfig) + val httpUtil = new HttpUtil + val task = new LiveVideoStreamGeneratorStreamTask(videoStreamConfig, kafkaUtil, httpUtil) task.process() } } diff --git a/live-video-stream-generator/src/test/resources/job_request.cql b/live-video-stream-generator/src/test/resources/job_request.cql new file mode 100644 index 000000000..820a372b7 --- /dev/null +++ b/live-video-stream-generator/src/test/resources/job_request.cql @@ -0,0 +1,2 @@ + +INSERT INTO local_platform_db.job_request (client_key,request_id,dt_expiration,dt_file_created,dt_first_event,dt_job_completed,dt_job_processing,dt_job_submitted,dt_last_event,err_message,execution_time,file_size,input_events,iteration,job_id,job_name,latency,location,output_events,request_data,stage,stage_status,status) values ('SYSTEM_LP', '32722dd03b737ed32014a01b8c1f7c83', null, null, null, null, '2019-01-02 22:30:08.033+0000', '2018-12-18 20:11:03.088+0000', null, '', 0, 0, 0, 1, 'do_3126597193576939521909_1605816926271', 'VIDEO_STREAMING', 0, 'https://testurl.com/test.mp4', 0, '{"content_id":"do_3126597193576939521909","channel":"01254290140407398431","artifactUrl":"https://testurl.com/test.mp4"}', 'STREAMING_JOB_SUBMISSION', 'PROCESSING', 'PROCESSING'); \ No newline at end of file diff --git a/credential-generator/collection-cert-pre-processor/src/test/resources/logback-test.xml b/live-video-stream-generator/src/test/resources/logback-test.xml similarity index 100% rename from credential-generator/collection-cert-pre-processor/src/test/resources/logback-test.xml rename to live-video-stream-generator/src/test/resources/logback-test.xml diff --git a/live-video-stream-generator/src/test/resources/test.conf b/live-video-stream-generator/src/test/resources/test.conf new file mode 100644 index 000000000..b1c703229 --- /dev/null +++ b/live-video-stream-generator/src/test/resources/test.conf @@ -0,0 +1,74 @@ +include "base-test.conf" + +kafka { + input.topic = "sunbirddev.live.video.stream.request" + groupId = "sunbirddev-live-video-stream-generator-group" +} + +task { + consumer.parallelism = 1 + timer.duration = 10 + max.retries = 10 +} + +lms-cassandra { + keyspace = "local_platform_db" + table = "job_request" +} + +service { + content { + basePath = "http://dev.sunbirded.org/content" + } +} + +threshold.batch.read.interval = 60 // In sec +threshold.batch.read.size = 1000 +threshold.batch.write.size = 4 + + azure { + location = "centralindia" + tenant = "tenant name" + subscription_id = "subscription id" + + login { + endpoint="https://login.microsoftonline.com" + } + + api { + endpoint="https://management.azure.com" + version = "2018-07-01" + } + + account_name = "account name" + resource_group_name = "Resource Group Name" + + transform { + default = "media_transform_default" + hls = "media_transform_hls" + } + + stream { + base_url = "https://sunbirdspikemedia-inct.streaming.media.azure.net" + endpoint_name = "default" + protocol = "Hls" + policy_name = "Predefined_ClearStreamingOnly" + } + + token { + client_key = "client key" + client_secret = "client secret" + } +} + +azure_tenant="test_tenant" +azure_subscription_id="test_id" +azure_account_name="test_account_name" +azure_resource_group_name="test_resource_group_name" +azure_token_client_key="test_client_key" +azure_token_client_secret="test_client_secret" +elasticsearch.service.endpoint="test_service_endpoint" +elasticsearch.index.compositesearch.name="test_compositesearch_name" + +media_service_type="azure" + diff --git a/live-video-stream-generator/src/test/resources/test.cql b/live-video-stream-generator/src/test/resources/test.cql new file mode 100644 index 000000000..6ed1eca28 --- /dev/null +++ b/live-video-stream-generator/src/test/resources/test.cql @@ -0,0 +1,31 @@ +CREATE KEYSPACE IF NOT EXISTS local_platform_db WITH replication = { + 'class': 'SimpleStrategy', + 'replication_factor': '1' +}; + +CREATE TABLE IF NOT EXISTS local_platform_db.job_request ( + client_key text, + request_id text, + job_id text, + status text, + request_data text, + location text, + dt_file_created timestamp, + dt_first_event timestamp, + dt_last_event timestamp, + dt_expiration timestamp, + iteration int, + dt_job_submitted timestamp, + dt_job_processing timestamp, + dt_job_completed timestamp, + input_events int, + output_events int, + file_size bigint, + latency int, + execution_time bigint, + err_message text, + stage text, + stage_status text, + job_name text, + PRIMARY KEY (client_key, request_id) +); \ No newline at end of file diff --git a/live-video-stream-generator/src/test/scala/org/sunbird/job/fixture/EventFixture.scala b/live-video-stream-generator/src/test/scala/org/sunbird/job/fixture/EventFixture.scala new file mode 100644 index 000000000..c56e49461 --- /dev/null +++ b/live-video-stream-generator/src/test/scala/org/sunbird/job/fixture/EventFixture.scala @@ -0,0 +1,14 @@ +package org.sunbird.job.fixture + +object EventFixture { + + val EVENT_1: String = + """ + |{"eid":"BE_JOB_REQUEST","ets":1598956686981,"mid":"LP.1598956686981.a260af12-cd9b-4ffd-a525-1d944df47c61","actor":{"id":"Post Publish Processor","type":"System"},"context":{"pdata":{"ver":"1.0","id":"org.ekstep.platform"},"channel":"01254290140407398431","env":"sunbirddev"},"object":{"ver":"1587632475439","id":"do_3126597193576939521910"},"edata":{"action":"post-publish-process","iteration":1,"identifier":"do_3126597193576939521910","artifactUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/assets/do_1126980548391075841140/ariel-view-of-earth.mp4","mimeType":"video/mp4","contentType":"Resource","pkgVersion":1,"status":"Live"}} + |""".stripMargin + + val EVENT_2: String = + """ + |{"eid":"BE_JOB_REQUEST","ets":1598956686981,"mid":"LP.1598956686981.a260af12-cd9b-4ffd-a525-1d944df47c61","actor":{"id":"Post Publish Processor","type":"System"},"context":{"pdata":{"ver":"1.0","id":"org.ekstep.platform"},"channel":"01254290140407398431","env":"sunbirddev"},"object":{"ver":"1587632475439","id":"do_3126597193576939521910"},"edata":{"action":"post-publish-process","iteration":1,"identifier":"do_3126597193576939521910","artifactUrl":"https://sunbirded.com/test.mp4","mimeType":"video","contentType":"Resource","pkgVersion":1,"status":"Live"}} + |""".stripMargin +} \ No newline at end of file diff --git a/live-video-stream-generator/src/test/scala/org/sunbird/job/spec/LiveVideoStreamGeneratorTaskTestSpec.scala b/live-video-stream-generator/src/test/scala/org/sunbird/job/spec/LiveVideoStreamGeneratorTaskTestSpec.scala new file mode 100644 index 000000000..d64fee22b --- /dev/null +++ b/live-video-stream-generator/src/test/scala/org/sunbird/job/spec/LiveVideoStreamGeneratorTaskTestSpec.scala @@ -0,0 +1,129 @@ +package org.sunbird.job.spec + +import com.datastax.driver.core.Row +import com.typesafe.config.{Config, ConfigFactory} +import org.apache.flink.api.common.typeinfo.TypeInformation +import org.apache.flink.api.java.typeutils.TypeExtractor +import org.apache.flink.runtime.testutils.MiniClusterResourceConfiguration +import org.apache.flink.streaming.api.functions.source.SourceFunction +import org.apache.flink.streaming.api.functions.source.SourceFunction.SourceContext +import org.apache.flink.test.util.MiniClusterWithClientResource +import org.cassandraunit.CQLDataLoader +import org.cassandraunit.dataset.cql.FileCQLDataSet +import org.cassandraunit.utils.EmbeddedCassandraServerHelper +import org.mockito.ArgumentMatchers.{any, anyString, contains} +import org.mockito.Mockito +import org.mockito.Mockito._ +import org.sunbird.job.connector.FlinkKafkaConnector +import org.sunbird.job.fixture.EventFixture +import org.sunbird.job.livevideostream.domain.Event +import org.sunbird.job.livevideostream.service.IMediaService +import org.sunbird.job.livevideostream.task.{LiveVideoStreamGeneratorConfig, LiveVideoStreamGeneratorStreamTask} +import org.sunbird.job.util.{CassandraUtil, HTTPResponse, HttpUtil, JSONUtil} +import org.sunbird.spec.{BaseMetricsReporter, BaseTestSpec} + +import java.util + +class LiveVideoStreamGeneratorTaskTestSpec extends BaseTestSpec { + + implicit val mapTypeInfo: TypeInformation[java.util.Map[String, AnyRef]] = TypeExtractor.getForClass(classOf[java.util.Map[String, AnyRef]]) + + val flinkCluster = new MiniClusterWithClientResource(new MiniClusterResourceConfiguration.Builder() + .setConfiguration(testConfiguration()) + .setNumberSlotsPerTaskManager(1) + .setNumberTaskManagers(1) + .build) + val mockKafkaUtil: FlinkKafkaConnector = mock[FlinkKafkaConnector](Mockito.withSettings().serializable()) + val mediaService: IMediaService = mock[IMediaService](Mockito.withSettings().serializable()) + val config: Config = ConfigFactory.load("test.conf") + val jobConfig: LiveVideoStreamGeneratorConfig = new LiveVideoStreamGeneratorConfig(config) + val mockHttpUtil:HttpUtil = mock[HttpUtil](Mockito.withSettings().serializable()) +// val httpUtil:HttpUtil = new HttpUtil + var cassandraUtil: CassandraUtil = _ + var currentMilliSecond = 1605816926271L + + val accessTokenResp = """{"token_type":"Bearer","expires_in":"3599","ext_expires_in":"3599","expires_on":"1605789466","not_before":"1605785566","resource":"https://management.core.windows.net/","access_token":"testToken"}""" + val assetJson = """{"name":"asset-do_3126597193576939521910_1605816926271","id":"/subscriptions/aaaaaaaa-6899-4ef6-aaaa-5a185b3b7254/resourceGroups/sunbird-devnew-env/providers/Microsoft.Media/mediaservices/sunbirddevmedia/assets/asset-do_3126597193576939521910_1605816926271","type":"Microsoft.Media/mediaservices/assets","properties":{"assetId":"aaaaaaa-13bb-45c7-aaaa-32ac2e97cf12","created":"2020-11-19T20:16:54.463Z","lastModified":"2020-11-19T20:20:33.613Z","alternateId":"asset-do_3126597193576939521910_1605816926271","description":"Output Asset for do_3126597193576939521910_1605816926271","container":"asset-aaaaaaa-13bb-45c7-b186-32ac2e97cf12","storageAccountName":"sunbirddevmedia","storageEncryptionFormat":"None"}}""" + val submitJobJson = """{"name":"do_3126597193576939521910_1605816926271","id":"/subscriptions/aaaaaaaa-6899-4ef6-8a14-5a185b3b7254/resourceGroups/sunbird-devnew-env/providers/Microsoft.Media/mediaservices/sunbirddevmedia/transforms/media_transform_default/jobs/do_3126597193576939521910_1605816926271","type":"Microsoft.Media/mediaservices/transforms/jobs","properties":{"created":"2020-11-19T20:26:49.7953248Z","state":"Scheduled","input":{"@odata.type":"#Microsoft.Media.JobInputHttp","files":["test.mp4"],"baseUri":"https://sunbirded.com/"},"lastModified":"2020-11-19T20:26:49.7953248Z","outputs":[{"@odata.type":"#Microsoft.Media.JobOutputAsset","state":"Queued","progress":0,"label":"BuiltInStandardEncoderPreset_0","assetName":"asset-do_3126597193576939521910_1605816926271"}],"priority":"Normal","correlationData":{}}}""" + val getJobJson = """{"name":"do_3126597193576939521910_1605816926271","job":{"status":"Finished"},"properties":{"created":"2020-11-19T20:26:49.7953248Z","state":"Scheduled","input":{"@odata.type":"#Microsoft.Media.JobInputHttp","files":["test.mp4"],"baseUri":"https://sunbirded.com/"},"lastModified":"2020-11-19T20:26:49.7953248Z","outputs":[{"@odata.type":"#Microsoft.Media.JobOutputAsset","state":"FINISHED","progress":0,"label":"BuiltInStandardEncoderPreset_0","assetName":"asset-do_3126597193576939521910_1605816926271"}],"priority":"Normal","correlationData":{}}}""" + val getStreamUrlJson = """{"streamingPaths":[{"streamingProtocol":"Hls","encryptionScheme":"NoEncryption","paths":["/4ddff5cd-6479-4572-bc95-ebad508b65ce/ariel-view-of-earth.ism/manifest(format=m3u8-aapl)","/4ddff5cd-6479-4572-bc95-ebad508b65ce/ariel-view-of-earth.ism/manifest(format=m3u8-cmaf)"]},{"streamingProtocol":"Dash","encryptionScheme":"NoEncryption","paths":["/4ddff5cd-6479-4572-bc95-ebad508b65ce/ariel-view-of-earth.ism/manifest(format=mpd-time-csf)","/4ddff5cd-6479-4572-bc95-ebad508b65ce/ariel-view-of-earth.ism/manifest(format=mpd-time-cmaf)"]},{"streamingProtocol":"SmoothStreaming","encryptionScheme":"NoEncryption","paths":["/4ddff5cd-6479-4572-bc95-ebad508b65ce/ariel-view-of-earth.ism/manifest"]}],"downloadPaths":[]}""" + val getStreamLocatorJson = """{"properties":{"streamingLocatorId":"adcacdd-13bb-45c7-aaaa-32ac2e97cf12"}}""" + + override protected def beforeAll(): Unit = { + EmbeddedCassandraServerHelper.startEmbeddedCassandra(80000L) + cassandraUtil = new CassandraUtil(jobConfig.lmsDbHost, jobConfig.lmsDbPort, jobConfig) + val session = cassandraUtil.session + val dataLoader = new CQLDataLoader(session); + dataLoader.load(new FileCQLDataSet(getClass.getResource("/test.cql").getPath, true, true)); + testCassandraUtil(cassandraUtil) + BaseMetricsReporter.gaugeMetrics.clear() + flinkCluster.before() + super.beforeAll() + } + + override protected def afterAll(): Unit = { + try { + EmbeddedCassandraServerHelper.cleanEmbeddedCassandra() + } catch { + case ex: Exception => { + } + } + flinkCluster.after() + super.afterAll() + } + + override protected def afterEach():Unit = { + super.afterEach() + } + + ignore should "submit a job" in { + when(mockKafkaUtil.kafkaJobRequestSource[Event](jobConfig.kafkaInputTopic)).thenReturn(new VideoStreamGeneratorMapSource) + + when(mockHttpUtil.post_map(contains("/oauth2/token"), any[Map[String, AnyRef]](), any[Map[String, String]]())).thenReturn(HTTPResponse(200, accessTokenResp)) + when(mockHttpUtil.put(contains("/providers/Microsoft.Media/mediaServices/"+jobConfig.getSystemConfig("azure.account.name")+"/assets/asset-"), anyString(), any())).thenReturn(HTTPResponse(200, assetJson)) + when(mockHttpUtil.put(contains("transforms/media_transform_default/jobs"), anyString(), any())).thenReturn(HTTPResponse(200, submitJobJson)) + when(mockHttpUtil.get(contains("transforms/media_transform_default/jobs"), any())).thenReturn(HTTPResponse(200, getJobJson)) + + when(mockHttpUtil.post(contains("/streamingLocators/sl-do_3126597193576939521910_1605816926271/listPaths?api-version="), any(), any())).thenReturn(HTTPResponse(200, getStreamUrlJson)) + when(mockHttpUtil.put(contains("/streamingLocators/sl-do_3126597193576939521910_1605816926271?api-version="), any(), any())).thenReturn(HTTPResponse(400, getJobJson)) + when(mockHttpUtil.get(contains("/streamingLocators/sl-do_3126597193576939521910_1605816926271?api-version="), any())).thenReturn(HTTPResponse(200, getStreamLocatorJson)) + when(mockHttpUtil.patch(contains(jobConfig.contentV4Update), any(), any())).thenReturn(HTTPResponse(200, getJobJson)) + + new LiveVideoStreamGeneratorStreamTask(jobConfig, mockKafkaUtil, mockHttpUtil).process() + val event1Progress = readFromCassandra(EventFixture.EVENT_1) + + BaseMetricsReporter.gaugeMetrics(s"${jobConfig.jobName}.${jobConfig.totalEventsCount}").getValue() should be(2) + BaseMetricsReporter.gaugeMetrics(s"${jobConfig.jobName}.${jobConfig.skippedEventCount}").getValue() should be(1) + BaseMetricsReporter.gaugeMetrics(s"${jobConfig.jobName}.${jobConfig.successEventCount}").getValue() should be(1) + event1Progress.size() should be(1) + + event1Progress.forEach(col => { + col.getObject("status") should be("FINISHED") + }) + + } + + def testCassandraUtil(cassandraUtil: CassandraUtil): Unit = { + cassandraUtil.reconnect() + } + + def readFromCassandra(event: String): util.List[Row] = { + val event1 = JSONUtil.deserialize[Map[String, Any]](event) + val contentId = event1("object").asInstanceOf[Map[String, AnyRef]]("id") + val query = s"select * from ${jobConfig.dbKeyspace}.${jobConfig.dbTable} where job_id='${contentId}_$currentMilliSecond' ALLOW FILTERING;" + cassandraUtil.find(query) + } + +} + +class VideoStreamGeneratorMapSource extends SourceFunction[Event] { + + override def run(ctx: SourceContext[Event]) { + ctx.collect(new Event(JSONUtil.deserialize[util.Map[String, Any]](EventFixture.EVENT_1),0, 10)) + ctx.collect(new Event(JSONUtil.deserialize[util.Map[String, Any]](EventFixture.EVENT_2),0, 11)) + + } + + override def cancel(): Unit = {} + +} \ No newline at end of file diff --git a/live-video-stream-generator/src/test/scala/org/sunbird/job/spec/service/LiveVideoStreamServiceTestSpec.scala b/live-video-stream-generator/src/test/scala/org/sunbird/job/spec/service/LiveVideoStreamServiceTestSpec.scala new file mode 100644 index 000000000..b32aa9725 --- /dev/null +++ b/live-video-stream-generator/src/test/scala/org/sunbird/job/spec/service/LiveVideoStreamServiceTestSpec.scala @@ -0,0 +1,96 @@ +package org.sunbird.job.spec.service + +import java.util +import com.datastax.driver.core.Row +import com.typesafe.config.{Config, ConfigFactory} +import org.cassandraunit.CQLDataLoader +import org.cassandraunit.dataset.cql.FileCQLDataSet +import org.cassandraunit.utils.EmbeddedCassandraServerHelper +import org.joda.time.DateTimeUtils +import org.mockito.ArgumentMatchers.{any, anyString, contains} +import org.mockito.Mockito +import org.mockito.Mockito._ +import org.sunbird.job.fixture.EventFixture +import org.sunbird.job.livevideostream.service.LiveVideoStreamService +import org.sunbird.job.livevideostream.task.LiveVideoStreamGeneratorConfig +import org.sunbird.spec.BaseTestSpec +import org.sunbird.job.util.{CassandraUtil, HTTPResponse, HttpUtil, JSONUtil} +import org.sunbird.job.livevideostream.domain.Event +import org.sunbird.job.Metrics + +class LiveVideoStreamServiceTestSpec extends BaseTestSpec { + var cassandraUtil: CassandraUtil = _ + val config: Config = ConfigFactory.load("test.conf") + lazy val jobConfig: LiveVideoStreamGeneratorConfig = new LiveVideoStreamGeneratorConfig(config) + val httpUtil: HttpUtil = new HttpUtil + val mockHttpUtil:HttpUtil = mock[HttpUtil](Mockito.withSettings().serializable()) + val metricJson = s"""{"${jobConfig.totalEventsCount}": 0, "${jobConfig.skippedEventCount}": 0}""" + val mockMetrics = mock[Metrics](Mockito.withSettings().serializable()) + + val accessTokenResp = """{"token_type":"Bearer","expires_in":"3599","ext_expires_in":"3599","expires_on":"1605789466","not_before":"1605785566","resource":"https://management.core.windows.net/","access_token":"testToken"}""" + val assetJson = """{"name":"asset-do_3126597193576939521910_1605816926271","id":"/subscriptions/aaaaaaaa-6899-4ef6-aaaa-5a185b3b7254/resourceGroups/sunbird-devnew-env/providers/Microsoft.Media/mediaservices/sunbirddevmedia/assets/asset-do_3126597193576939521910_1605816926271","type":"Microsoft.Media/mediaservices/assets","properties":{"assetId":"aaaaaaa-13bb-45c7-aaaa-32ac2e97cf12","created":"2020-11-19T20:16:54.463Z","lastModified":"2020-11-19T20:20:33.613Z","alternateId":"asset-do_3126597193576939521910_1605816926271","description":"Output Asset for do_3126597193576939521910_1605816926271","container":"asset-aaaaaaa-13bb-45c7-b186-32ac2e97cf12","storageAccountName":"sunbirddevmedia","storageEncryptionFormat":"None"}}""" + val submitJobJson = """{"name":"do_3126597193576939521910_1605816926271","id":"/subscriptions/aaaaaaaa-6899-4ef6-8a14-5a185b3b7254/resourceGroups/sunbird-devnew-env/providers/Microsoft.Media/mediaservices/sunbirddevmedia/transforms/media_transform_default/jobs/do_3126597193576939521910_1605816926271","type":"Microsoft.Media/mediaservices/transforms/jobs","properties":{"created":"2020-11-19T20:26:49.7953248Z","state":"Scheduled","input":{"@odata.type":"#Microsoft.Media.JobInputHttp","files":["test.mp4"],"baseUri":"https://sunbirded.com/"},"lastModified":"2020-11-19T20:26:49.7953248Z","outputs":[{"@odata.type":"#Microsoft.Media.JobOutputAsset","state":"Queued","progress":0,"label":"BuiltInStandardEncoderPreset_0","assetName":"asset-do_3126597193576939521910_1605816926271"}],"priority":"Normal","correlationData":{}}}""" + val getJobJson = """{"name":"do_3126597193576939521910_1605816926271","job":{"status":"Finished"},"properties":{"created":"2020-11-19T20:26:49.7953248Z","state":"Scheduled","input":{"@odata.type":"#Microsoft.Media.JobInputHttp","files":["test.mp4"],"baseUri":"https://sunbirded.com/"},"lastModified":"2020-11-19T20:26:49.7953248Z","outputs":[{"@odata.type":"#Microsoft.Media.JobOutputAsset","state":"FINISHED","progress":0,"label":"BuiltInStandardEncoderPreset_0","assetName":"asset-do_3126597193576939521910_1605816926271"}],"priority":"Normal","correlationData":{}}}""" + val getStreamUrlJson = """{"streamingPaths":[{"streamingProtocol":"Hls","encryptionScheme":"NoEncryption","paths":["/4ddff5cd-6479-4572-bc95-ebad508b65ce/ariel-view-of-earth.ism/manifest(format=m3u8-aapl)","/4ddff5cd-6479-4572-bc95-ebad508b65ce/ariel-view-of-earth.ism/manifest(format=m3u8-cmaf)"]},{"streamingProtocol":"Dash","encryptionScheme":"NoEncryption","paths":["/4ddff5cd-6479-4572-bc95-ebad508b65ce/ariel-view-of-earth.ism/manifest(format=mpd-time-csf)","/4ddff5cd-6479-4572-bc95-ebad508b65ce/ariel-view-of-earth.ism/manifest(format=mpd-time-cmaf)"]},{"streamingProtocol":"SmoothStreaming","encryptionScheme":"NoEncryption","paths":["/4ddff5cd-6479-4572-bc95-ebad508b65ce/ariel-view-of-earth.ism/manifest"]}],"downloadPaths":[]}""" + val getStreamLocatorJson = """{"properties":{"streamingLocatorId":"adcacdd-13bb-45c7-aaaa-32ac2e97cf12"}}""" + + override protected def beforeAll(): Unit = { + DateTimeUtils.setCurrentMillisFixed(1605816926271L); + EmbeddedCassandraServerHelper.startEmbeddedCassandra(80000L) + cassandraUtil = new CassandraUtil(jobConfig.lmsDbHost, jobConfig.lmsDbPort, jobConfig) + val session = cassandraUtil.session + val dataLoader = new CQLDataLoader(session); + dataLoader.load(new FileCQLDataSet(getClass.getResource("/test.cql").getPath, true, true)); + testCassandraUtil(cassandraUtil) + super.beforeAll() + } + + override protected def afterAll(): Unit = { + DateTimeUtils.setCurrentMillisSystem(); + try { + EmbeddedCassandraServerHelper.cleanEmbeddedCassandra() + } catch { + case ex: Exception => { + } + } + super.afterAll() + } + + "VideoStreamService" should "submit job request" in { + when(mockHttpUtil.post_map(contains("/oauth2/token"), any[Map[String, AnyRef]](), any[Map[String, String]]())).thenReturn(HTTPResponse(200, accessTokenResp)) + when(mockHttpUtil.put(contains("/providers/Microsoft.Media/mediaServices/"+jobConfig.getSystemConfig("azure.account.name")+"/assets/asset-"), anyString(), any())).thenReturn(HTTPResponse(200, assetJson)) + when(mockHttpUtil.put(contains("transforms/media_transform_default/jobs"), anyString(), any())).thenReturn(HTTPResponse(200, submitJobJson)) + when(mockHttpUtil.get(contains("transforms/media_transform_default/jobs"), any())).thenReturn(HTTPResponse(200, getJobJson)) + + when(mockHttpUtil.post(contains("/streamingLocators/sl-do_3126597193576939521910_1605816926271/listPaths?api-version="), any(), any())).thenReturn(HTTPResponse(200, getStreamUrlJson)) + when(mockHttpUtil.put(contains("/streamingLocators/sl-do_3126597193576939521910_1605816926271?api-version="), any(), any())).thenReturn(HTTPResponse(400, getJobJson)) + when(mockHttpUtil.get(contains("/streamingLocators/sl-do_3126597193576939521910_1605816926271?api-version="), any())).thenReturn(HTTPResponse(200, getStreamLocatorJson)) + when(mockHttpUtil.patch(contains(jobConfig.contentV4Update), any(), any())).thenReturn(HTTPResponse(200, getJobJson)) + doNothing().when(mockMetrics).incCounter(any()) + + val eventMap1 = new Event(JSONUtil.deserialize[util.Map[String, Any]](EventFixture.EVENT_1),0, 12) + + val videoStreamService = new LiveVideoStreamService()(jobConfig, mockHttpUtil); + videoStreamService.submitJobRequest(eventMap1.eData) + videoStreamService.processJobRequest(mockMetrics) + + val event1Progress = readFromCassandra(EventFixture.EVENT_1) + event1Progress.size() should be(1) + + event1Progress.forEach(col => { + col.getObject("status") should be("FINISHED") + }) + + } + + def testCassandraUtil(cassandraUtil: CassandraUtil): Unit = { + cassandraUtil.reconnect() + } + + def readFromCassandra(event: String): util.List[Row] = { + val event1 = JSONUtil.deserialize[Map[String, Any]](event) + val contentId = event1("object").asInstanceOf[Map[String, AnyRef]]("id") + val query = s"select * from ${jobConfig.dbKeyspace}.${jobConfig.dbTable} where job_id='${contentId}_1605816926271' ALLOW FILTERING;" + cassandraUtil.find(query) + } +} diff --git a/mvc-indexer/src/main/scala/org/sunbird/job/mvcindexer/functions/MVCIndexer.scala b/mvc-indexer/src/main/scala/org/sunbird/job/mvcindexer/functions/MVCIndexer.scala index c80a880e1..f89635616 100644 --- a/mvc-indexer/src/main/scala/org/sunbird/job/mvcindexer/functions/MVCIndexer.scala +++ b/mvc-indexer/src/main/scala/org/sunbird/job/mvcindexer/functions/MVCIndexer.scala @@ -26,7 +26,7 @@ class MVCIndexer(config: MVCIndexerConfig, var esUtil: ElasticSearchUtil, httpUt if (esUtil == null) { esUtil = new ElasticSearchUtil(config.esConnectionInfo, config.mvcProcessorIndex, config.mvcProcessorIndexType) } - cassandraUtil = new CassandraUtil(config.lmsDbHost, config.lmsDbPort) + cassandraUtil = new CassandraUtil(config.lmsDbHost, config.lmsDbPort, config) mvcIndexerService = new MVCIndexerService(config, esUtil, httpUtil, cassandraUtil) } diff --git a/mvc-indexer/src/test/scala/org/sunbird/job/spec/MVCProcessorIndexerTaskTestSpec.scala b/mvc-indexer/src/test/scala/org/sunbird/job/spec/MVCProcessorIndexerTaskTestSpec.scala index a069cc3c8..de6ad4b53 100644 --- a/mvc-indexer/src/test/scala/org/sunbird/job/spec/MVCProcessorIndexerTaskTestSpec.scala +++ b/mvc-indexer/src/test/scala/org/sunbird/job/spec/MVCProcessorIndexerTaskTestSpec.scala @@ -65,7 +65,7 @@ class MVCProcessorIndexerTaskTestSpec extends BaseTestSpec { esServer.start(9200) EmbeddedCassandraServerHelper.startEmbeddedCassandra(80000L) - cassandraUtil = new CassandraUtil(jobConfig.lmsDbHost, jobConfig.lmsDbPort) + cassandraUtil = new CassandraUtil(jobConfig.lmsDbHost, jobConfig.lmsDbPort, jobConfig) val session = cassandraUtil.session val dataLoader = new CQLDataLoader(session); dataLoader.load(new FileCQLDataSet(getClass.getResource("/test.cql").getPath, true, true)); diff --git a/mvc-indexer/src/test/scala/org/sunbird/job/spec/service/MVCProcessorIndexerServiceTestSpec.scala b/mvc-indexer/src/test/scala/org/sunbird/job/spec/service/MVCProcessorIndexerServiceTestSpec.scala index 90b149735..a433b34ee 100644 --- a/mvc-indexer/src/test/scala/org/sunbird/job/spec/service/MVCProcessorIndexerServiceTestSpec.scala +++ b/mvc-indexer/src/test/scala/org/sunbird/job/spec/service/MVCProcessorIndexerServiceTestSpec.scala @@ -38,7 +38,7 @@ class MVCProcessorIndexerServiceTestSpec extends BaseTestSpec { override protected def beforeAll(): Unit = { EmbeddedCassandraServerHelper.startEmbeddedCassandra(80000L) - cassandraUtil = new CassandraUtil(jobConfig.lmsDbHost, jobConfig.lmsDbPort) + cassandraUtil = new CassandraUtil(jobConfig.lmsDbHost, jobConfig.lmsDbPort, jobConfig) val session = cassandraUtil.session val dataLoader = new CQLDataLoader(session); dataLoader.load(new FileCQLDataSet(getClass.getResource("/test.cql").getPath, true, true)); diff --git a/pom.xml b/pom.xml index 2e9a8e2f4..3de752d78 100644 --- a/pom.xml +++ b/pom.xml @@ -27,22 +27,25 @@ jobs-core - relation-cache-updater - activity-aggregate-updater + + post-publish-processor - credential-generator + publish-pipeline - video-stream-generator search-indexer - enrolment-reconciliation + auto-creator-v2 content-auto-creator - asset-enrichment audit-history-indexer audit-event-generator - metrics-data-transformer + qrcode-image-generator dialcode-context-updater + cassandra-data-migration + live-video-stream-generator + csp-migrator + asset-enrichment + video-stream-generator @@ -100,6 +103,20 @@ ${scala.maj.version} + + + + javax.ws.rs + javax.ws.rs-api + 2.1.1 + + + jakarta.ws.rs + jakarta.ws.rs-api + 2.1.6 + + + diff --git a/post-publish-processor/src/main/resources/post-publish-processor.conf b/post-publish-processor/src/main/resources/post-publish-processor.conf index c9819d711..d7a09751f 100644 --- a/post-publish-processor/src/main/resources/post-publish-processor.conf +++ b/post-publish-processor/src/main/resources/post-publish-processor.conf @@ -40,4 +40,15 @@ service { dialcode { linkable.primaryCategory = ["Course"] -} \ No newline at end of file +} + +cloudstorage.metadata.replace_absolute_path=false +cloudstorage.read_base_path="https://sunbirddev.blob.core.windows.net" +cloudstorage.write_base_path=["https://sunbirddev.blob.core.windows.net","https://obj.dev.sunbird.org"] +cloudstorage.metadata.list=["appIcon","posterImage","artifactUrl","downloadUrl","variants","previewUrl","pdfUrl", "streamingUrl", "toc_url"] + +cloud_storage_type="azure" +cloud_storage_key="" +cloud_storage_secret="" +cloud_storage_container="" +cloud_storage_endpoint="" \ No newline at end of file diff --git a/post-publish-processor/src/main/scala/org/sunbird/job/postpublish/functions/DIALCodeLinkFunction.scala b/post-publish-processor/src/main/scala/org/sunbird/job/postpublish/functions/DIALCodeLinkFunction.scala index 0e16cc302..fadb467c8 100644 --- a/post-publish-processor/src/main/scala/org/sunbird/job/postpublish/functions/DIALCodeLinkFunction.scala +++ b/post-publish-processor/src/main/scala/org/sunbird/job/postpublish/functions/DIALCodeLinkFunction.scala @@ -24,8 +24,8 @@ class DIALCodeLinkFunction(config: PostPublishProcessorConfig, httpUtil: HttpUti override def open(parameters: Configuration): Unit = { super.open(parameters) - cassandraUtil = new CassandraUtil(config.dbHost, config.dbPort) - neo4JUtil = new Neo4JUtil(config.graphRoutePath, config.graphName) + cassandraUtil = new CassandraUtil(config.dbHost, config.dbPort, config) + neo4JUtil = new Neo4JUtil(config.graphRoutePath, config.graphName, config) } override def close(): Unit = { diff --git a/post-publish-processor/src/main/scala/org/sunbird/job/postpublish/functions/PostPublishEventRouter.scala b/post-publish-processor/src/main/scala/org/sunbird/job/postpublish/functions/PostPublishEventRouter.scala index 25487f146..2c69ef8ca 100644 --- a/post-publish-processor/src/main/scala/org/sunbird/job/postpublish/functions/PostPublishEventRouter.scala +++ b/post-publish-processor/src/main/scala/org/sunbird/job/postpublish/functions/PostPublishEventRouter.scala @@ -25,8 +25,8 @@ class PostPublishEventRouter(config: PostPublishProcessorConfig, httpUtil: HttpU override def open(parameters: Configuration): Unit = { super.open(parameters) - cassandraUtil = new CassandraUtil(config.dbHost, config.dbPort) - neo4JUtil = new Neo4JUtil(config.graphRoutePath, config.graphName) + cassandraUtil = new CassandraUtil(config.dbHost, config.dbPort, config) + neo4JUtil = new Neo4JUtil(config.graphRoutePath, config.graphName, config) } override def close(): Unit = { diff --git a/post-publish-processor/src/test/scala/org/sunbird/job/postpublish/helpers/DialHelperTest.scala b/post-publish-processor/src/test/scala/org/sunbird/job/postpublish/helpers/DialHelperTest.scala index a03f8f344..aecae381e 100644 --- a/post-publish-processor/src/test/scala/org/sunbird/job/postpublish/helpers/DialHelperTest.scala +++ b/post-publish-processor/src/test/scala/org/sunbird/job/postpublish/helpers/DialHelperTest.scala @@ -31,7 +31,7 @@ class DialHelperTest extends FlatSpec with BeforeAndAfterAll with Matchers with override protected def beforeAll(): Unit = { super.beforeAll() EmbeddedCassandraServerHelper.startEmbeddedCassandra(80000L) - cassandraUtil = new CassandraUtil(jobConfig.dbHost, jobConfig.dbPort) + cassandraUtil = new CassandraUtil(jobConfig.dbHost, jobConfig.dbPort, jobConfig) val session = cassandraUtil.session val dataLoader = new CQLDataLoader(session) dataLoader.load(new FileCQLDataSet(getClass.getResource("/test.cql").getPath, true, true)) diff --git a/post-publish-processor/src/test/scala/org/sunbird/job/spec/PostPublishProcessorTaskTestSpec.scala b/post-publish-processor/src/test/scala/org/sunbird/job/spec/PostPublishProcessorTaskTestSpec.scala index 1e1b6bdc2..0723ec59f 100644 --- a/post-publish-processor/src/test/scala/org/sunbird/job/spec/PostPublishProcessorTaskTestSpec.scala +++ b/post-publish-processor/src/test/scala/org/sunbird/job/spec/PostPublishProcessorTaskTestSpec.scala @@ -68,7 +68,7 @@ class PostPublishProcessorTaskTestSpec extends BaseTestSpec { override protected def beforeAll(): Unit = { super.beforeAll() EmbeddedCassandraServerHelper.startEmbeddedCassandra(80000L) - cassandraUtil = new CassandraUtil(jobConfig.dbHost, jobConfig.dbPort) + cassandraUtil = new CassandraUtil(jobConfig.dbHost, jobConfig.dbPort, jobConfig) val session = cassandraUtil.session val dataLoader = new CQLDataLoader(session) dataLoader.load(new FileCQLDataSet(getClass.getResource("/test.cql").getPath, true, true)) diff --git a/publish-pipeline/content-publish/src/main/resources/content-publish.conf b/publish-pipeline/content-publish/src/main/resources/content-publish.conf index f806a6e2d..4f41bef70 100644 --- a/publish-pipeline/content-publish/src/main/resources/content-publish.conf +++ b/publish-pipeline/content-publish/src/main/resources/content-publish.conf @@ -105,7 +105,7 @@ content { } hierarchy { - keyspace = "hierarchy_store" + keyspace = "dev_hierarchy_store" table = "content_hierarchy" } @@ -160,4 +160,16 @@ contentTypeToPrimaryCategory { } max_allowed_content_name = 120 -enableDIALContextUpdate = "Yes" \ No newline at end of file +enableDIALContextUpdate = "Yes" + +cloudstorage.metadata.replace_absolute_path=false +cloudstorage.relative_path_prefix= "CLOUD_STORAGE_BASE_PATH" +cloudstorage.read_base_path="https://sunbirddev.blob.core.windows.net" +cloudstorage.write_base_path=["https://sunbirddev.blob.core.windows.net","https://obj.dev.sunbird.org"] +cloudstorage.metadata.list=["appIcon","posterImage","artifactUrl","downloadUrl","variants","previewUrl","pdfUrl", "streamingUrl", "toc_url"] + +cloud_storage_type="azure" +cloud_storage_key="" +cloud_storage_secret="" +cloud_storage_container="" +cloud_storage_endpoint="" \ No newline at end of file diff --git a/publish-pipeline/content-publish/src/main/scala/org/sunbird/job/content/function/CollectionPublishFunction.scala b/publish-pipeline/content-publish/src/main/scala/org/sunbird/job/content/function/CollectionPublishFunction.scala index 7221ec49d..5dd7e0f63 100644 --- a/publish-pipeline/content-publish/src/main/scala/org/sunbird/job/content/function/CollectionPublishFunction.scala +++ b/publish-pipeline/content-publish/src/main/scala/org/sunbird/job/content/function/CollectionPublishFunction.scala @@ -45,8 +45,8 @@ class CollectionPublishFunction(config: ContentPublishConfig, httpUtil: HttpUtil override def open(parameters: Configuration): Unit = { super.open(parameters) - cassandraUtil = new CassandraUtil(config.cassandraHost, config.cassandraPort) - neo4JUtil = new Neo4JUtil(config.graphRoutePath, config.graphName) + cassandraUtil = new CassandraUtil(config.cassandraHost, config.cassandraPort, config) + neo4JUtil = new Neo4JUtil(config.graphRoutePath, config.graphName, config) esUtil = new ElasticSearchUtil(config.esConnectionInfo, config.compositeSearchIndexName, config.compositeSearchIndexType) cloudStorageUtil = new CloudStorageUtil(config) ec = ExecutionContexts.global @@ -71,7 +71,8 @@ class CollectionPublishFunction(config: ContentPublishConfig, httpUtil: HttpUtil val readerConfig = ExtDataConfig(config.hierarchyKeyspaceName, config.hierarchyTableName, definition.getExternalPrimaryKey, definition.getExternalProps) logger.info("Collection publishing started for : " + data.identifier) metrics.incCounter(config.collectionPublishEventCount) - val obj: ObjectData = getObject(data.identifier, data.pkgVersion, data.mimeType, data.publishType, readerConfig)(neo4JUtil, cassandraUtil) + val obj: ObjectData = getObject(data.identifier, data.pkgVersion, data.mimeType, data.publishType, readerConfig)(neo4JUtil, cassandraUtil, config) + logger.info(s"KN-856: Step:1 - From DB Collection: ${obj.identifier} | Hierarchy: ${obj.hierarchy}"); try { if (obj.pkgVersion > data.pkgVersion) { metrics.incCounter(config.skippedEventCount) @@ -82,9 +83,10 @@ class CollectionPublishFunction(config: ContentPublishConfig, httpUtil: HttpUtil if (messages.isEmpty) { // Pre-publish update updateProcessingNode(updObj)(neo4JUtil, cassandraUtil, readerConfig, definitionCache, definitionConfig) - + logger.info(s"KN-856: Step:2 - After updating processing status Collection: ${updObj.identifier} | Hierarchy: ${updObj.hierarchy}"); val isCollectionShallowCopy = isContentShallowCopy(updObj) val updatedObj = if (isCollectionShallowCopy) updateOriginPkgVersion(updObj)(neo4JUtil) else updObj + logger.info(s"KN-856: Step:3 - After shallow copy status check and update Collection: ${updatedObj.identifier} | isCollectionShallowCopy: $isCollectionShallowCopy | Hierarchy: ${updatedObj.hierarchy}"); // Clear redis cache cache.del(data.identifier) @@ -93,42 +95,47 @@ class CollectionPublishFunction(config: ContentPublishConfig, httpUtil: HttpUtil // Collection - add step to remove units of already Live content from redis - line 243 in PublishFinalizer val unitNodes = if (obj.metadata("identifier").asInstanceOf[String].endsWith(".img")) { - val childNodes = getUnitsFromLiveContent(updatedObj)(cassandraUtil, readerConfig) + val childNodes = getUnitsFromLiveContent(updatedObj)(cassandraUtil, readerConfig, config) childNodes.filter(rec => rec.nonEmpty).foreach(childId => cache.del(COLLECTION_CACHE_KEY_PREFIX + childId)) childNodes.filter(rec => rec.nonEmpty) } else List.empty logger.info("CollectionPublishFunction:: Live unitNodes: " + unitNodes) val enrichedObj = enrichObject(updatedObj)(neo4JUtil, cassandraUtil, readerConfig, cloudStorageUtil, config, definitionCache, definitionConfig) + logger.info(s"KN-856: Step:4 - After enriching the object Collection: ${enrichedObj.identifier} | Hierarchy: ${enrichedObj.hierarchy}"); logger.info("CollectionPublishFunction:: Collection Object Enriched: " + enrichedObj.identifier) val objWithEcar = getObjectWithEcar(enrichedObj, pkgTypes)(ec, neo4JUtil, cassandraUtil, readerConfig, cloudStorageUtil, config, definitionCache, definitionConfig, httpUtil) logger.info("CollectionPublishFunction:: ECAR generation completed for Collection Object: " + objWithEcar.identifier) + logger.info(s"KN-856: Step:5 - After WithEcar Collection: ${objWithEcar.identifier} | Hierarchy: ${objWithEcar.hierarchy}"); - val collRelationalMetadata = getRelationalMetadata(obj.identifier, obj.pkgVersion-1, readerConfig)(cassandraUtil).getOrElse(Map.empty[String, AnyRef]) + val collRelationalMetadata = getRelationalMetadata(obj.identifier, obj.pkgVersion-1, readerConfig)(cassandraUtil, config).getOrElse(Map.empty[String, AnyRef]) - val dialContextMap = if(config.enableDIALContextUpdate.equalsIgnoreCase("Yes")) fetchDialListForContextUpdate(obj)(neo4JUtil, cassandraUtil, readerConfig) else Map.empty[String, AnyRef] + val dialContextMap = if(config.enableDIALContextUpdate.equalsIgnoreCase("Yes")) fetchDialListForContextUpdate(obj)(neo4JUtil, cassandraUtil, readerConfig, config) else Map.empty[String, AnyRef] logger.info("CollectionPublishFunction:: dialContextMap: " + dialContextMap) - saveOnSuccess(new ObjectData(objWithEcar.identifier, objWithEcar.metadata.-("children"), objWithEcar.extData, objWithEcar.hierarchy))(neo4JUtil, cassandraUtil, readerConfig, definitionCache, definitionConfig) + saveOnSuccess(new ObjectData(objWithEcar.identifier, objWithEcar.metadata.-("children"), objWithEcar.extData, objWithEcar.hierarchy))(neo4JUtil, cassandraUtil, readerConfig, definitionCache, definitionConfig, config) logger.info("CollectionPublishFunction:: Published Collection Object metadata saved successfully to graph DB: " + objWithEcar.identifier) val variantsJsonString = ScalaJsonUtil.serialize(objWithEcar.metadata("variants")) val publishType = objWithEcar.getString("publish_type", "Public") val successObj = new ObjectData(objWithEcar.identifier, objWithEcar.metadata + ("status" -> (if (publishType.equalsIgnoreCase("Unlisted")) "Unlisted" else "Live"), "variants" -> variantsJsonString, "identifier" -> objWithEcar.identifier), objWithEcar.extData, objWithEcar.hierarchy) val children = successObj.hierarchy.getOrElse(Map()).getOrElse("children", List()).asInstanceOf[List[Map[String, AnyRef]]] - + logger.info(s"KN-856: Step:6 - After saveOnSuccess(Neo4J Save) Collection: ${successObj.identifier} | Hierarchy: $children"); // Collection - update and publish children - line 418 in PublishFinalizer val updatedChildren = updateHierarchyMetadata(children, successObj.metadata, collRelationalMetadata)(config) + logger.info(s"KN-856: Step:7 - After updateHierarchyMetadata Collection: ${successObj.identifier} | Hierarchy: $updatedChildren"); logger.info("CollectionPublishFunction:: Hierarchy Metadata updated for Collection Object: " + successObj.identifier + " || updatedChildren:: " + updatedChildren) publishHierarchy(updatedChildren, successObj, readerConfig, config)(cassandraUtil) - + logger.info(s"KN-856: Step:8 - After publishHierarchy Collection: ${successObj.identifier} | Hierarchy: $updatedChildren"); //TODO: Save IMAGE Object with enrichedObj children and collRelationalMetadata when pkgVersion is 1 - verify with MaheshG if(data.pkgVersion == 1) { saveImageHierarchy(enrichedObj, readerConfig, collRelationalMetadata)(cassandraUtil) + logger.info(s"KN-856: Step:8.1 - After saveImageHierarchy Collection: ${enrichedObj.identifier} | Hierarchy: ${enrichedObj.hierarchy}"); } if (!isCollectionShallowCopy) syncNodes(successObj, updatedChildren, unitNodes)(esUtil, neo4JUtil, cassandraUtil, readerConfig, definition, config) pushPostProcessEvent(successObj, dialContextMap, context)(metrics) + logger.info(s"KN-856: Step:9 - After pushPostProcessEvent Collection: ${successObj.identifier} | Hierarchy: ${successObj.hierarchy}"); metrics.incCounter(config.collectionPublishSuccessEventCount) logger.info("CollectionPublishFunction:: Collection publishing completed successfully for : " + data.identifier) } else { diff --git a/publish-pipeline/content-publish/src/main/scala/org/sunbird/job/content/function/ContentPublishFunction.scala b/publish-pipeline/content-publish/src/main/scala/org/sunbird/job/content/function/ContentPublishFunction.scala index debdd8bf4..be975fffc 100644 --- a/publish-pipeline/content-publish/src/main/scala/org/sunbird/job/content/function/ContentPublishFunction.scala +++ b/publish-pipeline/content-publish/src/main/scala/org/sunbird/job/content/function/ContentPublishFunction.scala @@ -42,8 +42,8 @@ class ContentPublishFunction(config: ContentPublishConfig, httpUtil: HttpUtil, override def open(parameters: Configuration): Unit = { super.open(parameters) - cassandraUtil = new CassandraUtil(config.cassandraHost, config.cassandraPort) - neo4JUtil = new Neo4JUtil(config.graphRoutePath, config.graphName) + cassandraUtil = new CassandraUtil(config.cassandraHost, config.cassandraPort, config) + neo4JUtil = new Neo4JUtil(config.graphRoutePath, config.graphName, config) cloudStorageUtil = new CloudStorageUtil(config) ec = ExecutionContexts.global definitionCache = new DefinitionCache() @@ -66,7 +66,7 @@ class ContentPublishFunction(config: ContentPublishConfig, httpUtil: HttpUtil, override def processElement(data: Event, context: ProcessFunction[Event, String]#Context, metrics: Metrics): Unit = { logger.info("Content publishing started for : " + data.identifier) metrics.incCounter(config.contentPublishEventCount) - val obj: ObjectData = getObject(data.identifier, data.pkgVersion, data.mimeType, data.publishType, readerConfig)(neo4JUtil, cassandraUtil) + val obj: ObjectData = getObject(data.identifier, data.pkgVersion, data.mimeType, data.publishType, readerConfig)(neo4JUtil, cassandraUtil, config) try { if (obj.pkgVersion > data.pkgVersion) { metrics.incCounter(config.skippedEventCount) @@ -87,7 +87,7 @@ class ContentPublishFunction(config: ContentPublishConfig, httpUtil: HttpUtil, val enrichedObj = enrichObject(ecmlVerifiedObj)(neo4JUtil, cassandraUtil, readerConfig, cloudStorageUtil, config, definitionCache, definitionConfig) val objWithEcar = getObjectWithEcar(enrichedObj, if (enrichedObj.getString("contentDisposition", "").equalsIgnoreCase("online-only")) List(EcarPackageType.SPINE) else pkgTypes)(ec, neo4JUtil, cloudStorageUtil, config, definitionCache, definitionConfig, httpUtil) logger.info("Ecar generation done for Content: " + objWithEcar.identifier) - saveOnSuccess(objWithEcar)(neo4JUtil, cassandraUtil, readerConfig, definitionCache, definitionConfig) + saveOnSuccess(objWithEcar)(neo4JUtil, cassandraUtil, readerConfig, definitionCache, definitionConfig, config) pushStreamingUrlEvent(enrichedObj, context)(metrics) pushMVCProcessorEvent(enrichedObj, context)(metrics) diff --git a/publish-pipeline/content-publish/src/main/scala/org/sunbird/job/content/publish/helpers/CollectionPublisher.scala b/publish-pipeline/content-publish/src/main/scala/org/sunbird/job/content/publish/helpers/CollectionPublisher.scala index 16cadb5fc..9e7ef8582 100644 --- a/publish-pipeline/content-publish/src/main/scala/org/sunbird/job/content/publish/helpers/CollectionPublisher.scala +++ b/publish-pipeline/content-publish/src/main/scala/org/sunbird/job/content/publish/helpers/CollectionPublisher.scala @@ -15,13 +15,13 @@ import org.sunbird.job.publish.helpers._ import org.sunbird.job.util._ import java.io.{File, IOException} +import java.text.{DecimalFormat, DecimalFormatSymbols, SimpleDateFormat} import java.util +import java.util.{Date, Locale} import scala.collection.JavaConverters._ import scala.collection.mutable import scala.collection.mutable.ListBuffer import scala.concurrent.ExecutionContext -import java.text.{DecimalFormat, DecimalFormatSymbols, SimpleDateFormat} -import java.util.{Date, Locale} trait CollectionPublisher extends ObjectReader with SyncMessagesGenerator with ObjectValidator with ObjectEnrichment with EcarGenerator with ObjectUpdater { @@ -34,20 +34,24 @@ trait CollectionPublisher extends ObjectReader with SyncMessagesGenerator with O private val PUBLISHED_STATUS_LIST = List("Live", "Unlisted") private val COLLECTION_MIME_TYPE = "application/vnd.ekstep.content-collection" - override def getExtData(identifier: String, pkgVersion: Double, mimeType: String, readerConfig: ExtDataConfig)(implicit cassandraUtil: CassandraUtil): Option[ObjectExtData] = None + override def getExtData(identifier: String, pkgVersion: Double, mimeType: String, readerConfig: ExtDataConfig)(implicit cassandraUtil: CassandraUtil, config: PublishConfig): Option[ObjectExtData] = None - override def getHierarchy(identifier: String, pkgVersion: Double, readerConfig: ExtDataConfig)(implicit cassandraUtil: CassandraUtil): Option[Map[String, AnyRef]] = { + override def getHierarchy(identifier: String, pkgVersion: Double, readerConfig: ExtDataConfig)(implicit cassandraUtil: CassandraUtil, config: PublishConfig): Option[Map[String, AnyRef]] = { val row: Row = Option(getCollectionHierarchy(getEditableObjId(identifier, pkgVersion), readerConfig)).getOrElse(getCollectionHierarchy(identifier, readerConfig)) if (null != row) { - val data: Map[String, AnyRef] = ScalaJsonUtil.deserialize[Map[String, AnyRef]](row.getString("hierarchy")) + val hierarchy = row.getString("hierarchy") + val updatedHierarchy = if (config.asInstanceOf[ContentPublishConfig].isrRelativePathEnabled) CSPMetaUtil.updateAbsolutePath(hierarchy) else hierarchy + val data: Map[String, AnyRef] = if(updatedHierarchy.nonEmpty) ScalaJsonUtil.deserialize[Map[String, AnyRef]](updatedHierarchy) else Map.empty[String, AnyRef] Option(data) } else Option(Map.empty[String, AnyRef]) } - def getLiveHierarchy(identifier: String, readerConfig: ExtDataConfig)(implicit cassandraUtil: CassandraUtil): Option[Map[String, AnyRef]] = { + private def getLiveHierarchy(identifier: String, readerConfig: ExtDataConfig)(implicit cassandraUtil: CassandraUtil, config: PublishConfig): Option[Map[String, AnyRef]] = { val row: Row = getCollectionHierarchy(identifier, readerConfig) if (null != row) { - val data: Map[String, AnyRef] = ScalaJsonUtil.deserialize[Map[String, AnyRef]](row.getString("hierarchy")) + val hierarchy = row.getString("hierarchy") + val updatedHierarchy = if(config.asInstanceOf[ContentPublishConfig].isrRelativePathEnabled) CSPMetaUtil.updateAbsolutePath(hierarchy) else hierarchy + val data: Map[String, AnyRef] = ScalaJsonUtil.deserialize[Map[String, AnyRef]](updatedHierarchy) Option(data) } else Option(Map.empty[String, AnyRef]) } @@ -60,10 +64,12 @@ trait CollectionPublisher extends ObjectReader with SyncMessagesGenerator with O cassandraUtil.findOne(selectWhere.toString) } - def getRelationalMetadata(identifier: String, pkgVersion: Double, readerConfig: ExtDataConfig)(implicit cassandraUtil: CassandraUtil): Option[Map[String, AnyRef]] = { + def getRelationalMetadata(identifier: String, pkgVersion: Double, readerConfig: ExtDataConfig)(implicit cassandraUtil: CassandraUtil, config: ContentPublishConfig): Option[Map[String, AnyRef]] = { val row: Row = Option(getCollectionHierarchy(getEditableObjId(identifier, pkgVersion), readerConfig)).getOrElse(getCollectionHierarchy(identifier, readerConfig)) if (null != row && row.getString("relational_metadata") != null && row.getString("relational_metadata").nonEmpty) { - val data: Map[String, AnyRef] = ScalaJsonUtil.deserialize[Map[String, AnyRef]](row.getString("relational_metadata")) + val relationalMetadata = row.getString("relational_metadata") + val updatedRelationalMetadata = if (config.isrRelativePathEnabled) CSPMetaUtil.updateAbsolutePath(relationalMetadata) else relationalMetadata + val data: Map[String, AnyRef] = ScalaJsonUtil.deserialize[Map[String, AnyRef]](updatedRelationalMetadata) Option(data) } else Option(Map.empty[String, AnyRef]) } @@ -131,7 +137,7 @@ trait CollectionPublisher extends ObjectReader with SyncMessagesGenerator with O override def deleteExternalData(obj: ObjectData, readerConfig: ExtDataConfig)(implicit cassandraUtil: CassandraUtil): Unit = None - def getObjectWithEcar(obj: ObjectData, pkgTypes: List[String])(implicit ec: ExecutionContext, neo4JUtil: Neo4JUtil, cassandraUtil: CassandraUtil, readerConfig: ExtDataConfig, cloudStorageUtil: CloudStorageUtil, config: PublishConfig, defCache: DefinitionCache, defConfig: DefinitionConfig, httpUtil: HttpUtil): ObjectData = { + def getObjectWithEcar(obj: ObjectData, pkgTypes: List[String])(implicit ec: ExecutionContext, neo4JUtil: Neo4JUtil, cassandraUtil: CassandraUtil, readerConfig: ExtDataConfig, cloudStorageUtil: CloudStorageUtil, config: ContentPublishConfig, defCache: DefinitionCache, defConfig: DefinitionConfig, httpUtil: HttpUtil): ObjectData = { val collRelationalMetadata = getRelationalMetadata(obj.identifier, obj.pkgVersion-1, readerConfig).getOrElse(Map.empty[String, AnyRef]) // Line 1107 in PublishFinalizer val children = obj.hierarchy.getOrElse(Map()).getOrElse("children", List()).asInstanceOf[List[Map[String, AnyRef]]] @@ -158,7 +164,7 @@ trait CollectionPublisher extends ObjectReader with SyncMessagesGenerator with O } else Some(updatedMeta) } - def getUnitsFromLiveContent(obj: ObjectData)(implicit cassandraUtil: CassandraUtil, readerConfig: ExtDataConfig): List[String] = { + def getUnitsFromLiveContent(obj: ObjectData)(implicit cassandraUtil: CassandraUtil, readerConfig: ExtDataConfig, config: ContentPublishConfig): List[String] = { logger.info("CollectionPublisher:: getUnitsFromLiveContent:: identifier: " + obj.identifier + " || pkgVersion: " + obj.metadata.getOrElse("pkgVersion", 1).asInstanceOf[Number]) val objHierarchy = getLiveHierarchy(obj.identifier, readerConfig).get val children = objHierarchy.getOrElse("children", List.empty).asInstanceOf[List[Map[String, AnyRef]]] @@ -202,7 +208,7 @@ trait CollectionPublisher extends ObjectReader with SyncMessagesGenerator with O } else obj } - private def enrichChildren(toEnrichChildren: ListBuffer[Map[String, AnyRef]], collectionResourceChildNodes: mutable.HashSet[String], childNodesToRemove: ListBuffer[String])(implicit neo4JUtil: Neo4JUtil, cassandraUtil: CassandraUtil, readerConfig: ExtDataConfig): ListBuffer[Map[String, AnyRef]] = { + private def enrichChildren(toEnrichChildren: ListBuffer[Map[String, AnyRef]], collectionResourceChildNodes: mutable.HashSet[String], childNodesToRemove: ListBuffer[String])(implicit neo4JUtil: Neo4JUtil, cassandraUtil: CassandraUtil, readerConfig: ExtDataConfig, config: PublishConfig): ListBuffer[Map[String, AnyRef]] = { val newChildren = toEnrichChildren.toList newChildren.map(child => { logger.info("CollectionPublisher:: enrichChildren:: child identifier:: " + child.getOrElse("identifier", "") + " || visibility:: " + child.getOrElse("visibility", "") + " || mimeType:: " + child.getOrElse("mimeType", "") + " || objectType:: " + child.getOrElse("objectType", "")) @@ -212,12 +218,7 @@ trait CollectionPublisher extends ObjectReader with SyncMessagesGenerator with O } if (StringUtils.equalsIgnoreCase(child.getOrElse("visibility", "").asInstanceOf[String], "Default") && EXPANDABLE_OBJECTS.contains(child.getOrElse("objectType", "").asInstanceOf[String])) { - val pkgVersion = child.getOrElse("pkgVersion", 0) match { - case _: Integer => child.getOrElse("pkgVersion", 0).asInstanceOf[Integer].doubleValue() - case _: Double => child.getOrElse("pkgVersion", 0).asInstanceOf[Double].doubleValue() - case _ => child.getOrElse("pkgVersion", "0").toString.toDouble - } - val childCollectionHierarchy = getHierarchy(child.getOrElse("identifier", "").asInstanceOf[String], pkgVersion, readerConfig).get + val childCollectionHierarchy = getLiveHierarchy(child.getOrElse("identifier", "").asInstanceOf[String], readerConfig).get if (childCollectionHierarchy.nonEmpty) { val childNodes = childCollectionHierarchy.getOrElse("childNodes", List.empty).asInstanceOf[List[String]] if (childNodes.nonEmpty && INCLUDE_CHILDNODE_OBJECTS.contains(child.getOrElse("objectType", "").asInstanceOf[String])) collectionResourceChildNodes ++= childNodes.toSet[String] @@ -658,7 +659,7 @@ trait CollectionPublisher extends ObjectReader with SyncMessagesGenerator with O result } - def fetchDialListForContextUpdate(obj: ObjectData)(implicit neo4JUtil: Neo4JUtil, cassandraUtil: CassandraUtil, readerConfig: ExtDataConfig): Map[String, AnyRef] = { + def fetchDialListForContextUpdate(obj: ObjectData)(implicit neo4JUtil: Neo4JUtil, cassandraUtil: CassandraUtil, readerConfig: ExtDataConfig, config: PublishConfig): Map[String, AnyRef] = { val isCollectionShallowCopy = isContentShallowCopy(obj) val DialContextMap: Map[String, AnyRef] = if (isCollectionShallowCopy) Map.empty[String, AnyRef] else { diff --git a/publish-pipeline/content-publish/src/main/scala/org/sunbird/job/content/publish/helpers/ContentPublisher.scala b/publish-pipeline/content-publish/src/main/scala/org/sunbird/job/content/publish/helpers/ContentPublisher.scala index 758d43dc5..98db2041c 100644 --- a/publish-pipeline/content-publish/src/main/scala/org/sunbird/job/content/publish/helpers/ContentPublisher.scala +++ b/publish-pipeline/content-publish/src/main/scala/org/sunbird/job/content/publish/helpers/ContentPublisher.scala @@ -31,7 +31,7 @@ trait ContentPublisher extends ObjectReader with ObjectValidator with ObjectEnri private val ignoreValidationMimeType = List(MimeType.Collection, MimeType.Plugin_Archive, MimeType.ASSETS) private val YOUTUBE_REGEX = "^(http(s)?:\\/\\/)?((w){3}.)?youtu(be|.be)?(\\.com)?\\/.+" - override def getExtData(identifier: String, pkgVersion: Double, mimeType: String, readerConfig: ExtDataConfig)(implicit cassandraUtil: CassandraUtil): Option[ObjectExtData] = { + override def getExtData(identifier: String, pkgVersion: Double, mimeType: String, readerConfig: ExtDataConfig)(implicit cassandraUtil: CassandraUtil, config: PublishConfig): Option[ObjectExtData] = { mimeType match { case MimeType.ECML_Archive => val ecmlBody = getContentBody(identifier, readerConfig) @@ -41,7 +41,7 @@ trait ContentPublisher extends ObjectReader with ObjectValidator with ObjectEnri } } - override def getHierarchy(identifier: String, pkgVersion: Double, readerConfig: ExtDataConfig)(implicit cassandraUtil: CassandraUtil): Option[Map[String, AnyRef]] = None + override def getHierarchy(identifier: String, pkgVersion: Double, readerConfig: ExtDataConfig)(implicit cassandraUtil: CassandraUtil, config: PublishConfig): Option[Map[String, AnyRef]] = None override def getExtDatas(identifiers: List[String], readerConfig: ExtDataConfig)(implicit cassandraUtil: CassandraUtil): Option[Map[String, AnyRef]] = None @@ -201,7 +201,12 @@ trait ContentPublisher extends ObjectReader with ObjectValidator with ObjectEnri None case MimeType.ECML_Archive | MimeType.HTML_Archive | MimeType.H5P_Archive => val latestFolderS3Url = ExtractableMimeTypeHelper.getCloudStoreURL(obj, cloudStorageUtil, config) - val updatedPreviewUrl = updatedMeta ++ Map("previewUrl" -> latestFolderS3Url, "streamingUrl" -> latestFolderS3Url) + val relativeLatestFolder = if(config.isrRelativePathEnabled) { + val paths = config.config.getStringList("cloudstorage.write_base_path").asScala.toArray + val repArray = CSPMetaUtil.getReplacementData(paths, config.getString("cloudstorage.read_base_path", "")) + StringUtils.replaceEach(latestFolderS3Url, paths, repArray) + } else latestFolderS3Url + val updatedPreviewUrl = updatedMeta ++ Map("previewUrl" -> relativeLatestFolder, "streamingUrl" -> latestFolderS3Url) Some(updatedPreviewUrl) case _ => val artifactUrl = obj.getString("artifactUrl", null) diff --git a/publish-pipeline/content-publish/src/main/scala/org/sunbird/job/content/task/ContentPublishConfig.scala b/publish-pipeline/content-publish/src/main/scala/org/sunbird/job/content/task/ContentPublishConfig.scala index 370a94adb..29eefc0e2 100644 --- a/publish-pipeline/content-publish/src/main/scala/org/sunbird/job/content/task/ContentPublishConfig.scala +++ b/publish-pipeline/content-publish/src/main/scala/org/sunbird/job/content/task/ContentPublishConfig.scala @@ -98,4 +98,6 @@ class ContentPublishConfig(override val config: Config) extends PublishConfig(co val allowedExtensionsWord: util.List[String] = if (config.hasPath("mimetype.allowed_extensions.word")) config.getStringList("mimetype.allowed_extensions.word") else util.Arrays.asList[String]("doc", "docx", "ppt", "pptx", "key", "odp", "pps", "odt", "wpd", "wps", "wks") val enableDIALContextUpdate: String = if (config.hasPath("enableDIALContextUpdate")) config.getString("enableDIALContextUpdate") else "No" + + val isrRelativePathEnabled: Boolean = if (config.hasPath("cloudstorage.metadata.replace_absolute_path")) config.getBoolean("cloudstorage.metadata.replace_absolute_path") else false } diff --git a/publish-pipeline/content-publish/src/main/scala/org/sunbird/job/content/task/ContentPublishStreamTask.scala b/publish-pipeline/content-publish/src/main/scala/org/sunbird/job/content/task/ContentPublishStreamTask.scala index 40dd938c4..64777a369 100644 --- a/publish-pipeline/content-publish/src/main/scala/org/sunbird/job/content/task/ContentPublishStreamTask.scala +++ b/publish-pipeline/content-publish/src/main/scala/org/sunbird/job/content/task/ContentPublishStreamTask.scala @@ -39,6 +39,7 @@ class ContentPublishStreamTask(config: ContentPublishConfig, kafkaConnector: Fli val collectionPublish = processStreamTask.getSideOutput(config.collectionPublishOutTag).process(new CollectionPublishFunction(config, httpUtil)) .name("collection-publish-process").uid("collection-publish-process").setParallelism(1) collectionPublish.getSideOutput(config.generatePostPublishProcessTag).addSink(kafkaConnector.kafkaStringSink(config.postPublishTopic)) + collectionPublish.getSideOutput(config.failedEventOutTag).addSink(kafkaConnector.kafkaStringSink(config.kafkaErrorTopic)) env.execute(config.jobName) } diff --git a/publish-pipeline/content-publish/src/test/scala/org/sunbird/job/publish/helpers/spec/CollectionPublisherSpec.scala b/publish-pipeline/content-publish/src/test/scala/org/sunbird/job/publish/helpers/spec/CollectionPublisherSpec.scala index 0b92c3e07..0d946431b 100644 --- a/publish-pipeline/content-publish/src/test/scala/org/sunbird/job/publish/helpers/spec/CollectionPublisherSpec.scala +++ b/publish-pipeline/content-publish/src/test/scala/org/sunbird/job/publish/helpers/spec/CollectionPublisherSpec.scala @@ -50,7 +50,7 @@ class CollectionPublisherSpec extends FlatSpec with BeforeAndAfterAll with Match override protected def beforeAll(): Unit = { super.beforeAll() EmbeddedCassandraServerHelper.startEmbeddedCassandra(80000L) - cassandraUtil = new CassandraUtil(jobConfig.cassandraHost, jobConfig.cassandraPort) + cassandraUtil = new CassandraUtil(jobConfig.cassandraHost, jobConfig.cassandraPort, jobConfig) val session = cassandraUtil.session val dataLoader = new CQLDataLoader(session) dataLoader.load(new FileCQLDataSet(getClass.getResource("/test.cql").getPath, true, true)) @@ -157,7 +157,7 @@ class CollectionPublisherSpec extends FlatSpec with BeforeAndAfterAll with Match val unpublishedChildrenObj: List[Map[String, AnyRef]] = ScalaJsonUtil.deserialize[List[Map[String, AnyRef]]](unpublishedChildrenData) val publishedCollectionNodeMetadataObj: Map[String,AnyRef] = ScalaJsonUtil.deserialize[Map[String,AnyRef]](publishedCollectionNodeMetadata) - val collRelationalMetadata = new TestCollectionPublisher().getRelationalMetadata("do_123", 1, readerConfig)(cassandraUtil).get + val collRelationalMetadata = new TestCollectionPublisher().getRelationalMetadata("do_123", 1, readerConfig)(cassandraUtil, jobConfig).get // Collection - update and publish children - line 418 in PublishFinalizer val updatedChildren: List[Map[String, AnyRef]] = new TestCollectionPublisher().updateHierarchyMetadata(unpublishedChildrenObj, publishedCollectionNodeMetadataObj, collRelationalMetadata)(jobConfig) @@ -170,17 +170,17 @@ class CollectionPublisherSpec extends FlatSpec with BeforeAndAfterAll with Match } "getRelationalMetadata" should "return empty Map when there is no entry in relational_metadata column" in { - val collRelationalMetadata = new TestCollectionPublisher().getRelationalMetadata("do_1234", 1, readerConfig)(cassandraUtil).get + val collRelationalMetadata = new TestCollectionPublisher().getRelationalMetadata("do_1234", 1, readerConfig)(cassandraUtil, jobConfig).get assert(collRelationalMetadata != null && collRelationalMetadata.isEmpty) } "getRelationalMetadata" should "return empty Map when there is empty entry in relational_metadata column" in { - val collRelationalMetadata = new TestCollectionPublisher().getRelationalMetadata("do_12345", 1, readerConfig)(cassandraUtil).get + val collRelationalMetadata = new TestCollectionPublisher().getRelationalMetadata("do_12345", 1, readerConfig)(cassandraUtil, jobConfig).get assert(collRelationalMetadata != null && collRelationalMetadata.isEmpty) } "getRelationalMetadata" should "return empty Map when there is empty object entry in relational_metadata column" in { - val collRelationalMetadata = new TestCollectionPublisher().getRelationalMetadata("do_123456", 1, readerConfig)(cassandraUtil).get + val collRelationalMetadata = new TestCollectionPublisher().getRelationalMetadata("do_123456", 1, readerConfig)(cassandraUtil, jobConfig).get assert(collRelationalMetadata != null && collRelationalMetadata.isEmpty) } @@ -194,7 +194,7 @@ class CollectionPublisherSpec extends FlatSpec with BeforeAndAfterAll with Match "getUnitsFromLiveContent" should "return object hierarchy" in { val data = new ObjectData("do_2133950809948078081503", Map("identifier" -> "do_2133950809948078081503"), Some(Map.empty[String, AnyRef])) - val fetchedChildren = new TestCollectionPublisher().getUnitsFromLiveContent(data)(cassandraUtil,readerConfig) + val fetchedChildren = new TestCollectionPublisher().getUnitsFromLiveContent(data)(cassandraUtil, readerConfig, jobConfig) assert(fetchedChildren.nonEmpty) } @@ -226,15 +226,15 @@ class CollectionPublisherSpec extends FlatSpec with BeforeAndAfterAll with Match "fetchDialListForContextUpdate" should "fetch the list of added and removed QR codes" in { val nodeObj = new ObjectData("do_21354027142511820812318.img", Map("objectType" -> "Collection", "identifier" -> "do_21354027142511820812318", "name" -> "DialCodeHierarchy", "lastPublishedOn" -> getTimeStamp, "lastUpdatedOn" -> getTimeStamp, "status" -> "Draft", "pkgVersion" -> 1.asInstanceOf[Number], "versionKey" -> "1652871771396"), Some(Map()), Some(Map())) - val DIALListMap = new TestCollectionPublisher().fetchDialListForContextUpdate(nodeObj)(mockNeo4JUtil, cassandraUtil, readerConfig) + val DIALListMap = new TestCollectionPublisher().fetchDialListForContextUpdate(nodeObj)(mockNeo4JUtil, cassandraUtil, readerConfig, jobConfig) println("DIALListMap:: " + DIALListMap) assert(DIALListMap.nonEmpty) } val collRelationalMetadataStr = "{\"do_123\":{\"name\":\"Collection Publish T21\",\"children\":[\"do_11340511137112064018\",\"do_11340511137080934412\"],\"root\":true},\"do_11340511137112064018\":{\"name\":\"Collection Parent\",\"children\":[\"do_11340096165525094411\"],\"root\":false,\"relationalMetadata\":{\"do_11340096165525094411\":{\"name\":\"Test Name RM L1 - R1\",\"keywords\":[\"Overwriting content KW1\"]}}},\"do_11340511137080934412\":{\"name\":\"Collection Parent\",\"children\":[\"do_11340096165525094411\"],\"root\":false,\"relationalMetadata\":{\"do_11340096165525094411\":{\"name\":\"Test Name RM L1 - R1\",\"keywords\":[\"Overwriting content KW1\"]}}},\"do_11340096165525094411\":{\"name\":\"PDF Content\",\"children\":[],\"root\":false},\"do_113405111371145216110\":{\"name\":\"test\",\"children\":[], \"root\":false}}" - val publishedChildrenData = "[{\"lastStatusChangedOn\":\"2021-11-08T12:40:36.586+0530\",\"parent\":\"do_11340502356035174411\",\"children\":[{\"lastStatusChangedOn\":\"2021-11-08T12:40:36.572+0530\",\"parent\":\"do_113405023736512512114\",\"children\":[{\"lastStatusChangedOn\":\"2021-11-08T12:40:36.575+0530\",\"parent\":\"do_11340502373639782416\",\"children\":[{\"copyright\":\"J H S BHARKHOKHA, Tamil Nadu\",\"lastStatusChangedOn\":\"2021-09-17T16:22:50.404+0530\",\"parent\":\"do_11340502373642240018\",\"licenseterms\":\"By creating any type of content (resources, books, courses etc.) on DIKSHA, you consent to publish it under the Creative Commons License Framework. Please choose the applicable creative commons license you wish to apply to your content.\",\"organisation\":[\"J H S BHARKHOKHA\",\"Tamil Nadu\"],\"mediaType\":\"content\",\"name\":\"jaga Aug 25th more than 200mp mp4 update 1\",\"discussionForum\":{\"enabled\":\"No\"},\"createdOn\":\"2021-09-17T16:05:29.019+0530\",\"channel\":\"0126825293972439041\",\"lastUpdatedOn\":\"2021-09-17T16:22:50.404+0530\",\"size\":363062652,\"identifier\":\"do_11336831941257625611\",\"resourceType\":\"Learn\",\"ownershipType\":[\"createdBy\"],\"compatibilityLevel\":1,\"audience\":[\"Student\"],\"os\":[\"All\"],\"primaryCategory\":\"eTextbook\",\"appIcon\":\"https://preprodall.blob.core.windows.net/ntp-content-preprod/content/do_2133520666218741761984/artifact/do_2132664483637248001189_1619439497677_1200px-flag_of_india.thumb.svg.thumb.png\",\"languageCode\":[\"en\"],\"downloadUrl\":\"\",\"framework\":\"tn_k-12_5\",\"creator\":\"सामग्री निर्माता TN\",\"versionKey\":\"1631875539805\",\"mimeType\":\"video/mp4\",\"code\":\"3bd7411e-c03c-4997-a247-4d43a5cc820b\",\"license\":\"CC BY 4.0\",\"version\":2,\"prevStatus\":\"Live\",\"contentType\":\"Resource\",\"prevState\":\"Draft\",\"language\":[\"English\"],\"lastPublishedOn\":\"2021-09-17T16:22:15.047+0530\",\"objectType\":\"Content\",\"status\":\"Live\",\"createdBy\":\"fca2925f-1eee-4654-9177-fece3fd6afc9\",\"dialcodeRequired\":\"No\",\"interceptionPoints\":{},\"idealScreenSize\":\"normal\",\"contentEncoding\":\"identity\",\"depth\":4,\"consumerId\":\"2eaff3db-cdd1-42e5-a611-bebbf906e6cf\",\"lastPublishedBy\":\"\",\"osId\":\"org.ekstep.quiz.app\",\"copyrightYear\":2021,\"se_FWIds\":[\"tn_k-12_5\"],\"contentDisposition\":\"online-only\",\"previewUrl\":\"https://preprodall.blob.core.windows.net/ntp-content-preprod/content/assets/do_2133520666218741761984/como-kids-tv-_-the-story-of-comos-family-_-30min-_-cartoon-video-for-kids.mp4\",\"artifactUrl\":\"https://preprodall.blob.core.windows.net/ntp-content-preprod/content/assets/do_2133520666218741761984/como-kids-tv-_-the-story-of-comos-family-_-30min-_-cartoon-video-for-kids.mp4\",\"visibility\":\"Default\",\"credentials\":{\"enabled\":\"No\"},\"variants\":{\"spine\":{\"ecarUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11336831941257625611/jaga-aug-25th-more-than-200mp-mp4-update-1_1631875935857_do_11336831941257625611_3_SPINE.ecar\",\"size\":\"2172\"}},\"index\":1,\"pkgVersion\":3,\"idealScreenDensity\":\"hdpi\"}],\"mediaType\":\"content\",\"name\":\"5.1.1 Key parts in the head\",\"discussionForum\":{\"enabled\":\"No\"},\"createdOn\":\"2021-11-08T12:40:36.575+0530\",\"channel\":\"0126825293972439041\",\"generateDIALCodes\":\"No\",\"lastUpdatedOn\":\"2021-11-08T12:46:52.715+0530\",\"identifier\":\"do_11340502373642240018\",\"description\":\"xyz\",\"ownershipType\":[\"createdBy\"],\"compatibilityLevel\":1,\"audience\":[\"Student\"],\"os\":[\"All\"],\"primaryCategory\":\"Textbook Unit\",\"languageCode\":[\"en\"],\"downloadUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/collection/do_11340502356035174411/collection-publish-t20_1636356202227_do_11340502356035174411_1_SPINE.ecar\",\"framework\":\"ncert_k-12\",\"versionKey\":\"1636355436575\",\"mimeType\":\"application/vnd.ekstep.content-collection\",\"code\":\"7991af5c2e51e4d3d7b83167aaac8829\",\"license\":\"CC BY 4.0\",\"leafNodes\":[\"do_11336831941257625611\"],\"version\":2,\"contentType\":\"TextBookUnit\",\"language\":[\"English\"],\"lastPublishedOn\":\"2021-11-08T12:53:09.398+0530\",\"objectType\":\"Collection\",\"status\":\"Live\",\"dialcodeRequired\":\"No\",\"idealScreenSize\":\"normal\",\"contentEncoding\":\"gzip\",\"leafNodesCount\":1,\"depth\":3,\"osId\":\"org.ekstep.launcher\",\"contentDisposition\":\"inline\",\"visibility\":\"Parent\",\"credentials\":{\"enabled\":\"No\"},\"variants\":\"{\\\"spine\\\":{\\\"ecarUrl\\\":\\\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/collection/do_11340502356035174411/collection-publish-t20_1636356202227_do_11340502356035174411_1_SPINE.ecar\\\",\\\"size\\\":\\\"12044\\\"},\\\"online\\\":{\\\"ecarUrl\\\":\\\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/collection/do_11340502356035174411/collection-publish-t20_1636356202412_do_11340502356035174411_1_ONLINE.ecar\\\",\\\"size\\\":\\\"5074\\\"}}\",\"index\":1,\"pkgVersion\":1,\"idealScreenDensity\":\"hdpi\"},{\"lastStatusChangedOn\":\"2021-11-08T12:40:36.534+0530\",\"parent\":\"do_11340502373639782416\",\"children\":[{\"lastStatusChangedOn\":\"2021-11-02T19:13:39.729+0530\",\"parent\":\"do_11340502373608652812\",\"mediaType\":\"content\",\"name\":\"Collection Publishing PDF Content\",\"discussionForum\":{\"enabled\":\"No\"},\"createdOn\":\"2021-11-02T18:56:17.917+0530\",\"createdFor\":[\"01309282781705830427\"],\"channel\":\"0126825293972439041\",\"lastUpdatedOn\":\"2021-11-02T19:13:39.729+0530\",\"streamingUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/assets/do_1134009478823116801129/chapter_1.pdf\",\"identifier\":\"do_11340096165525094411\",\"resourceType\":\"Learn\",\"ownershipType\":[\"createdBy\"],\"compatibilityLevel\":4,\"audience\":[\"Student\"],\"os\":[\"All\"],\"primaryCategory\":\"Explanation Content\",\"appIcon\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11340094790233292811/artifact/033019_sz_reviews_feat_1564126718632.thumb.jpg\",\"languageCode\":[\"en\"],\"downloadUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11340096165525094411/collection-publishing-pdf-content_1635860615969_do_11340096165525094411_1.ecar\",\"framework\":\"ekstep_ncert_k-12\",\"creator\":\"N131\",\"versionKey\":\"1635859577917\",\"mimeType\":\"application/pdf\",\"code\":\"c9ce1ce0-b9b4-402e-a9c3-556701070838\",\"license\":\"CC BY 4.0\",\"version\":2,\"prevStatus\":\"Processing\",\"contentType\":\"Resource\",\"prevState\":\"Draft\",\"language\":[\"English\"],\"lastPublishedOn\":\"2021-11-02T19:13:35.589+0530\",\"objectType\":\"Content\",\"status\":\"Live\",\"pragma\":[\"external\"],\"createdBy\":\"0b71985d-fcb0-4018-ab14-83f10c3b0426\",\"dialcodeRequired\":\"No\",\"interceptionPoints\":{},\"keywords\":[\"CPPDFContent1\",\"CPPDFContent2\",\"CollectionKW1\"],\"idealScreenSize\":\"normal\",\"contentEncoding\":\"identity\",\"depth\":4,\"lastPublishedBy\":\"\",\"osId\":\"org.ekstep.quiz.app\",\"copyrightYear\":2021,\"se_FWIds\":[\"ekstep_ncert_k-12\"],\"contentDisposition\":\"inline\",\"previewUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/assets/do_1134009478823116801129/chapter_1.pdf\",\"artifactUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/assets/do_1134009478823116801129/chapter_1.pdf\",\"visibility\":\"Default\",\"credentials\":{\"enabled\":\"No\"},\"variants\":{\"full\":{\"ecarUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11340096165525094411/collection-publishing-pdf-content_1635860615969_do_11340096165525094411_1.ecar\",\"size\":\"256918\"},\"spine\":{\"ecarUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11340096165525094411/collection-publishing-pdf-content_1635860619148_do_11340096165525094411_1_SPINE.ecar\",\"size\":\"6378\"}},\"index\":1,\"pkgVersion\":1,\"idealScreenDensity\":\"hdpi\"}],\"mediaType\":\"content\",\"name\":\"5.1.2 Other parts\",\"discussionForum\":{\"enabled\":\"No\"},\"createdOn\":\"2021-11-08T12:40:36.534+0530\",\"channel\":\"0126825293972439041\",\"generateDIALCodes\":\"No\",\"lastUpdatedOn\":\"2021-11-08T12:46:52.715+0530\",\"identifier\":\"do_11340502373608652812\",\"description\":\"\",\"ownershipType\":[\"createdBy\"],\"compatibilityLevel\":1,\"audience\":[\"Student\"],\"os\":[\"All\"],\"primaryCategory\":\"Textbook Unit\",\"languageCode\":[\"en\"],\"downloadUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/collection/do_11340502356035174411/collection-publish-t20_1636356202227_do_11340502356035174411_1_SPINE.ecar\",\"framework\":\"ncert_k-12\",\"versionKey\":\"1636355436534\",\"mimeType\":\"application/vnd.ekstep.content-collection\",\"code\":\"3bf70f06d3e8dba010d8806fd94259b1\",\"license\":\"CC BY 4.0\",\"leafNodes\":[\"do_11340096165525094411\"],\"version\":2,\"contentType\":\"TextBookUnit\",\"language\":[\"English\"],\"lastPublishedOn\":\"2021-11-08T12:53:09.398+0530\",\"objectType\":\"Collection\",\"status\":\"Live\",\"dialcodeRequired\":\"No\",\"idealScreenSize\":\"normal\",\"contentEncoding\":\"gzip\",\"leafNodesCount\":1,\"depth\":3,\"osId\":\"org.ekstep.launcher\",\"contentDisposition\":\"inline\",\"visibility\":\"Parent\",\"credentials\":{\"enabled\":\"No\"},\"variants\":\"{\\\"spine\\\":{\\\"ecarUrl\\\":\\\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/collection/do_11340502356035174411/collection-publish-t20_1636356202227_do_11340502356035174411_1_SPINE.ecar\\\",\\\"size\\\":\\\"12044\\\"},\\\"online\\\":{\\\"ecarUrl\\\":\\\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/collection/do_11340502356035174411/collection-publish-t20_1636356202412_do_11340502356035174411_1_ONLINE.ecar\\\",\\\"size\\\":\\\"5074\\\"}}\",\"index\":2,\"pkgVersion\":1,\"idealScreenDensity\":\"hdpi\"}],\"mediaType\":\"content\",\"name\":\"5.1 Parts of Body\",\"discussionForum\":{\"enabled\":\"No\"},\"createdOn\":\"2021-11-08T12:40:36.572+0530\",\"channel\":\"0126825293972439041\",\"generateDIALCodes\":\"No\",\"lastUpdatedOn\":\"2021-11-08T12:46:52.715+0530\",\"identifier\":\"do_11340502373639782416\",\"description\":\"This section describes about various part of the body such as head, hands, legs etc.\",\"ownershipType\":[\"createdBy\"],\"compatibilityLevel\":1,\"audience\":[\"Student\"],\"os\":[\"All\"],\"primaryCategory\":\"Textbook Unit\",\"languageCode\":[\"en\"],\"downloadUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/collection/do_11340502356035174411/collection-publish-t20_1636356202227_do_11340502356035174411_1_SPINE.ecar\",\"framework\":\"ncert_k-12\",\"versionKey\":\"1636355436572\",\"mimeType\":\"application/vnd.ekstep.content-collection\",\"code\":\"20cc1f31e62f924c6e47bf04c994376b\",\"license\":\"CC BY 4.0\",\"leafNodes\":[\"do_11336831941257625611\",\"do_11340096165525094411\"],\"version\":2,\"contentType\":\"TextBookUnit\",\"language\":[\"English\"],\"lastPublishedOn\":\"2021-11-08T12:53:09.398+0530\",\"objectType\":\"Collection\",\"status\":\"Live\",\"dialcodeRequired\":\"No\",\"idealScreenSize\":\"normal\",\"contentEncoding\":\"gzip\",\"leafNodesCount\":2,\"depth\":2,\"osId\":\"org.ekstep.launcher\",\"contentDisposition\":\"inline\",\"visibility\":\"Parent\",\"credentials\":{\"enabled\":\"No\"},\"variants\":\"{\\\"spine\\\":{\\\"ecarUrl\\\":\\\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/collection/do_11340502356035174411/collection-publish-t20_1636356202227_do_11340502356035174411_1_SPINE.ecar\\\",\\\"size\\\":\\\"12044\\\"},\\\"online\\\":{\\\"ecarUrl\\\":\\\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/collection/do_11340502356035174411/collection-publish-t20_1636356202412_do_11340502356035174411_1_ONLINE.ecar\\\",\\\"size\\\":\\\"5074\\\"}}\",\"index\":1,\"pkgVersion\":1,\"idealScreenDensity\":\"hdpi\"},{\"lastStatusChangedOn\":\"2021-11-08T12:40:36.582+0530\",\"parent\":\"do_113405023736512512114\",\"children\":[{\"lastStatusChangedOn\":\"2021-11-08T12:40:36.570+0530\",\"parent\":\"do_113405023736479744112\",\"children\":[{\"lastStatusChangedOn\":\"2021-11-08T12:40:36.579+0530\",\"parent\":\"do_11340502373638144014\",\"children\":[{\"lastStatusChangedOn\":\"2021-11-02T19:16:10.667+0530\",\"parent\":\"do_113405023736455168110\",\"mediaType\":\"content\",\"name\":\"Collection Publish MP4 content\",\"discussionForum\":{\"enabled\":\"No\"},\"createdOn\":\"2021-11-02T18:58:53.445+0530\",\"channel\":\"0126825293972439041\",\"lastUpdatedOn\":\"2021-11-02T19:16:10.667+0530\",\"identifier\":\"do_11340096293585715212\",\"resourceType\":\"Learn\",\"ownershipType\":[\"createdBy\"],\"compatibilityLevel\":1,\"audience\":[\"Student\"],\"os\":[\"All\"],\"primaryCategory\":\"Explanation Content\",\"appIcon\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_1134009488766730241130/artifact/033019_sz_reviews_feat_1564126718632.thumb.jpg\",\"languageCode\":[\"en\"],\"downloadUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11340096293585715212/collection-publish-mp4-content_1635860769119_do_11340096293585715212_1.ecar\",\"framework\":\"ekstep_ncert_k-12\",\"versionKey\":\"1635859733445\",\"mimeType\":\"video/mp4\",\"code\":\"e0b58864-3dc5-484a-b194-38c3eddcbce1\",\"license\":\"CC BY 4.0\",\"version\":2,\"prevStatus\":\"Draft\",\"contentType\":\"Resource\",\"prevState\":\"Draft\",\"language\":[\"English\"],\"lastPublishedOn\":\"2021-11-02T19:16:08.789+0530\",\"objectType\":\"Content\",\"status\":\"Live\",\"createdBy\":\"0b71985d-fcb0-4018-ab14-83f10c3b0426\",\"dialcodeRequired\":\"No\",\"interceptionPoints\":{},\"keywords\":[\"CPMP4ContentKW1\",\"CPMP4ContentKW2\"],\"idealScreenSize\":\"normal\",\"contentEncoding\":\"identity\",\"depth\":5,\"lastPublishedBy\":\"\",\"osId\":\"org.ekstep.quiz.app\",\"se_FWIds\":[\"ekstep_ncert_k-12\"],\"contentDisposition\":\"inline\",\"previewUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/assets/do_1134009488766730241130/amoeba-eat.mp4\",\"artifactUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/assets/do_1134009488766730241130/amoeba-eat.mp4\",\"visibility\":\"Default\",\"credentials\":{\"enabled\":\"No\"},\"variants\":{\"full\":{\"ecarUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11340096293585715212/collection-publish-mp4-content_1635860769119_do_11340096293585715212_1.ecar\",\"size\":\"2692101\"},\"spine\":{\"ecarUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11340096293585715212/collection-publish-mp4-content_1635860770277_do_11340096293585715212_1_SPINE.ecar\",\"size\":\"6275\"}},\"index\":1,\"pkgVersion\":1,\"idealScreenDensity\":\"hdpi\"}],\"mediaType\":\"content\",\"name\":\"dsffgdg\",\"discussionForum\":{\"enabled\":\"No\"},\"createdOn\":\"2021-11-08T12:40:36.579+0530\",\"channel\":\"0126825293972439041\",\"generateDIALCodes\":\"No\",\"lastUpdatedOn\":\"2021-11-08T12:46:52.715+0530\",\"identifier\":\"do_113405023736455168110\",\"description\":\"\",\"ownershipType\":[\"createdBy\"],\"compatibilityLevel\":1,\"audience\":[\"Student\"],\"os\":[\"All\"],\"primaryCategory\":\"Textbook Unit\",\"languageCode\":[\"en\"],\"downloadUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/collection/do_11340502356035174411/collection-publish-t20_1636356202227_do_11340502356035174411_1_SPINE.ecar\",\"framework\":\"ncert_k-12\",\"versionKey\":\"1636355436579\",\"mimeType\":\"application/vnd.ekstep.content-collection\",\"code\":\"9cf84ff2fb08f9af4c23eb09df9b2520\",\"license\":\"CC BY 4.0\",\"leafNodes\":[\"do_11340096293585715212\"],\"version\":2,\"contentType\":\"TextBookUnit\",\"language\":[\"English\"],\"lastPublishedOn\":\"2021-11-08T12:53:09.398+0530\",\"objectType\":\"Collection\",\"status\":\"Live\",\"dialcodeRequired\":\"No\",\"idealScreenSize\":\"normal\",\"contentEncoding\":\"gzip\",\"leafNodesCount\":1,\"depth\":4,\"osId\":\"org.ekstep.launcher\",\"contentDisposition\":\"inline\",\"visibility\":\"Parent\",\"credentials\":{\"enabled\":\"No\"},\"variants\":\"{\\\"spine\\\":{\\\"ecarUrl\\\":\\\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/collection/do_11340502356035174411/collection-publish-t20_1636356202227_do_11340502356035174411_1_SPINE.ecar\\\",\\\"size\\\":\\\"12044\\\"},\\\"online\\\":{\\\"ecarUrl\\\":\\\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/collection/do_11340502356035174411/collection-publish-t20_1636356202412_do_11340502356035174411_1_ONLINE.ecar\\\",\\\"size\\\":\\\"5074\\\"}}\",\"index\":1,\"pkgVersion\":1,\"idealScreenDensity\":\"hdpi\"}],\"mediaType\":\"content\",\"name\":\"5.2.1 Respiratory System\",\"discussionForum\":{\"enabled\":\"No\"},\"createdOn\":\"2021-11-08T12:40:36.570+0530\",\"channel\":\"0126825293972439041\",\"generateDIALCodes\":\"No\",\"lastUpdatedOn\":\"2021-11-08T12:46:52.715+0530\",\"identifier\":\"do_11340502373638144014\",\"description\":\"\",\"ownershipType\":[\"createdBy\"],\"compatibilityLevel\":1,\"audience\":[\"Student\"],\"os\":[\"All\"],\"primaryCategory\":\"Textbook Unit\",\"languageCode\":[\"en\"],\"downloadUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/collection/do_11340502356035174411/collection-publish-t20_1636356202227_do_11340502356035174411_1_SPINE.ecar\",\"attributions\":[],\"framework\":\"ncert_k-12\",\"versionKey\":\"1636355436570\",\"mimeType\":\"application/vnd.ekstep.content-collection\",\"code\":\"b186b1bbcc9c58db865f75e34345179e\",\"license\":\"CC BY 4.0\",\"leafNodes\":[\"do_11340096293585715212\"],\"version\":2,\"contentType\":\"TextBookUnit\",\"language\":[\"English\"],\"lastPublishedOn\":\"2021-11-08T12:53:09.398+0530\",\"objectType\":\"Collection\",\"status\":\"Live\",\"dialcodeRequired\":\"No\",\"keywords\":[\"UnitKW1\",\"UnitKW2\"],\"idealScreenSize\":\"normal\",\"contentEncoding\":\"gzip\",\"leafNodesCount\":1,\"depth\":3,\"osId\":\"org.ekstep.launcher\",\"contentDisposition\":\"inline\",\"visibility\":\"Parent\",\"credentials\":{\"enabled\":\"No\"},\"variants\":\"{\\\"spine\\\":{\\\"ecarUrl\\\":\\\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/collection/do_11340502356035174411/collection-publish-t20_1636356202227_do_11340502356035174411_1_SPINE.ecar\\\",\\\"size\\\":\\\"12044\\\"},\\\"online\\\":{\\\"ecarUrl\\\":\\\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/collection/do_11340502356035174411/collection-publish-t20_1636356202412_do_11340502356035174411_1_ONLINE.ecar\\\",\\\"size\\\":\\\"5074\\\"}}\",\"index\":1,\"pkgVersion\":1,\"idealScreenDensity\":\"hdpi\"}],\"mediaType\":\"content\",\"name\":\"5.2 Organ Systems\",\"discussionForum\":{\"enabled\":\"No\"},\"createdOn\":\"2021-11-08T12:40:36.582+0530\",\"channel\":\"0126825293972439041\",\"generateDIALCodes\":\"No\",\"lastUpdatedOn\":\"2021-11-08T12:46:52.715+0530\",\"identifier\":\"do_113405023736479744112\",\"description\":\"\",\"ownershipType\":[\"createdBy\"],\"compatibilityLevel\":1,\"audience\":[\"Student\"],\"os\":[\"All\"],\"primaryCategory\":\"Textbook Unit\",\"languageCode\":[\"en\"],\"downloadUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/collection/do_11340502356035174411/collection-publish-t20_1636356202227_do_11340502356035174411_1_SPINE.ecar\",\"framework\":\"ncert_k-12\",\"versionKey\":\"1636355436582\",\"mimeType\":\"application/vnd.ekstep.content-collection\",\"code\":\"40a1ed37e0fad94eca76b2a96fe086ab\",\"license\":\"CC BY 4.0\",\"leafNodes\":[\"do_11340096293585715212\"],\"version\":2,\"contentType\":\"TextBookUnit\",\"language\":[\"English\"],\"lastPublishedOn\":\"2021-11-08T12:53:09.398+0530\",\"objectType\":\"Collection\",\"status\":\"Live\",\"dialcodeRequired\":\"No\",\"idealScreenSize\":\"normal\",\"contentEncoding\":\"gzip\",\"leafNodesCount\":1,\"depth\":2,\"osId\":\"org.ekstep.launcher\",\"contentDisposition\":\"inline\",\"visibility\":\"Parent\",\"credentials\":{\"enabled\":\"No\"},\"variants\":\"{\\\"spine\\\":{\\\"ecarUrl\\\":\\\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/collection/do_11340502356035174411/collection-publish-t20_1636356202227_do_11340502356035174411_1_SPINE.ecar\\\",\\\"size\\\":\\\"12044\\\"},\\\"online\\\":{\\\"ecarUrl\\\":\\\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/collection/do_11340502356035174411/collection-publish-t20_1636356202412_do_11340502356035174411_1_ONLINE.ecar\\\",\\\"size\\\":\\\"5074\\\"}}\",\"index\":2,\"pkgVersion\":1,\"idealScreenDensity\":\"hdpi\"}],\"mediaType\":\"content\",\"name\":\"5. Human Body\",\"discussionForum\":{\"enabled\":\"No\"},\"createdOn\":\"2021-11-08T12:40:36.586+0530\",\"channel\":\"0126825293972439041\",\"generateDIALCodes\":\"No\",\"lastUpdatedOn\":\"2021-11-08T12:46:52.715+0530\",\"identifier\":\"do_113405023736512512114\",\"description\":\"This chapter describes about human body\",\"ownershipType\":[\"createdBy\"],\"compatibilityLevel\":1,\"audience\":[\"Student\"],\"os\":[\"All\"],\"primaryCategory\":\"Textbook Unit\",\"languageCode\":[\"en\"],\"downloadUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/collection/do_11340502356035174411/collection-publish-t20_1636356202227_do_11340502356035174411_1_SPINE.ecar\",\"framework\":\"ncert_k-12\",\"versionKey\":\"1636355436586\",\"mimeType\":\"application/vnd.ekstep.content-collection\",\"code\":\"76abafa2a0c2cfef90b52db1ef41fb82\",\"license\":\"CC BY 4.0\",\"leafNodes\":[\"do_11340096293585715212\",\"do_11336831941257625611\",\"do_11340096165525094411\"],\"version\":2,\"contentType\":\"TextBookUnit\",\"language\":[\"English\"],\"lastPublishedOn\":\"2021-11-08T12:53:09.398+0530\",\"objectType\":\"Collection\",\"status\":\"Live\",\"dialcodeRequired\":\"No\",\"idealScreenSize\":\"normal\",\"contentEncoding\":\"gzip\",\"leafNodesCount\":3,\"depth\":1,\"osId\":\"org.ekstep.launcher\",\"contentDisposition\":\"inline\",\"visibility\":\"Parent\",\"credentials\":{\"enabled\":\"No\"},\"variants\":\"{\\\"spine\\\":{\\\"ecarUrl\\\":\\\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/collection/do_11340502356035174411/collection-publish-t20_1636356202227_do_11340502356035174411_1_SPINE.ecar\\\",\\\"size\\\":\\\"12044\\\"},\\\"online\\\":{\\\"ecarUrl\\\":\\\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/collection/do_11340502356035174411/collection-publish-t20_1636356202412_do_11340502356035174411_1_ONLINE.ecar\\\",\\\"size\\\":\\\"5074\\\"}}\",\"index\":1,\"pkgVersion\":1,\"idealScreenDensity\":\"hdpi\"}]" - val unpublishedChildrenData = "[{\"lastStatusChangedOn\":\"2021-11-08T15:38:54.180+0530\",\"parent\":\"do_11340511118032076811\",\"children\":[{\"lastStatusChangedOn\":\"2021-11-08T15:38:54.166+0530\",\"parent\":\"do_113405111371202560114\",\"children\":[{\"lastStatusChangedOn\":\"2021-11-08T15:38:54.170+0530\",\"parent\":\"do_11340511137108787216\",\"children\":[{\"copyright\":\"J H S BHARKHOKHA, Tamil Nadu\",\"lastStatusChangedOn\":\"2021-09-17T16:22:50.404+0530\",\"parent\":\"do_11340511137112064018\",\"licenseterms\":\"By creating any type of content (resources, books, courses etc.) on DIKSHA, you consent to publish it under the Creative Commons License Framework. Please choose the applicable creative commons license you wish to apply to your content.\",\"organisation\":[\"J H S BHARKHOKHA\",\"Tamil Nadu\"],\"mediaType\":\"content\",\"name\":\"jaga Aug 25th more than 200mp mp4 update 1\",\"discussionForum\":{\"enabled\":\"No\"},\"createdOn\":\"2021-09-17T16:05:29.019+0530\",\"channel\":\"0126825293972439041\",\"lastUpdatedOn\":\"2021-09-17T16:22:50.404+0530\",\"size\":363062652,\"identifier\":\"do_11336831941257625611\",\"resourceType\":\"Learn\",\"ownershipType\":[\"createdBy\"],\"compatibilityLevel\":1,\"audience\":[\"Student\"],\"os\":[\"All\"],\"primaryCategory\":\"eTextbook\",\"appIcon\":\"https://preprodall.blob.core.windows.net/ntp-content-preprod/content/do_2133520666218741761984/artifact/do_2132664483637248001189_1619439497677_1200px-flag_of_india.thumb.svg.thumb.png\",\"languageCode\":[\"en\"],\"downloadUrl\":\"\",\"framework\":\"tn_k-12_5\",\"creator\":\"सामग्री निर्माता TN\",\"versionKey\":\"1631875539805\",\"mimeType\":\"video/mp4\",\"code\":\"3bd7411e-c03c-4997-a247-4d43a5cc820b\",\"license\":\"CC BY 4.0\",\"version\":2,\"prevStatus\":\"Live\",\"contentType\":\"Resource\",\"prevState\":\"Draft\",\"language\":[\"English\"],\"lastPublishedOn\":\"2021-09-17T16:22:15.047+0530\",\"objectType\":\"Content\",\"status\":\"Live\",\"createdBy\":\"fca2925f-1eee-4654-9177-fece3fd6afc9\",\"dialcodeRequired\":\"No\",\"interceptionPoints\":{},\"idealScreenSize\":\"normal\",\"contentEncoding\":\"identity\",\"depth\":4,\"consumerId\":\"2eaff3db-cdd1-42e5-a611-bebbf906e6cf\",\"lastPublishedBy\":\"\",\"osId\":\"org.ekstep.quiz.app\",\"copyrightYear\":2021,\"se_FWIds\":[\"tn_k-12_5\"],\"contentDisposition\":\"online-only\",\"previewUrl\":\"https://preprodall.blob.core.windows.net/ntp-content-preprod/content/assets/do_2133520666218741761984/como-kids-tv-_-the-story-of-comos-family-_-30min-_-cartoon-video-for-kids.mp4\",\"artifactUrl\":\"https://preprodall.blob.core.windows.net/ntp-content-preprod/content/assets/do_2133520666218741761984/como-kids-tv-_-the-story-of-comos-family-_-30min-_-cartoon-video-for-kids.mp4\",\"visibility\":\"Default\",\"credentials\":{\"enabled\":\"No\"},\"variants\":{\"spine\":{\"ecarUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11336831941257625611/jaga-aug-25th-more-than-200mp-mp4-update-1_1631875935857_do_11336831941257625611_3_SPINE.ecar\",\"size\":\"2172\"}},\"index\":1,\"pkgVersion\":3,\"idealScreenDensity\":\"hdpi\"}],\"mediaType\":\"content\",\"name\":\"5.1.1 Key parts in the head\",\"discussionForum\":{\"enabled\":\"No\"},\"createdOn\":\"2021-11-08T15:38:54.170+0530\",\"channel\":\"0126825293972439041\",\"generateDIALCodes\":\"No\",\"lastUpdatedOn\":\"2021-11-08T15:38:54.170+0530\",\"identifier\":\"do_11340511137112064018\",\"description\":\"xyz\",\"ownershipType\":[\"createdBy\"],\"compatibilityLevel\":1,\"audience\":[\"Student\"],\"os\":[\"All\"],\"primaryCategory\":\"Textbook Unit\",\"languageCode\":[\"en\"],\"framework\":\"ncert_k-12\",\"versionKey\":\"1636366134170\",\"mimeType\":\"application/vnd.ekstep.content-collection\",\"code\":\"7991af5c2e51e4d3d7b83167aaac8829\",\"license\":\"CC BY 4.0\",\"version\":2,\"contentType\":\"TextBookUnit\",\"language\":[\"English\"],\"objectType\":\"Collection\",\"status\":\"Draft\",\"dialcodeRequired\":\"No\",\"idealScreenSize\":\"normal\",\"contentEncoding\":\"gzip\",\"depth\":3,\"osId\":\"org.ekstep.launcher\",\"contentDisposition\":\"inline\",\"visibility\":\"Parent\",\"credentials\":{\"enabled\":\"No\"},\"index\":1,\"idealScreenDensity\":\"hdpi\"},{\"lastStatusChangedOn\":\"2021-11-08T15:38:54.133+0530\",\"parent\":\"do_11340511137108787216\",\"children\":[{\"lastStatusChangedOn\":\"2021-11-02T19:13:39.729+0530\",\"parent\":\"do_11340511137080934412\",\"mediaType\":\"content\",\"name\":\"Collection Publishing PDF Content\",\"discussionForum\":{\"enabled\":\"No\"},\"createdOn\":\"2021-11-02T18:56:17.917+0530\",\"createdFor\":[\"01309282781705830427\"],\"channel\":\"0126825293972439041\",\"lastUpdatedOn\":\"2021-11-02T19:13:39.729+0530\",\"streamingUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/assets/do_1134009478823116801129/chapter_1.pdf\",\"identifier\":\"do_11340096165525094411\",\"resourceType\":\"Learn\",\"ownershipType\":[\"createdBy\"],\"compatibilityLevel\":4,\"audience\":[\"Student\"],\"os\":[\"All\"],\"primaryCategory\":\"Explanation Content\",\"appIcon\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11340094790233292811/artifact/033019_sz_reviews_feat_1564126718632.thumb.jpg\",\"languageCode\":[\"en\"],\"downloadUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11340096165525094411/collection-publishing-pdf-content_1635860615969_do_11340096165525094411_1.ecar\",\"framework\":\"ekstep_ncert_k-12\",\"creator\":\"N131\",\"versionKey\":\"1635859577917\",\"mimeType\":\"application/pdf\",\"code\":\"c9ce1ce0-b9b4-402e-a9c3-556701070838\",\"license\":\"CC BY 4.0\",\"version\":2,\"prevStatus\":\"Processing\",\"contentType\":\"Resource\",\"prevState\":\"Draft\",\"language\":[\"English\"],\"lastPublishedOn\":\"2021-11-02T19:13:35.589+0530\",\"objectType\":\"Content\",\"status\":\"Live\",\"pragma\":[\"external\"],\"createdBy\":\"0b71985d-fcb0-4018-ab14-83f10c3b0426\",\"dialcodeRequired\":\"No\",\"interceptionPoints\":{},\"keywords\":[\"CPPDFContent1\",\"CPPDFContent2\",\"CollectionKW1\"],\"idealScreenSize\":\"normal\",\"contentEncoding\":\"identity\",\"depth\":4,\"lastPublishedBy\":\"\",\"osId\":\"org.ekstep.quiz.app\",\"copyrightYear\":2021,\"se_FWIds\":[\"ekstep_ncert_k-12\"],\"contentDisposition\":\"inline\",\"previewUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/assets/do_1134009478823116801129/chapter_1.pdf\",\"artifactUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/assets/do_1134009478823116801129/chapter_1.pdf\",\"visibility\":\"Default\",\"credentials\":{\"enabled\":\"No\"},\"variants\":{\"full\":{\"ecarUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11340096165525094411/collection-publishing-pdf-content_1635860615969_do_11340096165525094411_1.ecar\",\"size\":\"256918\"},\"spine\":{\"ecarUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11340096165525094411/collection-publishing-pdf-content_1635860619148_do_11340096165525094411_1_SPINE.ecar\",\"size\":\"6378\"}},\"index\":1,\"pkgVersion\":1,\"idealScreenDensity\":\"hdpi\"}],\"mediaType\":\"content\",\"name\":\"5.1.2 Other parts\",\"discussionForum\":{\"enabled\":\"No\"},\"createdOn\":\"2021-11-08T15:38:54.133+0530\",\"channel\":\"0126825293972439041\",\"generateDIALCodes\":\"No\",\"lastUpdatedOn\":\"2021-11-08T15:38:54.132+0530\",\"identifier\":\"do_11340511137080934412\",\"description\":\"\",\"ownershipType\":[\"createdBy\"],\"compatibilityLevel\":1,\"audience\":[\"Student\"],\"os\":[\"All\"],\"primaryCategory\":\"Textbook Unit\",\"languageCode\":[\"en\"],\"framework\":\"ncert_k-12\",\"versionKey\":\"1636366134133\",\"mimeType\":\"application/vnd.ekstep.content-collection\",\"code\":\"3bf70f06d3e8dba010d8806fd94259b1\",\"license\":\"CC BY 4.0\",\"version\":2,\"contentType\":\"TextBookUnit\",\"language\":[\"English\"],\"objectType\":\"Collection\",\"status\":\"Draft\",\"dialcodeRequired\":\"No\",\"idealScreenSize\":\"normal\",\"contentEncoding\":\"gzip\",\"depth\":3,\"osId\":\"org.ekstep.launcher\",\"contentDisposition\":\"inline\",\"visibility\":\"Parent\",\"credentials\":{\"enabled\":\"No\"},\"index\":2,\"idealScreenDensity\":\"hdpi\"}],\"mediaType\":\"content\",\"name\":\"5.1 Parts of Body\",\"discussionForum\":{\"enabled\":\"No\"},\"createdOn\":\"2021-11-08T15:38:54.166+0530\",\"channel\":\"0126825293972439041\",\"generateDIALCodes\":\"No\",\"lastUpdatedOn\":\"2021-11-08T15:38:54.165+0530\",\"identifier\":\"do_11340511137108787216\",\"description\":\"This section describes about various part of the body such as head, hands, legs etc.\",\"ownershipType\":[\"createdBy\"],\"compatibilityLevel\":1,\"audience\":[\"Student\"],\"os\":[\"All\"],\"primaryCategory\":\"Textbook Unit\",\"languageCode\":[\"en\"],\"framework\":\"ncert_k-12\",\"versionKey\":\"1636366134166\",\"mimeType\":\"application/vnd.ekstep.content-collection\",\"code\":\"20cc1f31e62f924c6e47bf04c994376b\",\"license\":\"CC BY 4.0\",\"version\":2,\"contentType\":\"TextBookUnit\",\"language\":[\"English\"],\"objectType\":\"Collection\",\"status\":\"Draft\",\"dialcodeRequired\":\"No\",\"idealScreenSize\":\"normal\",\"contentEncoding\":\"gzip\",\"depth\":2,\"osId\":\"org.ekstep.launcher\",\"contentDisposition\":\"inline\",\"visibility\":\"Parent\",\"credentials\":{\"enabled\":\"No\"},\"index\":1,\"idealScreenDensity\":\"hdpi\"},{\"lastStatusChangedOn\":\"2021-11-08T15:38:54.177+0530\",\"parent\":\"do_113405111371202560114\",\"children\":[{\"lastStatusChangedOn\":\"2021-11-08T15:38:54.162+0530\",\"parent\":\"do_113405111371169792112\",\"children\":[{\"lastStatusChangedOn\":\"2021-11-08T15:38:54.173+0530\",\"parent\":\"do_11340511137105510414\",\"children\":[{\"lastStatusChangedOn\":\"2021-11-02T19:16:10.667+0530\",\"parent\":\"do_113405111371145216110\",\"mediaType\":\"content\",\"name\":\"Collection Publish MP4 content\",\"discussionForum\":{\"enabled\":\"No\"},\"createdOn\":\"2021-11-02T18:58:53.445+0530\",\"channel\":\"0126825293972439041\",\"lastUpdatedOn\":\"2021-11-02T19:16:10.667+0530\",\"identifier\":\"do_11340096293585715212\",\"resourceType\":\"Learn\",\"ownershipType\":[\"createdBy\"],\"compatibilityLevel\":1,\"audience\":[\"Student\"],\"os\":[\"All\"],\"primaryCategory\":\"Explanation Content\",\"appIcon\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_1134009488766730241130/artifact/033019_sz_reviews_feat_1564126718632.thumb.jpg\",\"languageCode\":[\"en\"],\"downloadUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11340096293585715212/collection-publish-mp4-content_1635860769119_do_11340096293585715212_1.ecar\",\"framework\":\"ekstep_ncert_k-12\",\"versionKey\":\"1635859733445\",\"mimeType\":\"video/mp4\",\"code\":\"e0b58864-3dc5-484a-b194-38c3eddcbce1\",\"license\":\"CC BY 4.0\",\"version\":2,\"prevStatus\":\"Draft\",\"contentType\":\"Resource\",\"prevState\":\"Draft\",\"language\":[\"English\"],\"lastPublishedOn\":\"2021-11-02T19:16:08.789+0530\",\"objectType\":\"Content\",\"status\":\"Live\",\"createdBy\":\"0b71985d-fcb0-4018-ab14-83f10c3b0426\",\"dialcodeRequired\":\"No\",\"interceptionPoints\":{},\"keywords\":[\"CPMP4ContentKW1\",\"CPMP4ContentKW2\"],\"idealScreenSize\":\"normal\",\"contentEncoding\":\"identity\",\"depth\":5,\"lastPublishedBy\":\"\",\"osId\":\"org.ekstep.quiz.app\",\"se_FWIds\":[\"ekstep_ncert_k-12\"],\"contentDisposition\":\"inline\",\"previewUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/assets/do_1134009488766730241130/amoeba-eat.mp4\",\"artifactUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/assets/do_1134009488766730241130/amoeba-eat.mp4\",\"visibility\":\"Default\",\"credentials\":{\"enabled\":\"No\"},\"variants\":{\"full\":{\"ecarUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11340096293585715212/collection-publish-mp4-content_1635860769119_do_11340096293585715212_1.ecar\",\"size\":\"2692101\"},\"spine\":{\"ecarUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11340096293585715212/collection-publish-mp4-content_1635860770277_do_11340096293585715212_1_SPINE.ecar\",\"size\":\"6275\"}},\"index\":1,\"pkgVersion\":1,\"idealScreenDensity\":\"hdpi\"}],\"mediaType\":\"content\",\"name\":\"dsffgdg\",\"discussionForum\":{\"enabled\":\"No\"},\"createdOn\":\"2021-11-08T15:38:54.173+0530\",\"channel\":\"0126825293972439041\",\"generateDIALCodes\":\"No\",\"lastUpdatedOn\":\"2021-11-08T15:38:54.173+0530\",\"identifier\":\"do_113405111371145216110\",\"description\":\"\",\"ownershipType\":[\"createdBy\"],\"compatibilityLevel\":1,\"audience\":[\"Student\"],\"os\":[\"All\"],\"primaryCategory\":\"Textbook Unit\",\"languageCode\":[\"en\"],\"framework\":\"ncert_k-12\",\"versionKey\":\"1636366134173\",\"mimeType\":\"application/vnd.ekstep.content-collection\",\"code\":\"9cf84ff2fb08f9af4c23eb09df9b2520\",\"license\":\"CC BY 4.0\",\"version\":2,\"contentType\":\"TextBookUnit\",\"language\":[\"English\"],\"objectType\":\"Collection\",\"status\":\"Draft\",\"dialcodeRequired\":\"No\",\"idealScreenSize\":\"normal\",\"contentEncoding\":\"gzip\",\"depth\":4,\"osId\":\"org.ekstep.launcher\",\"contentDisposition\":\"inline\",\"visibility\":\"Parent\",\"credentials\":{\"enabled\":\"No\"},\"index\":1,\"idealScreenDensity\":\"hdpi\"}],\"mediaType\":\"content\",\"name\":\"5.2.1 Respiratory System\",\"discussionForum\":{\"enabled\":\"No\"},\"createdOn\":\"2021-11-08T15:38:54.162+0530\",\"channel\":\"0126825293972439041\",\"generateDIALCodes\":\"No\",\"lastUpdatedOn\":\"2021-11-08T15:53:32.894+0530\",\"identifier\":\"do_11340511137105510414\",\"description\":\"\",\"ownershipType\":[\"createdBy\"],\"compatibilityLevel\":1,\"audience\":[\"Student\"],\"os\":[\"All\"],\"primaryCategory\":\"Textbook Unit\",\"languageCode\":[\"en\"],\"attributions\":[],\"framework\":\"ncert_k-12\",\"versionKey\":\"1636366134162\",\"mimeType\":\"application/vnd.ekstep.content-collection\",\"code\":\"b186b1bbcc9c58db865f75e34345179e\",\"license\":\"CC BY 4.0\",\"version\":2,\"contentType\":\"TextBookUnit\",\"language\":[\"English\"],\"objectType\":\"Collection\",\"status\":\"Draft\",\"dialcodeRequired\":\"No\",\"keywords\":[\"UnitKW1\",\"UnitKW2\"],\"idealScreenSize\":\"normal\",\"contentEncoding\":\"gzip\",\"depth\":3,\"osId\":\"org.ekstep.launcher\",\"contentDisposition\":\"inline\",\"visibility\":\"Parent\",\"credentials\":{\"enabled\":\"No\"},\"index\":1,\"idealScreenDensity\":\"hdpi\"}],\"mediaType\":\"content\",\"name\":\"5.2 Organ Systems\",\"discussionForum\":{\"enabled\":\"No\"},\"createdOn\":\"2021-11-08T15:38:54.176+0530\",\"channel\":\"0126825293972439041\",\"generateDIALCodes\":\"No\",\"lastUpdatedOn\":\"2021-11-08T15:38:54.176+0530\",\"identifier\":\"do_113405111371169792112\",\"description\":\"\",\"ownershipType\":[\"createdBy\"],\"compatibilityLevel\":1,\"audience\":[\"Student\"],\"os\":[\"All\"],\"primaryCategory\":\"Textbook Unit\",\"languageCode\":[\"en\"],\"framework\":\"ncert_k-12\",\"versionKey\":\"1636366134176\",\"mimeType\":\"application/vnd.ekstep.content-collection\",\"code\":\"40a1ed37e0fad94eca76b2a96fe086ab\",\"license\":\"CC BY 4.0\",\"version\":2,\"contentType\":\"TextBookUnit\",\"language\":[\"English\"],\"objectType\":\"Collection\",\"status\":\"Draft\",\"dialcodeRequired\":\"No\",\"idealScreenSize\":\"normal\",\"contentEncoding\":\"gzip\",\"depth\":2,\"osId\":\"org.ekstep.launcher\",\"contentDisposition\":\"inline\",\"visibility\":\"Parent\",\"credentials\":{\"enabled\":\"No\"},\"index\":2,\"idealScreenDensity\":\"hdpi\"}],\"mediaType\":\"content\",\"name\":\"5. Human Body\",\"discussionForum\":{\"enabled\":\"No\"},\"createdOn\":\"2021-11-08T15:38:54.180+0530\",\"channel\":\"0126825293972439041\",\"generateDIALCodes\":\"No\",\"lastUpdatedOn\":\"2021-11-08T15:38:54.179+0530\",\"identifier\":\"do_113405111371202560114\",\"description\":\"This chapter describes about human body\",\"ownershipType\":[\"createdBy\"],\"compatibilityLevel\":1,\"audience\":[\"Student\"],\"os\":[\"All\"],\"primaryCategory\":\"Textbook Unit\",\"languageCode\":[\"en\"],\"framework\":\"ncert_k-12\",\"versionKey\":\"1636366134180\",\"mimeType\":\"application/vnd.ekstep.content-collection\",\"code\":\"76abafa2a0c2cfef90b52db1ef41fb82\",\"license\":\"CC BY 4.0\",\"version\":2,\"contentType\":\"TextBookUnit\",\"language\":[\"English\"],\"objectType\":\"Collection\",\"status\":\"Draft\",\"dialcodeRequired\":\"No\",\"idealScreenSize\":\"normal\",\"contentEncoding\":\"gzip\",\"depth\":1,\"osId\":\"org.ekstep.launcher\",\"contentDisposition\":\"inline\",\"visibility\":\"Parent\",\"credentials\":{\"enabled\":\"No\"},\"index\":1,\"idealScreenDensity\":\"hdpi\"}]" + val publishedChildrenData = "[{\"lastStatusChangedOn\":\"2021-11-08T12:40:36.586+0530\",\"parent\":\"do_11340502356035174411\",\"children\":[{\"lastStatusChangedOn\":\"2021-11-08T12:40:36.572+0530\",\"parent\":\"do_113405023736512512114\",\"children\":[{\"lastStatusChangedOn\":\"2021-11-08T12:40:36.575+0530\",\"parent\":\"do_11340502373639782416\",\"children\":[{\"copyright\":\"J H S BHARKHOKHA, Tamil Nadu\",\"lastStatusChangedOn\":\"2021-09-17T16:22:50.404+0530\",\"parent\":\"do_11340502373642240018\",\"licenseterms\":\"By creating any type of content (resources, books, courses etc.) on DIKSHA, you consent to publish it under the Creative Commons License Framework. Please choose the applicable creative commons license you wish to apply to your content.\",\"organisation\":[\"J H S BHARKHOKHA\",\"Tamil Nadu\"],\"mediaType\":\"content\",\"name\":\"jaga Aug 25th more than 200mp mp4 update 1\",\"discussionForum\":{\"enabled\":\"No\"},\"createdOn\":\"2021-09-17T16:05:29.019+0530\",\"channel\":\"0126825293972439041\",\"lastUpdatedOn\":\"2021-09-17T16:22:50.404+0530\",\"size\":363062652,\"identifier\":\"do_11336831941257625611\",\"resourceType\":\"Learn\",\"ownershipType\":[\"createdBy\"],\"compatibilityLevel\":1,\"audience\":[\"Student\"],\"os\":[\"All\"],\"primaryCategory\":\"eTextbook\",\"appIcon\":\"https://sunbirddevbbpublic.blob.core.windows.net/sunbird-content-staging/content/assets/do_2137327580080128001217/gateway-of-india.jpg\",\"languageCode\":[\"en\"],\"downloadUrl\":\"\",\"framework\":\"tn_k-12_5\",\"creator\":\"सामग्री निर्माता TN\",\"versionKey\":\"1631875539805\",\"mimeType\":\"video/mp4\",\"code\":\"3bd7411e-c03c-4997-a247-4d43a5cc820b\",\"license\":\"CC BY 4.0\",\"version\":2,\"prevStatus\":\"Live\",\"contentType\":\"Resource\",\"prevState\":\"Draft\",\"language\":[\"English\"],\"lastPublishedOn\":\"2021-09-17T16:22:15.047+0530\",\"objectType\":\"Content\",\"status\":\"Live\",\"createdBy\":\"fca2925f-1eee-4654-9177-fece3fd6afc9\",\"dialcodeRequired\":\"No\",\"interceptionPoints\":{},\"idealScreenSize\":\"normal\",\"contentEncoding\":\"identity\",\"depth\":4,\"consumerId\":\"2eaff3db-cdd1-42e5-a611-bebbf906e6cf\",\"lastPublishedBy\":\"\",\"osId\":\"org.ekstep.quiz.app\",\"copyrightYear\":2021,\"se_FWIds\":[\"tn_k-12_5\"],\"contentDisposition\":\"online-only\",\"previewUrl\":\"https://preprodall.blob.core.windows.net/ntp-content-preprod/content/assets/do_2133520666218741761984/como-kids-tv-_-the-story-of-comos-family-_-30min-_-cartoon-video-for-kids.mp4\",\"artifactUrl\":\"https://preprodall.blob.core.windows.net/ntp-content-preprod/content/assets/do_2133520666218741761984/como-kids-tv-_-the-story-of-comos-family-_-30min-_-cartoon-video-for-kids.mp4\",\"visibility\":\"Default\",\"credentials\":{\"enabled\":\"No\"},\"variants\":{\"spine\":{\"ecarUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11336831941257625611/jaga-aug-25th-more-than-200mp-mp4-update-1_1631875935857_do_11336831941257625611_3_SPINE.ecar\",\"size\":\"2172\"}},\"index\":1,\"pkgVersion\":3,\"idealScreenDensity\":\"hdpi\"}],\"mediaType\":\"content\",\"name\":\"5.1.1 Key parts in the head\",\"discussionForum\":{\"enabled\":\"No\"},\"createdOn\":\"2021-11-08T12:40:36.575+0530\",\"channel\":\"0126825293972439041\",\"generateDIALCodes\":\"No\",\"lastUpdatedOn\":\"2021-11-08T12:46:52.715+0530\",\"identifier\":\"do_11340502373642240018\",\"description\":\"xyz\",\"ownershipType\":[\"createdBy\"],\"compatibilityLevel\":1,\"audience\":[\"Student\"],\"os\":[\"All\"],\"primaryCategory\":\"Textbook Unit\",\"languageCode\":[\"en\"],\"downloadUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/collection/do_11340502356035174411/collection-publish-t20_1636356202227_do_11340502356035174411_1_SPINE.ecar\",\"framework\":\"ncert_k-12\",\"versionKey\":\"1636355436575\",\"mimeType\":\"application/vnd.ekstep.content-collection\",\"code\":\"7991af5c2e51e4d3d7b83167aaac8829\",\"license\":\"CC BY 4.0\",\"leafNodes\":[\"do_11336831941257625611\"],\"version\":2,\"contentType\":\"TextBookUnit\",\"language\":[\"English\"],\"lastPublishedOn\":\"2021-11-08T12:53:09.398+0530\",\"objectType\":\"Collection\",\"status\":\"Live\",\"dialcodeRequired\":\"No\",\"idealScreenSize\":\"normal\",\"contentEncoding\":\"gzip\",\"leafNodesCount\":1,\"depth\":3,\"osId\":\"org.ekstep.launcher\",\"contentDisposition\":\"inline\",\"visibility\":\"Parent\",\"credentials\":{\"enabled\":\"No\"},\"variants\":\"{\\\"spine\\\":{\\\"ecarUrl\\\":\\\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/collection/do_11340502356035174411/collection-publish-t20_1636356202227_do_11340502356035174411_1_SPINE.ecar\\\",\\\"size\\\":\\\"12044\\\"},\\\"online\\\":{\\\"ecarUrl\\\":\\\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/collection/do_11340502356035174411/collection-publish-t20_1636356202412_do_11340502356035174411_1_ONLINE.ecar\\\",\\\"size\\\":\\\"5074\\\"}}\",\"index\":1,\"pkgVersion\":1,\"idealScreenDensity\":\"hdpi\"},{\"lastStatusChangedOn\":\"2021-11-08T12:40:36.534+0530\",\"parent\":\"do_11340502373639782416\",\"children\":[{\"lastStatusChangedOn\":\"2021-11-02T19:13:39.729+0530\",\"parent\":\"do_11340502373608652812\",\"mediaType\":\"content\",\"name\":\"Collection Publishing PDF Content\",\"discussionForum\":{\"enabled\":\"No\"},\"createdOn\":\"2021-11-02T18:56:17.917+0530\",\"createdFor\":[\"01309282781705830427\"],\"channel\":\"0126825293972439041\",\"lastUpdatedOn\":\"2021-11-02T19:13:39.729+0530\",\"streamingUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/assets/do_1134009478823116801129/chapter_1.pdf\",\"identifier\":\"do_11340096165525094411\",\"resourceType\":\"Learn\",\"ownershipType\":[\"createdBy\"],\"compatibilityLevel\":4,\"audience\":[\"Student\"],\"os\":[\"All\"],\"primaryCategory\":\"Explanation Content\",\"appIcon\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11340094790233292811/artifact/033019_sz_reviews_feat_1564126718632.thumb.jpg\",\"languageCode\":[\"en\"],\"downloadUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11340096165525094411/collection-publishing-pdf-content_1635860615969_do_11340096165525094411_1.ecar\",\"framework\":\"ekstep_ncert_k-12\",\"creator\":\"N131\",\"versionKey\":\"1635859577917\",\"mimeType\":\"application/pdf\",\"code\":\"c9ce1ce0-b9b4-402e-a9c3-556701070838\",\"license\":\"CC BY 4.0\",\"version\":2,\"prevStatus\":\"Processing\",\"contentType\":\"Resource\",\"prevState\":\"Draft\",\"language\":[\"English\"],\"lastPublishedOn\":\"2021-11-02T19:13:35.589+0530\",\"objectType\":\"Content\",\"status\":\"Live\",\"pragma\":[\"external\"],\"createdBy\":\"0b71985d-fcb0-4018-ab14-83f10c3b0426\",\"dialcodeRequired\":\"No\",\"interceptionPoints\":{},\"keywords\":[\"CPPDFContent1\",\"CPPDFContent2\",\"CollectionKW1\"],\"idealScreenSize\":\"normal\",\"contentEncoding\":\"identity\",\"depth\":4,\"lastPublishedBy\":\"\",\"osId\":\"org.ekstep.quiz.app\",\"copyrightYear\":2021,\"se_FWIds\":[\"ekstep_ncert_k-12\"],\"contentDisposition\":\"inline\",\"previewUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/assets/do_1134009478823116801129/chapter_1.pdf\",\"artifactUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/assets/do_1134009478823116801129/chapter_1.pdf\",\"visibility\":\"Default\",\"credentials\":{\"enabled\":\"No\"},\"variants\":{\"full\":{\"ecarUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11340096165525094411/collection-publishing-pdf-content_1635860615969_do_11340096165525094411_1.ecar\",\"size\":\"256918\"},\"spine\":{\"ecarUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11340096165525094411/collection-publishing-pdf-content_1635860619148_do_11340096165525094411_1_SPINE.ecar\",\"size\":\"6378\"}},\"index\":1,\"pkgVersion\":1,\"idealScreenDensity\":\"hdpi\"}],\"mediaType\":\"content\",\"name\":\"5.1.2 Other parts\",\"discussionForum\":{\"enabled\":\"No\"},\"createdOn\":\"2021-11-08T12:40:36.534+0530\",\"channel\":\"0126825293972439041\",\"generateDIALCodes\":\"No\",\"lastUpdatedOn\":\"2021-11-08T12:46:52.715+0530\",\"identifier\":\"do_11340502373608652812\",\"description\":\"\",\"ownershipType\":[\"createdBy\"],\"compatibilityLevel\":1,\"audience\":[\"Student\"],\"os\":[\"All\"],\"primaryCategory\":\"Textbook Unit\",\"languageCode\":[\"en\"],\"downloadUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/collection/do_11340502356035174411/collection-publish-t20_1636356202227_do_11340502356035174411_1_SPINE.ecar\",\"framework\":\"ncert_k-12\",\"versionKey\":\"1636355436534\",\"mimeType\":\"application/vnd.ekstep.content-collection\",\"code\":\"3bf70f06d3e8dba010d8806fd94259b1\",\"license\":\"CC BY 4.0\",\"leafNodes\":[\"do_11340096165525094411\"],\"version\":2,\"contentType\":\"TextBookUnit\",\"language\":[\"English\"],\"lastPublishedOn\":\"2021-11-08T12:53:09.398+0530\",\"objectType\":\"Collection\",\"status\":\"Live\",\"dialcodeRequired\":\"No\",\"idealScreenSize\":\"normal\",\"contentEncoding\":\"gzip\",\"leafNodesCount\":1,\"depth\":3,\"osId\":\"org.ekstep.launcher\",\"contentDisposition\":\"inline\",\"visibility\":\"Parent\",\"credentials\":{\"enabled\":\"No\"},\"variants\":\"{\\\"spine\\\":{\\\"ecarUrl\\\":\\\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/collection/do_11340502356035174411/collection-publish-t20_1636356202227_do_11340502356035174411_1_SPINE.ecar\\\",\\\"size\\\":\\\"12044\\\"},\\\"online\\\":{\\\"ecarUrl\\\":\\\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/collection/do_11340502356035174411/collection-publish-t20_1636356202412_do_11340502356035174411_1_ONLINE.ecar\\\",\\\"size\\\":\\\"5074\\\"}}\",\"index\":2,\"pkgVersion\":1,\"idealScreenDensity\":\"hdpi\"}],\"mediaType\":\"content\",\"name\":\"5.1 Parts of Body\",\"discussionForum\":{\"enabled\":\"No\"},\"createdOn\":\"2021-11-08T12:40:36.572+0530\",\"channel\":\"0126825293972439041\",\"generateDIALCodes\":\"No\",\"lastUpdatedOn\":\"2021-11-08T12:46:52.715+0530\",\"identifier\":\"do_11340502373639782416\",\"description\":\"This section describes about various part of the body such as head, hands, legs etc.\",\"ownershipType\":[\"createdBy\"],\"compatibilityLevel\":1,\"audience\":[\"Student\"],\"os\":[\"All\"],\"primaryCategory\":\"Textbook Unit\",\"languageCode\":[\"en\"],\"downloadUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/collection/do_11340502356035174411/collection-publish-t20_1636356202227_do_11340502356035174411_1_SPINE.ecar\",\"framework\":\"ncert_k-12\",\"versionKey\":\"1636355436572\",\"mimeType\":\"application/vnd.ekstep.content-collection\",\"code\":\"20cc1f31e62f924c6e47bf04c994376b\",\"license\":\"CC BY 4.0\",\"leafNodes\":[\"do_11336831941257625611\",\"do_11340096165525094411\"],\"version\":2,\"contentType\":\"TextBookUnit\",\"language\":[\"English\"],\"lastPublishedOn\":\"2021-11-08T12:53:09.398+0530\",\"objectType\":\"Collection\",\"status\":\"Live\",\"dialcodeRequired\":\"No\",\"idealScreenSize\":\"normal\",\"contentEncoding\":\"gzip\",\"leafNodesCount\":2,\"depth\":2,\"osId\":\"org.ekstep.launcher\",\"contentDisposition\":\"inline\",\"visibility\":\"Parent\",\"credentials\":{\"enabled\":\"No\"},\"variants\":\"{\\\"spine\\\":{\\\"ecarUrl\\\":\\\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/collection/do_11340502356035174411/collection-publish-t20_1636356202227_do_11340502356035174411_1_SPINE.ecar\\\",\\\"size\\\":\\\"12044\\\"},\\\"online\\\":{\\\"ecarUrl\\\":\\\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/collection/do_11340502356035174411/collection-publish-t20_1636356202412_do_11340502356035174411_1_ONLINE.ecar\\\",\\\"size\\\":\\\"5074\\\"}}\",\"index\":1,\"pkgVersion\":1,\"idealScreenDensity\":\"hdpi\"},{\"lastStatusChangedOn\":\"2021-11-08T12:40:36.582+0530\",\"parent\":\"do_113405023736512512114\",\"children\":[{\"lastStatusChangedOn\":\"2021-11-08T12:40:36.570+0530\",\"parent\":\"do_113405023736479744112\",\"children\":[{\"lastStatusChangedOn\":\"2021-11-08T12:40:36.579+0530\",\"parent\":\"do_11340502373638144014\",\"children\":[{\"lastStatusChangedOn\":\"2021-11-02T19:16:10.667+0530\",\"parent\":\"do_113405023736455168110\",\"mediaType\":\"content\",\"name\":\"Collection Publish MP4 content\",\"discussionForum\":{\"enabled\":\"No\"},\"createdOn\":\"2021-11-02T18:58:53.445+0530\",\"channel\":\"0126825293972439041\",\"lastUpdatedOn\":\"2021-11-02T19:16:10.667+0530\",\"identifier\":\"do_11340096293585715212\",\"resourceType\":\"Learn\",\"ownershipType\":[\"createdBy\"],\"compatibilityLevel\":1,\"audience\":[\"Student\"],\"os\":[\"All\"],\"primaryCategory\":\"Explanation Content\",\"appIcon\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_1134009488766730241130/artifact/033019_sz_reviews_feat_1564126718632.thumb.jpg\",\"languageCode\":[\"en\"],\"downloadUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11340096293585715212/collection-publish-mp4-content_1635860769119_do_11340096293585715212_1.ecar\",\"framework\":\"ekstep_ncert_k-12\",\"versionKey\":\"1635859733445\",\"mimeType\":\"video/mp4\",\"code\":\"e0b58864-3dc5-484a-b194-38c3eddcbce1\",\"license\":\"CC BY 4.0\",\"version\":2,\"prevStatus\":\"Draft\",\"contentType\":\"Resource\",\"prevState\":\"Draft\",\"language\":[\"English\"],\"lastPublishedOn\":\"2021-11-02T19:16:08.789+0530\",\"objectType\":\"Content\",\"status\":\"Live\",\"createdBy\":\"0b71985d-fcb0-4018-ab14-83f10c3b0426\",\"dialcodeRequired\":\"No\",\"interceptionPoints\":{},\"keywords\":[\"CPMP4ContentKW1\",\"CPMP4ContentKW2\"],\"idealScreenSize\":\"normal\",\"contentEncoding\":\"identity\",\"depth\":5,\"lastPublishedBy\":\"\",\"osId\":\"org.ekstep.quiz.app\",\"se_FWIds\":[\"ekstep_ncert_k-12\"],\"contentDisposition\":\"inline\",\"previewUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/assets/do_1134009488766730241130/amoeba-eat.mp4\",\"artifactUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/assets/do_1134009488766730241130/amoeba-eat.mp4\",\"visibility\":\"Default\",\"credentials\":{\"enabled\":\"No\"},\"variants\":{\"full\":{\"ecarUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11340096293585715212/collection-publish-mp4-content_1635860769119_do_11340096293585715212_1.ecar\",\"size\":\"2692101\"},\"spine\":{\"ecarUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11340096293585715212/collection-publish-mp4-content_1635860770277_do_11340096293585715212_1_SPINE.ecar\",\"size\":\"6275\"}},\"index\":1,\"pkgVersion\":1,\"idealScreenDensity\":\"hdpi\"}],\"mediaType\":\"content\",\"name\":\"dsffgdg\",\"discussionForum\":{\"enabled\":\"No\"},\"createdOn\":\"2021-11-08T12:40:36.579+0530\",\"channel\":\"0126825293972439041\",\"generateDIALCodes\":\"No\",\"lastUpdatedOn\":\"2021-11-08T12:46:52.715+0530\",\"identifier\":\"do_113405023736455168110\",\"description\":\"\",\"ownershipType\":[\"createdBy\"],\"compatibilityLevel\":1,\"audience\":[\"Student\"],\"os\":[\"All\"],\"primaryCategory\":\"Textbook Unit\",\"languageCode\":[\"en\"],\"downloadUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/collection/do_11340502356035174411/collection-publish-t20_1636356202227_do_11340502356035174411_1_SPINE.ecar\",\"framework\":\"ncert_k-12\",\"versionKey\":\"1636355436579\",\"mimeType\":\"application/vnd.ekstep.content-collection\",\"code\":\"9cf84ff2fb08f9af4c23eb09df9b2520\",\"license\":\"CC BY 4.0\",\"leafNodes\":[\"do_11340096293585715212\"],\"version\":2,\"contentType\":\"TextBookUnit\",\"language\":[\"English\"],\"lastPublishedOn\":\"2021-11-08T12:53:09.398+0530\",\"objectType\":\"Collection\",\"status\":\"Live\",\"dialcodeRequired\":\"No\",\"idealScreenSize\":\"normal\",\"contentEncoding\":\"gzip\",\"leafNodesCount\":1,\"depth\":4,\"osId\":\"org.ekstep.launcher\",\"contentDisposition\":\"inline\",\"visibility\":\"Parent\",\"credentials\":{\"enabled\":\"No\"},\"variants\":\"{\\\"spine\\\":{\\\"ecarUrl\\\":\\\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/collection/do_11340502356035174411/collection-publish-t20_1636356202227_do_11340502356035174411_1_SPINE.ecar\\\",\\\"size\\\":\\\"12044\\\"},\\\"online\\\":{\\\"ecarUrl\\\":\\\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/collection/do_11340502356035174411/collection-publish-t20_1636356202412_do_11340502356035174411_1_ONLINE.ecar\\\",\\\"size\\\":\\\"5074\\\"}}\",\"index\":1,\"pkgVersion\":1,\"idealScreenDensity\":\"hdpi\"}],\"mediaType\":\"content\",\"name\":\"5.2.1 Respiratory System\",\"discussionForum\":{\"enabled\":\"No\"},\"createdOn\":\"2021-11-08T12:40:36.570+0530\",\"channel\":\"0126825293972439041\",\"generateDIALCodes\":\"No\",\"lastUpdatedOn\":\"2021-11-08T12:46:52.715+0530\",\"identifier\":\"do_11340502373638144014\",\"description\":\"\",\"ownershipType\":[\"createdBy\"],\"compatibilityLevel\":1,\"audience\":[\"Student\"],\"os\":[\"All\"],\"primaryCategory\":\"Textbook Unit\",\"languageCode\":[\"en\"],\"downloadUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/collection/do_11340502356035174411/collection-publish-t20_1636356202227_do_11340502356035174411_1_SPINE.ecar\",\"attributions\":[],\"framework\":\"ncert_k-12\",\"versionKey\":\"1636355436570\",\"mimeType\":\"application/vnd.ekstep.content-collection\",\"code\":\"b186b1bbcc9c58db865f75e34345179e\",\"license\":\"CC BY 4.0\",\"leafNodes\":[\"do_11340096293585715212\"],\"version\":2,\"contentType\":\"TextBookUnit\",\"language\":[\"English\"],\"lastPublishedOn\":\"2021-11-08T12:53:09.398+0530\",\"objectType\":\"Collection\",\"status\":\"Live\",\"dialcodeRequired\":\"No\",\"keywords\":[\"UnitKW1\",\"UnitKW2\"],\"idealScreenSize\":\"normal\",\"contentEncoding\":\"gzip\",\"leafNodesCount\":1,\"depth\":3,\"osId\":\"org.ekstep.launcher\",\"contentDisposition\":\"inline\",\"visibility\":\"Parent\",\"credentials\":{\"enabled\":\"No\"},\"variants\":\"{\\\"spine\\\":{\\\"ecarUrl\\\":\\\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/collection/do_11340502356035174411/collection-publish-t20_1636356202227_do_11340502356035174411_1_SPINE.ecar\\\",\\\"size\\\":\\\"12044\\\"},\\\"online\\\":{\\\"ecarUrl\\\":\\\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/collection/do_11340502356035174411/collection-publish-t20_1636356202412_do_11340502356035174411_1_ONLINE.ecar\\\",\\\"size\\\":\\\"5074\\\"}}\",\"index\":1,\"pkgVersion\":1,\"idealScreenDensity\":\"hdpi\"}],\"mediaType\":\"content\",\"name\":\"5.2 Organ Systems\",\"discussionForum\":{\"enabled\":\"No\"},\"createdOn\":\"2021-11-08T12:40:36.582+0530\",\"channel\":\"0126825293972439041\",\"generateDIALCodes\":\"No\",\"lastUpdatedOn\":\"2021-11-08T12:46:52.715+0530\",\"identifier\":\"do_113405023736479744112\",\"description\":\"\",\"ownershipType\":[\"createdBy\"],\"compatibilityLevel\":1,\"audience\":[\"Student\"],\"os\":[\"All\"],\"primaryCategory\":\"Textbook Unit\",\"languageCode\":[\"en\"],\"downloadUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/collection/do_11340502356035174411/collection-publish-t20_1636356202227_do_11340502356035174411_1_SPINE.ecar\",\"framework\":\"ncert_k-12\",\"versionKey\":\"1636355436582\",\"mimeType\":\"application/vnd.ekstep.content-collection\",\"code\":\"40a1ed37e0fad94eca76b2a96fe086ab\",\"license\":\"CC BY 4.0\",\"leafNodes\":[\"do_11340096293585715212\"],\"version\":2,\"contentType\":\"TextBookUnit\",\"language\":[\"English\"],\"lastPublishedOn\":\"2021-11-08T12:53:09.398+0530\",\"objectType\":\"Collection\",\"status\":\"Live\",\"dialcodeRequired\":\"No\",\"idealScreenSize\":\"normal\",\"contentEncoding\":\"gzip\",\"leafNodesCount\":1,\"depth\":2,\"osId\":\"org.ekstep.launcher\",\"contentDisposition\":\"inline\",\"visibility\":\"Parent\",\"credentials\":{\"enabled\":\"No\"},\"variants\":\"{\\\"spine\\\":{\\\"ecarUrl\\\":\\\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/collection/do_11340502356035174411/collection-publish-t20_1636356202227_do_11340502356035174411_1_SPINE.ecar\\\",\\\"size\\\":\\\"12044\\\"},\\\"online\\\":{\\\"ecarUrl\\\":\\\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/collection/do_11340502356035174411/collection-publish-t20_1636356202412_do_11340502356035174411_1_ONLINE.ecar\\\",\\\"size\\\":\\\"5074\\\"}}\",\"index\":2,\"pkgVersion\":1,\"idealScreenDensity\":\"hdpi\"}],\"mediaType\":\"content\",\"name\":\"5. Human Body\",\"discussionForum\":{\"enabled\":\"No\"},\"createdOn\":\"2021-11-08T12:40:36.586+0530\",\"channel\":\"0126825293972439041\",\"generateDIALCodes\":\"No\",\"lastUpdatedOn\":\"2021-11-08T12:46:52.715+0530\",\"identifier\":\"do_113405023736512512114\",\"description\":\"This chapter describes about human body\",\"ownershipType\":[\"createdBy\"],\"compatibilityLevel\":1,\"audience\":[\"Student\"],\"os\":[\"All\"],\"primaryCategory\":\"Textbook Unit\",\"languageCode\":[\"en\"],\"downloadUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/collection/do_11340502356035174411/collection-publish-t20_1636356202227_do_11340502356035174411_1_SPINE.ecar\",\"framework\":\"ncert_k-12\",\"versionKey\":\"1636355436586\",\"mimeType\":\"application/vnd.ekstep.content-collection\",\"code\":\"76abafa2a0c2cfef90b52db1ef41fb82\",\"license\":\"CC BY 4.0\",\"leafNodes\":[\"do_11340096293585715212\",\"do_11336831941257625611\",\"do_11340096165525094411\"],\"version\":2,\"contentType\":\"TextBookUnit\",\"language\":[\"English\"],\"lastPublishedOn\":\"2021-11-08T12:53:09.398+0530\",\"objectType\":\"Collection\",\"status\":\"Live\",\"dialcodeRequired\":\"No\",\"idealScreenSize\":\"normal\",\"contentEncoding\":\"gzip\",\"leafNodesCount\":3,\"depth\":1,\"osId\":\"org.ekstep.launcher\",\"contentDisposition\":\"inline\",\"visibility\":\"Parent\",\"credentials\":{\"enabled\":\"No\"},\"variants\":\"{\\\"spine\\\":{\\\"ecarUrl\\\":\\\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/collection/do_11340502356035174411/collection-publish-t20_1636356202227_do_11340502356035174411_1_SPINE.ecar\\\",\\\"size\\\":\\\"12044\\\"},\\\"online\\\":{\\\"ecarUrl\\\":\\\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/collection/do_11340502356035174411/collection-publish-t20_1636356202412_do_11340502356035174411_1_ONLINE.ecar\\\",\\\"size\\\":\\\"5074\\\"}}\",\"index\":1,\"pkgVersion\":1,\"idealScreenDensity\":\"hdpi\"}]" + val unpublishedChildrenData = "[{\"lastStatusChangedOn\":\"2021-11-08T15:38:54.180+0530\",\"parent\":\"do_11340511118032076811\",\"children\":[{\"lastStatusChangedOn\":\"2021-11-08T15:38:54.166+0530\",\"parent\":\"do_113405111371202560114\",\"children\":[{\"lastStatusChangedOn\":\"2021-11-08T15:38:54.170+0530\",\"parent\":\"do_11340511137108787216\",\"children\":[{\"copyright\":\"J H S BHARKHOKHA, Tamil Nadu\",\"lastStatusChangedOn\":\"2021-09-17T16:22:50.404+0530\",\"parent\":\"do_11340511137112064018\",\"licenseterms\":\"By creating any type of content (resources, books, courses etc.) on DIKSHA, you consent to publish it under the Creative Commons License Framework. Please choose the applicable creative commons license you wish to apply to your content.\",\"organisation\":[\"J H S BHARKHOKHA\",\"Tamil Nadu\"],\"mediaType\":\"content\",\"name\":\"jaga Aug 25th more than 200mp mp4 update 1\",\"discussionForum\":{\"enabled\":\"No\"},\"createdOn\":\"2021-09-17T16:05:29.019+0530\",\"channel\":\"0126825293972439041\",\"lastUpdatedOn\":\"2021-09-17T16:22:50.404+0530\",\"size\":363062652,\"identifier\":\"do_11336831941257625611\",\"resourceType\":\"Learn\",\"ownershipType\":[\"createdBy\"],\"compatibilityLevel\":1,\"audience\":[\"Student\"],\"os\":[\"All\"],\"primaryCategory\":\"eTextbook\",\"appIcon\":\"https://sunbirddevbbpublic.blob.core.windows.net/sunbird-content-staging/content/assets/do_2137327580080128001217/gateway-of-india.jpg\",\"languageCode\":[\"en\"],\"downloadUrl\":\"\",\"framework\":\"tn_k-12_5\",\"creator\":\"सामग्री निर्माता TN\",\"versionKey\":\"1631875539805\",\"mimeType\":\"video/mp4\",\"code\":\"3bd7411e-c03c-4997-a247-4d43a5cc820b\",\"license\":\"CC BY 4.0\",\"version\":2,\"prevStatus\":\"Live\",\"contentType\":\"Resource\",\"prevState\":\"Draft\",\"language\":[\"English\"],\"lastPublishedOn\":\"2021-09-17T16:22:15.047+0530\",\"objectType\":\"Content\",\"status\":\"Live\",\"createdBy\":\"fca2925f-1eee-4654-9177-fece3fd6afc9\",\"dialcodeRequired\":\"No\",\"interceptionPoints\":{},\"idealScreenSize\":\"normal\",\"contentEncoding\":\"identity\",\"depth\":4,\"consumerId\":\"2eaff3db-cdd1-42e5-a611-bebbf906e6cf\",\"lastPublishedBy\":\"\",\"osId\":\"org.ekstep.quiz.app\",\"copyrightYear\":2021,\"se_FWIds\":[\"tn_k-12_5\"],\"contentDisposition\":\"online-only\",\"previewUrl\":\"https://preprodall.blob.core.windows.net/ntp-content-preprod/content/assets/do_2133520666218741761984/como-kids-tv-_-the-story-of-comos-family-_-30min-_-cartoon-video-for-kids.mp4\",\"artifactUrl\":\"https://preprodall.blob.core.windows.net/ntp-content-preprod/content/assets/do_2133520666218741761984/como-kids-tv-_-the-story-of-comos-family-_-30min-_-cartoon-video-for-kids.mp4\",\"visibility\":\"Default\",\"credentials\":{\"enabled\":\"No\"},\"variants\":{\"spine\":{\"ecarUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11336831941257625611/jaga-aug-25th-more-than-200mp-mp4-update-1_1631875935857_do_11336831941257625611_3_SPINE.ecar\",\"size\":\"2172\"}},\"index\":1,\"pkgVersion\":3,\"idealScreenDensity\":\"hdpi\"}],\"mediaType\":\"content\",\"name\":\"5.1.1 Key parts in the head\",\"discussionForum\":{\"enabled\":\"No\"},\"createdOn\":\"2021-11-08T15:38:54.170+0530\",\"channel\":\"0126825293972439041\",\"generateDIALCodes\":\"No\",\"lastUpdatedOn\":\"2021-11-08T15:38:54.170+0530\",\"identifier\":\"do_11340511137112064018\",\"description\":\"xyz\",\"ownershipType\":[\"createdBy\"],\"compatibilityLevel\":1,\"audience\":[\"Student\"],\"os\":[\"All\"],\"primaryCategory\":\"Textbook Unit\",\"languageCode\":[\"en\"],\"framework\":\"ncert_k-12\",\"versionKey\":\"1636366134170\",\"mimeType\":\"application/vnd.ekstep.content-collection\",\"code\":\"7991af5c2e51e4d3d7b83167aaac8829\",\"license\":\"CC BY 4.0\",\"version\":2,\"contentType\":\"TextBookUnit\",\"language\":[\"English\"],\"objectType\":\"Collection\",\"status\":\"Draft\",\"dialcodeRequired\":\"No\",\"idealScreenSize\":\"normal\",\"contentEncoding\":\"gzip\",\"depth\":3,\"osId\":\"org.ekstep.launcher\",\"contentDisposition\":\"inline\",\"visibility\":\"Parent\",\"credentials\":{\"enabled\":\"No\"},\"index\":1,\"idealScreenDensity\":\"hdpi\"},{\"lastStatusChangedOn\":\"2021-11-08T15:38:54.133+0530\",\"parent\":\"do_11340511137108787216\",\"children\":[{\"lastStatusChangedOn\":\"2021-11-02T19:13:39.729+0530\",\"parent\":\"do_11340511137080934412\",\"mediaType\":\"content\",\"name\":\"Collection Publishing PDF Content\",\"discussionForum\":{\"enabled\":\"No\"},\"createdOn\":\"2021-11-02T18:56:17.917+0530\",\"createdFor\":[\"01309282781705830427\"],\"channel\":\"0126825293972439041\",\"lastUpdatedOn\":\"2021-11-02T19:13:39.729+0530\",\"streamingUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/assets/do_1134009478823116801129/chapter_1.pdf\",\"identifier\":\"do_11340096165525094411\",\"resourceType\":\"Learn\",\"ownershipType\":[\"createdBy\"],\"compatibilityLevel\":4,\"audience\":[\"Student\"],\"os\":[\"All\"],\"primaryCategory\":\"Explanation Content\",\"appIcon\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11340094790233292811/artifact/033019_sz_reviews_feat_1564126718632.thumb.jpg\",\"languageCode\":[\"en\"],\"downloadUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11340096165525094411/collection-publishing-pdf-content_1635860615969_do_11340096165525094411_1.ecar\",\"framework\":\"ekstep_ncert_k-12\",\"creator\":\"N131\",\"versionKey\":\"1635859577917\",\"mimeType\":\"application/pdf\",\"code\":\"c9ce1ce0-b9b4-402e-a9c3-556701070838\",\"license\":\"CC BY 4.0\",\"version\":2,\"prevStatus\":\"Processing\",\"contentType\":\"Resource\",\"prevState\":\"Draft\",\"language\":[\"English\"],\"lastPublishedOn\":\"2021-11-02T19:13:35.589+0530\",\"objectType\":\"Content\",\"status\":\"Live\",\"pragma\":[\"external\"],\"createdBy\":\"0b71985d-fcb0-4018-ab14-83f10c3b0426\",\"dialcodeRequired\":\"No\",\"interceptionPoints\":{},\"keywords\":[\"CPPDFContent1\",\"CPPDFContent2\",\"CollectionKW1\"],\"idealScreenSize\":\"normal\",\"contentEncoding\":\"identity\",\"depth\":4,\"lastPublishedBy\":\"\",\"osId\":\"org.ekstep.quiz.app\",\"copyrightYear\":2021,\"se_FWIds\":[\"ekstep_ncert_k-12\"],\"contentDisposition\":\"inline\",\"previewUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/assets/do_1134009478823116801129/chapter_1.pdf\",\"artifactUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/assets/do_1134009478823116801129/chapter_1.pdf\",\"visibility\":\"Default\",\"credentials\":{\"enabled\":\"No\"},\"variants\":{\"full\":{\"ecarUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11340096165525094411/collection-publishing-pdf-content_1635860615969_do_11340096165525094411_1.ecar\",\"size\":\"256918\"},\"spine\":{\"ecarUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11340096165525094411/collection-publishing-pdf-content_1635860619148_do_11340096165525094411_1_SPINE.ecar\",\"size\":\"6378\"}},\"index\":1,\"pkgVersion\":1,\"idealScreenDensity\":\"hdpi\"}],\"mediaType\":\"content\",\"name\":\"5.1.2 Other parts\",\"discussionForum\":{\"enabled\":\"No\"},\"createdOn\":\"2021-11-08T15:38:54.133+0530\",\"channel\":\"0126825293972439041\",\"generateDIALCodes\":\"No\",\"lastUpdatedOn\":\"2021-11-08T15:38:54.132+0530\",\"identifier\":\"do_11340511137080934412\",\"description\":\"\",\"ownershipType\":[\"createdBy\"],\"compatibilityLevel\":1,\"audience\":[\"Student\"],\"os\":[\"All\"],\"primaryCategory\":\"Textbook Unit\",\"languageCode\":[\"en\"],\"framework\":\"ncert_k-12\",\"versionKey\":\"1636366134133\",\"mimeType\":\"application/vnd.ekstep.content-collection\",\"code\":\"3bf70f06d3e8dba010d8806fd94259b1\",\"license\":\"CC BY 4.0\",\"version\":2,\"contentType\":\"TextBookUnit\",\"language\":[\"English\"],\"objectType\":\"Collection\",\"status\":\"Draft\",\"dialcodeRequired\":\"No\",\"idealScreenSize\":\"normal\",\"contentEncoding\":\"gzip\",\"depth\":3,\"osId\":\"org.ekstep.launcher\",\"contentDisposition\":\"inline\",\"visibility\":\"Parent\",\"credentials\":{\"enabled\":\"No\"},\"index\":2,\"idealScreenDensity\":\"hdpi\"}],\"mediaType\":\"content\",\"name\":\"5.1 Parts of Body\",\"discussionForum\":{\"enabled\":\"No\"},\"createdOn\":\"2021-11-08T15:38:54.166+0530\",\"channel\":\"0126825293972439041\",\"generateDIALCodes\":\"No\",\"lastUpdatedOn\":\"2021-11-08T15:38:54.165+0530\",\"identifier\":\"do_11340511137108787216\",\"description\":\"This section describes about various part of the body such as head, hands, legs etc.\",\"ownershipType\":[\"createdBy\"],\"compatibilityLevel\":1,\"audience\":[\"Student\"],\"os\":[\"All\"],\"primaryCategory\":\"Textbook Unit\",\"languageCode\":[\"en\"],\"framework\":\"ncert_k-12\",\"versionKey\":\"1636366134166\",\"mimeType\":\"application/vnd.ekstep.content-collection\",\"code\":\"20cc1f31e62f924c6e47bf04c994376b\",\"license\":\"CC BY 4.0\",\"version\":2,\"contentType\":\"TextBookUnit\",\"language\":[\"English\"],\"objectType\":\"Collection\",\"status\":\"Draft\",\"dialcodeRequired\":\"No\",\"idealScreenSize\":\"normal\",\"contentEncoding\":\"gzip\",\"depth\":2,\"osId\":\"org.ekstep.launcher\",\"contentDisposition\":\"inline\",\"visibility\":\"Parent\",\"credentials\":{\"enabled\":\"No\"},\"index\":1,\"idealScreenDensity\":\"hdpi\"},{\"lastStatusChangedOn\":\"2021-11-08T15:38:54.177+0530\",\"parent\":\"do_113405111371202560114\",\"children\":[{\"lastStatusChangedOn\":\"2021-11-08T15:38:54.162+0530\",\"parent\":\"do_113405111371169792112\",\"children\":[{\"lastStatusChangedOn\":\"2021-11-08T15:38:54.173+0530\",\"parent\":\"do_11340511137105510414\",\"children\":[{\"lastStatusChangedOn\":\"2021-11-02T19:16:10.667+0530\",\"parent\":\"do_113405111371145216110\",\"mediaType\":\"content\",\"name\":\"Collection Publish MP4 content\",\"discussionForum\":{\"enabled\":\"No\"},\"createdOn\":\"2021-11-02T18:58:53.445+0530\",\"channel\":\"0126825293972439041\",\"lastUpdatedOn\":\"2021-11-02T19:16:10.667+0530\",\"identifier\":\"do_11340096293585715212\",\"resourceType\":\"Learn\",\"ownershipType\":[\"createdBy\"],\"compatibilityLevel\":1,\"audience\":[\"Student\"],\"os\":[\"All\"],\"primaryCategory\":\"Explanation Content\",\"appIcon\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_1134009488766730241130/artifact/033019_sz_reviews_feat_1564126718632.thumb.jpg\",\"languageCode\":[\"en\"],\"downloadUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11340096293585715212/collection-publish-mp4-content_1635860769119_do_11340096293585715212_1.ecar\",\"framework\":\"ekstep_ncert_k-12\",\"versionKey\":\"1635859733445\",\"mimeType\":\"video/mp4\",\"code\":\"e0b58864-3dc5-484a-b194-38c3eddcbce1\",\"license\":\"CC BY 4.0\",\"version\":2,\"prevStatus\":\"Draft\",\"contentType\":\"Resource\",\"prevState\":\"Draft\",\"language\":[\"English\"],\"lastPublishedOn\":\"2021-11-02T19:16:08.789+0530\",\"objectType\":\"Content\",\"status\":\"Live\",\"createdBy\":\"0b71985d-fcb0-4018-ab14-83f10c3b0426\",\"dialcodeRequired\":\"No\",\"interceptionPoints\":{},\"keywords\":[\"CPMP4ContentKW1\",\"CPMP4ContentKW2\"],\"idealScreenSize\":\"normal\",\"contentEncoding\":\"identity\",\"depth\":5,\"lastPublishedBy\":\"\",\"osId\":\"org.ekstep.quiz.app\",\"se_FWIds\":[\"ekstep_ncert_k-12\"],\"contentDisposition\":\"inline\",\"previewUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/assets/do_1134009488766730241130/amoeba-eat.mp4\",\"artifactUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/assets/do_1134009488766730241130/amoeba-eat.mp4\",\"visibility\":\"Default\",\"credentials\":{\"enabled\":\"No\"},\"variants\":{\"full\":{\"ecarUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11340096293585715212/collection-publish-mp4-content_1635860769119_do_11340096293585715212_1.ecar\",\"size\":\"2692101\"},\"spine\":{\"ecarUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11340096293585715212/collection-publish-mp4-content_1635860770277_do_11340096293585715212_1_SPINE.ecar\",\"size\":\"6275\"}},\"index\":1,\"pkgVersion\":1,\"idealScreenDensity\":\"hdpi\"}],\"mediaType\":\"content\",\"name\":\"dsffgdg\",\"discussionForum\":{\"enabled\":\"No\"},\"createdOn\":\"2021-11-08T15:38:54.173+0530\",\"channel\":\"0126825293972439041\",\"generateDIALCodes\":\"No\",\"lastUpdatedOn\":\"2021-11-08T15:38:54.173+0530\",\"identifier\":\"do_113405111371145216110\",\"description\":\"\",\"ownershipType\":[\"createdBy\"],\"compatibilityLevel\":1,\"audience\":[\"Student\"],\"os\":[\"All\"],\"primaryCategory\":\"Textbook Unit\",\"languageCode\":[\"en\"],\"framework\":\"ncert_k-12\",\"versionKey\":\"1636366134173\",\"mimeType\":\"application/vnd.ekstep.content-collection\",\"code\":\"9cf84ff2fb08f9af4c23eb09df9b2520\",\"license\":\"CC BY 4.0\",\"version\":2,\"contentType\":\"TextBookUnit\",\"language\":[\"English\"],\"objectType\":\"Collection\",\"status\":\"Draft\",\"dialcodeRequired\":\"No\",\"idealScreenSize\":\"normal\",\"contentEncoding\":\"gzip\",\"depth\":4,\"osId\":\"org.ekstep.launcher\",\"contentDisposition\":\"inline\",\"visibility\":\"Parent\",\"credentials\":{\"enabled\":\"No\"},\"index\":1,\"idealScreenDensity\":\"hdpi\"}],\"mediaType\":\"content\",\"name\":\"5.2.1 Respiratory System\",\"discussionForum\":{\"enabled\":\"No\"},\"createdOn\":\"2021-11-08T15:38:54.162+0530\",\"channel\":\"0126825293972439041\",\"generateDIALCodes\":\"No\",\"lastUpdatedOn\":\"2021-11-08T15:53:32.894+0530\",\"identifier\":\"do_11340511137105510414\",\"description\":\"\",\"ownershipType\":[\"createdBy\"],\"compatibilityLevel\":1,\"audience\":[\"Student\"],\"os\":[\"All\"],\"primaryCategory\":\"Textbook Unit\",\"languageCode\":[\"en\"],\"attributions\":[],\"framework\":\"ncert_k-12\",\"versionKey\":\"1636366134162\",\"mimeType\":\"application/vnd.ekstep.content-collection\",\"code\":\"b186b1bbcc9c58db865f75e34345179e\",\"license\":\"CC BY 4.0\",\"version\":2,\"contentType\":\"TextBookUnit\",\"language\":[\"English\"],\"objectType\":\"Collection\",\"status\":\"Draft\",\"dialcodeRequired\":\"No\",\"keywords\":[\"UnitKW1\",\"UnitKW2\"],\"idealScreenSize\":\"normal\",\"contentEncoding\":\"gzip\",\"depth\":3,\"osId\":\"org.ekstep.launcher\",\"contentDisposition\":\"inline\",\"visibility\":\"Parent\",\"credentials\":{\"enabled\":\"No\"},\"index\":1,\"idealScreenDensity\":\"hdpi\"}],\"mediaType\":\"content\",\"name\":\"5.2 Organ Systems\",\"discussionForum\":{\"enabled\":\"No\"},\"createdOn\":\"2021-11-08T15:38:54.176+0530\",\"channel\":\"0126825293972439041\",\"generateDIALCodes\":\"No\",\"lastUpdatedOn\":\"2021-11-08T15:38:54.176+0530\",\"identifier\":\"do_113405111371169792112\",\"description\":\"\",\"ownershipType\":[\"createdBy\"],\"compatibilityLevel\":1,\"audience\":[\"Student\"],\"os\":[\"All\"],\"primaryCategory\":\"Textbook Unit\",\"languageCode\":[\"en\"],\"framework\":\"ncert_k-12\",\"versionKey\":\"1636366134176\",\"mimeType\":\"application/vnd.ekstep.content-collection\",\"code\":\"40a1ed37e0fad94eca76b2a96fe086ab\",\"license\":\"CC BY 4.0\",\"version\":2,\"contentType\":\"TextBookUnit\",\"language\":[\"English\"],\"objectType\":\"Collection\",\"status\":\"Draft\",\"dialcodeRequired\":\"No\",\"idealScreenSize\":\"normal\",\"contentEncoding\":\"gzip\",\"depth\":2,\"osId\":\"org.ekstep.launcher\",\"contentDisposition\":\"inline\",\"visibility\":\"Parent\",\"credentials\":{\"enabled\":\"No\"},\"index\":2,\"idealScreenDensity\":\"hdpi\"}],\"mediaType\":\"content\",\"name\":\"5. Human Body\",\"discussionForum\":{\"enabled\":\"No\"},\"createdOn\":\"2021-11-08T15:38:54.180+0530\",\"channel\":\"0126825293972439041\",\"generateDIALCodes\":\"No\",\"lastUpdatedOn\":\"2021-11-08T15:38:54.179+0530\",\"identifier\":\"do_113405111371202560114\",\"description\":\"This chapter describes about human body\",\"ownershipType\":[\"createdBy\"],\"compatibilityLevel\":1,\"audience\":[\"Student\"],\"os\":[\"All\"],\"primaryCategory\":\"Textbook Unit\",\"languageCode\":[\"en\"],\"framework\":\"ncert_k-12\",\"versionKey\":\"1636366134180\",\"mimeType\":\"application/vnd.ekstep.content-collection\",\"code\":\"76abafa2a0c2cfef90b52db1ef41fb82\",\"license\":\"CC BY 4.0\",\"version\":2,\"contentType\":\"TextBookUnit\",\"language\":[\"English\"],\"objectType\":\"Collection\",\"status\":\"Draft\",\"dialcodeRequired\":\"No\",\"idealScreenSize\":\"normal\",\"contentEncoding\":\"gzip\",\"depth\":1,\"osId\":\"org.ekstep.launcher\",\"contentDisposition\":\"inline\",\"visibility\":\"Parent\",\"credentials\":{\"enabled\":\"No\"},\"index\":1,\"idealScreenDensity\":\"hdpi\"}]" val publishedCollectionNodeMetadata = "{\"copyright\":\"tn\",\"lastStatusChangedOn\":\"2021-11-08T15:38:31.391+0530\",\"publish_type\":\"public\",\"author\":\"ContentcreatorTN\",\"children\":[{\"name\":\"5. Human Body\",\"identifier\":\"do_113405111371202560114\",\"description\":\"This chapter describes about human body\",\"objectType\":\"Collection\",\"index\":1}],\"body\":null,\"mediaType\":\"content\",\"name\":\"Collection Publish T20\",\"toc_url\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11340511118032076811/artifact/do_11340511118032076811_toc.json\",\"discussionForum\":\"{\\\"enabled\\\":\\\"No\\\"}\",\"createdOn\":\"2021-11-08T15:38:31.391+0530\",\"createdFor\":[\"0125196274181898243\"],\"channel\":\"0126825293972439041\",\"generateDIALCodes\":\"No\",\"lastUpdatedOn\":\"2021-11-08T15:53:33.587+0530\",\"size\":12048,\"publishError\":null,\"identifier\":\"do_11340511118032076811\",\"description\":\"Collection Publish\",\"resourceType\":\"Book\",\"ownershipType\":[\"createdBy\"],\"compatibilityLevel\":1,\"audience\":[\"Student\"],\"trackable\":\"{\\\"enabled\\\":\\\"No\\\",\\\"autoBatch\\\":\\\"No\\\"}\",\"os\":[\"All\"],\"primaryCategory\":\"Digital Textbook\",\"appIcon\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/collection/do_11340511118032076811/artifact/16_bvnvrokht6hn97eqcklwk2fs6ppx2z.thumb.png\",\"downloadUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/collection/do_11340511118032076811/collection-publish-t20_1636367178646_do_11340511118032076811_1_SPINE.ecar\",\"attributions\":[],\"framework\":\"ncert_k-12\",\"posterImage\":\"https://ntpproductionall.blob.core.windows.net/ntp-content-production/content/assets/do_31321903538537267212904/16_bvnvrokht6hn97eqcklwk2fs6ppx2z.png\",\"creator\":\"NCERT\",\"totalCompressedSize\":363062652,\"versionKey\":\"1636367013587\",\"mimeType\":\"application/vnd.ekstep.content-collection\",\"code\":\"0125196274181898243\",\"license\":\"CC BY 4.0\",\"leafNodes\":[\"do_11340096293585715212\",\"do_11336831941257625611\",\"do_11340096165525094411\"],\"version\":2,\"contentType\":\"TextBook\",\"language\":[\"English\"],\"lastPublishedOn\":\"2021-11-08T15:56:15.604+0530\",\"contentTypesCount\":\"{\\\"TextBookUnit\\\":7,\\\"Resource\\\":3}\",\"objectType\":\"Collection\",\"status\":\"Live\",\"createdBy\":\"220d4745-6764-498d-ad37-5e49b8cce716\",\"dialcodeRequired\":\"No\",\"keywords\":[\"CPPDFContent1\",\"UnitKW2\",\"CollectionKW1\",\"CPPDFContent2\",\"UnitKW1\",\"CPMP4ContentKW1\",\"CPMP4ContentKW2\"],\"userConsent\":\"Yes\",\"idealScreenSize\":\"normal\",\"contentEncoding\":\"gzip\",\"leafNodesCount\":3,\"depth\":0,\"flagReasons\":null,\"mimeTypesCount\":\"{\\\"application/pdf\\\":1,\\\"video/mp4\\\":2,\\\"application/vnd.ekstep.content-collection\\\":7}\",\"osId\":\"org.ekstep.quiz.app\",\"copyrightYear\":2021,\"se_FWIds\":[\"ncert_k-12\"],\"contentDisposition\":\"inline\",\"additionalCategories\":[],\"childNodes\":[\"do_11336831941257625611\",\"do_11340511137112064018\",\"do_11340511137108787216\",\"do_113405111371202560114\",\"do_11340096165525094411\",\"do_11340511137080934412\",\"do_11340096293585715212\",\"do_113405111371145216110\",\"do_11340511137105510414\",\"do_113405111371169792112\"],\"visibility\":\"Default\",\"credentials\":\"{\\\"enabled\\\":\\\"No\\\"}\",\"variants\":\"{\\\"spine\\\":{\\\"ecarUrl\\\":\\\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/collection/do_11340511118032076811/collection-publish-t20_1636367178646_do_11340511118032076811_1_SPINE.ecar\\\",\\\"size\\\":\\\"12048\\\"},\\\"online\\\":{\\\"ecarUrl\\\":\\\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/collection/do_11340511118032076811/collection-publish-t20_1636367178792_do_11340511118032076811_1_ONLINE.ecar\\\",\\\"size\\\":\\\"5081\\\"}}\",\"pkgVersion\":1,\"idealScreenDensity\":\"hdpi\"}" } diff --git a/publish-pipeline/content-publish/src/test/scala/org/sunbird/job/publish/helpers/spec/ContentPublisherSpec.scala b/publish-pipeline/content-publish/src/test/scala/org/sunbird/job/publish/helpers/spec/ContentPublisherSpec.scala index 20c7825b7..1dd5e99ca 100644 --- a/publish-pipeline/content-publish/src/test/scala/org/sunbird/job/publish/helpers/spec/ContentPublisherSpec.scala +++ b/publish-pipeline/content-publish/src/test/scala/org/sunbird/job/publish/helpers/spec/ContentPublisherSpec.scala @@ -37,7 +37,7 @@ class ContentPublisherSpec extends FlatSpec with BeforeAndAfterAll with Matchers override protected def beforeAll(): Unit = { super.beforeAll() EmbeddedCassandraServerHelper.startEmbeddedCassandra(80000L) - cassandraUtil = new CassandraUtil(jobConfig.cassandraHost, jobConfig.cassandraPort) + cassandraUtil = new CassandraUtil(jobConfig.cassandraHost, jobConfig.cassandraPort, jobConfig) val session = cassandraUtil.session val dataLoader = new CQLDataLoader(session) dataLoader.load(new FileCQLDataSet(getClass.getResource("/test.cql").getPath, true, true)) @@ -184,13 +184,13 @@ class ContentPublisherSpec extends FlatSpec with BeforeAndAfterAll with Matchers } "validateMetadata with mimeType application/msword and .pptx " should " not return exception messages if content is having valid artifactUrl" in { - val data = new ObjectData("do_123", Map[String, AnyRef]("name" -> "Content Name", "identifier" -> "do_123", "pkgVersion" -> 0.0.asInstanceOf[AnyRef], "mimeType" -> "application/msword", "artifactUrl" -> "https://ekstep-public-dev.s3-ap-south-1.amazonaws.com/content/do_112216616320983040129/artifact/performance_out_1491286194831.pptx"), None) + val data = new ObjectData("do_123", Map[String, AnyRef]("name" -> "Content Name", "identifier" -> "do_123", "pkgVersion" -> 0.0.asInstanceOf[AnyRef], "mimeType" -> "application/msword", "artifactUrl" -> "https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_112216616320983040129/artifact/performance_out_1491286194831.pptx"), None) val result: List[String] = new TestContentPublisher().validateMetadata(data, data.identifier, jobConfig) result.size should be(0) } "validateMetadata with mimeType application/msword and .docx " should " not return exception messages if content is having valid artifactUrl" in { - val data = new ObjectData("do_123", Map[String, AnyRef]("name" -> "Content Name", "identifier" -> "do_123", "pkgVersion" -> 0.0.asInstanceOf[AnyRef], "mimeType" -> "application/msword", "artifactUrl" -> "https://ekstep-public-dev.s3-ap-south-1.amazonaws.com/content/do_112216615190192128128/artifact/prdassetstagging-2_1491286084107.docx"), None) + val data = new ObjectData("do_123", Map[String, AnyRef]("name" -> "Content Name", "identifier" -> "do_123", "pkgVersion" -> 0.0.asInstanceOf[AnyRef], "mimeType" -> "application/msword", "artifactUrl" -> "https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_112216615190192128128/artifact/prdassetstagging-2_1491286084107.docx"), None) val result: List[String] = new TestContentPublisher().validateMetadata(data, data.identifier, jobConfig) result.size should be(0) } diff --git a/publish-pipeline/content-publish/src/test/scala/org/sunbird/job/spec/ContentPublishStreamTaskSpec.scala b/publish-pipeline/content-publish/src/test/scala/org/sunbird/job/spec/ContentPublishStreamTaskSpec.scala index 7afbeff3c..d02e89d7a 100644 --- a/publish-pipeline/content-publish/src/test/scala/org/sunbird/job/spec/ContentPublishStreamTaskSpec.scala +++ b/publish-pipeline/content-publish/src/test/scala/org/sunbird/job/spec/ContentPublishStreamTaskSpec.scala @@ -55,7 +55,7 @@ class ContentPublishStreamTaskSpec extends BaseTestSpec { override protected def beforeAll(): Unit = { super.beforeAll() EmbeddedCassandraServerHelper.startEmbeddedCassandra(80000L) - cassandraUtil = new CassandraUtil(jobConfig.cassandraHost, jobConfig.cassandraPort) + cassandraUtil = new CassandraUtil(jobConfig.cassandraHost, jobConfig.cassandraPort, jobConfig) val session = cassandraUtil.session val dataLoader = new CQLDataLoader(session) dataLoader.load(new FileCQLDataSet(getClass.getResource("/test.cql").getPath, true, true)) @@ -85,7 +85,7 @@ class ContentPublishStreamTaskSpec extends BaseTestSpec { "fetchDialListForContextUpdate" should "fetch the list of added and removed QR codes" in { val nodeObj = new ObjectData("do_21354027142511820812318.img", Map("objectType" -> "Collection", "identifier" -> "do_21354027142511820812318", "name" -> "DialCodeHierarchy", "lastPublishedOn" -> getTimeStamp, "lastUpdatedOn" -> getTimeStamp, "status" -> "Draft", "pkgVersion" -> 1.asInstanceOf[Number], "versionKey" -> "1652871771396", "channel" -> "0126825293972439041", "contentType" -> "TextBook"), Some(Map()), Some(Map())) - val DIALListMap = new CollectionPublishFunction(jobConfig, mockHttpUtil).fetchDialListForContextUpdate(nodeObj)(mockNeo4JUtil, cassandraUtil, readerConfig) + val DIALListMap = new CollectionPublishFunction(jobConfig, mockHttpUtil).fetchDialListForContextUpdate(nodeObj)(mockNeo4JUtil, cassandraUtil, readerConfig, jobConfig) assert(DIALListMap.nonEmpty) val postProcessEvent = new CollectionPublishFunction(jobConfig, mockHttpUtil).getPostProcessEvent(nodeObj, DIALListMap) diff --git a/publish-pipeline/live-node-publisher/pom.xml b/publish-pipeline/live-node-publisher/pom.xml new file mode 100644 index 000000000..19938fd90 --- /dev/null +++ b/publish-pipeline/live-node-publisher/pom.xml @@ -0,0 +1,207 @@ + + + + publish-pipeline + org.sunbird + 1.0 + + 4.0.0 + + live-node-publisher + 1.0.0 + jar + live-node-publisher + + Live node publisher job + + + + + org.apache.flink + flink-streaming-scala_${scala.version} + ${flink.version} + provided + + + org.sunbird + publish-core + 1.0.0 + + + joda-time + joda-time + 2.10.6 + + + org.sunbird + jobs-core + 1.0.0 + test-jar + test + + + jackson-module-scala_${scala.version} + com.fasterxml.jackson.module + + + + + org.apache.flink + flink-test-utils_${scala.version} + ${flink.version} + test + + + org.apache.flink + flink-runtime_${scala.version} + ${flink.version} + test + tests + + + org.apache.flink + flink-streaming-java_${scala.version} + ${flink.version} + test + tests + + + org.scalatest + scalatest_${scala.version} + 3.0.6 + test + + + org.mockito + mockito-core + 3.3.3 + test + + + org.cassandraunit + cassandra-unit + 3.11.2.0 + test + + + + + src/main/scala + src/test/scala + + + org.apache.maven.plugins + maven-shade-plugin + 3.2.1 + + + + package + + shade + + + + + com.google.code.findbugs:jsr305 + + + + + + *:* + + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + + + + + + + org.sunbird.job.livenodepublisher.task.LiveNodePublisherStreamTask + + + + reference.conf + + + + + + + + net.alchim31.maven + scala-maven-plugin + 4.4.0 + + 11 + 11 + ${scala.maj.version} + false + + + + scala-compile-first + process-resources + + add-source + compile + + + + scala-test-compile + process-test-resources + + testCompile + + + + + + + maven-surefire-plugin + 2.22.2 + + true + + + + + org.scalatest + scalatest-maven-plugin + 1.0 + + ${project.build.directory}/surefire-reports + . + live-node-publisher-testsuite.txt + + + + test + + test + + + + + + org.scoverage + scoverage-maven-plugin + ${scoverage.plugin.version} + + ${scala.version} + true + true + + + + + + \ No newline at end of file diff --git a/publish-pipeline/live-node-publisher/src/main/resources/live-node-publisher.conf b/publish-pipeline/live-node-publisher/src/main/resources/live-node-publisher.conf new file mode 100644 index 000000000..5c7c1b7af --- /dev/null +++ b/publish-pipeline/live-node-publisher/src/main/resources/live-node-publisher.conf @@ -0,0 +1,176 @@ +include "base-config.conf" + +job { + env = "sunbirddev" +} + +kafka { + input.topic = "sunbirddev.republish.job.request" + live_video_stream.topic = "sunbirddev.live.video.stream.request" + error.topic = "sunbirddev.learning.events.failed" + skipped.topic = "sunbirddev.learning.events.skipped" + groupId = "local-content-publish-group" +} + +task { + consumer.parallelism = 1 + parallelism = 1 + router.parallelism = 1 +} + +redis { + database { + contentCache.id = 0 + } +} + +content { + bundleLocation = "/tmp/contentBundle" + isECARExtractionEnabled = true + retry_asset_download_count = 1 + keyspace = "dev_content_store" + table = "content_data" + tmp_file_location = "/tmp" + objectType = ["Content", "ContentImage","Collection","CollectionImage"] + mimeType = ["application/pdf", + "application/vnd.ekstep.ecml-archive", + "application/vnd.ekstep.html-archive", + "application/vnd.android.package-archive", + "application/vnd.ekstep.content-archive", + "application/epub", + "application/msword", + "application/vnd.ekstep.h5p-archive", + "video/webm", + "video/mp4", + "application/vnd.ekstep.content-collection", + "video/quicktime", + "application/octet-stream", + "application/json", + "application/javascript", + "application/xml", + "text/plain", + "text/html", + "text/javascript", + "text/xml", + "text/css", + "image/jpeg", + "image/jpg", + "image/png", + "image/tiff", + "image/bmp", + "image/gif", + "image/svg+xml", + "image/x-quicktime", + "video/avi", + "video/mpeg", + "video/quicktime", + "video/3gpp", + "video/mp4", + "video/ogg", + "video/webm", + "video/msvideo", + "video/x-msvideo", + "video/x-qtc", + "video/x-mpeg", + "audio/mp3", + "audio/mp4", + "audio/mpeg", + "audio/ogg", + "audio/webm", + "audio/x-wav", + "audio/wav", + "audio/mpeg3", + "audio/x-mpeg-3", + "audio/vorbis", + "application/x-font-ttf", + "application/vnd.ekstep.plugin-archive", + "video/x-youtube", + "video/youtube", + "text/x-url"] + asset_download_duration = "60 seconds" + + stream { + enabled = true + mimeType = ["video/mp4", "video/webm"] + } + artifact.size.for_online=209715200 + + downloadFiles { + spine = ["appIcon"] + full = ["appIcon", "grayScaleAppIcon", "artifactUrl", "itemSetPreviewUrl", "media"] + } + + nested.fields=["badgeAssertions", "targets", "badgeAssociations", "plugins", "me_totalTimeSpent", "me_totalPlaySessionCount", "me_totalTimeSpentInSec", "batches", "trackable", "credentials", "discussionForum", "provider", "osMetadata", "actions"] + +} + +hierarchy { + keyspace = "hierarchy_store" + table = "content_hierarchy" +} + +cloud_storage { + folder { + content = "content" + artifact = "artifact" + } +} + +service { + print.basePath = "http://11.2.6.6/print" +} + + +contentTypeToPrimaryCategory { + ClassroomTeachingVideo: "Explanation Content" + ConceptMap: "Learning Resource" + Course: "Course" + CuriosityQuestionSet: "Practice Question Set" + eTextBook: "eTextbook" + Event: "Event" + EventSet: "Event Set" + ExperientialResource: "Learning Resource" + ExplanationResource: "Explanation Content" + ExplanationVideo: "Explanation Content" + FocusSpot: "Teacher Resource" + LearningOutcomeDefinition: "Teacher Resource" + MarkingSchemeRubric: "Teacher Resource" + PedagogyFlow: "Teacher Resource" + PracticeQuestionSet: "Practice Question Set" + PracticeResource: "Practice Question Set" + SelfAssess: "Course Assessment" + TeachingMethod: "Teacher Resource" + TextBook: "Digital Textbook" + Collection: "Content Playlist" + ExplanationReadingMaterial: "Learning Resource" + LearningActivity: "Learning Resource" + LessonPlan: "Content Playlist" + LessonPlanResource: "Teacher Resource" + PreviousBoardExamPapers: "Learning Resource" + TVLesson: "Explanation Content" + OnboardingResource: "Learning Resource" + ReadingMaterial: "Learning Resource" + Template: "Template" + Asset: "Asset" + Plugin: "Plugin" + LessonPlanUnit: "Lesson Plan Unit" + CourseUnit: "Course Unit" + TextBookUnit: "Textbook Unit" + Asset: "Certificate Template" +} + +max_allowed_content_name = 120 + +service.search.basePath = "http://11.2.6.6/search" + +cloudstorage.metadata.replace_absolute_path=false +cloudstorage.relative_path_prefix= "CONTENT_STORAGE_BASE_PATH" +cloudstorage.read_base_path="https://sunbirddev.blob.core.windows.net" +cloudstorage.write_base_path=["https://sunbirddev.blob.core.windows.net","https://obj.dev.sunbird.org"] +cloudstorage.metadacloudstorage.metadata.list=["appIcon","posterImage","artifactUrl","downloadUrl","variants","previewUrl","pdfUrl", "streamingUrl", "toc_url"] + +cloud_storage_type="" +cloud_storage_key="" +cloud_storage_secret="" +cloud_storage_container="" +cloud_storage_endpoint="" \ No newline at end of file diff --git a/enrolment-reconciliation/src/main/resources/log4j.properties b/publish-pipeline/live-node-publisher/src/main/resources/log4j.properties similarity index 91% rename from enrolment-reconciliation/src/main/resources/log4j.properties rename to publish-pipeline/live-node-publisher/src/main/resources/log4j.properties index 37a85c438..6eb0a07a7 100644 --- a/enrolment-reconciliation/src/main/resources/log4j.properties +++ b/publish-pipeline/live-node-publisher/src/main/resources/log4j.properties @@ -1,6 +1,6 @@ # log4j.appender.file=org.apache.log4j.FileAppender log4j.appender.file=org.apache.log4j.RollingFileAppender -log4j.appender.file.file=relation-cache-updater.log +log4j.appender.file.file=-live-node-publisher.log log4j.appender.file.append=true log4j.appender.file.layout=org.apache.log4j.PatternLayout log4j.appender.file.MaxFileSize=256KB diff --git a/publish-pipeline/live-node-publisher/src/main/scala/org/sunbird/job/livenodepublisher/function/LiveCollectionPublishFunction.scala b/publish-pipeline/live-node-publisher/src/main/scala/org/sunbird/job/livenodepublisher/function/LiveCollectionPublishFunction.scala new file mode 100644 index 000000000..b776e4206 --- /dev/null +++ b/publish-pipeline/live-node-publisher/src/main/scala/org/sunbird/job/livenodepublisher/function/LiveCollectionPublishFunction.scala @@ -0,0 +1,218 @@ +package org.sunbird.job.livenodepublisher.function + +import akka.dispatch.ExecutionContexts +import com.google.gson.reflect.TypeToken +import org.apache.flink.api.common.typeinfo.TypeInformation +import org.apache.flink.configuration.Configuration +import org.apache.flink.streaming.api.functions.ProcessFunction +import org.neo4j.driver.v1.exceptions.ClientException +import org.slf4j.LoggerFactory +import org.sunbird.job.cache.{DataCache, RedisConnect} +import org.sunbird.job.domain.`object`.{DefinitionCache, ObjectDefinition} +import org.sunbird.job.exception.{InvalidInputException, ServerException} +import org.sunbird.job.helper.FailedEventHelper +import org.sunbird.job.livenodepublisher.publish.domain.Event +import org.sunbird.job.livenodepublisher.publish.helpers.LiveCollectionPublisher +import org.sunbird.job.livenodepublisher.task.LiveNodePublisherConfig +import org.sunbird.job.publish.core.{DefinitionConfig, ExtDataConfig, ObjectData} +import org.sunbird.job.publish.helpers.EcarPackageType +import org.sunbird.job.util._ +import org.sunbird.job.{BaseProcessFunction, Metrics} + +import java.lang.reflect.Type +import java.util +import scala.concurrent.ExecutionContext +import scala.collection.JavaConverters._ + +class LiveCollectionPublishFunction(config: LiveNodePublisherConfig, httpUtil: HttpUtil, + @transient var neo4JUtil: Neo4JUtil = null, + @transient var cassandraUtil: CassandraUtil = null, + @transient var esUtil: ElasticSearchUtil = null, + @transient var cloudStorageUtil: CloudStorageUtil = null, + @transient var definitionCache: DefinitionCache = null, + @transient var definitionConfig: DefinitionConfig = null) + (implicit val stringTypeInfo: TypeInformation[String]) + extends BaseProcessFunction[Event, String](config) with LiveCollectionPublisher with FailedEventHelper { + + private[this] val logger = LoggerFactory.getLogger(classOf[LiveCollectionPublishFunction]) + val mapType: Type = new TypeToken[java.util.Map[String, AnyRef]]() {}.getType + private var cache: DataCache = _ + private val COLLECTION_CACHE_KEY_PREFIX = "hierarchy_" + private val COLLECTION_CACHE_KEY_SUFFIX = ":leafnodes" + private val PUBLISHED_STATUS_LIST = List("Live", "Unlisted") + @transient var ec: ExecutionContext = _ + private val pkgTypes = List(EcarPackageType.SPINE, EcarPackageType.ONLINE) + + override def open(parameters: Configuration): Unit = { + super.open(parameters) + cassandraUtil = new CassandraUtil(config.cassandraHost, config.cassandraPort, config) + neo4JUtil = new Neo4JUtil(config.graphRoutePath, config.graphName, config) + esUtil = new ElasticSearchUtil(config.esConnectionInfo, config.compositeSearchIndexName, config.compositeSearchIndexType) + cloudStorageUtil = new CloudStorageUtil(config) + ec = ExecutionContexts.global + definitionCache = new DefinitionCache() + definitionConfig = DefinitionConfig(config.schemaSupportVersionMap, config.definitionBasePath) + cache = new DataCache(config, new RedisConnect(config), config.nodeStore, List()) + cache.init() + } + + override def close(): Unit = { + super.close() + cassandraUtil.close() + cache.close() + } + + override def metricsList(): List[String] = { + List(config.collectionPublishEventCount, config.collectionPublishSuccessEventCount, config.collectionPublishFailedEventCount, config.skippedEventCount, config.collectionPostPublishProcessEventCount) + } + + override def processElement(data: Event, context: ProcessFunction[Event, String]#Context, metrics: Metrics): Unit = { + val definition: ObjectDefinition = definitionCache.getDefinition(data.objectType, config.schemaSupportVersionMap.getOrElse(data.objectType.toLowerCase(), "1.0").asInstanceOf[String], config.definitionBasePath) + val readerConfig = ExtDataConfig(config.hierarchyKeyspaceName, config.hierarchyTableName, definition.getExternalPrimaryKey(), definition.getExternalProps()) + logger.info("Collection publishing started for : " + data.identifier) + metrics.incCounter(config.collectionPublishEventCount) + val obj: ObjectData = getObject(data.identifier, data.pkgVersion, data.mimeType, data.publishType, readerConfig)(neo4JUtil, cassandraUtil, config) + try { + if (obj.pkgVersion > data.pkgVersion || !PUBLISHED_STATUS_LIST.contains(obj.metadata.getOrElse("status", "").asInstanceOf[String])) { + metrics.incCounter(config.skippedEventCount) + logger.info(s"""Either object status is invalid OR Event pkgVersion is not greater than or equal to the obj.pkgVersion for : ${obj.identifier}""") + } else { + val childNodesMetadata: List[String] = obj.metadata.getOrElse("childNodes", new java.util.ArrayList()).asInstanceOf[java.util.List[String]].asScala.toList + val addedResources: List[String] = searchContents(data.identifier, childNodesMetadata.toArray, config, httpUtil) + val addedResourcesMigrationVersion: List[String] = searchContents(data.identifier, childNodesMetadata.toArray, config, httpUtil, true) + + val isCollectionShallowCopy = isContentShallowCopy(obj) + val shallowCopyOriginMigrationVersion: Double = if (isCollectionShallowCopy) { + val originId = obj.metadata.getOrElse("origin", "").asInstanceOf[String] + val originNodeMetadata = neo4JUtil.getNodeProperties(originId) + if (null != originNodeMetadata) { + originNodeMetadata.getOrDefault("migrationVersion", "0").toString.toDouble + } else 0 + } else 0 + + if (obj.pkgVersion > data.pkgVersion) { + metrics.incCounter(config.skippedEventCount) + logger.info(s"""pkgVersion should be greater than or equal to the obj.pkgVersion for : ${obj.identifier}""") + } + else if (isCollectionShallowCopy && shallowCopyOriginMigrationVersion != 1.1) { + pushSkipEvent(data, "Origin node is found to be not migrated", context)(metrics) + } + else if (addedResources.size != addedResourcesMigrationVersion.size) { + val errorMessageIdentifiers = addedResources.filter(rec => !addedResourcesMigrationVersion.contains(rec)).mkString(",") + pushSkipEvent(data, "Non migrated contents found: " + errorMessageIdentifiers, context)(metrics) + } else { + val updObj = new ObjectData(obj.identifier, obj.metadata ++ Map("lastPublishedBy" -> data.lastPublishedBy, "dialcodes" -> obj.metadata.getOrElse("dialcodes", null)), obj.extData, obj.hierarchy) + + // Pre-publish update +// updateProcessingNode(updObj)(neo4JUtil, cassandraUtil, readerConfig, definitionCache, definitionConfig) + + val updatedObj = if (isCollectionShallowCopy) updateOriginPkgVersion(updObj)(neo4JUtil) else updObj + + // Clear redis cache + cache.del(data.identifier) + cache.del(data.identifier + COLLECTION_CACHE_KEY_SUFFIX) + cache.del(COLLECTION_CACHE_KEY_PREFIX + data.identifier) + + val enrichedObjTemp = enrichObjectMetadata(updatedObj)(neo4JUtil, cassandraUtil, readerConfig, cloudStorageUtil, config, definitionCache, definitionConfig) + val enrichedObj = enrichedObjTemp.getOrElse(updatedObj) + logger.info("CollectionPublishFunction:: Collection Object Enriched: " + enrichedObj.identifier) + val objWithEcar = getObjectWithEcar(enrichedObj, pkgTypes)(ec, neo4JUtil, cassandraUtil, readerConfig, cloudStorageUtil, config, definitionCache, definitionConfig, httpUtil) + logger.info("CollectionPublishFunction:: ECAR generation completed for Collection Object: " + objWithEcar.identifier) + + val collRelationalMetadata = getRelationalMetadata(obj.identifier, readerConfig)(cassandraUtil).getOrElse(Map.empty[String, AnyRef]) + + val variantsJsonString = ScalaJsonUtil.serialize(objWithEcar.metadata("variants")) + val publishType = objWithEcar.getString("publish_type", "Public") + val successObj = new ObjectData(objWithEcar.identifier, objWithEcar.metadata + ("status" -> (if (publishType.equalsIgnoreCase("Unlisted")) "Unlisted" else "Live"), "variants" -> variantsJsonString, "identifier" -> objWithEcar.identifier), objWithEcar.extData, objWithEcar.hierarchy) + val children = successObj.hierarchy.getOrElse(Map()).getOrElse("children", List()).asInstanceOf[List[Map[String, AnyRef]]] + + // Collection - update and publish children - line 418 in PublishFinalizer + val updatedChildren = updateHierarchyMetadata(children, successObj.metadata, collRelationalMetadata)(config) + logger.info("CollectionPublishFunction:: Hierarchy Metadata updated for Collection Object: " + successObj.identifier + " || updatedChildren:: " + updatedChildren) + publishHierarchy(updatedChildren, successObj, readerConfig, config)(cassandraUtil) + + if (!isCollectionShallowCopy) syncNodes(successObj, updatedChildren, List.empty)(esUtil, neo4JUtil, cassandraUtil, readerConfig, definition, config) + + metrics.incCounter(config.collectionPublishSuccessEventCount) + logger.info("CollectionPublishFunction:: Collection publishing completed successfully for : " + data.identifier) + + saveOnSuccess(new ObjectData(objWithEcar.identifier, objWithEcar.metadata.-("children"), objWithEcar.extData, objWithEcar.hierarchy))(neo4JUtil, cassandraUtil, readerConfig, definitionCache, definitionConfig, config) + logger.info("CollectionPublishFunction:: Published Collection Object metadata saved successfully to graph DB: " + objWithEcar.identifier) + } + } + } catch { + case ex@(_: InvalidInputException | _: ClientException) => // ClientException - Invalid input exception. + ex.printStackTrace() + saveOnFailure(obj, List(ex.getMessage), data.pkgVersion)(neo4JUtil) + val exMsg = if(ex.getMessage.length>2500) ex.getMessage.substring(0,2500) else ex.getMessage + pushFailedEvent(data, exMsg, null, context)(metrics) + logger.error(s"CollectionPublishFunction::Error while publishing collection :: ${data.partition} and Offset: ${data.offset}. Error : ${ex.getMessage}", ex) + case ex: Exception => + ex.printStackTrace() + saveOnFailure(obj, List(ex.getMessage), data.pkgVersion)(neo4JUtil) + logger.error(s"CollectionPublishFunction::Error while processing message for Partition: ${data.partition} and Offset: ${data.offset}. Error : ${ex.getMessage}", ex) + throw ex + } + } + + private def pushFailedEvent(event: Event, errorMessage: String, error: Throwable, context: ProcessFunction[Event, String]#Context)(implicit metrics: Metrics): Unit = { + val failedEvent = if (error == null) getFailedEvent(event.jobName, event.getMap(), errorMessage) else getFailedEvent(event.jobName, event.getMap(), error) + context.output(config.failedEventOutTag, failedEvent) + metrics.incCounter(config.collectionPublishFailedEventCount) + } + + private def pushSkipEvent(event: Event, skipMessage: String, context: ProcessFunction[Event, String]#Context)(implicit metrics: Metrics): Unit = { + val skipEvent = if(skipMessage.length>500) getFailedEvent(event.jobName, event.getMap(), skipMessage.substring(0,500)) else getFailedEvent(event.jobName, event.getMap(), skipMessage) + context.output(config.skippedEventOutTag, skipEvent) + metrics.incCounter(config.skippedEventCount) + } + + private def searchContents(collectionId: String, identifiers: Array[String], config: LiveNodePublisherConfig, httpUtil: HttpUtil, fetchMigratedVersion: Boolean = false): List[String] = { + try { + val migrationVersionList = new util.ArrayList[AnyRef] + migrationVersionList.add(1.asInstanceOf[Number]) + migrationVersionList.add(1.1.asInstanceOf[Number]) + + val reqMap = new java.util.HashMap[String, AnyRef]() { + put("request", new java.util.HashMap[String, AnyRef]() { + put("filters", new java.util.HashMap[String, AnyRef]() { + put("visibility", "Default") + put("identifier", identifiers) + if (fetchMigratedVersion) put("migrationVersion", migrationVersionList.toArray) + }) + put("fields", config.searchFields) + }) + } + + val requestUrl = s"${config.searchServiceBaseUrl}/v3/search" + logger.info("CollectionPublishFunction :: searchContent :: Search Content requestUrl: " + requestUrl) + logger.info("CollectionPublishFunction :: searchContent :: Search Content reqMap: " + reqMap) + val httpResponse = httpUtil.post(requestUrl, JSONUtil.serialize(reqMap)) + if (httpResponse.status == 200) { + val response = JSONUtil.deserialize[Map[String, AnyRef]](httpResponse.body) + val result = response.getOrElse("result", Map[String, AnyRef]()).asInstanceOf[Map[String, AnyRef]] + val contents = result.getOrElse("content", List[Map[String, AnyRef]]()).asInstanceOf[List[Map[String, AnyRef]]] + val count = result.getOrElse("count", 0).asInstanceOf[Int] + if (count > 0) { + contents.map(content => { + content.getOrElse("identifier", "").toString + }) + } else { + logger.info("CollectionPublishFunction :: searchContent :: Received 0 count while searching childNodes : ") + List.empty[String] + } + } else { + throw new ServerException("ERR_API_CALL", "Invalid Response received while searching childNodes : " + getErrorDetails(httpResponse)) + } + } catch { + case ex: Exception => throw new InvalidInputException("Exception while searching children data for collection:: " + collectionId + " || ex: " + ex.getMessage) + } + } + + private def getErrorDetails(httpResponse: HTTPResponse): String = { + val response = JSONUtil.deserialize[Map[String, AnyRef]](httpResponse.body) + if (null != response) " | Response Code :" + httpResponse.status + " | Result : " + response.getOrElse("result", Map[String, AnyRef]()).asInstanceOf[Map[String, AnyRef]] + " | Error Message : " + response.getOrElse("params", Map[String, AnyRef]()).asInstanceOf[Map[String, AnyRef]] + else " | Null Response Received." + } + +} diff --git a/publish-pipeline/live-node-publisher/src/main/scala/org/sunbird/job/livenodepublisher/function/LiveContentPublishFunction.scala b/publish-pipeline/live-node-publisher/src/main/scala/org/sunbird/job/livenodepublisher/function/LiveContentPublishFunction.scala new file mode 100644 index 000000000..029a2d3a4 --- /dev/null +++ b/publish-pipeline/live-node-publisher/src/main/scala/org/sunbird/job/livenodepublisher/function/LiveContentPublishFunction.scala @@ -0,0 +1,152 @@ +package org.sunbird.job.livenodepublisher.function + +import akka.dispatch.ExecutionContexts +import com.google.gson.reflect.TypeToken +import org.apache.flink.api.common.typeinfo.TypeInformation +import org.apache.flink.configuration.Configuration +import org.apache.flink.streaming.api.functions.ProcessFunction +import org.neo4j.driver.v1.exceptions.ClientException +import org.slf4j.LoggerFactory +import org.sunbird.job.cache.{DataCache, RedisConnect} +import org.sunbird.job.domain.`object`.DefinitionCache +import org.sunbird.job.exception.InvalidInputException +import org.sunbird.job.helper.FailedEventHelper +import org.sunbird.job.livenodepublisher.publish.domain.Event +import org.sunbird.job.livenodepublisher.publish.helpers.{ExtractableMimeTypeHelper, LiveContentPublisher} +import org.sunbird.job.livenodepublisher.task.LiveNodePublisherConfig +import org.sunbird.job.publish.core.{DefinitionConfig, ExtDataConfig, ObjectData} +import org.sunbird.job.publish.helpers.EcarPackageType +import org.sunbird.job.util.{CassandraUtil, CloudStorageUtil, HttpUtil, Neo4JUtil} +import org.sunbird.job.{BaseProcessFunction, Metrics} + +import java.lang.reflect.Type +import java.util.UUID +import scala.concurrent.ExecutionContext + +class LiveContentPublishFunction(config: LiveNodePublisherConfig, httpUtil: HttpUtil, + @transient var neo4JUtil: Neo4JUtil = null, + @transient var cassandraUtil: CassandraUtil = null, + @transient var cloudStorageUtil: CloudStorageUtil = null, + @transient var definitionCache: DefinitionCache = null, + @transient var definitionConfig: DefinitionConfig = null) + (implicit val stringTypeInfo: TypeInformation[String]) + extends BaseProcessFunction[Event, String](config) with LiveContentPublisher with FailedEventHelper { + + private[this] val logger = LoggerFactory.getLogger(classOf[LiveContentPublishFunction]) + val mapType: Type = new TypeToken[java.util.Map[String, AnyRef]]() {}.getType + private var cache: DataCache = _ + private val readerConfig = ExtDataConfig(config.contentKeyspaceName, config.contentTableName) + private val PUBLISHED_STATUS_LIST = List("Live", "Unlisted") + + @transient var ec: ExecutionContext = _ + private val pkgTypes = List(EcarPackageType.FULL, EcarPackageType.SPINE) + + override def open(parameters: Configuration): Unit = { + super.open(parameters) + cassandraUtil = new CassandraUtil(config.cassandraHost, config.cassandraPort, config) + neo4JUtil = new Neo4JUtil(config.graphRoutePath, config.graphName, config) + cloudStorageUtil = new CloudStorageUtil(config) + ec = ExecutionContexts.global + definitionCache = new DefinitionCache() + definitionConfig = DefinitionConfig(config.schemaSupportVersionMap, config.definitionBasePath) + cache = new DataCache(config, new RedisConnect(config), config.nodeStore, List()) + cache.init() + } + + override def close(): Unit = { + super.close() + cassandraUtil.close() + cache.close() + } + + override def metricsList(): List[String] = { + List(config.contentPublishEventCount, config.contentPublishSuccessEventCount, config.contentPublishFailedEventCount, + config.videoStreamingGeneratorEventCount, config.skippedEventCount) + } + + override def processElement(data: Event, context: ProcessFunction[Event, String]#Context, metrics: Metrics): Unit = { + logger.info("Content publishing started for : " + data.identifier) + metrics.incCounter(config.contentPublishEventCount) + val obj: ObjectData = getObject(data.identifier, data.pkgVersion, data.mimeType, data.publishType, readerConfig)(neo4JUtil, cassandraUtil, config) + try { + if (obj.pkgVersion > data.pkgVersion || !PUBLISHED_STATUS_LIST.contains(obj.metadata.getOrElse("status", "").asInstanceOf[String])) { + metrics.incCounter(config.skippedEventCount) + logger.info(s"""Either object status is invalid OR Event pkgVersion is not greater than or equal to the obj.pkgVersion for : ${obj.identifier}""") + } else { + val messages: List[String] = validate(obj, obj.identifier, config, validateMetadata) + if (messages.isEmpty) { +// // Pre-publish update +// updateProcessingNode(new ObjectData(obj.identifier, obj.metadata ++ Map("lastPublishedBy" -> data.lastPublishedBy), obj.extData, obj.hierarchy))(neo4JUtil, cassandraUtil, readerConfig, definitionCache, definitionConfig) + + val ecmlVerifiedObj = if (obj.mimeType.equalsIgnoreCase("application/vnd.ekstep.ecml-archive")) { + val ecarEnhancedObj = ExtractableMimeTypeHelper.processECMLBody(obj, config)(ec, cloudStorageUtil) + new ObjectData(obj.identifier, ecarEnhancedObj, obj.extData, obj.hierarchy) + } else obj + + // Clear redis cache + cache.del(data.identifier) + val enrichedObjTemp = enrichObjectMetadata(ecmlVerifiedObj)(neo4JUtil, cassandraUtil, readerConfig, cloudStorageUtil, config, definitionCache, definitionConfig) + val enrichedObj = enrichedObjTemp.getOrElse(ecmlVerifiedObj) + val objWithEcar = getObjectWithEcar(enrichedObj, if (enrichedObj.getString("contentDisposition", "").equalsIgnoreCase("online-only")) List(EcarPackageType.SPINE) else pkgTypes)(ec, neo4JUtil, cloudStorageUtil, config, definitionCache, definitionConfig, httpUtil) + logger.info("Ecar generation done for Content: " + objWithEcar.identifier) + saveOnSuccess(objWithEcar)(neo4JUtil, cassandraUtil, readerConfig, definitionCache, definitionConfig, config) + pushStreamingUrlEvent(enrichedObj, context)(metrics) + + metrics.incCounter(config.contentPublishSuccessEventCount) + logger.info("Content publishing completed successfully for : " + data.identifier) + logger.info(s"""{ identifier: \"${data.identifier}\", mimetype: \"${data.mimeType}\", status: \"Success\"}""") + } else { + saveOnFailure(obj, messages, data.pkgVersion)(neo4JUtil) + val errorMessages = messages.mkString("; ") + pushFailedEvent(data, errorMessages, null, context)(metrics) + logger.info("Content publishing failed for : " + data.identifier) + logger.info(s"""{ identifier: \"${data.identifier}\", mimetype: \"${data.mimeType}\", status: \"Failed\"}""") + } + } + } catch { + case ex@(_: InvalidInputException | _: ClientException | _:java.lang.IllegalArgumentException) => // ClientException - Invalid input exception. + ex.printStackTrace() + saveOnFailure(obj, List(ex.getMessage), data.pkgVersion)(neo4JUtil) + pushFailedEvent(data, null, ex, context)(metrics) + logger.error("Error while publishing content :: " + ex.getMessage) + logger.info(s"""{ identifier: \"${data.identifier}\", mimetype: \"${data.mimeType}\", status: \"Failed\"}""") + case ex: Exception => + ex.printStackTrace() + saveOnFailure(obj, List(ex.getMessage), data.pkgVersion)(neo4JUtil) + logger.error(s"""Error while processing message for Partition: ${data.partition} and Offset: ${data.offset}. Error : ${ex.getMessage}""", ex) + logger.info(s"""{ identifier: \"${data.identifier}\", mimetype: \"${data.mimeType}\", status: \"Failed\"}""") + throw ex + } + } + + private def pushStreamingUrlEvent(obj: ObjectData, context: ProcessFunction[Event, String]#Context)(implicit metrics: Metrics): Unit = { + if (config.isStreamingEnabled && config.streamableMimeType.contains(obj.mimeType)) { + val event = getStreamingEvent(obj) + context.output(config.generateVideoStreamingOutTag, event) + metrics.incCounter(config.videoStreamingGeneratorEventCount) + } + } + + def getStreamingEvent(obj: ObjectData): String = { + val ets = System.currentTimeMillis + val mid = s"""LP.$ets.${UUID.randomUUID}""" + val channelId = obj.getString("channel", "") + val ver = obj.getString("versionKey", "") + val artifactUrl = obj.getString("artifactUrl", "") + val contentType = obj.getString("contentType", "") + val status = obj.getString("status", "") + //TODO: deprecate using contentType in the event. + val event = s"""{"eid":"BE_JOB_REQUEST", "ets": $ets, "mid": "$mid", "actor": {"id": "Live Video Stream Generator", "type": "System"}, "context":{"pdata":{"ver":"1.0","id":"org.ekstep.platform"}, "channel":"$channelId","env":"${config.jobEnv}"},"object":{"ver":"$ver","id":"${obj.identifier}"},"edata": {"action":"live-video-stream-generate","iteration":1,"identifier":"${obj.identifier}","channel":"$channelId","artifactUrl":"$artifactUrl","mimeType":"${obj.mimeType}","contentType":"$contentType","pkgVersion":${obj.pkgVersion},"status":"$status"}}""".stripMargin + logger.info(s"Live Video Streaming Event for identifier ${obj.identifier} is : $event") + event + } + + + private def pushFailedEvent(event: Event, errorMessage: String, error: Throwable, context: ProcessFunction[Event, String]#Context)(implicit metrics: Metrics): Unit = { + val failedEvent = if (error == null) getFailedEvent(event.jobName, event.getMap(), errorMessage) else getFailedEvent(event.jobName, event.getMap(), error) + context.output(config.failedEventOutTag, failedEvent) + metrics.incCounter(config.contentPublishFailedEventCount) + } + + +} diff --git a/publish-pipeline/live-node-publisher/src/main/scala/org/sunbird/job/livenodepublisher/function/LivePublishEventRouter.scala b/publish-pipeline/live-node-publisher/src/main/scala/org/sunbird/job/livenodepublisher/function/LivePublishEventRouter.scala new file mode 100644 index 000000000..b288d6172 --- /dev/null +++ b/publish-pipeline/live-node-publisher/src/main/scala/org/sunbird/job/livenodepublisher/function/LivePublishEventRouter.scala @@ -0,0 +1,55 @@ +package org.sunbird.job.livenodepublisher.function + +import com.google.gson.reflect.TypeToken +import org.apache.flink.configuration.Configuration +import org.apache.flink.streaming.api.functions.ProcessFunction +import org.slf4j.LoggerFactory +import org.sunbird.job.livenodepublisher.publish.domain.Event +import org.sunbird.job.livenodepublisher.task.LiveNodePublisherConfig +import org.sunbird.job.{BaseProcessFunction, Metrics} + +import java.lang.reflect.Type + +class LivePublishEventRouter(config: LiveNodePublisherConfig) extends BaseProcessFunction[Event, String](config) { + + + private[this] val logger = LoggerFactory.getLogger(classOf[LivePublishEventRouter]) + val mapType: Type = new TypeToken[java.util.Map[String, AnyRef]]() {}.getType + + override def open(parameters: Configuration): Unit = { + super.open(parameters) + } + + override def close(): Unit = { + super.close() + } + + override def metricsList(): List[String] = { + List(config.skippedEventCount, config.totalEventsCount) + } + + override def processElement(event: Event, context: ProcessFunction[Event, String]#Context, metrics: Metrics): Unit = { + metrics.incCounter(config.totalEventsCount) + logger.info("PublishEventRouter :: Event: " + event) + // Event validation + if (event.validEvent(config)) { + event.objectType match { + case "Content" => { + logger.info("PublishEventRouter :: Sending Content For Publish Having Identifier: " + event.identifier) + context.output(config.contentPublishOutTag, event) + } + case "Collection" => { + logger.info("PublishEventRouter :: Sending Collection For Publish Having Identifier: " + event.identifier) + context.output(config.collectionPublishOutTag, event) + } + case _ => { + metrics.incCounter(config.skippedEventCount) + logger.info("Invalid Object Type Received For Publish.| Identifier : " + event.identifier + " , objectType : " + event.objectType) + } + } + } else { + logger.warn("Event skipped for identifier: " + event.identifier + " objectType: " + event.objectType) + metrics.incCounter(config.skippedEventCount) + } + } +} diff --git a/publish-pipeline/live-node-publisher/src/main/scala/org/sunbird/job/livenodepublisher/publish/domain/Event.scala b/publish-pipeline/live-node-publisher/src/main/scala/org/sunbird/job/livenodepublisher/publish/domain/Event.scala new file mode 100644 index 000000000..dabe9ef80 --- /dev/null +++ b/publish-pipeline/live-node-publisher/src/main/scala/org/sunbird/job/livenodepublisher/publish/domain/Event.scala @@ -0,0 +1,40 @@ +package org.sunbird.job.livenodepublisher.publish.domain + +import org.apache.commons.lang3.StringUtils +import org.sunbird.job.livenodepublisher.task.LiveNodePublisherConfig +import org.sunbird.job.domain.reader.JobRequest + +import java.util +import scala.collection.JavaConverters._ + +class Event(eventMap: java.util.Map[String, Any], partition: Int, offset: Long) extends JobRequest(eventMap, partition, offset) { + + val jobName = "live-node-publisher" + + def eData: Map[String, AnyRef] = readOrDefault("edata", new util.HashMap[String, AnyRef]()).asScala.toMap + + def action: String = readOrDefault[String]("edata.action", "") + + def mimeType: String = readOrDefault[String]("edata.metadata.mimeType", "") + + def identifier: String = readOrDefault[String]("edata.metadata.identifier", "") + + def objectType: String = readOrDefault[String]("edata.metadata.objectType", "") + + def contentType: String = readOrDefault[String]("edata.contentType", "") + + def publishType: String = readOrDefault[String]("edata.publish_type", "") + + def lastPublishedBy: String = readOrDefault[String]("edata.metadata.lastPublishedBy", "") + + def pkgVersion: Double = { + val pkgVersion: Number = readOrDefault[Number]("edata.metadata.pkgVersion", 0) + pkgVersion.doubleValue() + } + + def validEvent(config: LiveNodePublisherConfig): Boolean = { + ((StringUtils.equals("republish", action) && StringUtils.isNotBlank(identifier)) + && (config.supportedObjectType.contains(objectType) && config.supportedMimeType.contains(mimeType)) + && !StringUtils.equalsIgnoreCase("Asset", contentType)) + } +} diff --git a/publish-pipeline/live-node-publisher/src/main/scala/org/sunbird/job/livenodepublisher/publish/helpers/ECMLExtractor.scala b/publish-pipeline/live-node-publisher/src/main/scala/org/sunbird/job/livenodepublisher/publish/helpers/ECMLExtractor.scala new file mode 100644 index 000000000..8d96c8578 --- /dev/null +++ b/publish-pipeline/live-node-publisher/src/main/scala/org/sunbird/job/livenodepublisher/publish/helpers/ECMLExtractor.scala @@ -0,0 +1,8 @@ +package org.sunbird.job.livenodepublisher.publish.helpers + +import org.sunbird.job.livenodepublisher.publish.processor.{BaseProcessor, MissingAssetValidatorProcessor} +import org.sunbird.job.util.CloudStorageUtil + +class ECMLExtractor(basePath: String, identifier: String)(implicit cloudStorageUtil: CloudStorageUtil) extends BaseProcessor(basePath, identifier) with MissingAssetValidatorProcessor { + +} diff --git a/publish-pipeline/live-node-publisher/src/main/scala/org/sunbird/job/livenodepublisher/publish/helpers/ExtractableMimeTypeHelper.scala b/publish-pipeline/live-node-publisher/src/main/scala/org/sunbird/job/livenodepublisher/publish/helpers/ExtractableMimeTypeHelper.scala new file mode 100644 index 000000000..3a75fbd7b --- /dev/null +++ b/publish-pipeline/live-node-publisher/src/main/scala/org/sunbird/job/livenodepublisher/publish/helpers/ExtractableMimeTypeHelper.scala @@ -0,0 +1,254 @@ +package org.sunbird.job.livenodepublisher.publish.helpers + +import com.fasterxml.jackson.databind.{DeserializationFeature, ObjectMapper} +import org.apache.commons.io.FilenameUtils +import org.apache.commons.lang3.StringUtils +import org.slf4j.LoggerFactory +import org.sunbird.job.livenodepublisher.publish.processor.{JsonParser, Media, Plugin, XmlParser} +import org.sunbird.job.livenodepublisher.task.LiveNodePublisherConfig +import org.sunbird.job.exception.InvalidInputException +import org.sunbird.job.publish.core.ObjectData +import org.sunbird.job.util.{CloudStorageUtil, FileUtils, Slug} +import org.xml.sax.{InputSource, SAXException} + +import java.io._ +import javax.xml.parsers.{DocumentBuilderFactory, ParserConfigurationException} +import scala.concurrent.duration.Duration +import scala.concurrent.{Await, ExecutionContext, Future} + +object ExtractableMimeTypeHelper { + + private[this] val logger = LoggerFactory.getLogger("ExtractableMimeTypeHelper") + private val extractablePackageExtensions = List(".zip", ".h5p", ".epub") + + def getCloudStoreURL(obj: ObjectData, cloudStorageUtil: CloudStorageUtil, config: LiveNodePublisherConfig): String = { + val path = getExtractionPath(obj, config, "latest") + cloudStorageUtil.getURI(path, Option.apply(config.extractableMimeTypes.contains(obj.mimeType))) + } + + private def getExtractionPath(obj: ObjectData, config: LiveNodePublisherConfig, suffix: String): String = { + obj.mimeType match { + case "application/vnd.ekstep.ecml-archive" => config.contentFolder + File.separator + "ecml" + File.separator + obj.identifier + "-" + suffix + case "application/vnd.ekstep.html-archive" => config.contentFolder + File.separator + "html" + File.separator + obj.identifier + "-" + suffix + case "application/vnd.ekstep.h5p-archive" => config.contentFolder + File.separator + "h5p" + File.separator + obj.identifier + "-" + suffix + case _ => "" + } + } + + def getBasePath(objectId: String, tempFileLocation: String): String = { + if (!StringUtils.isBlank(objectId)) tempFileLocation + File.separator + System.currentTimeMillis + "_temp" + File.separator + objectId else "" + } + + def copyExtractedContentPackage(obj: ObjectData, contentConfig: LiveNodePublisherConfig, extractionType: String, cloudStorageUtil: CloudStorageUtil): Unit = { + if (!isExtractedSnapshotExist(obj)) throw new InvalidInputException("Error! Snapshot Type Extraction doesn't Exists.") + val sourcePrefix = getExtractionPath(obj, contentConfig, "snapshot") + val destinationPrefix = getExtractionPath(obj, contentConfig, extractionType) + cloudStorageUtil.copyObjectsByPrefix(sourcePrefix, destinationPrefix, isFolder = true) + } + + private def isExtractedSnapshotExist(obj: ObjectData): Boolean = { + extractablePackageExtensions.exists(key => StringUtils.endsWithIgnoreCase(obj.getString("artifactUrl", null), key)) + } + + def processECMLBody(obj: ObjectData, config: LiveNodePublisherConfig)(implicit ec: ExecutionContext, cloudStorageUtil: CloudStorageUtil): Map[String, AnyRef] = { + try { + val basePath = config.bundleLocation + "/" + System.currentTimeMillis + "_tmp" + "/" + obj.identifier + val ecmlBody = obj.extData.get.getOrElse("body", "").toString + val ecmlType: String = getECMLType(ecmlBody) + val ecrfObj: Plugin = getEcrfObject(ecmlType, ecmlBody) + + // localize assets - download assets to local base path (tmp folder) for validation + localizeAssets(obj.identifier, ecrfObj, basePath, config) + + // validate assets + val processedEcrf: Plugin = new ECMLExtractor(basePath, obj.identifier).process(ecrfObj) + + // getECMLString + val processedEcml: String = getEcmlStringFromEcrf(processedEcrf, ecmlType) + + // write ECML String to basePath + writeECMLFile(basePath, processedEcml, ecmlType) + + // create zip package + val zipFileName: String = basePath + File.separator + System.currentTimeMillis + "_" + Slug.makeSlug(obj.identifier) + ".zip" + FileUtils.createZipPackage(basePath, zipFileName) + + // upload zip file to blob and set artifactUrl + val result: Array[String] = uploadArtifactToCloud(new File(zipFileName), obj.identifier, None, config) + + // upload local extracted directory to blob + extractPackageInCloud(new File(zipFileName), obj, "snapshot", slugFile = true, basePath, config) + + val contentSize = (new File(zipFileName)).length + + // delete local folder + FileUtils.deleteQuietly(basePath) + + obj.metadata ++ Map("artifactUrl" -> result(1), "cloudStorageKey" -> result(0), "size" -> contentSize.asInstanceOf[AnyRef]) + } catch { + case ex@(_: org.sunbird.cloud.storage.exception.StorageServiceException | _: java.lang.NullPointerException | _:java.io.FileNotFoundException | _:java.io.IOException) => { + ex.printStackTrace() + throw new InvalidInputException(s"ECML Asset Files not found For $obj.identifier") + } + case anyEx: Exception => throw anyEx + } + } + + private def getEcrfObject(ecmlType: String, ecmlBody: String): Plugin = { + ecmlType match { + case "ecml" => XmlParser.parse(ecmlBody) + case "json" => JsonParser.parse(ecmlBody) + case _ => classOf[Plugin].newInstance() + } + } + + private def getEcmlStringFromEcrf(processedEcrf: Plugin, ecmlType: String): String = { + ecmlType match { + case "ecml" => XmlParser.toString(processedEcrf) + case "json" => JsonParser.toString(processedEcrf) + case _ => "" + } + } + + private def getECMLType(contentBody: String): String = { + if (!StringUtils.isBlank(contentBody)) { + if (isValidJSON(contentBody)) "json" + else if (isValidXML(contentBody)) "ecml" + else throw new InvalidInputException("Invalid Content Body") + } + else throw new InvalidInputException("Invalid Content Body. ECML content should have body.") + } + + private def isValidJSON(contentBody: String): Boolean = { + if (!StringUtils.isBlank(contentBody)) try { + val objectMapper = new ObjectMapper + objectMapper.enable(DeserializationFeature.FAIL_ON_READING_DUP_TREE_KEY) + objectMapper.readTree(contentBody) + true + } catch { + case ex@(_: IOException) => + logger.error("isValidJSON - Exception when validating the JSON :: ", ex) + false + } + else false + } + + private def isValidXML(contentBody: String): Boolean = { + if (!StringUtils.isBlank(contentBody)) try { + val dbFactory = DocumentBuilderFactory.newInstance + val dBuilder = dbFactory.newDocumentBuilder + dBuilder.parse(new InputSource(new StringReader(contentBody))) + true + } catch { + case ex@(_: ParserConfigurationException | _: SAXException | _: IOException) => + logger.error("isValidXML - Exception when validating the XML :: ", ex) + false + } else false + } + + private def writeECMLFile(basePath: String, ecml: String, ecmlType: String): Unit = { + try { + if (StringUtils.isBlank(ecml)) throw new InvalidInputException("[Unable to write Empty ECML File.]") + if (StringUtils.isBlank(ecmlType)) throw new InvalidInputException("[System is in a fix between (XML & JSON) ECML Type.]") + val file = new File(basePath + "/" + "index." + ecmlType) + FileUtils.writeStringToFile(file, ecml) + } catch { + case e: Exception => + logger.error("writeECMLFile - Exception when write ECML file :: ", e) + throw new Exception("[Unable to Write ECML File.]", e) + } + } + + private def extractPackageInCloud(uploadFile: File, obj: ObjectData, extractionType: String, slugFile: Boolean, basePath: String, config: LiveNodePublisherConfig)(implicit cloudStorageUtil: CloudStorageUtil) = { + val file = Slug.createSlugFile(uploadFile) + val mimeType = obj.mimeType + validationForCloudExtraction(file, extractionType, mimeType, config) + if (config.extractableMimeTypes.contains(mimeType)) { + cloudStorageUtil.uploadDirectory(getExtractionPath(obj, config, extractionType), new File(basePath), Option(slugFile)) + } + } + + private def uploadArtifactToCloud(uploadedFile: File, identifier: String, filePath: Option[String] = None, config: LiveNodePublisherConfig)(implicit cloudStorageUtil: CloudStorageUtil): Array[String] = { + val urlArray = { + try { + val folder = if (filePath.isDefined) filePath.get + File.separator + config.contentFolder + File.separator + Slug.makeSlug(identifier, isTransliterate = true) + File.separator + config.artifactFolder + else config.contentFolder + File.separator + Slug.makeSlug(identifier, isTransliterate = true) + File.separator + config.artifactFolder + cloudStorageUtil.uploadFile(folder, uploadedFile) + } catch { + case e: Exception => + cloudStorageUtil.deleteFile(uploadedFile.getAbsolutePath, Option(false)) + logger.error("Error while uploading the Artifact file.", e) + throw new Exception("Error while uploading the Artifact File.", e) + } + } + urlArray + } + + private def validationForCloudExtraction(file: File, extractionType: String, mimeType: String, config: LiveNodePublisherConfig): Unit = { + if (!file.exists() || (!extractablePackageExtensions.contains("." + FilenameUtils.getExtension(file.getName)) && config.extractableMimeTypes.contains(mimeType))) + throw new InvalidInputException("Error! File doesn't Exist.") + if (extractionType == null) + throw new InvalidInputException("Error! Invalid Content Extraction Type.") + } + + + private def localizeAssets(contentId: String, ecrfObj: Plugin, basePath: String, config: LiveNodePublisherConfig)(implicit ec: ExecutionContext, cloudStorageUtil: CloudStorageUtil): Unit = { + val medias: List[Media] = if (null != ecrfObj && null != ecrfObj.manifest) ecrfObj.manifest.medias else List.empty + if (null != medias && medias.nonEmpty) processAssetsDownload(contentId, medias, basePath, config) + } + + private def processAssetsDownload(contentId: String, medias: List[Media], basePath: String, config: LiveNodePublisherConfig)(implicit ec: ExecutionContext, cloudStorageUtil: CloudStorageUtil): Map[String, String] = { + val downloadResultMap = Await.result(downloadAssetFiles(contentId, medias, basePath, config), Duration.apply(config.assetDownloadDuration)) + downloadResultMap.filter(record => record.nonEmpty).flatten.toMap + } + + private def downloadAssetFiles(identifier: String, mediaFiles: List[Media], basePath: String, config: LiveNodePublisherConfig)(implicit ec: ExecutionContext, cloudStorageUtil: CloudStorageUtil): Future[List[Map[String, String]]] = { + val futures = mediaFiles.map(mediaFile => { + logger.info(s"ExtractableMimeTypeHelper ::: downloadAssetFiles ::: Processing file: ${mediaFile.id} for : " + identifier) + if (!StringUtils.equals("youtube", mediaFile.`type`) && !StringUtils.isBlank(mediaFile.src) && !StringUtils.isBlank(mediaFile.`type`)) { + val downloadPath = if (isWidgetTypeAsset(mediaFile.`type`)) basePath + "/" + "widgets" else basePath + "/" + "assets" + val subFolder = { + if (!mediaFile.src.startsWith("http") && !mediaFile.src.startsWith("https")) { + val f = new File(mediaFile.src) + if (f.exists) f.delete + StringUtils.stripStart(f.getParent, "/") + } else "" + } + val fDownloadPath = if (StringUtils.isNotBlank(subFolder)) downloadPath + File.separator + subFolder + File.separator else downloadPath + File.separator + createDirectoryIfNeeded(fDownloadPath) + logger.info(s"ExtractableMimeTypeHelper ::: downloadAssetFiles ::: fDownloadPath: $fDownloadPath & src : ${mediaFile.src}") + + if (mediaFile.src.startsWith("https://") || mediaFile.src.startsWith("http://")) { + FileUtils.downloadFile(mediaFile.src, fDownloadPath) + } + else { + if (mediaFile.src.contains("assets/public")) { + try { + cloudStorageUtil.downloadFile(fDownloadPath, StringUtils.replace(mediaFile.src, "//", "/").substring(mediaFile.src.indexOf("assets/public") + 14)) + } catch { + case _: Exception => cloudStorageUtil.downloadFile(fDownloadPath, mediaFile.src.substring(mediaFile.src.indexOf("assets/public") + 13)) + } + } + else if (mediaFile.src.startsWith(File.separator)) { + cloudStorageUtil.downloadFile(fDownloadPath, StringUtils.replace(mediaFile.src.substring(1), "//", "/")) + } else { + cloudStorageUtil.downloadFile(fDownloadPath, StringUtils.replace(mediaFile.src, "//", "/")) + } + } + val downloadedFile = new File(fDownloadPath + mediaFile.src.split("/").last) + logger.info("Downloaded file : " + mediaFile.src + " - " + downloadedFile + " | [Content Id '" + identifier + "']") + + Map(mediaFile.id -> downloadedFile.getName) + } else Map.empty[String, String] + }) + Future(futures) + } + + private def isWidgetTypeAsset(assetType: String): Boolean = StringUtils.equalsIgnoreCase(assetType, "js") || StringUtils.equalsIgnoreCase(assetType, "css") || StringUtils.equalsIgnoreCase(assetType, "json") || StringUtils.equalsIgnoreCase(assetType, "plugin") + + private def createDirectoryIfNeeded(directoryName: String): Unit = { + val theDir = new File(directoryName) + if (!theDir.exists) theDir.mkdirs + } + +} diff --git a/publish-pipeline/live-node-publisher/src/main/scala/org/sunbird/job/livenodepublisher/publish/helpers/LiveCollectionPublisher.scala b/publish-pipeline/live-node-publisher/src/main/scala/org/sunbird/job/livenodepublisher/publish/helpers/LiveCollectionPublisher.scala new file mode 100644 index 000000000..8dd9c6cde --- /dev/null +++ b/publish-pipeline/live-node-publisher/src/main/scala/org/sunbird/job/livenodepublisher/publish/helpers/LiveCollectionPublisher.scala @@ -0,0 +1,649 @@ +package org.sunbird.job.livenodepublisher.publish.helpers + +import com.datastax.driver.core.Row +import com.datastax.driver.core.querybuilder.{Insert, QueryBuilder, Select} +import com.fasterxml.jackson.core.JsonProcessingException +import org.apache.commons.io.FileUtils +import org.apache.commons.lang3.StringUtils +import org.slf4j.LoggerFactory +import org.sunbird.job.livenodepublisher.task.LiveNodePublisherConfig +import org.sunbird.job.domain.`object`.{DefinitionCache, ObjectDefinition} +import org.sunbird.job.exception.InvalidInputException +import org.sunbird.job.publish.config.PublishConfig +import org.sunbird.job.publish.core.{DefinitionConfig, ExtDataConfig, ObjectData, ObjectExtData} +import org.sunbird.job.publish.helpers._ +import org.sunbird.job.util._ + +import java.io.{File, IOException} +import java.util +import scala.collection.JavaConverters._ +import scala.collection.mutable +import scala.collection.mutable.ListBuffer +import scala.concurrent.ExecutionContext +import java.text.{DecimalFormat, DecimalFormatSymbols, SimpleDateFormat} +import java.util.{Date, Locale} + +trait LiveCollectionPublisher extends LiveObjectReader with SyncMessagesGenerator with ObjectValidator with ObjectEnrichment with EcarGenerator with LiveObjectUpdater { + + private[this] val logger = LoggerFactory.getLogger(classOf[LiveCollectionPublisher]) + private val level4ContentTypes = List("Course", "CourseUnit", "LessonPlan", "LessonPlanUnit") + private val EXPANDABLE_OBJECTS = List("Collection", "QuestionSet") + private val EXCLUDE_LEAFNODE_OBJECTS = List("Collection", "Question") + private val INCLUDE_LEAFNODE_OBJECTS = List("QuestionSet") + private val INCLUDE_CHILDNODE_OBJECTS = List("Collection") + private val PUBLISHED_STATUS_LIST = List("Live", "Unlisted") + private val COLLECTION_MIME_TYPE = "application/vnd.ekstep.content-collection" + + override def getExtData(identifier: String, mimeType: String, readerConfig: ExtDataConfig)(implicit cassandraUtil: CassandraUtil, config: PublishConfig): Option[ObjectExtData] = None + + override def getHierarchy(identifier: String, readerConfig: ExtDataConfig)(implicit cassandraUtil: CassandraUtil, config: PublishConfig): Option[Map[String, AnyRef]] = { + try { + val row: Row = getCollectionHierarchy(identifier, readerConfig) + if (null != row) { + val hierarchy = row.getString("hierarchy") + val updatedHierarchy = if (config.asInstanceOf[LiveNodePublisherConfig].isrRelativePathEnabled) CSPMetaUtil.updateAbsolutePath(hierarchy) else hierarchy + val data: Map[String, AnyRef] = if(updatedHierarchy.nonEmpty) ScalaJsonUtil.deserialize[Map[String, AnyRef]](updatedHierarchy) else Map.empty[String, AnyRef] + Option(data) + } else Option(Map.empty[String, AnyRef]) + } catch { + case _: Exception => throw new InvalidInputException("Exception while reading Hierarchy for collection:: " + identifier) + } + } + + private def getCollectionHierarchy(identifier: String, readerConfig: ExtDataConfig)(implicit cassandraUtil: CassandraUtil): Row = { + val selectWhere: Select.Where = QueryBuilder.select().all() + .from(readerConfig.keyspace, readerConfig.table). + where() + selectWhere.and(QueryBuilder.eq("identifier", identifier)) + cassandraUtil.findOne(selectWhere.toString) + } + + def getRelationalMetadata(identifier: String, readerConfig: ExtDataConfig)(implicit cassandraUtil: CassandraUtil): Option[Map[String, AnyRef]] = { + val row: Row = getCollectionHierarchy(identifier, readerConfig) + if (null != row && row.getString("relational_metadata") != null && row.getString("relational_metadata").nonEmpty) { + val data: Map[String, AnyRef] = ScalaJsonUtil.deserialize[Map[String, AnyRef]](row.getString("relational_metadata")) + Option(data) + } else Option(Map.empty[String, AnyRef]) + } + + override def getExtDatas(identifiers: List[String], readerConfig: ExtDataConfig)(implicit cassandraUtil: CassandraUtil): Option[Map[String, AnyRef]] = None + + override def getHierarchies(identifiers: List[String], readerConfig: ExtDataConfig)(implicit cassandraUtil: CassandraUtil): Option[Map[String, AnyRef]] = None + + override def enrichObjectMetadata(obj: ObjectData)(implicit neo4JUtil: Neo4JUtil, cassandraUtil: CassandraUtil, readerConfig: ExtDataConfig, cloudStorageUtil: CloudStorageUtil, config: PublishConfig, definitionCache: DefinitionCache, definitionConfig: DefinitionConfig): Option[ObjectData] = { + val contentConfig = config.asInstanceOf[LiveNodePublisherConfig] + val extraMeta = Map("pkgVersion" -> (obj.pkgVersion + 1).asInstanceOf[AnyRef], "lastPublishedOn" -> getTimeStamp, + "flagReasons" -> null, "body" -> null, "publishError" -> null, "variants" -> null, "downloadUrl" -> null) + val contentSize = obj.metadata.getOrElse("size", 0).toString.toDouble + val configSize = contentConfig.artifactSizeForOnline + val updatedMeta: Map[String, AnyRef] = if (contentSize > configSize) obj.metadata ++ extraMeta ++ Map("contentDisposition" -> "online-only") else obj.metadata ++ extraMeta + + val publishType = obj.getString("publish_type", "Public") + val status = if (StringUtils.equalsIgnoreCase("Unlisted", publishType)) "Unlisted" else "Live" + val updatedCompatibilityLevelMeta: Map[String, AnyRef] = setCompatibilityLevel(obj, updatedMeta).get + ("status" -> status) + + val isCollectionShallowCopy = isContentShallowCopy(obj) + + // Collection - Enrich Children - line 345 + val collectionHierarchy: Map[String, AnyRef] = if (isCollectionShallowCopy) { + val originData: Map[String, AnyRef] = obj.metadata.getOrElse("originData", "").asInstanceOf[Map[String, AnyRef]] + getHierarchy(obj.metadata("origin").asInstanceOf[String], readerConfig).get + } else getHierarchy(obj.identifier, readerConfig).get + logger.info("LiveCollectionPublisher:: enrichObjectMetadata:: collectionHierarchy:: " + collectionHierarchy) + val children = if (collectionHierarchy.nonEmpty) { + collectionHierarchy.getOrElse("children", List.empty[Map[String, AnyRef]]).asInstanceOf[List[Map[String, AnyRef]]] + } else List.empty[Map[String, AnyRef]] + val toEnrichChildren = children.to[ListBuffer] + val enrichedObj: ObjectData = if (collectionHierarchy.nonEmpty && !isCollectionShallowCopy) { + val childNodesToRemove = ListBuffer.empty[String] + val collectionResourceChildNodes: mutable.HashSet[String] = mutable.HashSet.empty[String] + val enrichedChildrenData = enrichChildren(toEnrichChildren, collectionResourceChildNodes, childNodesToRemove) + val collectionChildNodes = (updatedCompatibilityLevelMeta.getOrElse("childNodes", new java.util.ArrayList()).asInstanceOf[java.util.List[String]].asScala.toList ++ collectionResourceChildNodes).distinct + new ObjectData(obj.identifier, updatedCompatibilityLevelMeta ++ Map("childNodes" -> collectionChildNodes.filter(rec => !childNodesToRemove.contains(rec))), obj.extData, Option(collectionHierarchy + ("children" -> enrichedChildrenData.toList))) + } else new ObjectData(obj.identifier, updatedCompatibilityLevelMeta, obj.extData, Option(collectionHierarchy ++ Map("children" -> toEnrichChildren.toList))) + + logger.info("LiveCollectionPublisher:: enrichObjectMetadata:: Collection data after processing for : " + enrichedObj.identifier + " | Metadata : " + enrichedObj.metadata) + logger.debug("LiveCollectionPublisher:: enrichObjectMetadata:: Collection children data after processing : " + enrichedObj.hierarchy.get("children")) + + Some(enrichedObj) + } + + override def getDataForEcar(obj: ObjectData): Option[List[Map[String, AnyRef]]] = { + val hChildren: List[Map[String, AnyRef]] = obj.hierarchy.getOrElse(Map()).getOrElse("children", List()).asInstanceOf[List[Map[String, AnyRef]]] + Some(getFlatStructure(List(obj.metadata ++ obj.extData.getOrElse(Map()) ++ Map("children" -> hChildren)), List())) + } + + override def saveExternalData(obj: ObjectData, readerConfig: ExtDataConfig)(implicit cassandraUtil: CassandraUtil): Unit = { + val identifier = obj.identifier.replace(".img", "") + val query: Insert = QueryBuilder.insertInto(readerConfig.keyspace, readerConfig.table) + query.value(readerConfig.primaryKey(0), identifier) + query.value("relational_metadata", null) + val result = cassandraUtil.upsert(query.toString) + if (result) { + logger.info(s"relational_metadata emptied successfully for ${identifier}") + } else { + val msg = s"relational_metadata emptying Failed For $identifier" + logger.error(msg) + } + } + + override def deleteExternalData(obj: ObjectData, readerConfig: ExtDataConfig)(implicit cassandraUtil: CassandraUtil): Unit = None + + def getObjectWithEcar(obj: ObjectData, pkgTypes: List[String])(implicit ec: ExecutionContext, neo4JUtil: Neo4JUtil, cassandraUtil: CassandraUtil, readerConfig: ExtDataConfig, cloudStorageUtil: CloudStorageUtil, config: PublishConfig, defCache: DefinitionCache, defConfig: DefinitionConfig, httpUtil: HttpUtil): ObjectData = { + val collRelationalMetadata = getRelationalMetadata(obj.identifier, readerConfig).getOrElse(Map.empty[String, AnyRef]) + // Line 1107 in PublishFinalizer + val children = obj.hierarchy.getOrElse(Map()).getOrElse("children", List()).asInstanceOf[List[Map[String, AnyRef]]] + val updatedChildren = updateHierarchyMetadata(children, obj.metadata, collRelationalMetadata)(config) + val enrichedObj = processCollection(obj, updatedChildren) + val updatedObj = updateRootChildrenList(enrichedObj, updatedChildren) + val nodes = ListBuffer.empty[ObjectData] + val nodeIds = ListBuffer.empty[String] + nodes += obj + nodeIds += obj.identifier + + val ecarMap: Map[String, String] = try{ + generateEcar(updatedObj, pkgTypes) + } catch { + case ex@(_: org.sunbird.cloud.storage.exception.StorageServiceException | _: java.lang.NullPointerException | _:java.io.FileNotFoundException | _:java.io.IOException) => { + ex.printStackTrace() + throw new InvalidInputException(s"ECAR bundling failed for ${obj.identifier}:: " + ex.getMessage) + } + case anyEx: Exception => throw anyEx + } + + val variants: java.util.Map[String, java.util.Map[String, String]] = ecarMap.map { case (key, value) => key.toLowerCase -> Map[String, String]("ecarUrl" -> value, "size" -> httpUtil.getSize(value).toString).asJava }.asJava + logger.info("CollectionPulisher ::: getObjectWithEcar ::: variants ::: " + variants) + + val meta: Map[String, AnyRef] = Map("downloadUrl" -> ecarMap.getOrElse(EcarPackageType.SPINE, ""), "variants" -> variants, "size" -> httpUtil.getSize(ecarMap.getOrElse(EcarPackageType.SPINE, "")).asInstanceOf[AnyRef]) + new ObjectData(updatedObj.identifier, updatedObj.metadata ++ meta, updatedObj.extData, updatedObj.hierarchy) + } + + private def setCompatibilityLevel(obj: ObjectData, updatedMeta: Map[String, AnyRef]): Option[Map[String, AnyRef]] = { + if (level4ContentTypes.contains(obj.getString("contentType", ""))) { + logger.info("LiveCollectionPublisher:: setCompatibilityLevel:: setting compatibility level for content id : " + obj.identifier + " as 4.") + Some(updatedMeta ++ Map("compatibilityLevel" -> 4.asInstanceOf[AnyRef])) + } else Some(updatedMeta) + } + + def getUnitsFromLiveContent(obj: ObjectData)(implicit cassandraUtil: CassandraUtil, readerConfig: ExtDataConfig, config: PublishConfig): List[String] = { + logger.info("LiveCollectionPublisher:: getUnitsFromLiveContent:: identifier: " + obj.identifier + " || pkgVersion: " + obj.metadata.getOrElse("pkgVersion", 1).asInstanceOf[Number]) + val objHierarchy = getHierarchy(obj.identifier, readerConfig).get + val children = objHierarchy.getOrElse("children", List.empty).asInstanceOf[List[Map[String, AnyRef]]] + getUnits(children) + } + + private def getUnits(children: List[Map[String, AnyRef]]): List[String] = { + if (children.nonEmpty) { + children.flatMap(child => { + if (child.getOrElse("visibility", "").asInstanceOf[String].equalsIgnoreCase("Parent")) { + val childUnits = if(child.contains("children")) getUnits(child.getOrElse("children", List.empty).asInstanceOf[List[Map[String, AnyRef]]]) else List.empty + childUnits ++ List(child.getOrElse("identifier", "").asInstanceOf[String]) + } else List.empty[String] + }).filter(rec => rec.nonEmpty) + } else List.empty[String] + } + + def isContentShallowCopy(obj: ObjectData): Boolean = { + val originData: Map[String, AnyRef] = if(obj.metadata.contains("originData")) { + obj.metadata("originData") match { + case strValue: String => ScalaJsonUtil.deserialize[Map[String, AnyRef]](strValue) + case mapValue:util.Map[String, AnyRef] => mapValue.asScala.toMap[String, AnyRef] + case _ => obj.metadata("originData").asInstanceOf[Map[String,AnyRef]] + } + } else Map.empty[String, AnyRef] + originData.nonEmpty && originData.getOrElse("copyType", "").asInstanceOf[String].equalsIgnoreCase("shallow") + } + + def updateOriginPkgVersion(obj: ObjectData)(implicit neo4JUtil: Neo4JUtil): ObjectData = { + val originId = obj.metadata.getOrElse("origin", "").asInstanceOf[String] + val originNodeMetadata = Option(neo4JUtil.getNodeProperties(originId)).getOrElse(neo4JUtil.getNodeProperties(originId)) + if (null != originNodeMetadata) { + val originPkgVer: Double = originNodeMetadata.getOrDefault("pkgVersion", "0").toString.toDouble + if (originPkgVer != 0) { + val originData = obj.metadata("originData") match { + case propVal: String => ScalaJsonUtil.deserialize[Map[String, AnyRef]](propVal) + ("pkgVersion" -> originPkgVer) + case _ => obj.metadata.getOrElse("originData", Map.empty[String, AnyRef]).asInstanceOf[Map[String, AnyRef]] + ("pkgVersion" -> originPkgVer) + } + new ObjectData(obj.identifier, obj.metadata ++ Map("originData" -> originData), obj.extData, obj.hierarchy) + } else obj + } else obj + } + + private def enrichChildren(toEnrichChildren: ListBuffer[Map[String, AnyRef]], collectionResourceChildNodes: mutable.HashSet[String], childNodesToRemove: ListBuffer[String])(implicit neo4JUtil: Neo4JUtil, cassandraUtil: CassandraUtil, readerConfig: ExtDataConfig, config: PublishConfig): ListBuffer[Map[String, AnyRef]] = { + val newChildren = toEnrichChildren.toList + newChildren.map(child => { + logger.info("LiveCollectionPublisher:: enrichChildren:: child identifier:: " + child.getOrElse("identifier", "") + " || visibility:: " + child.getOrElse("visibility", "") + " || mimeType:: " + child.getOrElse("mimeType", "") + " || objectType:: " + child.getOrElse("objectType", "")) + if (StringUtils.equalsIgnoreCase(child.getOrElse("visibility", "").asInstanceOf[String], "Parent") && StringUtils.equalsIgnoreCase(child.getOrElse("mimeType", "").asInstanceOf[String], COLLECTION_MIME_TYPE)) { + val updatedChildrenData = enrichChildren(child.getOrElse("children", List.empty).asInstanceOf[List[Map[String, AnyRef]]].to[ListBuffer], collectionResourceChildNodes, childNodesToRemove) + toEnrichChildren(newChildren.indexOf(child)) = child + ("children" -> updatedChildrenData.toList) + } + + if (StringUtils.equalsIgnoreCase(child.getOrElse("visibility", "").asInstanceOf[String], "Default") && EXPANDABLE_OBJECTS.contains(child.getOrElse("objectType", "").asInstanceOf[String])) { + val pkgVersion = child.getOrElse("pkgVersion", 0) match { + case _: Integer => child.getOrElse("pkgVersion", 0).asInstanceOf[Integer].doubleValue() + case _: Double => child.getOrElse("pkgVersion", 0).asInstanceOf[Double].doubleValue() + case _ => child.getOrElse("pkgVersion", "0").toString.toDouble + } + val childCollectionHierarchy = getHierarchy(child.getOrElse("identifier", "").asInstanceOf[String], readerConfig).get + if (childCollectionHierarchy.nonEmpty) { + val childNodes = childCollectionHierarchy.getOrElse("childNodes", List.empty).asInstanceOf[List[String]] + if (childNodes.nonEmpty && INCLUDE_CHILDNODE_OBJECTS.contains(child.getOrElse("objectType", "").asInstanceOf[String])) collectionResourceChildNodes ++= childNodes.toSet[String] + toEnrichChildren(newChildren.indexOf(child)) = childCollectionHierarchy ++ Map("index" -> child.getOrElse("index", 0).asInstanceOf[AnyRef], "depth" -> child.getOrElse("depth", 0).asInstanceOf[AnyRef], "parent" -> child.getOrElse("parent", ""), "objectType" -> child.getOrElse("objectType", "Collection").asInstanceOf[String]) + } + } + + if (StringUtils.equalsIgnoreCase(child.getOrElse("visibility", "").asInstanceOf[String], "Default") && !EXPANDABLE_OBJECTS.contains(child.getOrElse("objectType", "").asInstanceOf[String])) { + val childNode = Option(neo4JUtil.getNodeProperties(child.getOrElse("identifier", "").asInstanceOf[String])).getOrElse(neo4JUtil.getNodeProperties(child.getOrElse("identifier", "").asInstanceOf[String])).asScala.toMap + if (PUBLISHED_STATUS_LIST.contains(childNode.getOrElse("status", "").asInstanceOf[String])) { + logger.info("LiveCollectionPublisher:: enrichChildren:: fetched child node:: " + childNode.getOrElse("IL_UNIQUE_ID", "").asInstanceOf[String] + " || objectType:: " + childNode.getOrElse("IL_FUNC_OBJECT_TYPE", "").asInstanceOf[String]) + toEnrichChildren(newChildren.indexOf(child)) = childNode ++ Map("identifier" ->childNode.getOrElse("IL_UNIQUE_ID", "").asInstanceOf[String], "objectType" ->childNode.getOrElse("IL_FUNC_OBJECT_TYPE", "").asInstanceOf[String], "index" -> child.getOrElse("index", 0).asInstanceOf[AnyRef], "parent" -> child.getOrElse("parent", "").asInstanceOf[String], "depth" -> child.getOrElse("depth", 0).asInstanceOf[AnyRef]) - ("collections", "children", "IL_FUNC_OBJECT_TYPE", "IL_SYS_NODE_TYPE","IL_UNIQUE_ID") + } else childNodesToRemove += child.getOrElse("identifier", "").asInstanceOf[String] + } + }) + + toEnrichChildren + } + + + private def processCollection(obj: ObjectData, children: List[Map[String, AnyRef]])(implicit neo4JUtil: Neo4JUtil, cassandraUtil: CassandraUtil, readerConfig: ExtDataConfig, cloudStorageUtil: CloudStorageUtil, config: PublishConfig): ObjectData = { + val dataMap: mutable.Map[String, AnyRef] = processChildren(children) + logger.info("LiveCollectionPublisher:: processCollection:: dataMap: " + dataMap) + val updatedObj: ObjectData = if (dataMap.nonEmpty) { + val updatedMetadataMap: Map[String, AnyRef] = dataMap.flatMap(record => { + if (!"concepts".equalsIgnoreCase(record._1) && !"keywords".equalsIgnoreCase(record._1)) { + Map(record._1 -> record._2.asInstanceOf[Set[String]].toArray[String]) + } else Map.empty[String, AnyRef] + }).filter(record => record._1.nonEmpty).toMap[String, AnyRef] + val keywords = dataMap.getOrElse("keywords", Set.empty).asInstanceOf[Set[String]].toArray[String] + val finalKeywords: Array[String] = if (null != keywords && keywords.nonEmpty) { + val updatedKeywords: Array[String] = if (obj.metadata.contains("keywords")) { + obj.metadata("keywords") match { + case _: Array[String] => keywords ++ obj.metadata.getOrElse("keywords", Array.empty).asInstanceOf[Array[String]] + case kwValue: String => keywords ++ Array[String](kwValue) + case _: util.Collection[String] => keywords ++ obj.metadata.getOrElse("keywords", Array.empty).asInstanceOf[util.Collection[String]].asScala.toArray[String] + case _ => keywords + } + } else keywords + updatedKeywords.filter(record => record.trim.nonEmpty).distinct + } else if(obj.metadata.contains("keywords")) { + obj.metadata("keywords") match { + case _: Array[String] => obj.metadata.getOrElse("keywords", Array.empty).asInstanceOf[Array[String]] + case kwValue: String => Array[String](kwValue) + case _: util.Collection[String] => obj.metadata.getOrElse("keywords", Array.empty).asInstanceOf[util.Collection[String]].asScala.toArray[String] + case _ => Array.empty[String] + } + } else Array.empty[String] + new ObjectData(obj.identifier, obj.metadata ++ updatedMetadataMap + ("keywords" -> finalKeywords), obj.extData, obj.hierarchy) + } else obj + + val enrichedObject = if(children.nonEmpty) enrichCollection(updatedObj) else updatedObj + // addResourceToCollection(enrichedObject, children.to[ListBuffer]) - TODO + enrichedObject + } + + private def processChildren(children: List[Map[String, AnyRef]]): mutable.Map[String, AnyRef] = { + val dataMap: mutable.Map[String, AnyRef] = mutable.Map.empty + processChildren(children, dataMap) + dataMap + } + + private def processChildren(children: List[Map[String, AnyRef]], dataMap: mutable.Map[String, AnyRef]): Unit = { + if (null != children && children.nonEmpty) { + for (child <- children) { + mergeMap(dataMap, processChild(child)) + if (child.contains("children")) processChildren(child.getOrElse("children", List.empty).asInstanceOf[List[Map[String, AnyRef]]], dataMap) + } + } + } + + private def processChild(childMetadata: Map[String, AnyRef]): Map[String, AnyRef] = { + val taggingProperties = List("language", "domain", "ageGroup", "genre", "theme", "keywords") + val result: Map[String, AnyRef] = childMetadata.flatMap(prop => { + if (taggingProperties.contains(prop._1)) { + childMetadata(prop._1) match { + case propStrValue: String => Map(prop._1 -> Set(propStrValue)) + case propListValue: List[_] => Map(prop._1 -> propListValue.toSet) + case propVal: java.util.List[String] => Map(prop._1 -> propVal.asScala.toSet[String]) + case _ => Map.empty[String, AnyRef] + } + } else Map.empty[String, AnyRef] + }).filter(rec => rec._1.nonEmpty) + result + } + + private def mergeMap(dataMap: mutable.Map[String, AnyRef], childDataMap: Map[String, AnyRef]): mutable.Map[String, AnyRef] = { + if (dataMap.isEmpty) dataMap ++= childDataMap + else { + dataMap.map(record => { + dataMap += (record._1 -> (if (childDataMap.contains(record._1)) childDataMap(record._1).asInstanceOf[Set[String]] ++ record._2.asInstanceOf[Set[String]] else record._2.asInstanceOf[Set[String]])) + }) + if (!dataMap.equals(childDataMap)) { + childDataMap.map(record => { + if (!dataMap.contains(record._1)) dataMap += record + }) + } + } + dataMap + } + + def enrichCollection(obj: ObjectData)(implicit neo4JUtil: Neo4JUtil, cassandraUtil: CassandraUtil, readerConfig: ExtDataConfig, cloudStorageUtil: CloudStorageUtil, config: PublishConfig): ObjectData = { + val nodeMetadata = mutable.Map.empty[String, AnyRef] ++ obj.metadata + val contentId = obj.identifier + logger.info("LiveCollectionPublisher:: enrichCollection:: Processing Collection Content :" + contentId) + val content = obj.hierarchy.get + if (content.isEmpty) return obj + val leafCount = getLeafNodeCount(content) + val totalCompressedSize = getTotalCompressedSize(content, 0.0) + + val df = new DecimalFormat("0", DecimalFormatSymbols.getInstance(Locale.ENGLISH)) + df.setMaximumFractionDigits(0) + + nodeMetadata.put("leafNodesCount", leafCount.asInstanceOf[AnyRef]) + nodeMetadata.put("totalCompressedSize", df.format(totalCompressedSize).toLong.asInstanceOf[AnyRef]) + + nodeMetadata.put("leafNodes", updateLeafNodeIds(content)) + val mimeTypeMap: mutable.Map[String, AnyRef] = mutable.Map.empty[String, AnyRef] + val contentTypeMap: mutable.Map[String, AnyRef] = mutable.Map.empty[String, AnyRef] + getTypeCount(content, "mimeType", mimeTypeMap) + getTypeCount(content, "contentType", contentTypeMap) + + val updatedContent = content ++ Map("leafNodesCount" -> leafCount, "totalCompressedSize" -> df.format(totalCompressedSize).toLong, "mimeTypesCount" -> ScalaJsonUtil.serialize(mimeTypeMap), "contentTypesCount" -> ScalaJsonUtil.serialize(contentTypeMap)).asInstanceOf[Map[String, AnyRef]] + nodeMetadata.put("mimeTypesCount", ScalaJsonUtil.serialize(mimeTypeMap)) + nodeMetadata.put("contentTypesCount", ScalaJsonUtil.serialize(contentTypeMap)) + val uploadedFileUrl: Array[String] = generateTOC(obj, nodeMetadata.toMap) + if(uploadedFileUrl.nonEmpty) { + nodeMetadata.put("toc_url", uploadedFileUrl(1)) + nodeMetadata.put("s3Key", uploadedFileUrl(0)) + } + + val sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ") + val updatedOn = sdf.format(new Date()) + nodeMetadata.put("sYS_INTERNAL_LAST_UPDATED_ON", updatedOn) + val updatedMetadata: Map[String, AnyRef] = try { + setContentAndCategoryTypes(nodeMetadata.toMap) + } catch { + case e: Exception => logger.error("LiveCollectionPublisher:: enrichCollection:: Error while stringify mimeTypeCount or contentTypesCount:", e) + nodeMetadata.toMap + } + + new ObjectData(obj.identifier, updatedMetadata, obj.extData, Option(updatedContent)) + } + + private def updateLeafNodeIds(content: Map[String, AnyRef]): Array[String] = { + val leafNodeIds: mutable.Set[String] = mutable.Set.empty[String] + getLeafNodesIds(content, leafNodeIds) + leafNodeIds.toArray + } + + private def getTypeCount(data: Map[String, AnyRef], `type`: String, typeMap: mutable.Map[String, AnyRef]): Unit = { + val children = data.getOrElse("children", List.empty).asInstanceOf[List[AnyRef]] + if (null != children && children.nonEmpty) { + for (child <- children) { + val childMap = child.asInstanceOf[Map[String, AnyRef]] + val typeValue = childMap.getOrElse(`type`, "").asInstanceOf[String] + if (null != typeValue) if (typeMap.contains(typeValue)) { + var count = typeMap.getOrElse(typeValue, 0).asInstanceOf[Int] + count += 1 + typeMap.put(typeValue, count.asInstanceOf[AnyRef]) + } + else typeMap.put(typeValue, 1.asInstanceOf[AnyRef]) + if (childMap.contains("children")) getTypeCount(childMap, `type`, typeMap) + } + } + } + + @SuppressWarnings(Array("unchecked")) + private def getLeafNodeCount(data: Map[String, AnyRef]): Int = { + val leafNodeIds: mutable.Set[String] = mutable.Set.empty[String] + getLeafNodesIds(data, leafNodeIds) + leafNodeIds.size + } + + private def getLeafNodesIds(data: Map[String, AnyRef], leafNodeIds: mutable.Set[String]): Unit = { + if (INCLUDE_LEAFNODE_OBJECTS.contains(data.getOrElse("objectType", "")) && StringUtils.equals(data.getOrElse("visibility", "").asInstanceOf[String], "Default")) leafNodeIds += data.getOrElse("identifier", "").asInstanceOf[String] + val children = data.getOrElse("children", List.empty).asInstanceOf[List[Map[String, AnyRef]]] + if (children.nonEmpty) { + for (child <- children) { + getLeafNodesIds(child, leafNodeIds) + } + } + else if (!EXCLUDE_LEAFNODE_OBJECTS.contains(data.getOrElse("objectType", "").asInstanceOf[String])) leafNodeIds.add(data.getOrElse("identifier", "").asInstanceOf[String]) + } + + private def getTotalCompressedSize(data: Map[String, AnyRef], totalCompressed: Double): Double = { + val children = data.getOrElse("children", List.empty).asInstanceOf[List[Map[String, AnyRef]]] + if (children.nonEmpty) { + val childrenSizes = children.map(child => { + val childSize = + if (!EXPANDABLE_OBJECTS.contains(child.getOrElse("objectType", "").asInstanceOf[String]) && StringUtils.equals(child.getOrElse("visibility", "").asInstanceOf[String], "Default")) { + child.getOrElse("totalCompressedSize", child.getOrElse("size", 0).asInstanceOf[Number].doubleValue).asInstanceOf[Number].doubleValue + } else 0 + + getTotalCompressedSize(child, childSize) + }).sum + totalCompressed + childrenSizes + } + else totalCompressed + } + + def generateTOC(obj: ObjectData, content: Map[String, AnyRef])(implicit cloudStorageUtil: CloudStorageUtil, config: PublishConfig): Array[String] = { + logger.info("LiveCollectionPublisher:: generateTOC:: Write hierarchy to JSON File :" + obj.identifier) + val file = new File(getTOCBasePath(obj.identifier) + "_toc.json") + try { + val data = ScalaJsonUtil.serialize(content) + FileUtils.writeStringToFile(file, data, "UTF-8") + if (file.exists) { + logger.debug("LiveCollectionPublisher:: generateTOC:: Upload File to cloud storage :" + file.getName) + val uploadedFileUrl = cloudStorageUtil.uploadFile(getAWSPath(obj.identifier), file, Option.apply(true)) + if (null != uploadedFileUrl && uploadedFileUrl.length > 1) { + logger.info("LiveCollectionPublisher:: generateTOC:: Update cloud storage url to node" + uploadedFileUrl(1)) + uploadedFileUrl + } else Array.empty + } else Array.empty + } catch { + case e: JsonProcessingException => logger.error("LiveCollectionPublisher:: generateTOC:: Error while parsing map object to string.", e) + throw new InvalidInputException("LiveCollectionPublisher:: generateTOC:: Error while parsing map object to string.", e) + case e: Exception => logger.error("LiveCollectionPublisher:: generateTOC:: Error while uploading file ", e) + throw new InvalidInputException("LiveCollectionPublisher:: generateTOC:: Error while uploading file", e) + } finally try { + logger.info("LiveCollectionPublisher:: generateTOC:: Deleting Uploaded files") + FileUtils.deleteDirectory(file.getParentFile) + } catch { + case e: IOException => + logger.error("LiveCollectionPublisher:: generateTOC::Error while deleting file ", e) + } + } + + private def getTOCBasePath(contentId: String)(implicit cloudStorageUtil: CloudStorageUtil, config: PublishConfig): String = { + if (contentId.nonEmpty) "/tmp" + File.separator + System.currentTimeMillis + "_temp" + File.separator + contentId else "" + } + + private def getAWSPath(identifier: String)(implicit cloudStorageUtil: CloudStorageUtil, config: PublishConfig): String = { + val contentConfig = config.asInstanceOf[LiveNodePublisherConfig] + val folderName = contentConfig.contentFolder + if (folderName.nonEmpty) folderName + File.separator + Slug.makeSlug(identifier, isTransliterate = true) + File.separator + contentConfig.artifactFolder else folderName + } + + def setContentAndCategoryTypes(input: Map[String, AnyRef])(implicit config: PublishConfig): Map[String, AnyRef] = { + val contentConfig = config.asInstanceOf[LiveNodePublisherConfig] + val categoryMap = contentConfig.categoryMap + val contentType = input.getOrElse("contentType", "").asInstanceOf[String] + val primaryCategory = input.getOrElse("primaryCategory","").asInstanceOf[String] + val (updatedContentType, updatedPrimaryCategory): (String, String) = + if(contentType.nonEmpty && (primaryCategory.isEmpty || primaryCategory.isBlank)) { (contentType, categoryMap.getOrDefault(contentType,"").asInstanceOf[String]) } + else if((contentType.isEmpty || contentType.isBlank) && primaryCategory.nonEmpty) { (categoryMap.asScala.filter(entry => StringUtils.equalsIgnoreCase(entry._2.asInstanceOf[String], primaryCategory)).keys.headOption.getOrElse(""), primaryCategory) } + else (contentType, primaryCategory) + + input ++ Map("contentType" -> updatedContentType, "primaryCategory" -> updatedPrimaryCategory) + } + + def updateHierarchyMetadata(children: List[Map[String, AnyRef]], objMetadata: Map[String, AnyRef], collRelationalMetadata: Map[String, AnyRef])(implicit config: PublishConfig): List[Map[String, AnyRef]] = { + if (children.nonEmpty) { + children.map(child => { + if (StringUtils.equalsIgnoreCase("Parent", child.getOrElse("visibility", "").asInstanceOf[String])) { //set child metadata -- compatibilityLevel, appIcon, posterImage, lastPublishedOn, pkgVersion, status + val updatedChild = populatePublishMetadata(child, objMetadata) + updatedChild + ("children" -> updateHierarchyMetadata(updatedChild.getOrElse("children", List.empty).asInstanceOf[List[Map[String, AnyRef]]], objMetadata, collRelationalMetadata)) + } else { + //TODO: Populate relationalMetadata here for child contents + if (collRelationalMetadata.nonEmpty) { + val parent = child.getOrElse("parent", "").asInstanceOf[String] + val unitRelationalMetadata = collRelationalMetadata(parent).asInstanceOf[Map[String, AnyRef]].getOrElse("relationalMetadata", Map.empty).asInstanceOf[Map[String, AnyRef]] + if (unitRelationalMetadata.nonEmpty) { + val childRelationalMetadata = unitRelationalMetadata.getOrElse(child.getOrElse("identifier","").asInstanceOf[String], Map.empty).asInstanceOf[Map[String, AnyRef]] + if(childRelationalMetadata.nonEmpty) { + child + ("relationalMetadata" -> childRelationalMetadata) + } else child + } else child + } else child + } + }) + } else children + } + + private def populatePublishMetadata(content: Map[String, AnyRef], objMetadata: Map[String, AnyRef])(implicit config: PublishConfig): Map[String, AnyRef] = { + //TODO: For appIcon, posterImage and screenshot createThumbNail method has to be implemented. + val leafNodeIds: mutable.Set[String] = mutable.Set.empty[String] + getLeafNodesIds(content, leafNodeIds) + + val updatedContent = content ++ + Map("compatibilityLevel" -> (if (null != content.get("compatibilityLevel")) content.getOrElse("compatibilityLevel", 1).asInstanceOf[Number].intValue else 1), + "lastPublishedOn" -> objMetadata("lastPublishedOn"), "pkgVersion" -> objMetadata.getOrElse("pkgVersion", 1).asInstanceOf[Number].intValue, "leafNodesCount" -> getLeafNodeCount(content), + "leafNodes" -> leafNodeIds.toArray[String], "status" -> objMetadata("status"), "lastUpdatedOn" -> objMetadata("lastUpdatedOn"), + "downloadUrl" -> objMetadata("downloadUrl"), "variants" -> objMetadata("variants")).asInstanceOf[Map[String, AnyRef]] + + // PRIMARY CATEGORY MAPPING IS DONE + setContentAndCategoryTypes(updatedContent) + } + + def publishHierarchy(children: List[Map[String, AnyRef]], obj: ObjectData, readerConfig: ExtDataConfig, config: PublishConfig)(implicit cassandraUtil: CassandraUtil): Boolean = { + val identifier = obj.identifier.replace(".img", "") + val contentConfig = config.asInstanceOf[LiveNodePublisherConfig] + val nestedFields = contentConfig.nestedFields.asScala.toList + val nodeMetadata = obj.metadata.map(property => { + property._2 match { + case propVal: String => if (nestedFields.contains(property._1)) (property._1 -> ScalaJsonUtil.deserialize[AnyRef](propVal)) else property + case _ => property + } + }) + val hierarchy: Map[String, AnyRef] = nodeMetadata ++ Map("children" -> children) + val data = Map("hierarchy" -> hierarchy) ++ obj.extData.getOrElse(Map()) + val query: Insert = QueryBuilder.insertInto(readerConfig.keyspace, readerConfig.table) + query.value(readerConfig.primaryKey.head, identifier) + data.map(d => { + readerConfig.propsMapping.getOrElse(d._1, "") match { + case "blob" => query.value(d._1.toLowerCase, QueryBuilder.fcall("textAsBlob", d._2)) + case "string" => d._2 match { + case value: String => query.value(d._1.toLowerCase, value) + case _ => query.value(d._1.toLowerCase, ScalaJsonUtil.serialize(d._2)) + } + case _ => query.value(d._1, d._2) + } + }) + logger.info(s"LiveCollectionPublisher:: publishHierarchy:: Publishing Hierarchy data for $identifier | Query : ${query.toString}") + val result = cassandraUtil.upsert(query.toString) + if (result) { + logger.info(s"LiveCollectionPublisher:: publishHierarchy:: Hierarchy data saved successfully for $identifier") + } else { + val msg = s"LiveCollectionPublisher:: publishHierarchy:: Hierarchy Data Insertion Failed For $identifier" + logger.error(msg) + throw new InvalidInputException(msg) + } + result + } + + private def updateRootChildrenList(obj: ObjectData, nextLevelNodes: List[Map[String, AnyRef]]): ObjectData = { + val childrenMap: List[Map[String, AnyRef]] = + nextLevelNodes.map(record => { + Map("identifier" -> record.getOrElse("identifier", "").asInstanceOf[String], + "name" -> record.getOrElse("name", "").asInstanceOf[String], + "objectType" -> record.getOrElse("objectType", "").asInstanceOf[String], + "description" -> record.getOrElse("description", "").asInstanceOf[String], + "index" -> record.getOrElse("index", 0).asInstanceOf[AnyRef]) + }) + + new ObjectData(obj.identifier, obj.metadata ++ Map("children" -> childrenMap, "objectType" -> "content"), obj.extData, Option(obj.hierarchy.get + ("children" -> nextLevelNodes))) + } + + def syncNodes(successObj: ObjectData, children: List[Map[String, AnyRef]], unitNodes: List[String])(implicit esUtil: ElasticSearchUtil, neo4JUtil: Neo4JUtil, cassandraUtil: CassandraUtil, readerConfig: ExtDataConfig, definition: ObjectDefinition, config: PublishConfig): Map[String, Map[String, AnyRef]] = { + val contentConfig = config.asInstanceOf[LiveNodePublisherConfig] + val nestedFields = contentConfig.nestedFields.asScala.toList + val nodes = ListBuffer.empty[ObjectData] + val nodeIds = ListBuffer.empty[String] + + getNodeForSyncing(children, nodes, nodeIds) + logger.info("LiveCollectionPublisher:: syncNodes:: after getNodeForSyncing:: nodes:: " + nodes.toList + " || nodeIds:: " + nodeIds) + logger.info("LiveCollectionPublisher:: syncNodes:: unitNodes:: " + unitNodes) + + // Filtering for removed nodes from Live version. nodeIds is list of nodes from Draft version. unitNodes is list of nodes from Live version. + val orphanUnitNodes = if (unitNodes.nonEmpty) unitNodes.filter(unitNode => !nodeIds.contains(unitNode)) else unitNodes + logger.info("LiveCollectionPublisher:: syncNodes:: after getNodeForSyncing:: orphanUnitNodes:: " + orphanUnitNodes) + + if (nodes.isEmpty && orphanUnitNodes.isEmpty) return Map.empty + + val errors = mutable.Map.empty[String, String] + val messages: Map[String, Map[String, AnyRef]] = getMessages(nodes.toList, definition, nestedFields, errors)(esUtil) + logger.info("LiveCollectionPublisher:: syncNodes:: after getMessages:: messages:: " + messages) + if (errors.nonEmpty) logger.error("LiveCollectionPublisher:: syncNodes:: Error! while forming ES document data from nodes, below nodes are ignored: " + errors) + if (messages.nonEmpty) + try { + logger.info("LiveCollectionPublisher:: syncNodes:: Number of units to be synced : " + messages.size + " || " + messages.keySet) + esUtil.bulkIndexWithIndexId(contentConfig.compositeSearchIndexName, contentConfig.compositeSearchIndexType, messages) + logger.info("LiveCollectionPublisher:: syncNodes:: UnitIds synced : " + messages.keySet) + } catch { + case e: Exception => e.printStackTrace() + logger.error("LiveCollectionPublisher:: syncNodes:: Elastic Search indexing failed: " + e) + } + + try //Unindexing not utilized units + if (orphanUnitNodes.nonEmpty) orphanUnitNodes.map(unitNodeId => esUtil.deleteDocument(unitNodeId)) + catch { + case e: Exception => + logger.error("LiveCollectionPublisher:: syncNodes:: Elastic Search indexing failed: " + e) + } + + // Syncing collection metadata + val doc: Map[String, AnyRef] = getDocument(new ObjectData(successObj.identifier, successObj.metadata.-("children"), successObj.extData, successObj.hierarchy), true, nestedFields)(esUtil) + val jsonDoc: String = ScalaJsonUtil.serialize(doc) + logger.info("LiveCollectionPublisher:: syncNodes:: collection doc: " + jsonDoc) + esUtil.addDocument(successObj.identifier, jsonDoc) + + messages + } + + private def getNodeForSyncing(children: List[Map[String, AnyRef]], nodes: ListBuffer[ObjectData], nodeIds: ListBuffer[String])(implicit neo4JUtil: Neo4JUtil, cassandraUtil: CassandraUtil, readerConfig: ExtDataConfig): Unit = { + if (children.nonEmpty) { + children.foreach((child: Map[String, AnyRef]) => { + try { + if (StringUtils.equalsIgnoreCase("Parent", child.getOrElse("visibility", "").asInstanceOf[String])) { + logger.info("LiveCollectionPublisher:: getNodeForSyncing:: child identifier: " + child.getOrElse("identifier", "").asInstanceOf[String]) + + val nodeMetadata = mutable.Map() ++ child + + // TODO - Relation related CODE is MISSING - Line 735 in Publish Finalizer + + if (nodeMetadata.getOrElse("objectType", "").asInstanceOf[String].isEmpty) { + nodeMetadata += ("objectType" -> "Collection") + } + if (nodeMetadata.getOrElse("graphId", "").asInstanceOf[String].isEmpty) { + nodeMetadata += ("graph_id" -> "domain") + } + + if (nodeMetadata.contains("children")) nodeMetadata.remove("children") + + logger.info("LiveCollectionPublisher:: getNodeForSyncing:: nodeMetadata: " + nodeMetadata) + + if (!nodeIds.contains(child.getOrElse("identifier", "").asInstanceOf[String])) { + nodes += new ObjectData(child.getOrElse("identifier", "").asInstanceOf[String], nodeMetadata.toMap[String, AnyRef], Option(Map.empty[String, AnyRef]), Option(Map.empty[String, AnyRef])) + nodeIds += child.getOrElse("identifier", "").asInstanceOf[String] + } + + getNodeForSyncing(child.getOrElse("children", List.empty).asInstanceOf[List[Map[String, AnyRef]]], nodes, nodeIds) + } + } catch { + case e: Exception => logger.error("LiveCollectionPublisher:: getNodeForSyncing:: Error while generating node map. ", e) + } + }) + } + } + +} diff --git a/publish-pipeline/live-node-publisher/src/main/scala/org/sunbird/job/livenodepublisher/publish/helpers/LiveContentPublisher.scala b/publish-pipeline/live-node-publisher/src/main/scala/org/sunbird/job/livenodepublisher/publish/helpers/LiveContentPublisher.scala new file mode 100644 index 000000000..d74ba18ee --- /dev/null +++ b/publish-pipeline/live-node-publisher/src/main/scala/org/sunbird/job/livenodepublisher/publish/helpers/LiveContentPublisher.scala @@ -0,0 +1,230 @@ +package org.sunbird.job.livenodepublisher.publish.helpers + +import org.apache.commons.io.FilenameUtils +import org.apache.commons.lang3.StringUtils +import org.apache.tika.Tika +import org.neo4j.driver.v1.StatementResult +import org.slf4j.LoggerFactory +import org.sunbird.job.livenodepublisher.task.LiveNodePublisherConfig +import org.sunbird.job.domain.`object`.DefinitionCache +import org.sunbird.job.exception.InvalidInputException +import org.sunbird.job.publish.config.PublishConfig +import org.sunbird.job.publish.core.{DefinitionConfig, ExtDataConfig, ObjectData, ObjectExtData} +import org.sunbird.job.publish.helpers._ +import org.sunbird.job.util.CSPMetaUtil.updateRelativePath +import org.sunbird.job.util._ + +import java.io.{File, IOException} +import java.nio.file.Files +import java.util +import java.util.regex.Pattern +import scala.collection.JavaConverters._ +import scala.collection.mutable.ListBuffer +import scala.concurrent.ExecutionContext + +trait LiveContentPublisher extends LiveObjectReader with ObjectValidator with ObjectEnrichment with EcarGenerator with LiveObjectUpdater { + + private[this] val logger = LoggerFactory.getLogger(classOf[LiveContentPublisher]) + private val level4MimeTypes = List(MimeType.X_Youtube, MimeType.PDF, MimeType.MSWORD, MimeType.EPUB, MimeType.H5P_Archive, MimeType.X_URL) + private val level4ContentTypes = List("Course", "CourseUnit", "LessonPlan", "LessonPlanUnit") + private val pragmaMimeTypes = List(MimeType.X_Youtube, MimeType.PDF) // Ne to check for other mimetype + private val youtubeMimetypes = List(MimeType.X_Youtube, MimeType.Youtube) + private val validateArtifactUrlMimetypes = List(MimeType.PDF, MimeType.EPUB, MimeType.MSWORD) + private val ignoreValidationMimeType = List(MimeType.Collection, MimeType.Plugin_Archive, MimeType.ASSETS) + private val YOUTUBE_REGEX = "^(http(s)?:\\/\\/)?((w){3}.)?youtu(be|.be)?(\\.com)?\\/.+" + + def getExtData(identifier: String, mimeType: String, readerConfig: ExtDataConfig)(implicit cassandraUtil: CassandraUtil, config: PublishConfig): Option[ObjectExtData] = { + mimeType match { + case MimeType.ECML_Archive => + val ecmlBody = getContentBody(identifier, readerConfig) + Some(ObjectExtData(Some(Map[String, AnyRef]("body" -> ecmlBody)))) + case _ => + None + } + } + + override def getHierarchy(identifier: String, readerConfig: ExtDataConfig)(implicit cassandraUtil: CassandraUtil, config: PublishConfig): Option[Map[String, AnyRef]] = None + + override def getExtDatas(identifiers: List[String], readerConfig: ExtDataConfig)(implicit cassandraUtil: CassandraUtil): Option[Map[String, AnyRef]] = None + + override def getHierarchies(identifiers: List[String], readerConfig: ExtDataConfig)(implicit cassandraUtil: CassandraUtil): Option[Map[String, AnyRef]] = None + + override def enrichObjectMetadata(obj: ObjectData)(implicit neo4JUtil: Neo4JUtil, cassandraUtil: CassandraUtil, readerConfig: ExtDataConfig, + cloudStorageUtil: CloudStorageUtil, config: PublishConfig, definitionCache: DefinitionCache, + definitionConfig: DefinitionConfig): Option[ObjectData] = { + val contentConfig = config.asInstanceOf[LiveNodePublisherConfig] + val extraMeta = Map("pkgVersion" -> (obj.pkgVersion + 1).asInstanceOf[AnyRef], "lastPublishedOn" -> getTimeStamp, + "flagReasons" -> null, "body" -> null, "publishError" -> null, "variants" -> null, "downloadUrl" -> null) + val contentSize = obj.metadata.getOrElse("size", 0).toString.toDouble + val configSize = contentConfig.artifactSizeForOnline + val publishType = obj.getString("publish_type", "Public") + val status = if (StringUtils.equalsIgnoreCase("Unlisted", publishType)) "Unlisted" else "Live" + val updatedMeta: Map[String, AnyRef] = if (contentSize > configSize) obj.metadata ++ extraMeta ++ Map("contentDisposition" -> "online-only", "status" -> status) else obj.metadata ++ extraMeta ++ Map("status" -> status) + + val updatedCompatibilityLevel = setCompatibilityLevel(obj, updatedMeta).getOrElse(updatedMeta) + val updatedPragma = setPragma(obj, updatedCompatibilityLevel).getOrElse(updatedCompatibilityLevel) + + //delete basePath if exists + Files.deleteIfExists(new File(ExtractableMimeTypeHelper.getBasePath(obj.identifier, contentConfig.bundleLocation)).toPath) + + try { + if (contentConfig.isECARExtractionEnabled && contentConfig.extractableMimeTypes.contains(obj.mimeType)) { + ExtractableMimeTypeHelper.copyExtractedContentPackage(obj, contentConfig, "version", cloudStorageUtil) + ExtractableMimeTypeHelper.copyExtractedContentPackage(obj, contentConfig, "latest", cloudStorageUtil) + } + } catch { + case _:Exception => neo4JUtil.executeQuery(s"""MATCH (n:domain{IL_UNIQUE_ID:"${obj.identifier}"}) SET n.migrationVersion=0.5;""") + throw new InvalidInputException(s"Invalid input found For $obj.identifier") + } + val updatedPreviewUrl = updatePreviewUrl(obj, updatedPragma, neo4JUtil, cloudStorageUtil, contentConfig).getOrElse(updatedPragma) + Some(new ObjectData(obj.identifier, updatedPreviewUrl, obj.extData, obj.hierarchy)) + } + + override def getDataForEcar(obj: ObjectData): Option[List[Map[String, AnyRef]]] = { + Some(List(obj.metadata ++ obj.extData.getOrElse(Map()).filter(p => !excludeBundleMeta.contains(p._1)))) + } + + override def saveExternalData(obj: ObjectData, readerConfig: ExtDataConfig)(implicit cassandraUtil: CassandraUtil): Unit = None + + override def deleteExternalData(obj: ObjectData, readerConfig: ExtDataConfig)(implicit cassandraUtil: CassandraUtil): Unit = None + + def validateMetadata(obj: ObjectData, identifier: String, config: PublishConfig): List[String] = { + logger.info("Validating Content metadata for : " + obj.identifier) + val messages = ListBuffer[String]() + val artifactUrl = obj.getString("artifactUrl", "") + if (ignoreValidationMimeType.contains(obj.mimeType)) { + // Validation not required. Nothing to do. + } else if (obj.mimeType.equalsIgnoreCase(MimeType.ECML_Archive)) { // Either 'body' or 'artifactUrl' is needed + if ((obj.extData.isEmpty || !obj.extData.get.contains("body") || obj.extData.get.getOrElse("body",null) == null || obj.extData.get.getOrElse("body", "").asInstanceOf[String].isEmpty || obj.extData.get.getOrElse("body", "").asInstanceOf[String].isBlank) && (artifactUrl.isEmpty || artifactUrl.isBlank)) { + messages += s"""Either 'body' or 'artifactUrl' are required for processing of ECML content for : $identifier""" + } + } else { + val allowedExtensionsWord: util.List[String] = config.asInstanceOf[LiveNodePublisherConfig].allowedExtensionsWord + + if (StringUtils.isBlank(artifactUrl)) + messages += s"""There is no artifactUrl available for : $identifier""" + else if (youtubeMimetypes.contains(obj.mimeType) && !isValidYouTubeUrl(artifactUrl)) + messages += s"""Invalid youtube Url = $artifactUrl for : $identifier""" + else if (validateArtifactUrlMimetypes.contains(obj.mimeType) && !isValidUrl(artifactUrl, obj.mimeType, allowedExtensionsWord)) { // valid url check by downloading the file and then delete it + // artifactUrl + valid url check by downloading the file + obj.mimeType match { + case MimeType.PDF => + messages += s"""Error! Invalid File Extension. Uploaded file $artifactUrl is not a pdf file for : $identifier""" + case MimeType.EPUB => + messages += s"""Error! Invalid File Extension. Uploaded file $artifactUrl is not a epub file for : $identifier""" + case MimeType.MSWORD => + messages += s"""Error! Invalid File Extension. | Uploaded file $artifactUrl should be among the Allowed_file_extensions for mimeType doc $allowedExtensionsWord for : $identifier""" + } + } + } + messages.toList + } + + private def isValidYouTubeUrl(artifactUrl: String): Boolean = { + logger.info(s"Validating if the given youtube url = $artifactUrl is valid or not.") + Pattern.matches(YOUTUBE_REGEX, artifactUrl) + } + + private def isValidUrl(url: String, mimeType: String, allowedExtensionsWord: util.List[String]): Boolean = { + val destPath = s"""${File.separator}tmp${File.separator}validUrl""" + var isValid = false + try { + val file: File = FileUtils.downloadFile(url, destPath) + if (exceptionChecks(mimeType, file, allowedExtensionsWord)) isValid = true + } catch { + case e: Exception => + logger.error("isValidUrl: Error while checking mimeType.") + e.printStackTrace() + throw new InvalidInputException("isValidUrl: Error while checking mimeType ", e) + } finally { + FileUtils.deleteQuietly(destPath) + } + isValid + } + + private def exceptionChecks(mimeType: String, file: File, allowedExtensionsWord: util.List[String]): Boolean = { + try { + val extension = FilenameUtils.getExtension(file.getPath) + logger.info("Validating File For MimeType: " + file.getName) + if (extension.nonEmpty) { + val tika = new Tika + val fileType = tika.detect(file) + mimeType match { + case MimeType.PDF => + if (StringUtils.equalsIgnoreCase(extension, "pdf") && fileType == MimeType.PDF) return true + case MimeType.EPUB => + if (StringUtils.equalsIgnoreCase(extension, "epub") && fileType == "application/epub+zip") return true + case MimeType.MSWORD => + if (allowedExtensionsWord.contains(extension)) return true + } + } + } catch { + case e: IOException => + logger.error("exceptionChecks: Error while checking mimeType.") + e.printStackTrace() + throw new InvalidInputException("exceptionChecks: Error while checking mimeType ", e) + } + false + } + + def getObjectWithEcar(data: ObjectData, pkgTypes: List[String])(implicit ec: ExecutionContext, neo4JUtil: Neo4JUtil, cloudStorageUtil: CloudStorageUtil, config: PublishConfig, defCache: DefinitionCache, defConfig: DefinitionConfig, httpUtil: HttpUtil): ObjectData = { + try { + logger.info("LiveContentPublisher :: getObjectWithEcar:: Ecar generation done for Content: " + data.identifier) + val ecarMap: Map[String, String] = generateEcar(data, pkgTypes) + val variants: java.util.Map[String, java.util.Map[String, String]] = ecarMap.map { case (key, value) => key.toLowerCase -> Map[String, String]("ecarUrl" -> value, "size" -> httpUtil.getSize(value).toString).asJava }.asJava + logger.info("LiveContentPublisher :: getObjectWithEcar :: ecar map :: " + ecarMap) + val meta: Map[String, AnyRef] = Map("downloadUrl" -> ecarMap.getOrElse(EcarPackageType.FULL, ""), "variants" -> variants) + new ObjectData(data.identifier, data.metadata ++ meta, data.extData, data.hierarchy) + } catch { + case _: java.lang.IllegalArgumentException => throw new InvalidInputException(s"Invalid input found For $data.identifier") + case iex@(_: java.lang.InterruptedException | _:java.io.FileNotFoundException) => + neo4JUtil.executeQuery(s"""MATCH (n:domain{IL_UNIQUE_ID:"${data.identifier}"}) SET n.migrationVersion=0.5;""") + throw new InvalidInputException(s"Invalid input found For $data.identifier") + } + } + + private def setCompatibilityLevel(obj: ObjectData, updatedMeta: Map[String, AnyRef]): Option[Map[String, AnyRef]] = { + if (level4MimeTypes.contains(obj.mimeType) + || level4ContentTypes.contains(obj.getString("contentType", ""))) { + logger.info("setting compatibility level for content id : " + obj.identifier + " as 4.") + Some(updatedMeta ++ Map("compatibilityLevel" -> 4.asInstanceOf[AnyRef])) + } else None + } + + private def setPragma(obj: ObjectData, updatedMeta: Map[String, AnyRef]): Option[Map[String, AnyRef]] = { + if (pragmaMimeTypes.contains(obj.mimeType)) { + val pgm: java.util.List[String] = obj.metadata.getOrElse("pragma", new java.util.ArrayList[String]()).asInstanceOf[java.util.List[String]] + val pragma: List[String] = pgm.asScala.toList + val value = "external" + if (!pragma.contains(value)) { + Some(updatedMeta ++ Map("pragma" -> (pragma ++ List(value)))) + } else None + } else None + } + + private def updatePreviewUrl(obj: ObjectData, updatedMeta: Map[String, AnyRef], neo4JUtil: Neo4JUtil, cloudStorageUtil: CloudStorageUtil, config: LiveNodePublisherConfig): Option[Map[String, AnyRef]] = { + try { + if (StringUtils.isNotBlank(obj.mimeType)) { + logger.debug("Checking Required Fields For: " + obj.mimeType) + obj.mimeType match { + case MimeType.Collection | MimeType.Plugin_Archive | MimeType.Android_Package | MimeType.ASSETS => + None + case MimeType.ECML_Archive | MimeType.HTML_Archive | MimeType.H5P_Archive => + val latestFolderS3Url = ExtractableMimeTypeHelper.getCloudStoreURL(obj, cloudStorageUtil, config) + val relativeLatestFolder = if(config.isrRelativePathEnabled) StringUtils.replaceEach(latestFolderS3Url, config.config.getStringList("cloudstorage.write_base_path").asScala.toArray, Array(config.getString("cloudstorage.read_base_path", ""))) else latestFolderS3Url + val updatedPreviewUrl = updatedMeta ++ Map("previewUrl" -> relativeLatestFolder, "streamingUrl" -> latestFolderS3Url) + Some(updatedPreviewUrl) + case _ => + val artifactUrl = obj.getString("artifactUrl", null) + val updatedPreviewUrl = updatedMeta ++ Map("previewUrl" -> artifactUrl) + if (config.isStreamingEnabled && !config.streamableMimeType.contains(obj.mimeType)) Some(updatedPreviewUrl ++ Map("streamingUrl" -> artifactUrl)) else Some(updatedPreviewUrl) + } + } else None + } catch { + case ex: Exception => logger.debug("Exception for updatePreviewUrl: " + ex.getMessage) + val query = s"""MATCH (n:domain{IL_UNIQUE_ID:"${obj.identifier}"}) SET n.migrationVersion=0.5;""" + val result: StatementResult = neo4JUtil.executeQuery(query) + None + } + } +} diff --git a/publish-pipeline/live-node-publisher/src/main/scala/org/sunbird/job/livenodepublisher/publish/helpers/LiveObjectReader.scala b/publish-pipeline/live-node-publisher/src/main/scala/org/sunbird/job/livenodepublisher/publish/helpers/LiveObjectReader.scala new file mode 100644 index 000000000..c7c331981 --- /dev/null +++ b/publish-pipeline/live-node-publisher/src/main/scala/org/sunbird/job/livenodepublisher/publish/helpers/LiveObjectReader.scala @@ -0,0 +1,39 @@ +package org.sunbird.job.livenodepublisher.publish.helpers + +import org.slf4j.LoggerFactory +import org.sunbird.job.publish.config.PublishConfig +import org.sunbird.job.publish.core.{ExtDataConfig, ObjectData, ObjectExtData} +import org.sunbird.job.util.{CassandraUtil, Neo4JUtil} + +import scala.collection.JavaConverters._ + +trait LiveObjectReader { + + private[this] val logger = LoggerFactory.getLogger(classOf[LiveObjectReader]) + + def getObject(identifier: String, pkgVersion: Double, mimeType: String, publishType: String, readerConfig: ExtDataConfig)(implicit neo4JUtil: Neo4JUtil, cassandraUtil: CassandraUtil, config: PublishConfig): ObjectData = { + logger.info("Reading editable object data for: " + identifier + " with pkgVersion: " + pkgVersion) + val metadata = getMetadata(identifier, mimeType, publishType, pkgVersion) + logger.info("Reading metadata for: " + identifier + " with metadata: " + metadata) + val extData = getExtData(identifier, mimeType, readerConfig) + logger.info("Reading extData for: " + identifier + " with extData: " + extData) + new ObjectData(identifier, metadata, extData.getOrElse(ObjectExtData()).data, extData.getOrElse(ObjectExtData()).hierarchy) + } + + private def getMetadata(identifier: String, mimeType: String, publishType: String, pkgVersion: Double)(implicit neo4JUtil: Neo4JUtil): Map[String, AnyRef] = { + val metaData = neo4JUtil.getNodeProperties(identifier).asScala.toMap + val id = metaData.getOrElse("IL_UNIQUE_ID", identifier).asInstanceOf[String] + val objType = metaData.getOrElse("IL_FUNC_OBJECT_TYPE", "").asInstanceOf[String] + logger.info("ObjectReader:: getMetadata:: identifier: " + identifier + " with objType: " + objType) + metaData ++ Map[String, AnyRef]("identifier" -> id, "objectType" -> objType, "publish_type" -> publishType) - ("IL_UNIQUE_ID", "IL_FUNC_OBJECT_TYPE", "IL_SYS_NODE_TYPE") + } + + def getExtData(identifier: String, mimeType: String, readerConfig: ExtDataConfig)(implicit cassandraUtil: CassandraUtil, config: PublishConfig): Option[ObjectExtData] + + def getHierarchy(identifier: String, readerConfig: ExtDataConfig)(implicit cassandraUtil: CassandraUtil, config: PublishConfig): Option[Map[String, AnyRef]] + + def getExtDatas(identifiers: List[String], readerConfig: ExtDataConfig)(implicit cassandraUtil: CassandraUtil): Option[Map[String, AnyRef]] + + def getHierarchies(identifiers: List[String], readerConfig: ExtDataConfig)(implicit cassandraUtil: CassandraUtil): Option[Map[String, AnyRef]] + +} diff --git a/publish-pipeline/live-node-publisher/src/main/scala/org/sunbird/job/livenodepublisher/publish/helpers/LiveObjectUpdater.scala b/publish-pipeline/live-node-publisher/src/main/scala/org/sunbird/job/livenodepublisher/publish/helpers/LiveObjectUpdater.scala new file mode 100644 index 000000000..b9d852ffe --- /dev/null +++ b/publish-pipeline/live-node-publisher/src/main/scala/org/sunbird/job/livenodepublisher/publish/helpers/LiveObjectUpdater.scala @@ -0,0 +1,147 @@ +package org.sunbird.job.livenodepublisher.publish.helpers + +import com.datastax.driver.core.querybuilder.{QueryBuilder, Select} +import org.apache.commons.lang3.StringUtils +import org.neo4j.driver.v1.StatementResult +import org.slf4j.LoggerFactory +import org.sunbird.job.domain.`object`.DefinitionCache +import org.sunbird.job.exception.InvalidInputException +import org.sunbird.job.livenodepublisher.task.LiveNodePublisherConfig +import org.sunbird.job.publish.config.PublishConfig +import org.sunbird.job.publish.core.{DefinitionConfig, ExtDataConfig, ObjectData} +import org.sunbird.job.util.{CSPMetaUtil, CassandraUtil, JSONUtil, Neo4JUtil, ScalaJsonUtil} + +import java.text.SimpleDateFormat +import java.util +import java.util.Date + +trait LiveObjectUpdater { + + private[this] val logger = LoggerFactory.getLogger(classOf[LiveObjectUpdater]) + + @throws[Exception] + def saveOnSuccess(obj: ObjectData)(implicit neo4JUtil: Neo4JUtil, cassandraUtil: CassandraUtil, readerConfig: ExtDataConfig, definitionCache: DefinitionCache, definitionConfig: DefinitionConfig, config: PublishConfig): Unit = { + val publishType = obj.getString("publish_type", "Public") + val status = if (StringUtils.equalsIgnoreCase("Unlisted", publishType)) "Unlisted" else "Live" + val identifier = obj.identifier + val migrationVersion: Double = if(config.getConfig().hasPath("migrationVersion")) config.getConfig().getDouble("migrationVersion") else 1.0 + 0.1 + val metadataUpdateQuery = metaDataQuery(obj)(definitionCache, definitionConfig) + val query = s"""MATCH (n:domain{IL_UNIQUE_ID:"$identifier"}) SET n.status="$status",n.pkgVersion=${obj.pkgVersion},n.prevStatus="Processing",n.migrationVersion=$migrationVersion,$metadataUpdateQuery,$auditPropsUpdateQuery;""" + logger.info("ObjectUpdater:: saveOnSuccess:: Query: " + query) + logger.info(s"ObjectUpdater:: saveOnSuccess:: DB ID for ${obj.identifier} is : ${obj.dbId} | pkgVersion : ${obj.pkgVersion}" ) + + if (obj.mimeType.equalsIgnoreCase("application/vnd.ekstep.ecml-archive")) { + val ecmlBody = getContentBody(identifier, readerConfig) + updateContentBody(identifier,ecmlBody,readerConfig) + } + + val result: StatementResult = neo4JUtil.executeQuery(query) + if (null != result && result.hasNext) + logger.info(s"ObjectUpdater:: saveOnSuccess:: statement result : ${result.next().asMap()}") + saveExternalData(obj, readerConfig) + } + +// @throws[Exception] +// def updateProcessingNode(obj: ObjectData)(implicit neo4JUtil: Neo4JUtil, cassandraUtil: CassandraUtil, readerConfig: ExtDataConfig, definitionCache: DefinitionCache, config: DefinitionConfig): Unit = { +// val status = "Processing" +// val prevState = obj.getString("status", "Draft") +// val identifier = obj.dbId +// val metadataUpdateQuery = metaDataQuery(obj)(definitionCache, config) +// val query = s"""MATCH (n:domain{IL_UNIQUE_ID:"$identifier"}) SET n.status="$status",n.prevState="$prevState",$metadataUpdateQuery,$auditPropsUpdateQuery;""" +// logger.info("ObjectUpdater:: updateProcessingNode:: Query: " + query) +// val result: StatementResult = neo4JUtil.executeQuery(query) +// if (null != result && result.hasNext) +// logger.info(s"ObjectUpdater:: updateProcessingNode:: statement result : ${result.next().asMap()}") +// } + + def saveExternalData(obj: ObjectData, readerConfig: ExtDataConfig)(implicit cassandraUtil: CassandraUtil): Unit + + def deleteExternalData(obj: ObjectData, readerConfig: ExtDataConfig)(implicit cassandraUtil: CassandraUtil): Unit + + @throws[Exception] + def saveOnFailure(obj: ObjectData, messages: List[String], pkgVersion: Double)(implicit neo4JUtil: Neo4JUtil): Unit = { + val errorMessages = messages.mkString("; ") + val nodeId = obj.dbId + val query = s"""MATCH (n:domain{IL_UNIQUE_ID:"$nodeId"}) SET n.pkgVersion=${obj.pkgVersion},n.migrationVersion=0.2, n.publishError="$errorMessages", $auditPropsUpdateQuery;""" + logger.info("ObjectUpdater:: saveOnFailure:: Query: " + query) + neo4JUtil.executeQuery(query) + } + + def metaDataQuery(obj: ObjectData)(definitionCache: DefinitionCache, config: DefinitionConfig): String = { + val version = config.supportedVersion.getOrElse(obj.dbObjType.toLowerCase(), "1.0").asInstanceOf[String] + val definition = definitionCache.getDefinition(obj.dbObjType, version, config.basePath) + val metadata = obj.metadata - ("IL_UNIQUE_ID", "identifier", "IL_FUNC_OBJECT_TYPE", "IL_SYS_NODE_TYPE", "pkgVersion", "lastStatusChangedOn", "lastUpdatedOn", "status", "objectType", "publish_type", "migrationVersion") + metadata.map(prop => { + if (null == prop._2) s"n.${prop._1}=${prop._2}" + else if (definition.objectTypeProperties.contains(prop._1)) { + prop._2 match { + case _: Map[String, AnyRef] => + val strValue = JSONUtil.serialize(ScalaJsonUtil.serialize(prop._2)) + s"""n.${prop._1}=$strValue""" + case _: util.Map[String, AnyRef] => + val strValue = JSONUtil.serialize(JSONUtil.serialize(prop._2)) + s"""n.${prop._1}=$strValue""" + case _ => + val strValue = JSONUtil.serialize(prop._2) + s"""n.${prop._1}=$strValue""" + } + } else { + prop._2 match { + case _: Map[String, AnyRef] => + val strValue = JSONUtil.serialize(ScalaJsonUtil.serialize(prop._2)) + s"""n.${prop._1}=$strValue""" + case _: util.Map[String, AnyRef] => + val strValue = JSONUtil.serialize(JSONUtil.serialize(prop._2)) + s"""n.${prop._1}=$strValue""" + case _: List[String] => + val strValue = ScalaJsonUtil.serialize(prop._2) + s"""n.${prop._1}=$strValue""" + case _: util.List[String] => + val strValue = JSONUtil.serialize(prop._2) + s"""n.${prop._1}=$strValue""" + case _ => + val strValue = JSONUtil.serialize(prop._2) + s"""n.${prop._1}=$strValue""" + } + } + }).mkString(",") + } + + private def auditPropsUpdateQuery(): String = { + val sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ") + val updatedOn = sdf.format(new Date()) + s"""n.lastUpdatedOn="$updatedOn",n.lastStatusChangedOn="$updatedOn"""" + } + + def getContentBody(identifier: String, readerConfig: ExtDataConfig)(implicit cassandraUtil: CassandraUtil, config: PublishConfig): String = { + // fetch content body from cassandra + val selectId = QueryBuilder.select() + selectId.fcall("blobAsText", QueryBuilder.column("body")).as("body") + val selectWhereId: Select.Where = selectId.from(readerConfig.keyspace, readerConfig.table).where().and(QueryBuilder.eq("content_id", identifier)) + logger.info("ObjectUpdater:: getContentBody:: Cassandra Fetch Query :: " + selectWhereId.toString) + val rowId = cassandraUtil.findOne(selectWhereId.toString) + if (null != rowId) { + val body = rowId.getString("body") + val updatedBody = if (isrRelativePathEnabled(config)) CSPMetaUtil.updateAbsolutePath(body) else body + updatedBody + } else "" + } + + private def isrRelativePathEnabled(config: PublishConfig): Boolean = { + config.getBoolean("cloudstorage.metadata.replace_absolute_path", false) + } + + def updateContentBody(identifier: String, ecmlBody: String, readerConfig: ExtDataConfig)(implicit cassandraUtil: CassandraUtil): Unit = { + val updateQuery = QueryBuilder.update(readerConfig.keyspace, readerConfig.table) + .where(QueryBuilder.eq("content_id", identifier)) + .`with`(QueryBuilder.set("body", QueryBuilder.fcall("textAsBlob", ecmlBody))) + logger.info(s"ObjectUpdater:: updateContentBody:: Updating Content Body in Cassandra For $identifier : ${updateQuery.toString}") + val result = cassandraUtil.upsert(updateQuery.toString) + if (result) logger.info(s"ObjectUpdater:: updateContentBody:: Content Body Updated Successfully For $identifier") + else { + logger.error(s"ObjectUpdater:: updateContentBody:: Content Body Update Failed For $identifier") + throw new InvalidInputException(s"Content Body Update Failed For $identifier") + } + } + +} diff --git a/publish-pipeline/live-node-publisher/src/main/scala/org/sunbird/job/livenodepublisher/publish/helpers/SyncMessagesGenerator.scala b/publish-pipeline/live-node-publisher/src/main/scala/org/sunbird/job/livenodepublisher/publish/helpers/SyncMessagesGenerator.scala new file mode 100644 index 000000000..51a7a92f1 --- /dev/null +++ b/publish-pipeline/live-node-publisher/src/main/scala/org/sunbird/job/livenodepublisher/publish/helpers/SyncMessagesGenerator.scala @@ -0,0 +1,167 @@ +package org.sunbird.job.livenodepublisher.publish.helpers + +import org.apache.commons.lang3.StringUtils +import org.slf4j.LoggerFactory +import org.sunbird.job.domain.`object`.ObjectDefinition +import org.sunbird.job.publish.core.ObjectData +import org.sunbird.job.util.{ElasticSearchUtil, ScalaJsonUtil} + +import scala.collection.mutable +import scala.collection.mutable.ListBuffer + +trait SyncMessagesGenerator { + + private[this] val logger = LoggerFactory.getLogger(classOf[SyncMessagesGenerator]) + + private def getIndexDocument(identifier: String)(esUtil: ElasticSearchUtil): scala.collection.mutable.Map[String, AnyRef] = { + val documentJson: String = esUtil.getDocumentAsString(identifier) + if (documentJson != null && documentJson.nonEmpty) ScalaJsonUtil.deserialize[scala.collection.mutable.Map[String, AnyRef]](documentJson) else scala.collection.mutable.Map[String, AnyRef]() + } + + private def getJsonMessage(message: Map[String, Any], definition: ObjectDefinition, nestedFields: List[String], ignoredFields: List[String]): Map[String, AnyRef] = { + val indexDocument = scala.collection.mutable.Map[String, AnyRef]() + val transactionData = message.getOrElse("transactionData", Map[String, Any]()).asInstanceOf[Map[String, Any]] + logger.debug("SyncMessagesGenerator:: getJsonMessage:: transactionData:: " + transactionData) + if (transactionData.nonEmpty) { + val addedProperties = transactionData.getOrElse("properties", Map[String, AnyRef]()).asInstanceOf[Map[String, AnyRef]] + logger.debug("SyncMessagesGenerator:: getJsonMessage:: definition.externalProperties:: " + definition.externalProperties) + addedProperties.foreach(property => { + if (!definition.externalProperties.contains(property._1)) { + val propertyNewValue: AnyRef = property._2.asInstanceOf[Map[String, AnyRef]].getOrElse("nv", null) + if (propertyNewValue == null) indexDocument.remove(property._1) else indexDocument.put(property._1, addMetadataToDocument(property._1, propertyNewValue, nestedFields)) + } + }) + +// val addedRelations = transactionData.getOrElse("addedRelations", List[Map[String, AnyRef]]()).asInstanceOf[List[Map[String, AnyRef]]] +// if (addedRelations.nonEmpty) { +// addedRelations.foreach(rel => { +// val direction = rel.getOrElse("dir", "").asInstanceOf[String] +// val relationType = rel.getOrElse("rel", "").asInstanceOf[String] +// val targetObjType = rel.getOrElse("type", "").asInstanceOf[String] +// val title = definition.relationLabel(targetObjType, direction, relationType) +// if (title.nonEmpty) { +// val list = indexDocument.getOrElse(title.get, List[String]()).asInstanceOf[List[String]] +// val id = rel.getOrElse("id", "").asInstanceOf[String] +// if (!list.contains(id)) indexDocument.put(title.get, (id :: list).asInstanceOf[AnyRef]) +// } +// }) +// } +// +// val removedRelations = transactionData.getOrElse("removedRelations", List[Map[String, AnyRef]]()).asInstanceOf[List[Map[String, AnyRef]]] +// removedRelations.foreach(rel => { +// val direction = rel.getOrElse("dir", "").asInstanceOf[String] +// val relationType = rel.getOrElse("rel", "").asInstanceOf[String] +// val targetObjType = rel.getOrElse("type", "").asInstanceOf[String] +// val title = definition.relationLabel(targetObjType, direction, relationType) +// if (title.nonEmpty) { +// val list = indexDocument.getOrElse(title.get, List[String]()).asInstanceOf[List[String]] +// val id = rel.getOrElse("id", "").asInstanceOf[String] +// if (list.contains(id)) { +// val updatedList = list diff List(id) +// indexDocument.put(title.get, updatedList.asInstanceOf[AnyRef]) +// } +// } +// }) + } + + //Ignored fields are removed-> it can be a propertyName or relation Name + indexDocument --= ignoredFields + + indexDocument.put("graph_id", message.getOrElse("graphId", "domain").asInstanceOf[String]) + indexDocument.put("node_id", message.getOrElse("nodeGraphId",0).asInstanceOf[AnyRef]) + indexDocument.put("identifier", message.getOrElse("nodeUniqueId", "").asInstanceOf[String]) + indexDocument.put("objectType", message.getOrElse("objectType", "").asInstanceOf[String]) + indexDocument.put("nodeType", message.getOrElse("nodeType", "").asInstanceOf[String]) + + logger.info("SyncMessagesGenerator:: getJsonMessage:: final indexDocument:: " + indexDocument) + + indexDocument.toMap + } + + private def addMetadataToDocument(propertyName: String, propertyValue: AnyRef, nestedFields: List[String]): AnyRef = { + if (nestedFields.contains(propertyName)) { + propertyValue match { + case propVal: String => ScalaJsonUtil.deserialize[AnyRef](propVal) + case _ => propertyValue + } + } else propertyValue + } + + def getMessages(nodes: List[ObjectData], definition: ObjectDefinition, nestedFields: List[String], errors: mutable.Map[String, String])(esUtil: ElasticSearchUtil): Map[String, Map[String, AnyRef]] = { + val messages = collection.mutable.Map.empty[String, Map[String, AnyRef]] + for (node <- nodes) { + try { + if (definition.getRelationLabels() != null) { + val nodeMap = getNodeMap(node) + logger.debug("SyncMessagesGenerator:: getMessages:: nodeMap:: " + nodeMap) + val message = getJsonMessage(nodeMap, definition, nestedFields, List.empty) + logger.debug("SyncMessagesGenerator:: getMessages:: message:: " + message) + messages.put(node.identifier, message) + } + } catch { + case e: Exception => e.printStackTrace() + errors.put(node.identifier, e.getMessage) + } + } + messages.toMap + } + + + private def getNodeMap(node: ObjectData): Map[String, AnyRef] = { + val transactionData = collection.mutable.Map.empty[String, AnyRef] + if (null != node.metadata && node.metadata.nonEmpty) { + val propertyMap = collection.mutable.Map.empty[String, AnyRef] + for (key <- node.metadata.keySet) { + if (StringUtils.isNotBlank(key)) { + val valueMap = collection.mutable.Map.empty[String, AnyRef] + valueMap.put("ov", null) // old value + valueMap.put("nv", node.metadata(key)) // new value + + // temporary check to not sync body and editorState + if (!StringUtils.equalsIgnoreCase("body", key) && !StringUtils.equalsIgnoreCase("editorState", key)) propertyMap.put(key, valueMap.toMap) + } + } + transactionData.put("properties", propertyMap.toMap) + } + else transactionData.put("properties", Map.empty[String,AnyRef]) + + val relations = ListBuffer.empty[Map[String, AnyRef]] + // add IN relations +// if (null != node.metadata.inRelations && node.metadata.inRelations.nonEmpty) { +// for (rel <- node.metadata.inRelations) { +// val relMap = Map("rel" -> rel.relationType, "id" -> rel.startNodeId, "dir" -> "IN", "type" -> rel.startNodeObjectType, "label" -> getLabel(rel.getStartNodeMetadata)) +// relations += relMap +// } +// } +// // add OUT relations +// if (null != node.getOutRelations && !node.getOutRelations.isEmpty) { +// for (rel <- node.getOutRelations) { +// val relMap = Map("rel" -> rel.getRelationType, "id" -> rel.getEndNodeId, "dir" -> "OUT", "type" -> rel.getEndNodeObjectType, "label" -> getLabel(rel.getEndNodeMetadata)) +// relations += relMap +// } +// } + transactionData.put("addedRelations", relations.toList) + + Map("operationType"-> "UPDATE", "graphId" -> node.metadata.getOrElse("graphId","domain").asInstanceOf[String], "nodeGraphId"-> 0.asInstanceOf[AnyRef], "nodeUniqueId"-> node.identifier, "objectType"-> node.dbObjType, + "nodeType"-> "DATA_NODE", "transactionData" -> transactionData.toMap, "syncMessage" -> true.asInstanceOf[AnyRef]) + } + + + def getDocument(node: ObjectData, updateRequest: Boolean, nestedFields: List[String])(esUtil: ElasticSearchUtil): Map[String, AnyRef] = { + val message = getNodeMap(node) + val identifier: String = message.getOrElse("nodeUniqueId", "").asInstanceOf[String] + val indexDocument = mutable.Map[String, AnyRef]() + val transactionData: Map[String, AnyRef] = message.getOrElse("transactionData", Map[String, AnyRef]()).asInstanceOf[Map[String, AnyRef]] + if (transactionData.nonEmpty) { + val addedProperties: Map[String, AnyRef] = transactionData.getOrElse("properties", Map[String, AnyRef]()).asInstanceOf[Map[String, AnyRef]] + addedProperties.foreach(property => { + val propertyNewValue: AnyRef = property._2.asInstanceOf[Map[String, AnyRef]].getOrElse("nv", null) + if (propertyNewValue == null) indexDocument.remove(property._1) else indexDocument.put(property._1, addMetadataToDocument(property._1, propertyNewValue, nestedFields)) + }) + } + indexDocument.put("identifier", message.getOrElse("nodeUniqueId", "").asInstanceOf[String]) + indexDocument.put("objectType", message.getOrElse("objectType", "").asInstanceOf[String]) + indexDocument.toMap + } + +} diff --git a/publish-pipeline/live-node-publisher/src/main/scala/org/sunbird/job/livenodepublisher/publish/processor/BaseProcessor.scala b/publish-pipeline/live-node-publisher/src/main/scala/org/sunbird/job/livenodepublisher/publish/processor/BaseProcessor.scala new file mode 100644 index 000000000..4599b62df --- /dev/null +++ b/publish-pipeline/live-node-publisher/src/main/scala/org/sunbird/job/livenodepublisher/publish/processor/BaseProcessor.scala @@ -0,0 +1,9 @@ +package org.sunbird.job.livenodepublisher.publish.processor + +import org.sunbird.job.util.CloudStorageUtil + +class BaseProcessor(basePath: String, identifier: String)(implicit cloudStorageUtil: CloudStorageUtil) extends IProcessor(basePath, identifier) { + override def process(ecrf: Plugin): Plugin = { + ecrf + } +} diff --git a/publish-pipeline/live-node-publisher/src/main/scala/org/sunbird/job/livenodepublisher/publish/processor/EcrfObject.scala b/publish-pipeline/live-node-publisher/src/main/scala/org/sunbird/job/livenodepublisher/publish/processor/EcrfObject.scala new file mode 100644 index 000000000..59e7500c9 --- /dev/null +++ b/publish-pipeline/live-node-publisher/src/main/scala/org/sunbird/job/livenodepublisher/publish/processor/EcrfObject.scala @@ -0,0 +1,19 @@ +package org.sunbird.job.livenodepublisher.publish.processor + +case class Plugin(id: String, data: Map[String, AnyRef], innerText: String, cData: String, childrenPlugin: List[Plugin], manifest: Manifest, controllers: List[Controller], events: List[Event]) { + def this() = this("", null, "", "", null, null, null, null) +} +case class Manifest(id: String, data: Map[String, AnyRef], innerText: String, cData: String, medias: List[Media]) { + def this() = this("", null, "", "", null) +} +case class Controller(id: String, data: Map[String, AnyRef], innerText: String, cData: String) { + def this() = this("", null, "", "") +} +case class Media(id: String, data: Map[String, AnyRef], innerText: String, cData: String, src: String, `type`: String, childrenPlugin: List[Plugin]) { + def this() = this("", null, "", "", "", "", null) +} +case class Event(id: String, data: Map[String, AnyRef], innerText: String, cData: String, childrenPlugin: List[Plugin]) { + def this() = this("", null, "", "", null) +} + + diff --git a/publish-pipeline/live-node-publisher/src/main/scala/org/sunbird/job/livenodepublisher/publish/processor/IProcessor.scala b/publish-pipeline/live-node-publisher/src/main/scala/org/sunbird/job/livenodepublisher/publish/processor/IProcessor.scala new file mode 100644 index 000000000..839d55aa2 --- /dev/null +++ b/publish-pipeline/live-node-publisher/src/main/scala/org/sunbird/job/livenodepublisher/publish/processor/IProcessor.scala @@ -0,0 +1,16 @@ +package org.sunbird.job.livenodepublisher.publish.processor + +import org.sunbird.job.util.CloudStorageUtil + +abstract class IProcessor(basePath: String, identifier: String)(implicit cloudStorageUtil: CloudStorageUtil) { + + implicit val ss = cloudStorageUtil.getService + + val widgetTypeAssets: List[String] = List("js", "css", "json", "plugin") + + def process(ecrf: Plugin): Plugin + + def getBasePath(): String = basePath + + def getIdentifier(): String = identifier +} diff --git a/publish-pipeline/live-node-publisher/src/main/scala/org/sunbird/job/livenodepublisher/publish/processor/JsonParser.scala b/publish-pipeline/live-node-publisher/src/main/scala/org/sunbird/job/livenodepublisher/publish/processor/JsonParser.scala new file mode 100644 index 000000000..cc3110f1b --- /dev/null +++ b/publish-pipeline/live-node-publisher/src/main/scala/org/sunbird/job/livenodepublisher/publish/processor/JsonParser.scala @@ -0,0 +1,218 @@ +package org.sunbird.job.livenodepublisher.publish.processor + +import org.apache.commons.lang3.StringUtils +import org.sunbird.job.exception.InvalidInputException +import org.sunbird.job.util.ScalaJsonUtil + +import scala.collection.mutable.ListBuffer + +object JsonParser { + val nonPluginElements: List[String] = List("manifest", "controller", "media", "events", "event", "__cdata", "__text") + + def parse(jsonString: String): Plugin = { + val jsonMap: Map[String, AnyRef] = ScalaJsonUtil.deserialize[Map[String, AnyRef]](jsonString) + processDocument(jsonMap) + } + + def processDocument(json: Map[String, AnyRef]): Plugin = { + if (json.keySet.contains("theme")) { + val root = json("theme").asInstanceOf[Map[String, AnyRef]] + Plugin(getId(root), getData(root, "theme"), getInnerText(root), getCdata(root), getChildrenPlugin(root), getManifest(root, validateMedia = true), getControllers(root), getEvents(root)) + } else classOf[Plugin].newInstance() + } + + private def getDataFromMap(map: Map[String, AnyRef], key: String): String = { + if (null != map && map.keySet.contains(key)) { + map(key) match { + case str: String => str + case _ => map(key).toString + } + } else "" + } + + def getId(jsonObject: Map[String, AnyRef]): String = getDataFromMap(jsonObject, "id") + + def getData(jsonObject: Map[String, AnyRef], elementName: String): Map[String, AnyRef] = { + if (null != jsonObject && StringUtils.isNotBlank(elementName)) { + var result = jsonObject.filter(p => !p._1.equalsIgnoreCase("__cdata") && !p._1.equalsIgnoreCase("__text")) + result += ("cwp_element_name" -> elementName) + result + } else Map[String, AnyRef]() + } + + def getInnerText(jsonObject: Map[String, AnyRef]): String = getDataFromMap(jsonObject, "__text") + + def getCdata(jsonObject: Map[String, AnyRef]): String = ScalaJsonUtil.serialize(jsonObject.getOrElse("__cdata", "")) + + def getChildrenPlugin(jsonObject: Map[String, AnyRef]): List[Plugin] = { + val childPluginList: ListBuffer[Plugin] = ListBuffer() + val filteredObject = jsonObject.filter(entry => null != entry._2) + childPluginList ++= filteredObject.filter(entry => entry._2.isInstanceOf[List[Map[String, AnyRef]]] && !nonPluginElements.contains(entry._1)).map(entry => { + val objectList: List[Map[String, AnyRef]] = entry._2.asInstanceOf[List[Map[String, AnyRef]]] + objectList.map(obj => Plugin(getId(obj), getData(obj, entry._1), getInnerText(obj), getCdata(obj), getChildrenPlugin(obj), getManifest(obj, validateMedia = false), getControllers(obj), getEvents(obj))) + }).toList.flatten + childPluginList ++= filteredObject.filter(entry => entry._2.isInstanceOf[Map[String, AnyRef]] && !nonPluginElements.contains(entry._2)).map(entry => { + val obj = entry._2.asInstanceOf[Map[String, AnyRef]] + Plugin(getId(obj), getData(obj, entry._1), getInnerText(obj), getCdata(obj), getChildrenPlugin(obj), getManifest(obj, validateMedia = false), getControllers(obj), getEvents(obj)) + }).toList + childPluginList.toList + } + + def getManifest(jsonObject: Map[String, AnyRef], validateMedia: Boolean): Manifest = { + if (null != jsonObject && jsonObject.keySet.contains("manifest") && jsonObject("manifest").isInstanceOf[List[Map[String, AnyRef]]]) throw new Exception("Error! JSON Object is Expected for the Element. manifest") + else if (null != jsonObject && jsonObject.keySet.contains("manifest") && jsonObject("manifest").isInstanceOf[Map[String, AnyRef]] && jsonObject("manifest").asInstanceOf[Map[String, AnyRef]].keySet.contains("media")) { + val manifestObject = jsonObject("manifest").asInstanceOf[Map[String, AnyRef]] + Manifest(getId(manifestObject), getData(manifestObject, "manifest"), getInnerText(manifestObject), getCdata(manifestObject), getMedias(manifestObject("media"), validateMedia)) + } else classOf[Manifest].newInstance() + } + + def getControllers(jsonObject: Map[String, AnyRef]): List[Controller] = { + if (null != jsonObject && jsonObject.keySet.contains("controller") && jsonObject("controller").isInstanceOf[List[Map[String, Object]]]) { + val controllerList: List[Map[String, AnyRef]] = jsonObject("controller").asInstanceOf[List[Map[String, Object]]] + controllerList.map(obj => { + validateController(obj) + Controller(getId(obj), getData(obj, "controller"), getInnerText(obj), getCdata(obj)) + }) + } else if (null != jsonObject && jsonObject.keySet.contains("controller") && jsonObject("controller").isInstanceOf[Map[String, Object]]) { + val obj = jsonObject("controller").asInstanceOf[Map[String, Object]] + validateController(obj) + List(Controller(getId(obj), getData(obj, "controller"), getInnerText(obj), getCdata(obj))) + } else List() + } + + def validateController(obj: Map[String, AnyRef]): Unit = { + if (StringUtils.isBlank(getDataFromMap(obj, "id"))) + throw new InvalidInputException("Error! Invalid Controller ('id' is required.)") + + val `type` = getDataFromMap(obj, "type") + if (StringUtils.isBlank(getDataFromMap(obj, "type"))) + throw new InvalidInputException("Error! Invalid Controller ('type' is required.)") + if (!"items".equalsIgnoreCase(`type`) && !"data".equalsIgnoreCase(`type`)) + throw new InvalidInputException("Error! Invalid Controller ('type' should be either 'items' or 'data')") + } + + private def getEventsFromObject(jsonObject: AnyRef): List[Event] = { + if (null != jsonObject && jsonObject.isInstanceOf[List[Map[String, AnyRef]]]) { + val jsonList: List[Map[String, AnyRef]] = jsonObject.asInstanceOf[List[Map[String, AnyRef]]] + jsonList.map(obj => Event(getId(obj), getData(obj, "event"), getInnerText(obj), getCdata(obj), getChildrenPlugin(obj))) + } else if (null != jsonObject && jsonObject.isInstanceOf[Map[String, AnyRef]]) { + List(Event(getId(jsonObject.asInstanceOf[Map[String, AnyRef]]), getData(jsonObject.asInstanceOf[Map[String, AnyRef]], "event"), getInnerText(jsonObject.asInstanceOf[Map[String, AnyRef]]), getCdata(jsonObject.asInstanceOf[Map[String, AnyRef]]), getChildrenPlugin(jsonObject.asInstanceOf[Map[String, AnyRef]]))) + } else List() + } + + def getEvents(jsonObject: Map[String, AnyRef]): List[Event] = { + val eventList: ListBuffer[Event] = ListBuffer() + if (jsonObject != null) { + if (jsonObject.keySet.contains("events")) { + val value = jsonObject("events") + value match { + case jsonList: List[Map[String, AnyRef]] => + eventList ++= jsonList.map(obj => Event(getId(obj), getData(obj, "event"), getInnerText(obj), getCdata(obj), getChildrenPlugin(obj))) + case _: Map[String, AnyRef] => + eventList ++= getEventsFromObject(value) + case _ => + } + } else if (jsonObject.keySet.contains("event")) { + eventList ++= getEventsFromObject(jsonObject("event")) + } + } + eventList.toList + } + + def getMedias(manifestObject: AnyRef, validateMedia: Boolean): List[Media] = { + if (null != manifestObject && manifestObject.isInstanceOf[List[Map[String, AnyRef]]]) { + val jsonList: List[Map[String, AnyRef]] = manifestObject.asInstanceOf[List[Map[String, AnyRef]]] + jsonList.map(json => getMedia(json, validateMedia)) + } else if (null != manifestObject && manifestObject.isInstanceOf[Map[String, AnyRef]]) { + List(getMedia(manifestObject.asInstanceOf[Map[String, AnyRef]], validateMedia)) + } else List() + } + + def getMedia(mediaJson: Map[String, AnyRef], validateMedia: Boolean): Media = { + if (null != mediaJson) { + val id = getDataFromMap(mediaJson, "id") + val src = getDataFromMap(mediaJson, "src") + val `type` = getDataFromMap(mediaJson, "type") + if (validateMedia) { + if (StringUtils.isBlank(id) && isMediaIdRequiredForMediaType(`type`)) + throw new InvalidInputException("Error! Invalid Media ('id' is required.)") + if (StringUtils.isBlank(`type`)) + throw new InvalidInputException("Error! Invalid Media ('type' is required.)") + if (StringUtils.isBlank(src)) + throw new InvalidInputException("Error! Invalid Media ('src' is required.)") + } + Media(id, getData(mediaJson, "media"), getInnerText(mediaJson), getCdata(mediaJson), src, `type`, getChildrenPlugin(mediaJson)) + } else classOf[Media].newInstance() + } + + private def isMediaIdRequiredForMediaType(`type`: String): Boolean = { + !(StringUtils.isNotBlank(`type`) && (`type`.equalsIgnoreCase("js") || `type`.equalsIgnoreCase("css"))) + } + + /** + * serialize + * + * @param plugin + * @return + */ + def toString(plugin: Plugin): String = { + val map = plugin.data ++ Map[String, AnyRef]("__text" -> plugin.innerText, "__cdata" -> plugin.cData) ++ getManifestMap(plugin.manifest) ++ getControllersMap(plugin.controllers) ++ getEventsMap(plugin.events) + ScalaJsonUtil.serialize(Map[String, AnyRef]("theme" -> map)) + } + + def getManifestMap(manifest: Manifest): Map[String, AnyRef] = { + if (null != manifest && null != manifest.medias && manifest.medias.nonEmpty) { + val map: Map[String, AnyRef] = manifest.data ++ Map[String, AnyRef]("__text" -> manifest.innerText, "__cdata" -> manifest.cData) ++ getMediaMap(manifest.medias) + Map[String, AnyRef]("manifest" -> map) + } else Map[String, AnyRef]() + } + + def getControllersMap(controllers: List[Controller]): Map[String, AnyRef] = { + if (null != controllers && controllers.nonEmpty) { + val controllerMap: List[Map[String, AnyRef]] = controllers.map(controller => { + controller.data ++ Map[String, AnyRef]("__text" -> controller.innerText, "__cdata" -> controller.cData) + }) + Map[String, AnyRef]("controller" -> controllerMap) + } else Map[String, AnyRef]() + } + + def getEventsMap(events: List[Event]): Map[String, AnyRef] = { + if (null != events && events.nonEmpty) { + val eventsList: List[Map[String, AnyRef]] = events.map(event => event.data ++ Map[String, AnyRef]("__text" -> event.innerText, "__cdata" -> event.cData) ++ getChildPluginMap(event.childrenPlugin)) + + if (eventsList.length > 1) { + Map[String, AnyRef]("events" -> Map[String, AnyRef]("event" -> eventsList)) + } else { + Map[String, AnyRef]("event" -> eventsList.head) + } + } else Map[String, AnyRef]() + } + + def getMediaMap(medias: List[Media]): Map[String, AnyRef] = { + val mediasMap = medias.map(media => { + + val updatedMediaData = media.data.map(mediaInfo => { + if(mediaInfo._1.equalsIgnoreCase("src")) + ("src" -> (StringUtils.stripStart(mediaInfo._2.toString, "/"))) + else mediaInfo + }) + + updatedMediaData ++ Map[String, AnyRef]("__text" -> media.innerText, "__cdata" -> media.cData) ++ getChildPluginMap(media.childrenPlugin) + }) + Map[String, AnyRef]("media" -> mediasMap) + } + + def getChildPluginMap(plugins: List[Plugin]): Map[String, AnyRef] = { + if (null != plugins && plugins.nonEmpty) { + plugins.filter(plugin => StringUtils.isNotBlank(getDataFromMap(plugin.data, "cwp_element_name"))) + .groupBy(plugin => getDataFromMap(plugin.data, "cwp_element_name")) + .map(entry => { + if (entry._2.size == 1) + entry._1 -> entry._2.map(plugin => plugin.data ++ Map[String, AnyRef]("__text" -> plugin.innerText, "__cdata" -> plugin.cData) ++ getManifestMap(plugin.manifest) ++ getControllersMap(plugin.controllers) ++ getEventsMap(plugin.events)).head + else + entry._1 -> entry._2.map(plugin => plugin.data ++ Map[String, AnyRef]("__text" -> plugin.innerText, "__cdata" -> plugin.cData) ++ getManifestMap(plugin.manifest) ++ getControllersMap(plugin.controllers) ++ getEventsMap(plugin.events)) + }) + } else Map[String, AnyRef]() + } + +} diff --git a/publish-pipeline/live-node-publisher/src/main/scala/org/sunbird/job/livenodepublisher/publish/processor/MissingAssetValidatorProcessor.scala b/publish-pipeline/live-node-publisher/src/main/scala/org/sunbird/job/livenodepublisher/publish/processor/MissingAssetValidatorProcessor.scala new file mode 100644 index 000000000..f79dd7f3b --- /dev/null +++ b/publish-pipeline/live-node-publisher/src/main/scala/org/sunbird/job/livenodepublisher/publish/processor/MissingAssetValidatorProcessor.scala @@ -0,0 +1,45 @@ +package org.sunbird.job.livenodepublisher.publish.processor + +import org.sunbird.job.exception.InvalidInputException + +import java.io.File + +trait MissingAssetValidatorProcessor extends IProcessor { + + abstract override def process(ecrf: Plugin): Plugin = { + validateMissingAssets(ecrf) + super.process(ecrf) + } + + def getMediaId(media: Media): String = { + if(null != media.data && media.data.nonEmpty){ + val plugin = media.data.get("plugin") + val ver = media.data.get("version") + if((null != plugin && plugin.toString.nonEmpty) && (null != ver && ver.toString.nonEmpty)) + media.id + "_" + plugin+ "_" + ver + else media.id + }else media.id + } + + def validateMissingAssets(ecrf: Plugin): Any = { + if(null != ecrf.manifest){ + val medias:List[Media] = ecrf.manifest.medias + if(null != medias){ + val mediaIds = medias.map(media => getMediaId(media)).toList + if(mediaIds.size != mediaIds.distinct.size) + throw new InvalidInputException("Error! Duplicate Asset Id used in the manifest. Asset Ids are: " + + mediaIds.groupBy(identity).mapValues(_.size).filter(p => p._2 > 1).keySet) + + val nonYoutubeMedias = medias.filter(media => !"youtube".equalsIgnoreCase(media.`type`)) + nonYoutubeMedias.map(media => { + if(widgetTypeAssets.contains(media.`type`) && !new File(getBasePath() + File.separator + "widgets" + File.separator + media.src).exists()) + throw new InvalidInputException("Error! Missing Asset. | [Asset Id '" + media.id) + else if(!widgetTypeAssets.contains(media.`type`) && !media.src.startsWith("http") && !media.src.startsWith("https") && !new File(getBasePath() + File.separator + "assets" + File.separator + media.src).exists()) + throw new InvalidInputException("Error! Missing Asset. | [Asset Id '" + media.id) + else if (!widgetTypeAssets.contains(media.`type`) && (media.src.startsWith("http") || media.src.startsWith("https")) && !new File(getBasePath() + File.separator + "assets" + File.separator + media.src.split("/").last).exists()) + throw new InvalidInputException("Error! Missing Asset. | [Asset Id '" + media.id) + }) + } + } + } +} diff --git a/publish-pipeline/live-node-publisher/src/main/scala/org/sunbird/job/livenodepublisher/publish/processor/XMLLoaderWithCData.scala b/publish-pipeline/live-node-publisher/src/main/scala/org/sunbird/job/livenodepublisher/publish/processor/XMLLoaderWithCData.scala new file mode 100644 index 000000000..f27e8f07b --- /dev/null +++ b/publish-pipeline/live-node-publisher/src/main/scala/org/sunbird/job/livenodepublisher/publish/processor/XMLLoaderWithCData.scala @@ -0,0 +1,36 @@ +package org.sunbird.job.livenodepublisher.publish.processor + +import org.xml.sax.InputSource +import org.xml.sax.ext.{DefaultHandler2, LexicalHandler} + +import scala.xml.factory.XMLLoader +import scala.xml.parsing.FactoryAdapter +import scala.xml.{Elem, PCData, SAXParser, TopScope} + +object XMLLoaderWithCData extends XMLLoader[Elem] { + def lexicalHandler(adapter: FactoryAdapter): LexicalHandler = + new DefaultHandler2 { + def captureCData(): Unit = { + adapter.hStack push PCData(adapter.buffer.toString) + adapter.buffer.clear() + } + + override def startCDATA(): Unit = adapter.captureText() + override def endCDATA(): Unit = captureCData() + } + + override def loadXML(source: InputSource, parser: SAXParser): Elem = { + val newAdapter = adapter + + val xmlReader = parser.getXMLReader + xmlReader.setProperty( + "http://xml.org/sax/properties/lexical-handler", + lexicalHandler(newAdapter)) + + newAdapter.scopeStack push TopScope + parser.parse(source, newAdapter) + newAdapter.scopeStack.pop() + + newAdapter.rootElem.asInstanceOf[Elem] + } +} diff --git a/publish-pipeline/live-node-publisher/src/main/scala/org/sunbird/job/livenodepublisher/publish/processor/XmlParser.scala b/publish-pipeline/live-node-publisher/src/main/scala/org/sunbird/job/livenodepublisher/publish/processor/XmlParser.scala new file mode 100644 index 000000000..90b7c7246 --- /dev/null +++ b/publish-pipeline/live-node-publisher/src/main/scala/org/sunbird/job/livenodepublisher/publish/processor/XmlParser.scala @@ -0,0 +1,232 @@ +package org.sunbird.job.livenodepublisher.publish.processor + +import org.apache.commons.lang3.{StringEscapeUtils, StringUtils} +import org.sunbird.job.exception.InvalidInputException + +import scala.collection.mutable.ListBuffer +import scala.xml._ + +object XmlParser { + + val nonPluginElements: List[String] = List("manifest", "controller", "media", "events", "event", "__cdata", "__text") + val START_TAG_OPENING: String = "<" + val END_TAG_OPENING: String = "" + val ATTRIBUTE_KEY_VALUE_SEPARATOR: String = "=" + val BLANK_SPACE: String = " " + val DOUBLE_QUOTE: String = "\"" + + def parse(xml: String): Plugin = { + val xmlObj: Node = XMLLoaderWithCData.loadString(xml) + processDocument(xmlObj) + } + + def processDocument(root: Node): Plugin = { + if (null != root) { + Plugin(getId(root), getData(root), "", getCdata(root), getChildrenPlugin(root), getManifest(root, validateNode = true), getControllers(root \ "controllers"), getEvents(root)) + } else classOf[Plugin].newInstance() + } + + def getAttributesMap(node: Node): Map[String, AnyRef] = { + node.attributes.asAttrMap + } + + def getId(node: Node): String = { + getAttributesMap(node).getOrElse("id", "").asInstanceOf[String] + } + + def getData(node: Node): Map[String, AnyRef] = { + if (null != node) Map("cwp_element_name" -> node.label) ++ getAttributesMap(node) else Map() + } + + //TODO: Review the below code, this is as per the existing logic + def getCdata(node: Node): String = { + if (null != node && node.child.nonEmpty) { + val childNodes = node.child + var cdata = "" + childNodes.toList.filter(childNode => childNode.isInstanceOf[PCData]).map(childNode => { + cdata = childNode.text + }) + cdata + } else "" + } + + def getChildrenPlugin(node: Node): List[Plugin] = { + if (null != node && node.child.nonEmpty) { + val nodeList = node.child + nodeList.toList.filter(childNode => childNode.isInstanceOf[Elem] && !nonPluginElements.contains(childNode.label) && !"event".equalsIgnoreCase(childNode.label)) + .map(chilNode => Plugin(getId(chilNode), getData(chilNode), getInnerText(chilNode), getCdata(chilNode), getChildrenPlugin(chilNode), getManifest(chilNode, validateNode = false), getControllers(chilNode \ "controllers"), getEvents(chilNode))) + } else { + List() + } + } + + def getInnerText(node: Node): String = { + if (null != node && node.isInstanceOf[Elem] && node.child.nonEmpty) { + val childNodes = node.child + val innerTextlist = childNodes.toList.filter(childNode => childNode.isInstanceOf[Text]).map(item => item.text) + if (innerTextlist.nonEmpty) innerTextlist.head else "" + } else "" + } + + def getMedia(node: Node, validateNode: Boolean): Media = { + if (null != node) { + val attributeMap = getAttributesMap(node) + val id: String = attributeMap.getOrElse("id", "").asInstanceOf[String] + val `type`: String = attributeMap.getOrElse("type", "").asInstanceOf[String] + val src: String = attributeMap.getOrElse("src", "").asInstanceOf[String] + if (validateNode) { + if (StringUtils.isBlank(id) && !(StringUtils.isNotBlank(`type`) && (StringUtils.equalsIgnoreCase(`type`, "js") || StringUtils.equalsIgnoreCase(`type`, "css")))) + throw new InvalidInputException("Error! Invalid Media ('id' is required.) in '" + node.buildString(true) + "' ...") + if (StringUtils.isBlank(`type`)) + throw new InvalidInputException("Error! Invalid Media ('type' is required.) in '" + node.buildString(true) + "' ...") + if (StringUtils.isBlank(src)) + throw new InvalidInputException("Error! Invalid Media ('src' is required.) in '" + node.buildString(true) + "' ...") + } + Media(id, getData(node), getInnerText(node), getCdata(node), src, `type`, getChildrenPlugin(node)) + } else classOf[Media].newInstance() + } + + def getManifest(node: Node, validateNode: Boolean): Manifest = { + val childNodes = node.child + var manifestNode: Node = null + childNodes.toList.filter(childNode => StringUtils.equalsIgnoreCase(childNode.label, "manifest")).map(childNode => manifestNode = childNode) + val mediaList = { + if (null != manifestNode && manifestNode.child.nonEmpty) { + manifestNode.child.toList.filter(childNode => childNode.isInstanceOf[Elem] && "media".equalsIgnoreCase(childNode.label)).map(childNode => getMedia(childNode, validateNode)) + } else List() + } + if (null != manifestNode) { + Manifest(getId(manifestNode), getData(manifestNode), getInnerText(manifestNode), getCdata(manifestNode), mediaList) + } else classOf[Manifest].newInstance() + } + + def getControllers(nodeList: Seq[Node]): List[Controller] = { + if (null != nodeList && nodeList.length > 0) { + nodeList.toList.filter(node => node.isInstanceOf[Elem]).map(node => Controller(node.\@("id"), getData(node), getInnerText(node), getCdata(node))) + } else { + List() + } + } + + def getEvents(node: Node): List[Event] = { + var eventsList: ListBuffer[Event] = ListBuffer() + if (null != node && node.child.nonEmpty) { + val childNodes = node.child + childNodes.toList.map(childNode => { + if (childNode.isInstanceOf[Elem] && "events".equalsIgnoreCase(childNode.label)) { + eventsList ++= getEvents(childNode) + } + if (childNode.isInstanceOf[Elem] && "event".equalsIgnoreCase(childNode.label)) { + eventsList += Event(getId(childNode), getData(childNode), getInnerText(childNode), getCdata(childNode), getChildrenPlugin(childNode)) + } + }) + } + eventsList.toList + } + + /** + * serialize + * + */ + def toString(plugin: Plugin): String = { + val strBuilder = StringBuilder.newBuilder + if (null != plugin) { + strBuilder.append(getElementXml(plugin.data)) + .append(getInnerTextXml(plugin.innerText)) + .append(getCdataXml(plugin.cData)) + .append(getContentManifestXml(plugin.manifest)) + .append(getContentControllersXml(plugin.controllers)) + .append(getPluginsXml(plugin.childrenPlugin)) + .append(getEventsXml(plugin.events)) + .append(getEndTag(plugin.data.getOrElse("cwp_element_name", "").asInstanceOf[String])) + } + strBuilder.toString() + } + + def getElementXml(data: Map[String, AnyRef]): StringBuilder = { + val strBuilder = StringBuilder.newBuilder + if (null != data) { + strBuilder.append(START_TAG_OPENING + data("cwp_element_name")) + data.filterKeys(key => !StringUtils.equalsIgnoreCase("cwp_element_name", key)).map(entry => strBuilder.append(BLANK_SPACE + entry._1 + ATTRIBUTE_KEY_VALUE_SEPARATOR + DOUBLE_QUOTE + entry._2 + DOUBLE_QUOTE)) + strBuilder.append(TAG_CLOSING) + } + strBuilder + } + + def getInnerTextXml(innerText: String): StringBuilder = { + val strBuilder = StringBuilder.newBuilder + if (StringUtils.isNotBlank(innerText)) strBuilder.append(StringEscapeUtils.escapeXml(innerText)) + strBuilder + } + + def getCdataXml(cData: String): StringBuilder = { + val strBuilder = StringBuilder.newBuilder + if (StringUtils.isNotBlank(cData)) strBuilder.append("") + strBuilder + } + + def getContentManifestXml(manifest: Manifest): StringBuilder = { + val strBuilder = StringBuilder.newBuilder + if (null != manifest && null != manifest.medias && manifest.medias.nonEmpty) { + strBuilder.append(getElementXml(manifest.data)).append(getInnerTextXml(manifest.innerText)) + .append(getCdataXml(manifest.cData)) + .append(getMediaXml(manifest.medias)) + .append(getEndTag(manifest.data.getOrElse("cwp_element_name", "").asInstanceOf[String])) + } + strBuilder + } + + def getPluginsXml(childrenPlugin: List[Plugin]): StringBuilder = { + val strBuilder = StringBuilder.newBuilder + if (null != childrenPlugin && childrenPlugin.nonEmpty) { + childrenPlugin.map(plugin => strBuilder.append(toString(plugin))) + } + strBuilder + } + + def getContentControllersXml(controllers: List[Controller]): StringBuilder = { + val strBuilder = StringBuilder.newBuilder + if (null != controllers && controllers.nonEmpty) { + controllers.map(controller => { + strBuilder.append(getElementXml(controller.data)) + .append(getInnerTextXml(controller.innerText)) + .append(getCdataXml(controller.cData)) + .append(getEndTag(controller.data.getOrElse("cwp_element_name", "").asInstanceOf[String])) + }) + } + strBuilder + } + + def getEventsXml(events: List[Event]): StringBuilder = { + val strBuilder = StringBuilder.newBuilder + if (null != events && events.nonEmpty) { + if (events.size > 1) strBuilder.append(START_TAG_OPENING + "events" + TAG_CLOSING) + events.map(event => strBuilder.append(getElementXml(event.data)).append(getInnerTextXml(event.innerText)).append(getCdataXml(event.cData)).append(getPluginsXml(event.childrenPlugin)).append(getEndTag("event"))) + if (events.size > 1) strBuilder.append(getEndTag("events")) + } + strBuilder + } + + def getEndTag(str: String): String = { + if (StringUtils.isNotBlank(str)) END_TAG_OPENING + str + TAG_CLOSING + else "" + } + + def getMediaXml(medias: List[Media]): StringBuilder = { + val strBuilder = StringBuilder.newBuilder + if (null != medias && medias.nonEmpty) { + medias.map(media => { + + val updatedMediaData = media.data.map(mediaInfo => { + if(mediaInfo._1.equalsIgnoreCase("src")) + ("src" -> (StringUtils.stripStart(mediaInfo._2.toString, "/"))) + else mediaInfo + }) + + strBuilder.append(getElementXml(updatedMediaData)).append(getInnerTextXml(media.innerText)).append(media.cData).append(getPluginsXml(media.childrenPlugin)).append(getEndTag("media")) + }) + } + strBuilder + } +} diff --git a/publish-pipeline/live-node-publisher/src/main/scala/org/sunbird/job/livenodepublisher/task/LiveNodePublisherConfig.scala b/publish-pipeline/live-node-publisher/src/main/scala/org/sunbird/job/livenodepublisher/task/LiveNodePublisherConfig.scala new file mode 100644 index 000000000..066d36d84 --- /dev/null +++ b/publish-pipeline/live-node-publisher/src/main/scala/org/sunbird/job/livenodepublisher/task/LiveNodePublisherConfig.scala @@ -0,0 +1,105 @@ +package org.sunbird.job.livenodepublisher.task + +import com.typesafe.config.Config +import org.apache.flink.api.common.typeinfo.TypeInformation +import org.apache.flink.api.java.typeutils.TypeExtractor +import org.apache.flink.streaming.api.scala.OutputTag +import org.sunbird.job.publish.config.PublishConfig +import org.sunbird.job.livenodepublisher.publish.domain.Event + +import java.util +import scala.collection.JavaConverters._ + +class LiveNodePublisherConfig(override val config: Config) extends PublishConfig(config, "live-node-publisher") { + + implicit val mapTypeInfo: TypeInformation[util.Map[String, AnyRef]] = TypeExtractor.getForClass(classOf[util.Map[String, AnyRef]]) + implicit val stringTypeInfo: TypeInformation[String] = TypeExtractor.getForClass(classOf[String]) + implicit val publishMetaTypeInfo: TypeInformation[Event] = TypeExtractor.getForClass(classOf[Event]) + + // Job Configuration + val jobEnv: String = config.getString("job.env") + + // Kafka Topics Configuration + val kafkaInputTopic: String = config.getString("kafka.input.topic") + val liveVideoStreamTopic: String = config.getString("kafka.live_video_stream.topic") + val kafkaErrorTopic: String = config.getString("kafka.error.topic") + val kafkaSkippedTopic: String = config.getString("kafka.skipped.topic") + val inputConsumerName = "live-node-publisher-consumer" + + // Parallelism + override val kafkaConsumerParallelism: Int = config.getInt("task.consumer.parallelism") + val eventRouterParallelism: Int = config.getInt("task.router.parallelism") + + // Metric List + val totalEventsCount = "total-events-count" + val skippedEventCount = "skipped-event-count" + val contentPublishEventCount = "content-publish-count" + val contentPublishSuccessEventCount = "content-publish-success-count" + val contentPublishFailedEventCount = "content-publish-failed-count" + val videoStreamingGeneratorEventCount = "video-streaming-event-count" + val collectionPublishEventCount = "collection-publish-count" + val collectionPublishSuccessEventCount = "collection-publish-success-count" + val collectionPublishFailedEventCount = "collection-publish-failed-count" + val collectionPostPublishProcessEventCount = "collection-post-publish-process-count" + val mvProcessorEventCount = "mvc-processor-event-count" + val dialcodeContextUpdaterEventCount = "dialcode-context-updater-event-count" + + // Cassandra Configurations + val cassandraHost: String = config.getString("lms-cassandra.host") + val cassandraPort: Int = config.getInt("lms-cassandra.port") + val contentKeyspaceName: String = config.getString("content.keyspace") + val contentTableName: String = config.getString("content.table") + val hierarchyKeyspaceName: String = config.getString("hierarchy.keyspace") + val hierarchyTableName: String = config.getString("hierarchy.table") + + // Neo4J Configurations + val graphRoutePath: String = config.getString("neo4j.routePath") + val graphName: String = config.getString("neo4j.graph") + + // Redis Configurations + val nodeStore: Int = config.getInt("redis.database.contentCache.id") + + // Out Tags + val contentPublishOutTag: OutputTag[Event] = OutputTag[Event]("live-content-publish") + val collectionPublishOutTag: OutputTag[Event] = OutputTag[Event]("live-collection-publish") + val generateVideoStreamingOutTag: OutputTag[String] = OutputTag[String]("live-video-streaming-generator-request") + val failedEventOutTag: OutputTag[String] = OutputTag[String]("failed-event") + val skippedEventOutTag: OutputTag[String] = OutputTag[String]("skipped-event") + val generatePostPublishProcessTag: OutputTag[String] = OutputTag[String]("post-publish-process-request") + + // Service Urls + val printServiceBaseUrl: String = config.getString("service.print.basePath") + + val definitionBasePath: String = if (config.hasPath("schema.basePath")) config.getString("schema.basePath") else "https://sunbirddev.blob.core.windows.net/sunbird-content-dev/schemas/local" + val schemaSupportVersionMap: Map[String, AnyRef] = if (config.hasPath("schema.supportedVersion")) config.getObject("schema.supportedVersion").unwrapped().asScala.toMap else Map[String, AnyRef]() + + val supportedObjectType: util.List[String] = if (config.hasPath("content.objectType")) config.getStringList("content.objectType") else util.Arrays.asList[String]("Content", "ContentImage") + val supportedMimeType: util.List[String] = if (config.hasPath("content.mimeType")) config.getStringList("content.mimeType") else util.Arrays.asList[String]("application/pdf") + val streamableMimeType: util.List[String] = if (config.hasPath("content.stream.mimeType")) config.getStringList("content.stream.mimeType") else util.Arrays.asList[String]("video/mp4") + val isStreamingEnabled: Boolean = if (config.hasPath("content.stream.enabled")) config.getBoolean("content.stream.enabled") else false + val assetDownloadDuration: String = if (config.hasPath("content.asset_download_duration")) config.getString("content.asset_download_duration") else "60 seconds" + + val isECARExtractionEnabled: Boolean = if (config.hasPath("content.isECARExtractionEnabled")) config.getBoolean("content.isECARExtractionEnabled") else true + val contentFolder: String = if (config.hasPath("cloud_storage.folder.content")) config.getString("cloud_storage.folder.content") else "content" + val artifactFolder: String = if (config.hasPath("cloud_storage.folder.artifact")) config.getString("cloud_storage.folder.artifact") else "artifact" + val retryAssetDownloadsCount: Integer = if (config.hasPath("content.retry_asset_download_count")) config.getInt("content.retry_asset_download_count") else 1 + val artifactSizeForOnline: Double = if (config.hasPath("content.artifact.size.for_online")) config.getDouble("content.artifact.size.for_online") else 209715200 + val bundleLocation: String = if (config.hasPath("content.bundleLocation")) config.getString("content.bundleLocation") else "/data/contentBundle/" + + val extractableMimeTypes = List("application/vnd.ekstep.ecml-archive", "application/vnd.ekstep.html-archive", "application/vnd.ekstep.plugin-archive", "application/vnd.ekstep.h5p-archive") + + val categoryMap: java.util.Map[String, AnyRef] = if (config.hasPath("contentTypeToPrimaryCategory")) config.getAnyRef("contentTypeToPrimaryCategory").asInstanceOf[java.util.Map[String, AnyRef]] else new util.HashMap[String, AnyRef]() + + val esConnectionInfo: String = config.getString("es.basePath") + val compositeSearchIndexName: String = if (config.hasPath("compositesearch.index.name")) config.getString("compositesearch.index.name") else "compositesearch" + val compositeSearchIndexType: String = if (config.hasPath("search.document.type")) config.getString("search.document.type") else "cs" + val nestedFields: util.List[String] = if (config.hasPath("content.nested.fields")) config.getStringList("content.nested.fields") else util.Arrays.asList[String]("badgeAssertions","targets","badgeAssociations") + + val allowedExtensionsWord: util.List[String] = if (config.hasPath("mimetype.allowed_extensions.word")) config.getStringList("mimetype.allowed_extensions.word") else util.Arrays.asList[String]("doc", "docx", "ppt", "pptx", "key", "odp", "pps", "odt", "wpd", "wps", "wks") + + val searchServiceBaseUrl : String = config.getString("service.search.basePath") + val searchFields: util.List[String] = util.Arrays.asList[String]("identifier","migratedVersion") + + val isrRelativePathEnabled: Boolean = if (config.hasPath("cloudstorage.metadata.replace_absolute_path")) config.getBoolean("cloudstorage.metadata.replace_absolute_path") else false + val migrationVersion: Double = if(config.hasPath("migrationVersion")) config.getDouble("migrationVersion") else 1.0 +} diff --git a/publish-pipeline/live-node-publisher/src/main/scala/org/sunbird/job/livenodepublisher/task/LiveNodePublisherStreamTask.scala b/publish-pipeline/live-node-publisher/src/main/scala/org/sunbird/job/livenodepublisher/task/LiveNodePublisherStreamTask.scala new file mode 100644 index 000000000..503aaf128 --- /dev/null +++ b/publish-pipeline/live-node-publisher/src/main/scala/org/sunbird/job/livenodepublisher/task/LiveNodePublisherStreamTask.scala @@ -0,0 +1,62 @@ +package org.sunbird.job.livenodepublisher.task + +import com.typesafe.config.ConfigFactory +import org.apache.flink.api.common.typeinfo.TypeInformation +import org.apache.flink.api.java.typeutils.TypeExtractor +import org.apache.flink.api.java.utils.ParameterTool +import org.apache.flink.streaming.api.scala.StreamExecutionEnvironment +import org.sunbird.job.connector.FlinkKafkaConnector +import org.sunbird.job.livenodepublisher.function.{LiveCollectionPublishFunction, LiveContentPublishFunction, LivePublishEventRouter} +import org.sunbird.job.livenodepublisher.publish.domain.Event +import org.sunbird.job.util.{FlinkUtil, HttpUtil} + +import java.io.File +import java.util + +class LiveNodePublisherStreamTask(config: LiveNodePublisherConfig, kafkaConnector: FlinkKafkaConnector, httpUtil: HttpUtil) { + + def process(): Unit = { + implicit val env: StreamExecutionEnvironment = FlinkUtil.getExecutionContext(config) +// implicit val env: StreamExecutionEnvironment = StreamExecutionEnvironment.createLocalEnvironment() + implicit val eventTypeInfo: TypeInformation[Event] = TypeExtractor.getForClass(classOf[Event]) + implicit val mapTypeInfo: TypeInformation[util.Map[String, AnyRef]] = TypeExtractor.getForClass(classOf[util.Map[String, AnyRef]]) + implicit val stringTypeInfo: TypeInformation[String] = TypeExtractor.getForClass(classOf[String]) + + val source = kafkaConnector.kafkaJobRequestSource[Event](config.kafkaInputTopic) + val processStreamTask = env.addSource(source).name(config.inputConsumerName) + .uid(config.inputConsumerName).setParallelism(config.kafkaConsumerParallelism) + .rebalance + .process(new LivePublishEventRouter(config)) + .name("publish-event-router").uid("publish-event-router") + .setParallelism(config.eventRouterParallelism) + + val contentPublish = processStreamTask.getSideOutput(config.contentPublishOutTag).process(new LiveContentPublishFunction(config, httpUtil)) + .name("live-content-publish-process").uid("live-content-publish-process").setParallelism(config.kafkaConsumerParallelism) + + contentPublish.getSideOutput(config.generateVideoStreamingOutTag).addSink(kafkaConnector.kafkaStringSink(config.liveVideoStreamTopic)) + contentPublish.getSideOutput(config.failedEventOutTag).addSink(kafkaConnector.kafkaStringSink(config.kafkaErrorTopic)) + + val collectionPublish = processStreamTask.getSideOutput(config.collectionPublishOutTag).process(new LiveCollectionPublishFunction(config, httpUtil)) + .name("live-collection-publish-process").uid("live-collection-publish-process").setParallelism(config.kafkaConsumerParallelism) + collectionPublish.getSideOutput(config.skippedEventOutTag).addSink(kafkaConnector.kafkaStringSink(config.kafkaSkippedTopic)) + collectionPublish.getSideOutput(config.failedEventOutTag).addSink(kafkaConnector.kafkaStringSink(config.kafkaErrorTopic)) + + env.execute(config.jobName) + } +} + +// $COVERAGE-OFF$ Disabling scoverage as the below code can only be invoked within flink cluster +object LiveNodePublisherStreamTask { + + def main(args: Array[String]): Unit = { + val configFilePath = Option(ParameterTool.fromArgs(args).get("config.file.path")) + val config = configFilePath.map { + path => ConfigFactory.parseFile(new File(path)).resolve() + }.getOrElse(ConfigFactory.load("live-node-publisher.conf").withFallback(ConfigFactory.systemEnvironment())) + val publishConfig = new LiveNodePublisherConfig(config) + val kafkaUtil = new FlinkKafkaConnector(publishConfig) + val httpUtil = new HttpUtil + val task = new LiveNodePublisherStreamTask(publishConfig, kafkaUtil, httpUtil) + task.process() + } +} \ No newline at end of file diff --git a/enrolment-reconciliation/src/test/resources/logback-test.xml b/publish-pipeline/live-node-publisher/src/test/resources/logback-test.xml similarity index 100% rename from enrolment-reconciliation/src/test/resources/logback-test.xml rename to publish-pipeline/live-node-publisher/src/test/resources/logback-test.xml diff --git a/publish-pipeline/live-node-publisher/src/test/resources/test.conf b/publish-pipeline/live-node-publisher/src/test/resources/test.conf new file mode 100644 index 000000000..70c1a6b4c --- /dev/null +++ b/publish-pipeline/live-node-publisher/src/test/resources/test.conf @@ -0,0 +1,140 @@ +include "base-test.conf" + +job { + env = "sunbirddev" +} + +kafka { + input.topic = "sunbirddev.publish.job.request" + live_video_stream.topic = "sunbirddev.live.video.stream.request" + error.topic = "sunbirddev.learning.events.failed" + skipped.topic = "sunbirddev.learning.events.skipped" + groupId = "local-content-publish-group" +} + +task { + consumer.parallelism = 1 + parallelism = 1 + router.parallelism = 1 +} + +redis { + database { + contentCache.id = 0 + } +} + +content { + bundleLocation = "/tmp/contentBundle" + isECARExtractionEnabled = true + retry_asset_download_count = 1 + keyspace = "dev_content_store" + table = "content_data" + tmp_file_location = "/tmp" + objectType = ["Content", "ContentImage","Collection","CollectionImage"] + mimeType = ["application/pdf", "video/avi", "video/mpeg", "video/quicktime", "video/3gpp", "video/mpeg", "video/mp4", "video/ogg", "video/webm", "application/vnd.ekstep.html-archive","application/vnd.ekstep.ecml-archive","application/vnd.ekstep.content-collection" + "application/vnd.ekstep.ecml-archive", + "application/vnd.ekstep.html-archive", + "application/vnd.android.package-archive", + "application/vnd.ekstep.content-archive", + "application/octet-stream", + "application/json", + "application/javascript", + "application/xml", + "text/plain", + "text/html", + "text/javascript", + "text/xml", + "text/css", + "image/jpeg", "image/jpg", "image/png", "image/tiff", "image/bmp", "image/gif", "image/svg+xml", + "image/x-quicktime", + "video/avi", "video/mpeg", "video/quicktime", "video/3gpp", "video/mpeg", "video/mp4", "video/ogg", "video/webm", + "video/msvideo", + "video/x-msvideo", + "video/x-qtc", + "video/x-mpeg", + "audio/mp3", "audio/mp4", "audio/mpeg", "audio/ogg", "audio/webm", "audio/x-wav", "audio/wav", + "audio/mpeg3", + "audio/x-mpeg-3", + "audio/vorbis", + "application/x-font-ttf", + "application/pdf", "application/epub", "application/msword", + "application/vnd.ekstep.h5p-archive", + "application/vnd.ekstep.plugin-archive", + "video/x-youtube", "video/youtube", + "text/x-url"] + asset_download_duration = "60 seconds" + + stream { + enabled = true + mimeType = ["video/mp4", "video/webm"] + } + artifact.size.for_online=209715200 + + downloadFiles { + spine = ["appIcon"] + full = ["appIcon", "grayScaleAppIcon", "artifactUrl", "itemSetPreviewUrl", "media"] + } + + nested.fields=["badgeAssertions", "targets", "badgeAssociations", "plugins", "me_totalTimeSpent", "me_totalPlaySessionCount", "me_totalTimeSpentInSec", "batches", "trackable", "credentials", "discussionForum", "provider", "osMetadata", "actions"] +} + +hierarchy { + keyspace = "dev_hierarchy_store" + table = "content_hierarchy" +} + +cloud_storage { + folder { + content = "content" + artifact = "artifact" + } +} + +service { + print.basePath = "http://11.2.6.6/print" +} + + +contentTypeToPrimaryCategory { + ClassroomTeachingVideo: "Explanation Content" + ConceptMap: "Learning Resource" + Course: "Course" + CuriosityQuestionSet: "Practice Question Set" + eTextBook: "eTextbook" + Event: "Event" + EventSet: "Event Set" + ExperientialResource: "Learning Resource" + ExplanationResource: "Explanation Content" + ExplanationVideo: "Explanation Content" + FocusSpot: "Teacher Resource" + LearningOutcomeDefinition: "Teacher Resource" + MarkingSchemeRubric: "Teacher Resource" + PedagogyFlow: "Teacher Resource" + PracticeQuestionSet: "Practice Question Set" + PracticeResource: "Practice Question Set" + SelfAssess: "Course Assessment" + TeachingMethod: "Teacher Resource" + TextBook: "Digital Textbook" + Collection: "Content Playlist" + ExplanationReadingMaterial: "Learning Resource" + LearningActivity: "Learning Resource" + LessonPlan: "Content Playlist" + LessonPlanResource: "Teacher Resource" + PreviousBoardExamPapers: "Learning Resource" + TVLesson: "Explanation Content" + OnboardingResource: "Learning Resource" + ReadingMaterial: "Learning Resource" + Template: "Template" + Asset: "Asset" + Plugin: "Plugin" + LessonPlanUnit: "Lesson Plan Unit" + CourseUnit: "Course Unit" + TextBookUnit: "Textbook Unit" + Asset: "Certificate Template" +} + +enableDIALContextUpdate = "Yes" + + +service.search.basePath = "http://11.2.6.6/search" \ No newline at end of file diff --git a/publish-pipeline/live-node-publisher/src/test/resources/test.cql b/publish-pipeline/live-node-publisher/src/test/resources/test.cql new file mode 100644 index 000000000..3416fa82a --- /dev/null +++ b/publish-pipeline/live-node-publisher/src/test/resources/test.cql @@ -0,0 +1,58 @@ +CREATE KEYSPACE IF NOT EXISTS dev_content_store WITH replication = {'class':'SimpleStrategy','replication_factor':1}; +CREATE TABLE IF NOT EXISTS dev_content_store.content_data ( + content_id text, + body blob, + PRIMARY KEY (content_id) +); + +CREATE KEYSPACE IF NOT EXISTS dev_hierarchy_store WITH replication = {'class':'SimpleStrategy','replication_factor':1}; +CREATE TABLE IF NOT EXISTS dev_hierarchy_store.content_hierarchy ( + identifier text, + hierarchy text, + relational_metadata text, + PRIMARY KEY (identifier) +); + +INSERT INTO dev_content_store.content_data(content_id, body) VALUES ( +'do_11321328578759884811663', +textAsBlob('

A person having less ‘haemoglobin’ is suffering from:

Jaundice

Anaemia

Malaria

Chikungunya

\",\"media\":[],\"responseDeclaration\":{\"responseValue\":{\"cardinality\":\"single\",\"type\":\"integer\",\"correct_response\":{\"value\":\"2\"}}},\"options\":[{\"answer\":false,\"value\":{\"type\":\"text\",\"body\":\"

Jaundice

\",\"resvalue\":0,\"resindex\":0}},{\"answer\":false,\"value\":{\"type\":\"text\",\"body\":\"

Anaemia

\",\"resvalue\":1,\"resindex\":1}},{\"answer\":true,\"value\":{\"type\":\"text\",\"body\":\"

Malaria

\",\"resvalue\":2,\"resindex\":2}},{\"answer\":false,\"value\":{\"type\":\"text\",\"body\":\"

Chikungunya

\",\"resvalue\":3,\"resindex\":3}}],\"questionCount\":0}]]>
') +); + +INSERT INTO dev_hierarchy_store.content_hierarchy(identifier, hierarchy, relational_metadata) VALUES ( +'do_2133950809948078081503', +'{"ownershipType":["createdBy"],"copyright":"firstorg","se_gradeLevelIds":["tn_k-12_5_gradelevel_class1"],"keywords":["pdf"],"subject":["English"],"channel":"0126825293972439041","downloadUrl":"https://preprodall.blob.core.windows.net/ntp-content-preprod/ecar_files/do_2133950809948078081503/test-collection-publishing_1635141950055_do_2133950809948078081503_1.0_spine.ecar","organisation":["First Org","ST MARYS SCHOOL BHATHUBASTI"],"language":["English"],"mimeType":"application/vnd.ekstep.content-collection","variants":{"online":{"ecarUrl":"https://preprodall.blob.core.windows.net/ntp-content-preprod/ecar_files/do_2133950809948078081503/test-collection-publishing_1635141950127_do_2133950809948078081503_1.0_online.ecar","size":8755.0},"spine":{"ecarUrl":"https://preprodall.blob.core.windows.net/ntp-content-preprod/ecar_files/do_2133950809948078081503/test-collection-publishing_1635141950055_do_2133950809948078081503_1.0_spine.ecar","size":12426.0}},"leafNodes":["do_2133916718042972161142","do_2132926691727114241157"],"objectType":"Content","se_mediums":["English"],"gradeLevel":["Class 1"],"appIcon":"","primaryCategory":"Digital Textbook","children":[{"ownershipType":["createdBy"],"parent":"do_2133950809948078081503","code":"76abafa2a0c2cfef90b52db1ef41fb82","credentials":{"enabled":"No"},"channel":"0126825293972439041","downloadUrl":"https://preprodall.blob.core.windows.net/ntp-content-preprod/ecar_files/do_2133950809948078081503/test-collection-publishing_1635141950055_do_2133950809948078081503_1.0_spine.ecar","description":"This chapter describes about human body","language":["English"],"mimeType":"application/vnd.ekstep.content-collection","variants":{"online":{"ecarUrl":"https://preprodall.blob.core.windows.net/ntp-content-preprod/ecar_files/do_2133950809948078081503/test-collection-publishing_1635141950127_do_2133950809948078081503_1.0_online.ecar","size":8755.0},"spine":{"ecarUrl":"https://preprodall.blob.core.windows.net/ntp-content-preprod/ecar_files/do_2133950809948078081503/test-collection-publishing_1635141950055_do_2133950809948078081503_1.0_spine.ecar","size":12426.0}},"leafNodes":["do_2133916718042972161142","do_2132926691727114241157"],"idealScreenSize":"normal","createdOn":"2021-10-25T06:03:17.254+0000","objectType":"Content","primaryCategory":"Textbook Unit","children":[{"ownershipType":["createdBy"],"parent":"do_2133950816031047681516","code":"20cc1f31e62f924c6e47bf04c994376b","credentials":{"enabled":"No"},"channel":"0126825293972439041","downloadUrl":"https://preprodall.blob.core.windows.net/ntp-content-preprod/ecar_files/do_2133950809948078081503/test-collection-publishing_1635141950055_do_2133950809948078081503_1.0_spine.ecar","description":"This section describes about various part of the body such as head, hands, legs etc.","language":["English"],"mimeType":"application/vnd.ekstep.content-collection","variants":{"online":{"ecarUrl":"https://preprodall.blob.core.windows.net/ntp-content-preprod/ecar_files/do_2133950809948078081503/test-collection-publishing_1635141950127_do_2133950809948078081503_1.0_online.ecar","size":8755.0},"spine":{"ecarUrl":"https://preprodall.blob.core.windows.net/ntp-content-preprod/ecar_files/do_2133950809948078081503/test-collection-publishing_1635141950055_do_2133950809948078081503_1.0_spine.ecar","size":12426.0}},"leafNodes":[],"idealScreenSize":"normal","createdOn":"2021-10-25T06:03:17.252+0000","objectType":"Content","primaryCategory":"Textbook Unit","children":[{"ownershipType":["createdBy"],"parent":"do_2133950816030883841508","code":"7991af5c2e51e4d3d7b83167aaac8829","credentials":{"enabled":"No"},"channel":"0126825293972439041","downloadUrl":"https://preprodall.blob.core.windows.net/ntp-content-preprod/ecar_files/do_2133950809948078081503/test-collection-publishing_1635141950055_do_2133950809948078081503_1.0_spine.ecar","description":"xyz","language":["English"],"mimeType":"application/vnd.ekstep.content-collection","variants":{"online":{"ecarUrl":"https://preprodall.blob.core.windows.net/ntp-content-preprod/ecar_files/do_2133950809948078081503/test-collection-publishing_1635141950127_do_2133950809948078081503_1.0_online.ecar","size":8755.0},"spine":{"ecarUrl":"https://preprodall.blob.core.windows.net/ntp-content-preprod/ecar_files/do_2133950809948078081503/test-collection-publishing_1635141950055_do_2133950809948078081503_1.0_spine.ecar","size":12426.0}},"leafNodes":[],"idealScreenSize":"normal","createdOn":"2021-10-25T06:03:17.253+0000","objectType":"Content","primaryCategory":"Textbook Unit","contentDisposition":"inline","lastUpdatedOn":"2021-10-25T06:05:49.619+0000","contentEncoding":"gzip","generateDIALCodes":"No","contentType":"TextBookUnit","dialcodeRequired":"No","identifier":"do_2133950816030965761510","lastStatusChangedOn":"2021-10-25T06:03:17.253+0000","audience":["Student"],"os":["All"],"visibility":"Parent","discussionForum":{"enabled":"No"},"index":1,"mediaType":"content","osId":"org.ekstep.launcher","languageCode":["en"],"version":2,"pkgVersion":1.0,"versionKey":"1635141797253","license":"CC BY 4.0","idealScreenDensity":"hdpi","framework":"tn_k-12_5","depth":3,"lastPublishedOn":"2021-10-25T06:05:49.988+0000","compatibilityLevel":1,"leafNodesCount":0,"name":"5.1.1 Key parts in the head","status":"Draft"},{"ownershipType":["createdBy"],"parent":"do_2133950816030883841508","code":"3bf70f06d3e8dba010d8806fd94259b1","credentials":{"enabled":"No"},"channel":"0126825293972439041","downloadUrl":"https://preprodall.blob.core.windows.net/ntp-content-preprod/ecar_files/do_2133950809948078081503/test-collection-publishing_1635141950055_do_2133950809948078081503_1.0_spine.ecar","description":"","language":["English"],"mimeType":"application/vnd.ekstep.content-collection","variants":{"online":{"ecarUrl":"https://preprodall.blob.core.windows.net/ntp-content-preprod/ecar_files/do_2133950809948078081503/test-collection-publishing_1635141950127_do_2133950809948078081503_1.0_online.ecar","size":8755.0},"spine":{"ecarUrl":"https://preprodall.blob.core.windows.net/ntp-content-preprod/ecar_files/do_2133950809948078081503/test-collection-publishing_1635141950055_do_2133950809948078081503_1.0_spine.ecar","size":12426.0}},"leafNodes":[],"idealScreenSize":"normal","createdOn":"2021-10-25T06:03:17.228+0000","objectType":"Content","primaryCategory":"Textbook Unit","contentDisposition":"inline","lastUpdatedOn":"2021-10-25T06:05:49.619+0000","contentEncoding":"gzip","generateDIALCodes":"No","contentType":"TextBookUnit","dialcodeRequired":"No","identifier":"do_2133950816028917761504","lastStatusChangedOn":"2021-10-25T06:03:17.228+0000","audience":["Student"],"os":["All"],"visibility":"Parent","discussionForum":{"enabled":"No"},"index":2,"mediaType":"content","osId":"org.ekstep.launcher","languageCode":["en"],"version":2,"pkgVersion":1.0,"versionKey":"1635141797228","license":"CC BY 4.0","idealScreenDensity":"hdpi","framework":"tn_k-12_5","depth":3,"lastPublishedOn":"2021-10-25T06:05:49.988+0000","compatibilityLevel":1,"leafNodesCount":0,"name":"5.1.2 Other parts","status":"Draft"}],"contentDisposition":"inline","lastUpdatedOn":"2021-10-25T06:05:49.619+0000","contentEncoding":"gzip","generateDIALCodes":"No","contentType":"TextBookUnit","dialcodeRequired":"No","identifier":"do_2133950816030883841508","lastStatusChangedOn":"2021-10-25T06:03:17.252+0000","audience":["Student"],"os":["All"],"visibility":"Parent","discussionForum":{"enabled":"No"},"index":1,"mediaType":"content","osId":"org.ekstep.launcher","languageCode":["en"],"version":2,"pkgVersion":1.0,"versionKey":"1635141797252","license":"CC BY 4.0","idealScreenDensity":"hdpi","framework":"tn_k-12_5","depth":2,"lastPublishedOn":"2021-10-25T06:05:49.988+0000","compatibilityLevel":1,"leafNodesCount":0,"name":"5.1 Parts of Body","status":"Draft"},{"ownershipType":["createdBy"],"parent":"do_2133950816031047681516","code":"40a1ed37e0fad94eca76b2a96fe086ab","credentials":{"enabled":"No"},"channel":"0126825293972439041","downloadUrl":"https://preprodall.blob.core.windows.net/ntp-content-preprod/ecar_files/do_2133950809948078081503/test-collection-publishing_1635141950055_do_2133950809948078081503_1.0_spine.ecar","description":"","language":["English"],"mimeType":"application/vnd.ekstep.content-collection","variants":{"online":{"ecarUrl":"https://preprodall.blob.core.windows.net/ntp-content-preprod/ecar_files/do_2133950809948078081503/test-collection-publishing_1635141950127_do_2133950809948078081503_1.0_online.ecar","size":8755.0},"spine":{"ecarUrl":"https://preprodall.blob.core.windows.net/ntp-content-preprod/ecar_files/do_2133950809948078081503/test-collection-publishing_1635141950055_do_2133950809948078081503_1.0_spine.ecar","size":12426.0}},"leafNodes":["do_2133916718042972161142"],"idealScreenSize":"normal","createdOn":"2021-10-25T06:03:17.254+0000","objectType":"Content","primaryCategory":"Textbook Unit","children":[{"ownershipType":["createdBy"],"parent":"do_2133950816031047681514","code":"b186b1bbcc9c58db865f75e34345179e","credentials":{"enabled":"No"},"channel":"0126825293972439041","downloadUrl":"https://preprodall.blob.core.windows.net/ntp-content-preprod/ecar_files/do_2133950809948078081503/test-collection-publishing_1635141950055_do_2133950809948078081503_1.0_spine.ecar","description":"","language":["English"],"mimeType":"application/vnd.ekstep.content-collection","variants":{"online":{"ecarUrl":"https://preprodall.blob.core.windows.net/ntp-content-preprod/ecar_files/do_2133950809948078081503/test-collection-publishing_1635141950127_do_2133950809948078081503_1.0_online.ecar","size":8755.0},"spine":{"ecarUrl":"https://preprodall.blob.core.windows.net/ntp-content-preprod/ecar_files/do_2133950809948078081503/test-collection-publishing_1635141950055_do_2133950809948078081503_1.0_spine.ecar","size":12426.0}},"leafNodes":["do_2133916718042972161142"],"idealScreenSize":"normal","createdOn":"2021-10-25T06:03:17.251+0000","objectType":"Content","primaryCategory":"Textbook Unit","children":[{"ownershipType":["createdBy"],"parent":"do_2133950816030801921506","code":"9cf84ff2fb08f9af4c23eb09df9b2520","credentials":{"enabled":"No"},"channel":"0126825293972439041","downloadUrl":"https://preprodall.blob.core.windows.net/ntp-content-preprod/ecar_files/do_2133950809948078081503/test-collection-publishing_1635141950055_do_2133950809948078081503_1.0_spine.ecar","description":"","language":["English"],"mimeType":"application/vnd.ekstep.content-collection","variants":{"online":{"ecarUrl":"https://preprodall.blob.core.windows.net/ntp-content-preprod/ecar_files/do_2133950809948078081503/test-collection-publishing_1635141950127_do_2133950809948078081503_1.0_online.ecar","size":8755.0},"spine":{"ecarUrl":"https://preprodall.blob.core.windows.net/ntp-content-preprod/ecar_files/do_2133950809948078081503/test-collection-publishing_1635141950055_do_2133950809948078081503_1.0_spine.ecar","size":12426.0}},"leafNodes":["do_2133916718042972161142"],"idealScreenSize":"normal","createdOn":"2021-10-25T06:03:17.253+0000","objectType":"Content","primaryCategory":"Textbook Unit","children":[{"ownershipType":["createdBy"],"parent":"do_2133950816030965761512","copyright":"Tamil Nadu, P P MAHAPUR","previewUrl":"https://preprodall.blob.core.windows.net/ntp-content-preprod/content/assets/do_2133916718042972161142/sample-mp4-file.mp4","subject":["Home Science"],"channel":"01269878797503692810","downloadUrl":"https://preprodall.blob.core.windows.net/ntp-content-preprod/content/do_2133916718042972161142/oct20-mp4_1634728008255_do_2133916718042972161142_2.ecar","organisation":["Tamil Nadu","P P MAHAPUR"],"showNotification":true,"language":["English"],"mimeType":"video/mp4","variants":{"full":{"ecarUrl":"https://preprodall.blob.core.windows.net/ntp-content-preprod/content/do_2133916718042972161142/oct20-mp4_1634728008255_do_2133916718042972161142_2.ecar","size":"10504301"},"spine":{"ecarUrl":"https://preprodall.blob.core.windows.net/ntp-content-preprod/content/do_2133916718042972161142/oct20-mp4_1634728009035_do_2133916718042972161142_2_SPINE.ecar","size":"1465"}},"objectType":"Content","se_mediums":["English","Tamil"],"gradeLevel":["Class 4","Class 2"],"primaryCategory":"Explanation Content","appId":"preprod.diksha.app","contentEncoding":"identity","artifactUrl":"https://preprodall.blob.core.windows.net/ntp-content-preprod/content/assets/do_2133916718042972161142/sample-mp4-file.mp4","me_totalPlaySessionCount":{"portal":4},"lockKey":"68bcaed4-8eaf-4f01-b5b4-a34a08887de7","sYS_INTERNAL_LAST_UPDATED_ON":"2021-10-25T05:46:53.202+0000","contentType":"Resource","se_gradeLevels":["Class 4","Class 2"],"identifier":"do_2133916718042972161142","lastUpdatedBy":"4cd4c690-eab6-4938-855a-447c7b1b8ea9","audience":["Teacher"],"me_totalTimeSpentInSec":{"portal":325},"visibility":"Default","author":"Adarsh","consumerId":"962257b5-dab6-44a6-b8db-6933b3963dc7","discussionForum":{"enabled":"No"},"index":1,"mediaType":"content","osId":"org.ekstep.quiz.app","languageCode":["en"],"lastPublishedBy":"08631a74-4b94-4cf7-a818-831135248a4a","version":2,"se_subjects":["Home Science"],"license":"CC BY 4.0","prevState":"Review","size":1.054662E7,"lastPublishedOn":"2021-10-20T11:06:48.254+0000","name":"Oct20 Mp4","attributions":["Tester"],"status":"Live","code":"9194086b-4c4c-45d6-92c9-98828a4aae74","interceptionPoints":{},"credentials":{"enabled":"No"},"prevStatus":"Review","streamingUrl":"https://ntppreprodmedia-inct.streaming.media.azure.net/29886fa7-95d5-4cc3-bf5f-be9c66fb78cb/sample-mp4-file.ism/manifest(format=m3u8-aapl-v3)","medium":["English","Tamil"],"idealScreenSize":"normal","createdOn":"2021-10-20T10:26:02.051+0000","se_boards":["State (Tamil Nadu)"],"copyrightYear":2021,"contentDisposition":"inline","additionalCategories":["Textbook"],"licenseterms":"By creating any type of content (resources, books, courses etc.) on DIKSHA, you consent to publish it under the Creative Commons License Framework. Please choose the applicable creative commons license you wish to apply to your content.","lastUpdatedOn":"2021-10-20T11:36:17.429+0000","dialcodeRequired":"No","lastStatusChangedOn":"2021-10-20T11:06:49.079+0000","createdFor":["01269878797503692810","0133480174794670081832"],"creator":"ContentcreatorTN","os":["All"],"se_FWIds":["tn_k-12_5"],"pkgVersion":2,"versionKey":"1634729777429","idealScreenDensity":"hdpi","framework":"tn_k-12_5","depth":5,"lastSubmittedOn":"2021-10-20T10:30:29.896+0000","createdBy":"4cd4c690-eab6-4938-855a-447c7b1b8ea9","compatibilityLevel":1,"board":"State (Tamil Nadu)","resourceType":"Learn"}],"contentDisposition":"inline","lastUpdatedOn":"2021-10-25T06:05:49.619+0000","contentEncoding":"gzip","generateDIALCodes":"No","contentType":"TextBookUnit","dialcodeRequired":"No","identifier":"do_2133950816030965761512","lastStatusChangedOn":"2021-10-25T06:03:17.253+0000","audience":["Student"],"os":["All"],"visibility":"Parent","discussionForum":{"enabled":"No"},"index":1,"mediaType":"content","osId":"org.ekstep.launcher","languageCode":["en"],"version":2,"pkgVersion":1.0,"versionKey":"1635141797253","license":"CC BY 4.0","idealScreenDensity":"hdpi","framework":"tn_k-12_5","depth":4,"lastPublishedOn":"2021-10-25T06:05:49.988+0000","compatibilityLevel":1,"leafNodesCount":1,"name":"dsffgdg","status":"Draft"}],"contentDisposition":"inline","lastUpdatedOn":"2021-10-25T06:05:49.619+0000","contentEncoding":"gzip","generateDIALCodes":"No","contentType":"TextBookUnit","dialcodeRequired":"No","identifier":"do_2133950816030801921506","lastStatusChangedOn":"2021-10-25T06:03:17.251+0000","audience":["Student"],"os":["All"],"visibility":"Parent","discussionForum":{"enabled":"No"},"index":1,"mediaType":"content","osId":"org.ekstep.launcher","languageCode":["en"],"version":2,"pkgVersion":1.0,"versionKey":"1635141797251","license":"CC BY 4.0","idealScreenDensity":"hdpi","framework":"tn_k-12_5","depth":3,"lastPublishedOn":"2021-10-25T06:05:49.988+0000","compatibilityLevel":1,"leafNodesCount":1,"name":"5.2.1 Respiratory System","status":"Draft"}],"contentDisposition":"inline","lastUpdatedOn":"2021-10-25T06:05:49.619+0000","contentEncoding":"gzip","generateDIALCodes":"No","contentType":"TextBookUnit","dialcodeRequired":"No","identifier":"do_2133950816031047681514","lastStatusChangedOn":"2021-10-25T06:03:17.254+0000","audience":["Student"],"os":["All"],"visibility":"Parent","discussionForum":{"enabled":"No"},"index":2,"mediaType":"content","osId":"org.ekstep.launcher","languageCode":["en"],"version":2,"pkgVersion":1.0,"versionKey":"1635141797254","license":"CC BY 4.0","idealScreenDensity":"hdpi","framework":"tn_k-12_5","depth":2,"lastPublishedOn":"2021-10-25T06:05:49.988+0000","compatibilityLevel":1,"leafNodesCount":1,"name":"5.2 Organ Systems","status":"Draft"},{"ownershipType":["createdBy"],"parent":"do_2133950816031047681516","unitIdentifiers":["do_21329099910853427215263"],"copyright":"Kendriya_Vidyalaya,2020","organisationId":"32625405-bc59-4622-89bb-d06934d690ef","previewUrl":"https://preprodall.blob.core.windows.net/ntp-content-preprod/content/do_2132926691727114241157/artifact/do_2132926691727114241157_1622640280065_do_21329119747088384015821_1622462440773_pdf_1.pdf","keywords":["pdf"],"subject":["Hindi"],"channel":"013085024460783616158023","downloadUrl":"https://preprodall.blob.core.windows.net/ntp-content-preprod/content/do_2132926691727114241157/pdf_1.pdf_1634813465403_do_2132926691727114241157_2.ecar","language":["English"],"source":"https://dock.preprod.ntp.net.in/api/content/v1/read/do_21329119747088384015821","mimeType":"application/pdf","variants":{"full":{"ecarUrl":"https://preprodall.blob.core.windows.net/ntp-content-preprod/content/do_2132926691727114241157/pdf_1.pdf_1634813465403_do_2132926691727114241157_2.ecar","size":"259984"},"spine":{"ecarUrl":"https://preprodall.blob.core.windows.net/ntp-content-preprod/content/do_2132926691727114241157/pdf_1.pdf_1634813465462_do_2132926691727114241157_2_SPINE.ecar","size":"5422"}},"objectType":"Content","se_mediums":["English"],"gradeLevel":["Class 10"],"appIcon":"https://preprodall.blob.core.windows.net/ntp-content-preprod/content/do_2132926691727114241157/artifact/content.thumb.jpg","primaryCategory":"Teacher Resource","appId":"preprod.diksha.portal","contentEncoding":"identity","artifactUrl":"https://preprodall.blob.core.windows.net/ntp-content-preprod/content/do_2132926691727114241157/artifact/do_2132926691727114241157_1622640280065_do_21329119747088384015821_1622462440773_pdf_1.pdf","me_totalPlaySessionCount":{"portal":3},"sYS_INTERNAL_LAST_UPDATED_ON":"2021-10-25T05:46:52.419+0000","contentType":"MarkingSchemeRubric","se_gradeLevels":["Class 10"],"identifier":"do_2132926691727114241157","audience":["Administrator"],"me_totalTimeSpentInSec":{"portal":221},"visibility":"Default","consumerId":"962257b5-dab6-44a6-b8db-6933b3963dc7","discussionForum":{"enabled":"Yes"},"index":3,"mediaType":"content","osId":"org.ekstep.quiz.app","languageCode":["en"],"lastPublishedBy":"fcc5bf38-810b-4543-aa00-e96a1d59573a","version":2,"pragma":["external"],"se_subjects":["Hindi"],"license":"CC BY 4.0","prevState":"Review","size":256184.0,"lastPublishedOn":"2021-10-21T10:51:03.459+0000","name":"PDF_1.pdf","attributions":["kanmani"],"status":"Live","code":"c644316c-0a39-7c95-9ff4-c4f027d4c024","credentials":{"enabled":"No"},"prevStatus":"Processing","origin":"do_21329119747088384015821","description":"pdf","streamingUrl":"https://preprodall.blob.core.windows.net/ntp-content-preprod/content/do_2132926691727114241157/artifact/do_2132926691727114241157_1622640280065_do_21329119747088384015821_1622462440773_pdf_1.pdf","medium":["English"],"posterImage":"https://drive.google.com/uc?export=download&id=1z2kHz_wfjcOcDKfenkxWwqIlwtro6uv0","idealScreenSize":"normal","createdOn":"2021-06-02T13:24:39.874+0000","se_boards":["CBSE"],"processId":"2bb3d2ed-a0b5-4502-831b-e7e0e3423e4c","contentDisposition":"inline","lastUpdatedOn":"2021-10-21T10:51:05.506+0000","originData":{"identifier":"do_21329119747088384015821","repository":"https://dock.preprod.ntp.net.in/api/content/v1/read/do_21329119747088384015821"},"collectionId":"do_21329099910788710415244","dialcodeRequired":"No","lastStatusChangedOn":"2021-10-21T10:51:05.506+0000","creator":"కాయల్ నధి","os":["All"],"cloudStorageKey":"content/do_2132926691727114241157/artifact/do_2132926691727114241157_1622640280065_do_21329119747088384015821_1622462440773_pdf_1.pdf","se_FWIds":["ekstep_ncert_k-12"],"pkgVersion":2,"versionKey":"1622641657267","idealScreenDensity":"hdpi","framework":"ekstep_ncert_k-12","depth":2,"s3Key":"ecar_files/do_2132926691727114241157/pdf_1.pdf_1622641665371_do_2132926691727114241157_1.0.ecar","lastSubmittedOn":"2021-06-02T13:24:41.290+0000","createdBy":"4096d5cf-af62-48d6-88a3-e007219b8e59","compatibilityLevel":4,"board":"CBSE","programId":"013c04f0-c1c9-11eb-be1d-e5048cd700e9"}],"contentDisposition":"inline","lastUpdatedOn":"2021-10-25T10:29:28.137+0000","contentEncoding":"gzip","generateDIALCodes":"No","contentType":"TextBookUnit","dialcodeRequired":"No","identifier":"do_2133950816031047681516","lastStatusChangedOn":"2021-10-25T06:03:17.254+0000","audience":["Student"],"os":["All"],"visibility":"Parent","discussionForum":{"enabled":"No"},"index":1,"mediaType":"content","osId":"org.ekstep.launcher","languageCode":["en"],"version":2,"pkgVersion":1.0,"versionKey":"1635141797254","license":"CC BY 4.0","idealScreenDensity":"hdpi","framework":"tn_k-12_5","depth":1,"lastPublishedOn":"2021-10-25T06:05:49.988+0000","compatibilityLevel":1,"leafNodesCount":2,"name":"5. Human Body","attributions":[],"status":"Draft"},{"ownershipType":["createdBy"],"parent":"do_2133950809948078081503","code":"c1bed6fb-7e28-c033-d87a-365511c10614","credentials":{"enabled":"No"},"channel":"0126825293972439041","language":["English"],"mimeType":"application/vnd.ekstep.content-collection","idealScreenSize":"normal","createdOn":"2021-10-25T10:29:28.133+0000","objectType":"Content","primaryCategory":"Textbook Unit","children":[{"ownershipType":["createdBy"],"parent":"do_2133952124365455361558","code":"2ebb6e7c-e01b-5be1-7c33-d00334fcdd46","credentials":{"enabled":"No"},"channel":"0126825293972439041","language":["English"],"mimeType":"application/vnd.ekstep.content-collection","idealScreenSize":"normal","createdOn":"2021-10-25T10:29:28.137+0000","objectType":"Content","primaryCategory":"Textbook Unit","contentDisposition":"inline","lastUpdatedOn":"2021-10-25T10:29:28.137+0000","contentEncoding":"gzip","generateDIALCodes":"No","contentType":"TextBookUnit","dialcodeRequired":"No","identifier":"do_2133952124365783041560","lastStatusChangedOn":"2021-10-25T10:29:28.137+0000","audience":["Student"],"os":["All"],"visibility":"Parent","discussionForum":{"enabled":"No"},"index":1,"mediaType":"content","osId":"org.ekstep.launcher","languageCode":["en"],"version":2,"versionKey":"1635157768137","license":"CC BY 4.0","idealScreenDensity":"hdpi","depth":2,"compatibilityLevel":1,"name":"6.1 Medula Oblongata","attributions":[],"status":"Draft"}],"contentDisposition":"inline","lastUpdatedOn":"2021-10-25T10:29:28.133+0000","contentEncoding":"gzip","generateDIALCodes":"No","contentType":"TextBookUnit","dialcodeRequired":"No","identifier":"do_2133952124365455361558","lastStatusChangedOn":"2021-10-25T10:29:28.133+0000","audience":["Student"],"os":["All"],"visibility":"Parent","discussionForum":{"enabled":"No"},"index":2,"mediaType":"content","osId":"org.ekstep.launcher","languageCode":["en"],"version":2,"versionKey":"1635157768133","license":"CC BY 4.0","idealScreenDensity":"hdpi","depth":1,"compatibilityLevel":1,"name":"6. Inhuman Mind","attributions":[],"status":"Draft"}],"contentEncoding":"gzip","lockKey":"02bcb42d-d22d-4b08-a872-b8a22372d2a0","generateDIALCodes":"Yes","totalCompressedSize":1.0802804E7,"mimeTypesCount":"{\"application/pdf\":1,\"application/vnd.ekstep.content-collection\":7,\"video/mp4\":1}","sYS_INTERNAL_LAST_UPDATED_ON":"2021-10-25T06:05:50.280+0000","contentType":"TextBook","se_gradeLevels":["Class 1"],"trackable":{"enabled":"No","autoBatch":"No"},"identifier":"do_2133950809948078081503","audience":["Student"],"se_boardIds":["tn_k-12_5_board_statetamilnadu"],"subjectIds":["tn_k-12_5_subject_english"],"toc_url":"https://preprodall.blob.core.windows.net/ntp-content-preprod/content/do_2133950809948078081503/artifact/do_2133950809948078081503_toc.json","visibility":"Default","contentTypesCount":"{\"TextBookUnit\":7,\"MarkingSchemeRubric\":1,\"Resource\":1}","author":"Content 1 Creator1","consumerId":"962257b5-dab6-44a6-b8db-6933b3963dc7","childNodes":["do_2133950816030965761510","do_2133950816030883841508","do_2133950816031047681516","do_2133950816028917761504","do_2133916718042972161142","do_2133950816030965761512","do_2133950816030801921506","do_2133950816031047681514","do_2132926691727114241157","do_2133952124365783041560","do_2133952124365455361558"],"discussionForum":{"enabled":"No"},"mediaType":"content","osId":"org.ekstep.quiz.app","lastPublishedBy":"30b2571f-08f9-49ce-b97a-c643df0c82f7","languageCode":["en"],"version":2,"se_subjects":["English"],"license":"CC BY 4.0","prevState":"Review","size":12426.0,"lastPublishedOn":"2021-10-25T06:05:49.988+0000","name":"Test collection publishing","mediumIds":["tn_k-12_5_medium_english"],"status":"Draft","code":"org.sunbird.nV84DB","credentials":{"enabled":"No"},"prevStatus":"Processing","description":"Enter description for TextBook","medium":["English"],"idealScreenSize":"normal","createdOn":"2021-10-25T06:02:03.002+0000","se_boards":["State (Tamil Nadu)"],"se_mediumIds":["tn_k-12_5_medium_english"],"copyrightYear":2021,"contentDisposition":"inline","additionalCategories":["Textbook"],"lastUpdatedOn":"2021-10-25T06:05:49.619+0000","dialcodeRequired":"No","lastStatusChangedOn":"2021-10-25T10:29:28.562+0000","createdFor":["0126825293972439041","01323953134532198492165"],"creator":"Content 1 Creator1","os":["All"],"se_subjectIds":["tn_k-12_5_subject_english"],"se_FWIds":["tn_k-12_5"],"pkgVersion":1.0,"versionKey":"1635157768888","idealScreenDensity":"hdpi","framework":"tn_k-12_5","depth":0,"s3Key":"ecar_files/do_2133950809948078081503/test-collection-publishing_1635141950055_do_2133950809948078081503_1.0_spine.ecar","boardIds":["tn_k-12_5_board_statetamilnadu"],"lastSubmittedOn":"2021-10-25T06:04:24.346+0000","createdBy":"0b96635f-fe2b-4ab0-a511-05cfce8faa3f","compatibilityLevel":1,"leafNodesCount":2,"userConsent":"Yes","gradeLevelIds":["tn_k-12_5_gradelevel_class1"],"board":"State (Tamil Nadu)","resourceType":"Book"}', +'{"do_2133950809948078081503":{"name":"Collection Publish T21","children":["do_2133916718042972161142"],"root":true},"do_2133916718042972161142":{"name":"Collection Parent","children":["do_11340096165525094411"],"root":false,"relationalMetadata":{"do_11340096165525094411":{"name":"Test Name RM L1 - R1","keywords":["Overwriting content KW1"]}}},"do_11340096165525094411":{"name":"PDF Content","children":[],"root":false}' +); + +INSERT INTO dev_hierarchy_store.content_hierarchy(identifier, hierarchy, relational_metadata) VALUES ( +'do_123', +'{"identifier":"do_123","children":[{"ownershipType":["createdBy"],"parent":"do_123","code":"7cf20ea47763d420865bcc713def7a7b","keywords":["UnitKW1","UnitKW2"],"credentials":{"enabled":"No"},"channel":"0126825293972439041","description":"","language":["English"],"mimeType":"application/vnd.ekstep.content-collection","idealScreenSize":"normal","createdOn":"2021-12-21T19:54:31.617+0530","objectType":"Collection","primaryCategory":"Textbook Unit","children":[{"ownershipType":["createdBy"],"parent":"do_11340511137112064018","code":"9e1862f6518a7c87ee693cebb4fec278","keywords":["UnitKW1L2","UnitKW2L2"],"credentials":{"enabled":"No"},"channel":"0126825293972439041","description":"This section describes about various part of the body such as head, hands, legs etc.","language":["English"],"mimeType":"application/vnd.ekstep.content-collection","idealScreenSize":"normal","createdOn":"2021-12-21T19:54:31.660+0530","objectType":"Collection","primaryCategory":"Textbook Unit","children":[{"ownershipType":["createdBy"],"parent":"do_11343567193423872014","previewUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/assets/do_1134009478823116801129/chapter_1.pdf","keywords":["CPPDFContent1","CPPDFContent2","CollectionKW1"],"channel":"0126825293972439041","downloadUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11340096165525094411/collection-publishing-pdf-content_1635860615969_do_11340096165525094411_1.ecar","language":["English"],"mimeType":"application/pdf","variants":{"full":{"ecarUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11340096165525094411/collection-publishing-pdf-content_1635860615969_do_11340096165525094411_1.ecar","size":"256918"},"spine":{"ecarUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11340096165525094411/collection-publishing-pdf-content_1635860619148_do_11340096165525094411_1_SPINE.ecar","size":"6378"}},"objectType":"Content","appIcon":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11340094790233292811/artifact/033019_sz_reviews_feat_1564126718632.thumb.jpg","primaryCategory":"Explanation Content","contentEncoding":"identity","artifactUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/assets/do_1134009478823116801129/chapter_1.pdf","contentType":"Resource","identifier":"do_11340096165525094411","audience":["Student"],"visibility":"Default","discussionForum":{"enabled":"No"},"index":1,"mediaType":"content","osId":"org.ekstep.quiz.app","languageCode":["en"],"lastPublishedBy":"","version":2,"pragma":["external"],"license":"CC BY 4.0","prevState":"Draft","lastPublishedOn":"2021-11-02T19:13:35.589+0530","name":"Collection Publishing PDF Content","status":"Live","code":"c9ce1ce0-b9b4-402e-a9c3-556701070838","interceptionPoints":{},"credentials":{"enabled":"No"},"prevStatus":"Processing","streamingUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/assets/do_1134009478823116801129/chapter_1.pdf","idealScreenSize":"normal","createdOn":"2021-11-02T18:56:17.917+0530","copyrightYear":2021,"contentDisposition":"inline","lastUpdatedOn":"2021-11-02T19:13:39.729+0530","dialcodeRequired":"No","lastStatusChangedOn":"2021-11-02T19:13:39.729+0530","createdFor":["01309282781705830427"],"creator":"N131","os":["All"],"se_FWIds":["ekstep_ncert_k-12"],"pkgVersion":1,"versionKey":"1635859577917","idealScreenDensity":"hdpi","framework":"ekstep_ncert_k-12","depth":3,"createdBy":"0b71985d-fcb0-4018-ab14-83f10c3b0426","compatibilityLevel":4,"resourceType":"Learn"}],"contentDisposition":"inline","lastUpdatedOn":"2021-12-21T19:55:11.840+0530","contentEncoding":"gzip","generateDIALCodes":"No","contentType":"TextBookUnit","dialcodeRequired":"No","identifier":"do_11343567193423872014","lastStatusChangedOn":"2021-12-21T19:54:31.661+0530","audience":["Student"],"os":["All"],"visibility":"Parent","discussionForum":{"enabled":"No"},"index":1,"mediaType":"content","osId":"org.ekstep.launcher","languageCode":["en"],"version":2,"versionKey":"1640096671660","license":"CC BY 4.0","idealScreenDensity":"hdpi","framework":"ncert_k-12","depth":2,"compatibilityLevel":1,"name":"L2 Folder","attributions":[],"status":"Draft"},{"ownershipType":["createdBy"],"parent":"do_11340511137112064018","previewUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/assets/do_1134009478823116801129/chapter_1.pdf","keywords":["CPPDFContent1","CPPDFContent2","CollectionKW1"],"channel":"0126825293972439041","downloadUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11340096165525094411/collection-publishing-pdf-content_1635860615969_do_11340096165525094411_1.ecar","language":["English"],"mimeType":"application/pdf","variants":{"full":{"ecarUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11340096165525094411/collection-publishing-pdf-content_1635860615969_do_11340096165525094411_1.ecar","size":"256918"},"spine":{"ecarUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11340096165525094411/collection-publishing-pdf-content_1635860619148_do_11340096165525094411_1_SPINE.ecar","size":"6378"}},"objectType":"Content","appIcon":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11340094790233292811/artifact/033019_sz_reviews_feat_1564126718632.thumb.jpg","primaryCategory":"Explanation Content","contentEncoding":"identity","artifactUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/assets/do_1134009478823116801129/chapter_1.pdf","contentType":"Resource","identifier":"do_11340096165525094411","audience":["Student"],"visibility":"Default","discussionForum":{"enabled":"No"},"index":2,"mediaType":"content","osId":"org.ekstep.quiz.app","languageCode":["en"],"lastPublishedBy":"","version":2,"pragma":["external"],"license":"CC BY 4.0","prevState":"Draft","lastPublishedOn":"2021-11-02T19:13:35.589+0530","name":"Collection Publishing PDF Content","status":"Live","code":"c9ce1ce0-b9b4-402e-a9c3-556701070838","interceptionPoints":{},"credentials":{"enabled":"No"},"prevStatus":"Processing","streamingUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/assets/do_1134009478823116801129/chapter_1.pdf","idealScreenSize":"normal","createdOn":"2021-11-02T18:56:17.917+0530","copyrightYear":2021,"contentDisposition":"inline","lastUpdatedOn":"2021-11-02T19:13:39.729+0530","dialcodeRequired":"No","lastStatusChangedOn":"2021-11-02T19:13:39.729+0530","createdFor":["01309282781705830427"],"creator":"N131","os":["All"],"se_FWIds":["ekstep_ncert_k-12"],"pkgVersion":1,"versionKey":"1635859577917","idealScreenDensity":"hdpi","framework":"ekstep_ncert_k-12","depth":2,"createdBy":"0b71985d-fcb0-4018-ab14-83f10c3b0426","compatibilityLevel":4,"resourceType":"Learn"}],"contentDisposition":"inline","lastUpdatedOn":"2021-12-21T19:55:11.837+0530","contentEncoding":"gzip","generateDIALCodes":"No","contentType":"TextBookUnit","dialcodeRequired":"No","identifier":"do_11340511137112064018","lastStatusChangedOn":"2021-12-21T19:54:31.617+0530","audience":["Student"],"os":["All"],"visibility":"Parent","discussionForum":{"enabled":"No"},"index":1,"mediaType":"content","osId":"org.ekstep.launcher","languageCode":["en"],"version":2,"versionKey":"1640096671617","license":"CC BY 4.0","idealScreenDensity":"hdpi","framework":"ncert_k-12","depth":1,"compatibilityLevel":1,"name":"Collection Parent","attributions":[],"status":"Draft"}]}', +'{"do_123":{"name":"Collection Publish T21","children":["do_11340511137112064018","do_11340511137080934412"],"root":true},"do_11340511137112064018":{"name":"Collection Parent","children":["do_11340096165525094411"],"root":false,"relationalMetadata":{"do_11340096165525094411":{"name":"Test Name RM L1 - R1","keywords":["Overwriting content KW1"]}}},"do_11340511137080934412":{"name":"Collection Parent","children":["do_11340096165525094411"],"root":false,"relationalMetadata":{"do_11340096165525094411":{"name":"Test Name RM L1 - R1","keywords":["Overwriting content KW1"]}}},"do_11340096165525094411":{"name":"PDF Content","children":[],"root":false},"do_113405111371145216110":{"name":"test","children":[], "root":false}}' +); + +INSERT INTO dev_hierarchy_store.content_hierarchy(identifier, hierarchy) VALUES ( +'do_21354027142511820812318.img', +'{ "ownershipType": [ "createdBy" ], "copyright": "tn", "se_gradeLevelIds": [ "tn_k-12_5_gradelevel_class1" ], "keywords": [ "வாக்கியங்கள்", "വളരെ", "बतख़!", "Drop", "മലയാളം" ], "subject": [ "English" ], "channel": "01269878797503692810", "downloadUrl": "https://sunbirdstagingpublic.blob.core.windows.net/sunbird-content-staging/content/do_21354027142511820812318/dialcodehierarchy_1652871221390_do_21354027142511820812318_1_SPINE.ecar", "organisation": [ "Tamil Nadu" ], "language": [ "English" ], "mimeType": "application/vnd.ekstep.content-collection", "variants": { "spine": { "ecarUrl": "https://sunbirdstagingpublic.blob.core.windows.net/sunbird-content-staging/content/do_21354027142511820812318/dialcodehierarchy_1652871221390_do_21354027142511820812318_1_SPINE.ecar", "size": "16926" }, "online": { "ecarUrl": "https://sunbirdstagingpublic.blob.core.windows.net/sunbird-content-staging/content/do_21354027142511820812318/dialcodehierarchy_1652871221531_do_21354027142511820812318_1_ONLINE.ecar", "size": "8313" } }, "leafNodes": [ "do_21353901951355289618149" ], "objectType": "Content", "se_mediums": [ "English" ], "gradeLevel": [ "Class 1" ], "appIcon": "", "primaryCategory": "Digital Textbook", "children": [ { "ownershipType": [ "createdBy" ], "parent": "do_21354027142511820812318", "code": "80986053-eb44-0c35-1c3b-c84806d4775a", "credentials": { "enabled": "No" }, "channel": "01269878797503692810", "downloadUrl": "https://sunbirdstagingpublic.blob.core.windows.net/sunbird-content-staging/content/do_21354027142511820812318/dialcodehierarchy_1652871221390_do_21354027142511820812318_1_SPINE.ecar", "language": [ "English" ], "mimeType": "application/vnd.ekstep.content-collection", "variants": { "spine": { "ecarUrl": "https://sunbirdstagingpublic.blob.core.windows.net/sunbird-content-staging/content/do_21354027142511820812318/dialcodehierarchy_1652871221390_do_21354027142511820812318_1_SPINE.ecar", "size": "16926" }, "online": { "ecarUrl": "https://sunbirdstagingpublic.blob.core.windows.net/sunbird-content-staging/content/do_21354027142511820812318/dialcodehierarchy_1652871221531_do_21354027142511820812318_1_ONLINE.ecar", "size": "8313" } }, "leafNodes": [ "do_21353901951355289618149" ], "idealScreenSize": "normal", "createdOn": "2022-05-18T10:50:55.849+0000", "objectType": "Content", "primaryCategory": "Textbook Unit", "children": [ { "ownershipType": [ "createdBy" ], "parent": "do_21354031968951500812320", "copyright": "tn", "se_gradeLevelIds": [ "tn_k-12_5_gradelevel_class1" ], "keywords": [ "Drop" ], "subject": [ "Accountancy" ], "targetMediumIds": [ "tn_k-12_5_medium_english" ], "channel": "01269878797503692810", "downloadUrl": "https://sunbirdstagingpublic.blob.core.windows.net/sunbird-content-staging/content/do_21354022673739776012312/newcert_1652859862583_do_21354022673739776012312_1_SPINE.ecar", "organisation": [ "Tamil Nadu", "MPPS GYARAGONDANAHALLI" ], "language": [ "English" ], "mimeType": "application/vnd.ekstep.content-collection", "variants": { "spine": { "ecarUrl": "https://sunbirdstagingpublic.blob.core.windows.net/sunbird-content-staging/content/do_21354022673739776012312/newcert_1652859862583_do_21354022673739776012312_1_SPINE.ecar", "size": "13797" }, "online": { "ecarUrl": "https://sunbirdstagingpublic.blob.core.windows.net/sunbird-content-staging/content/do_21354022673739776012312/newcert_1652859862667_do_21354022673739776012312_1_ONLINE.ecar", "size": "5181" } }, "leafNodes": [ "do_21353901951355289618149" ], "targetGradeLevelIds": [ "tn_k-12_5_gradelevel_class1" ], "objectType": "Content", "se_mediums": [ "English" ], "primaryCategory": "Course", "appId": "staging.sunbird.portal", "contentEncoding": "gzip", "lockKey": "3efeaad1-490b-4044-8185-52a421057844", "generateDIALCodes": "No", "totalCompressedSize": 209850, "mimeTypesCount": "{\"application/pdf\":1,\"application/vnd.ekstep.content-collection\":1}", "sYS_INTERNAL_LAST_UPDATED_ON": "2022-05-18T07:45:16.264+0000", "contentType": "Course", "se_gradeLevels": [ "Class 1" ], "trackable": { "enabled": "Yes", "autoBatch": "No" }, "identifier": "do_21354022673739776012312", "audience": [ "Student" ], "se_boardIds": [ "tn_k-12_5_board_statetamilnadu" ], "subjectIds": [ "tn_k-12_5_subject_accountancy" ], "toc_url": "https://sunbirdstagingpublic.blob.core.windows.net/sunbird-content-staging/content/do_21354022673739776012312/artifact/do_21354022673739776012312_toc.json", "visibility": "Default", "contentTypesCount": "{\"eTextBook\":1,\"CourseUnit\":1}", "author": "Guest name changed", "consumerId": "cb069f8d-e4e1-46c5-831f-d4a83b323ada", "childNodes": [ "do_21353901951355289618149", "do_21354022740915814412313" ], "discussionForum": { "enabled": "Yes" }, "index": 1, "mediaType": "content", "osId": "org.ekstep.quiz.app", "languageCode": [ "en" ], "lastPublishedBy": "08631a74-4b94-4cf7-a818-831135248a4a", "version": 2, "se_subjects": [ "Accountancy" ], "license": "CC BY 4.0", "prevState": "Review", "size": 13797, "lastPublishedOn": "2022-05-18T07:44:22.530+0000", "name": "newcert", "targetBoardIds": [ "tn_k-12_5_board_statetamilnadu" ], "status": "Live", "code": "org.sunbird.11SAbB", "credentials": { "enabled": "Yes" }, "prevStatus": "Processing", "description": "Enter description for Course", "idealScreenSize": "normal", "createdOn": "2022-05-18T07:41:49.160+0000", "reservedDialcodes": { "U5X4C5": 0 }, "batches": [ { "createdFor": [ "01269878797503692810", "01275630040485068839017" ], "endDate": "2022-05-20", "name": "nc", "batchId": "013540229187338240118", "enrollmentType": "open", "enrollmentEndDate": "2022-05-19", "startDate": "2022-05-18", "status": 1 } ], "se_boards": [ "State (Tamil Nadu)" ], "targetSubjectIds": [ "tn_k-12_5_subject_english" ], "se_mediumIds": [ "tn_k-12_5_medium_english" ], "copyrightYear": 2022, "contentDisposition": "inline", "additionalCategories": [ "Textbook" ], "lastUpdatedOn": "2022-05-18T07:44:22.749+0000", "dialcodeRequired": "No", "lastStatusChangedOn": "2022-05-18T07:44:22.749+0000", "createdFor": [ "01269878797503692810" ], "creator": "Guest name changed", "os": [ "All" ], "se_subjectIds": [ "tn_k-12_5_subject_accountancy", "tn_k-12_5_subject_english" ], "se_FWIds": [ "tn_k-12_5" ], "targetFWIds": [ "tn_k-12_5" ], "pkgVersion": 1, "versionKey": "1652859827944", "idealScreenDensity": "hdpi", "framework": "tn_k-12_5", "dialcodes": [ "U5X4C5" ], "depth": 0, "s3Key": "content/do_21354022673739776012312/artifact/do_21354022673739776012312_toc.json", "lastSubmittedOn": "2022-05-18T07:43:47.614+0000", "createdBy": "fca2925f-1eee-4654-9177-fece3fd6afc9", "compatibilityLevel": 4, "leafNodesCount": 1, "userConsent": "Yes", "resourceType": "Course" } ], "contentDisposition": "inline", "lastUpdatedOn": "2022-05-18T10:50:56.492+0000", "contentEncoding": "gzip", "generateDIALCodes": "No", "contentType": "TextBookUnit", "dialcodeRequired": "Yes", "identifier": "do_21354031968951500812320", "lastStatusChangedOn": "2022-05-18T10:50:55.849+0000", "audience": [ "Student" ], "os": [ "All" ], "visibility": "Parent", "discussionForum": { "enabled": "No" }, "index": 1, "mediaType": "content", "osId": "org.ekstep.launcher", "languageCode": [ "en" ], "version": 2, "pkgVersion": 1, "versionKey": "1652871055849", "license": "CC BY 4.0", "idealScreenDensity": "hdpi", "dialcodes": [ "R8C7G2" ], "depth": 1, "lastPublishedOn": "2022-05-18T10:53:41.260+0000", "compatibilityLevel": 1, "leafNodesCount": 1, "name": "Ch1 ", "attributions": [], "status": "Draft" }, { "ownershipType": [ "createdBy" ], "parent": "do_21354027142511820812318", "code": "22c05b75-ae2a-6280-c192-2e2a4157d141", "credentials": { "enabled": "No" }, "channel": "01269878797503692810", "downloadUrl": "https://sunbirdstagingpublic.blob.core.windows.net/sunbird-content-staging/content/do_21354027142511820812318/dialcodehierarchy_1652871221390_do_21354027142511820812318_1_SPINE.ecar", "language": [ "English" ], "mimeType": "application/vnd.ekstep.content-collection", "variants": { "spine": { "ecarUrl": "https://sunbirdstagingpublic.blob.core.windows.net/sunbird-content-staging/content/do_21354027142511820812318/dialcodehierarchy_1652871221390_do_21354027142511820812318_1_SPINE.ecar", "size": "16926" }, "online": { "ecarUrl": "https://sunbirdstagingpublic.blob.core.windows.net/sunbird-content-staging/content/do_21354027142511820812318/dialcodehierarchy_1652871221531_do_21354027142511820812318_1_ONLINE.ecar", "size": "8313" } }, "leafNodes": [], "idealScreenSize": "normal", "createdOn": "2022-05-18T10:50:55.854+0000", "objectType": "Content", "primaryCategory": "Textbook Unit", "children": [ { "ownershipType": [ "createdBy" ], "parent": "do_21354031968955596812322", "copyright": "tn", "se_gradeLevelIds": [ "tn_k-12_5_gradelevel_class1" ], "keywords": [ "बतख़!", "വളരെ", "வாக்கியங்கள்", "ಕನ್ನಡ" ], "subject": [ "English" ], "channel": "01269878797503692810", "downloadUrl": "https://sunbirdstagingpublic.blob.core.windows.net/sunbird-content-staging/content/do_21353947184001843212121/knndd_1652767948813_do_21353947184001843212121_1_SPINE.ecar", "organisation": [ "Tamil Nadu", "MPPS HANUMANNAHALLI" ], "language": [ "English" ], "mimeType": "application/vnd.ekstep.content-collection", "variants": { "spine": { "ecarUrl": "https://sunbirdstagingpublic.blob.core.windows.net/sunbird-content-staging/content/do_21353947184001843212121/knndd_1652767948813_do_21353947184001843212121_1_SPINE.ecar", "size": "4019" }, "online": { "ecarUrl": "https://sunbirdstagingpublic.blob.core.windows.net/sunbird-content-staging/content/do_21353947184001843212121/knndd_1652767948873_do_21353947184001843212121_1_ONLINE.ecar", "size": "4018" } }, "objectType": "Content", "se_mediums": [ "English" ], "gradeLevel": [ "Class 1" ], "appIcon": "", "primaryCategory": "Digital Textbook", "contentEncoding": "gzip", "lockKey": "35daaa23-b864-40b6-8709-fedc31de95c0", "generateDIALCodes": "Yes", "totalCompressedSize": 0, "mimeTypesCount": "{\"application/vnd.ekstep.content-collection\":3}", "sYS_INTERNAL_LAST_UPDATED_ON": "2022-05-17T06:12:28.810+0000", "contentType": "TextBook", "se_gradeLevels": [ "Class 1" ], "trackable": { "enabled": "No", "autoBatch": "No" }, "identifier": "do_21353947184001843212121", "audience": [ "Student" ], "se_boardIds": [ "tn_k-12_5_board_statetamilnadu" ], "subjectIds": [ "tn_k-12_5_subject_english" ], "toc_url": "https://sunbirdstagingpublic.blob.core.windows.net/sunbird-content-staging/content/do_21353947184001843212121/artifact/do_21353947184001843212121_toc.json", "visibility": "Default", "contentTypesCount": "{\"TextBookUnit\":3}", "author": "Guest name changed", "consumerId": "cb069f8d-e4e1-46c5-831f-d4a83b323ada", "childNodes": [ "do_21353947322769408012133", "do_21353947287477452812131", "do_21353947357250355212135", "do_21353947535707340812137" ], "discussionForum": { "enabled": "No" }, "index": 1, "mediaType": "content", "osId": "org.ekstep.quiz.app", "languageCode": [ "en" ], "lastPublishedBy": "08631a74-4b94-4cf7-a818-831135248a4a", "version": 2, "se_subjects": [ "English" ], "license": "CC BY 4.0", "prevState": "Review", "size": 4019, "lastPublishedOn": "2022-05-17T06:12:27.957+0000", "name": "ಕನ್ನಡ", "mediumIds": [ "tn_k-12_5_medium_english" ], "status": "Draft", "code": "org.sunbird.zRvSTj", "credentials": { "enabled": "No" }, "prevStatus": "Processing", "description": "ಕನ್ನಡ", "medium": [ "English" ], "idealScreenSize": "normal", "createdOn": "2022-05-17T06:05:58.602+0000", "se_boards": [ "State (Tamil Nadu)" ], "se_mediumIds": [ "tn_k-12_5_medium_english" ], "copyrightYear": 2020, "contentDisposition": "inline", "additionalCategories": [ "Textbook" ], "lastUpdatedOn": "2022-05-17T06:13:08.755+0000", "dialcodeRequired": "No", "lastStatusChangedOn": "2022-05-17T06:13:08.396+0000", "createdFor": [ "01269878797503692810" ], "creator": "Guest name changed", "os": [ "All" ], "se_subjectIds": [ "tn_k-12_5_subject_english" ], "se_FWIds": [ "tn_k-12_5" ], "pkgVersion": 1, "versionKey": "1652767988755", "idealScreenDensity": "hdpi", "framework": "tn_k-12_5", "depth": 0, "s3Key": "content/do_21353947184001843212121/artifact/do_21353947184001843212121_toc.json", "boardIds": [ "tn_k-12_5_board_statetamilnadu" ], "lastSubmittedOn": "2022-05-17T06:12:01.588+0000", "createdBy": "fca2925f-1eee-4654-9177-fece3fd6afc9", "compatibilityLevel": 1, "leafNodesCount": 0, "userConsent": "Yes", "gradeLevelIds": [ "tn_k-12_5_gradelevel_class1" ], "board": "State (Tamil Nadu)", "resourceType": "Book" } ], "contentDisposition": "inline", "lastUpdatedOn": "2022-05-18T10:50:56.492+0000", "contentEncoding": "gzip", "generateDIALCodes": "No", "contentType": "TextBookUnit", "dialcodeRequired": "Yes", "identifier": "do_21354031968955596812322", "lastStatusChangedOn": "2022-05-18T10:50:55.854+0000", "audience": [ "Student" ], "os": [ "All" ], "visibility": "Parent", "discussionForum": { "enabled": "No" }, "index": 2, "mediaType": "content", "osId": "org.ekstep.launcher", "languageCode": [ "en" ], "version": 2, "pkgVersion": 1, "versionKey": "1652871055854", "license": "CC BY 4.0", "idealScreenDensity": "hdpi", "dialcodes": [ "L5L9A2" ], "depth": 1, "lastPublishedOn": "2022-05-18T10:53:41.260+0000", "compatibilityLevel": 1, "leafNodesCount": 0, "name": "Ch2", "attributions": [], "status": "Draft" }, { "ownershipType": [ "createdBy" ], "parent": "do_21354027142511820812318", "code": "6b8ccba1-9aa8-19d7-fe3b-35ca4be5d6cf", "credentials": { "enabled": "No" }, "channel": "01269878797503692810", "language": [ "English" ], "mimeType": "application/vnd.ekstep.content-collection", "idealScreenSize": "normal", "createdOn": "2022-05-18T11:01:38.790+0000", "objectType": "Content", "primaryCategory": "Textbook Unit", "children": [ { "ownershipType": [ "createdBy" ], "parent": "do_21354032495648768012324", "copyright": "2013", "se_gradeLevelIds": [ "ekstep_ncert_k-12_gradelevel_class3" ], "previewUrl": "https://sunbirdstagingpublic.blob.core.windows.net/sunbird-content-staging/content/do_21353901750772531217809/artifact/cbse-copy-88.pdf", "keywords": [ "Drop" ], "subject": [ "English" ], "channel": "01345815127107174426", "downloadUrl": "https://sunbirdstagingpublic.blob.core.windows.net/sunbird-content-staging/content/do_21353901750772531217809/aparna-86_1652722733905_do_21353901750772531217809_1.ecar", "language": [ "English" ], "source": "https://dockstaging.sunbirded.org/api/content/v1/read/do_21353901750772531217809", "mimeType": "application/pdf", "variants": { "full": { "ecarUrl": "https://sunbirdstagingpublic.blob.core.windows.net/sunbird-content-staging/content/do_21353901750772531217809/aparna-86_1652722733905_do_21353901750772531217809_1.ecar", "size": "193281" }, "spine": { "ecarUrl": "https://sunbirdstagingpublic.blob.core.windows.net/sunbird-content-staging/content/do_21353901750772531217809/aparna-86_1652722733964_do_21353901750772531217809_1_SPINE.ecar", "size": "10073" } }, "objectType": "Content", "se_mediums": [ "English" ], "gradeLevel": [ "Class 3" ], "appIcon": "https://sunbirdstagingpublic.blob.core.windows.net/sunbird-content-staging/content/do_21353901750772531217809/artifact/rose.thumb.jpg", "primaryCategory": "eTextbook", "contentEncoding": "identity", "artifactUrl": "https://sunbirdstagingpublic.blob.core.windows.net/sunbird-content-staging/content/do_21353901750772531217809/artifact/cbse-copy-88.pdf", "contentType": "eTextBook", "se_gradeLevels": [ "Class 3" ], "trackable": { "enabled": "No", "autoBatch": "No" }, "identifier": "do_21353901750772531217809", "audience": [ "Student" ], "se_boardIds": [ "ekstep_ncert_k-12_board_cbse" ], "subjectIds": [ "ekstep_ncert_k-12_subject_english" ], "visibility": "Default", "author": "Aparna", "discussionForum": { "enabled": "No" }, "index": 1, "mediaType": "content", "osId": "org.ekstep.quiz.app", "languageCode": [ "en" ], "lastPublishedBy": "56c84dee-7149-47af-902d-0138e080cec0", "version": 2, "pragma": [ "external" ], "se_subjects": [ "English" ], "license": "CC BY 4.0", "prevState": "Review", "size": 209850.0, "lastPublishedOn": "2022-05-16T17:38:53.877+0000", "name": "Aparna 86", "topic": [ "Water" ], "mediumIds": [ "ekstep_ncert_k-12_medium_english" ], "attributions": [ "Nadiya Anusha" ], "status": "Live", "topicsIds": [ "ekstep_ncert_k-12_topic_environmentalstudies_l1con_3" ], "code": "3d09fc2e-5705-8f9f-2e1d-4c9c8fd19de2", "interceptionPoints": {}, "credentials": { "enabled": "No" }, "prevStatus": "Processing", "origin": "do_21353901750772531217809", "description": "about water", "streamingUrl": "https://sunbirdstagingpublic.blob.core.windows.net/sunbird-content-staging/content/do_21353901750772531217809/artifact/cbse-copy-88.pdf", "medium": [ "English" ], "posterImage": "https://stagingdock.blob.core.windows.net/sunbird-content-dock/content/do_21353901750772531217809/artifact/rose.jpg", "idealScreenSize": "normal", "createdOn": "2022-05-16T17:38:50.225+0000", "se_boards": [ "CBSE" ], "se_mediumIds": [ "ekstep_ncert_k-12_medium_english" ], "processId": "497335f0-51d2-4cec-a3ba-6eb81309aa87", "contentDisposition": "inline", "lastUpdatedOn": "2022-05-16T17:38:54.005+0000", "originData": { "identifier": "do_21353901750772531217809", "repository": "https://dockstaging.sunbirded.org/api/content/v1/read/do_21353901750772531217809" }, "se_topicIds": [ "ekstep_ncert_k-12_topic_environmentalstudies_l1con_3" ], "dialcodeRequired": "No", "lastStatusChangedOn": "2022-05-16T17:38:54.005+0000", "createdFor": [ "01345815127107174426" ], "creator": "Aparna", "os": [ "All" ], "se_subjectIds": [ "ekstep_ncert_k-12_subject_english" ], "se_FWIds": [ "ekstep_ncert_k-12" ], "pkgVersion": 1, "versionKey": "1652722732649", "idealScreenDensity": "hdpi", "framework": "ekstep_ncert_k-12", "depth": 2, "boardIds": [ "ekstep_ncert_k-12_board_cbse" ], "lastSubmittedOn": "2022-05-16T17:38:52.212+0000", "createdBy": "c16ccf69-bd42-4dd3-a1e6-435322e78c97", "se_topics": [ "Water" ], "compatibilityLevel": 4, "gradeLevelIds": [ "ekstep_ncert_k-12_gradelevel_class3" ], "board": "CBSE", "programId": "9ae297d0-d51a-11ec-907e-e7dd0cd587df" } ], "contentDisposition": "inline", "lastUpdatedOn": "2022-05-18T11:01:38.790+0000", "contentEncoding": "gzip", "generateDIALCodes": "No", "contentType": "TextBookUnit", "dialcodeRequired": "Yes", "identifier": "do_21354032495648768012324", "lastStatusChangedOn": "2022-05-18T11:01:38.790+0000", "audience": [ "Student" ], "os": [ "All" ], "visibility": "Parent", "discussionForum": { "enabled": "No" }, "index": 3, "mediaType": "content", "osId": "org.ekstep.launcher", "languageCode": [ "en" ], "version": 2, "versionKey": "1652871698790", "license": "CC BY 4.0", "idealScreenDensity": "hdpi", "dialcodes": [ "D1D5H9" ], "depth": 1, "compatibilityLevel": 1, "name": "Ch3", "attributions": [], "status": "Draft" }, { "ownershipType": [ "createdBy" ], "parent": "do_21354027142511820812318", "code": "bf120af0-d50b-b700-baca-1f30214bdb27", "credentials": { "enabled": "No" }, "channel": "01269878797503692810", "language": [ "English" ], "mimeType": "application/vnd.ekstep.content-collection", "idealScreenSize": "normal", "createdOn": "2022-05-18T11:01:38.794+0000", "objectType": "Content", "primaryCategory": "Textbook Unit", "children": [ { "ownershipType": [ "createdBy" ], "parent": "do_21354032495652044812326", "copyright": "2013", "se_gradeLevelIds": [ "ekstep_ncert_k-12_gradelevel_class3" ], "previewUrl": "https://sunbirdstagingpublic.blob.core.windows.net/sunbird-content-staging/content/do_21353901951355289618149/artifact/cbse-copy-89.pdf", "keywords": [ "Drop" ], "subject": [ "English" ], "channel": "01345815127107174426", "downloadUrl": "https://sunbirdstagingpublic.blob.core.windows.net/sunbird-content-staging/content/do_21353901951355289618149/aparna-87_1652722745251_do_21353901951355289618149_1.ecar", "language": [ "English" ], "source": "https://dockstaging.sunbirded.org/api/content/v1/read/do_21353901951355289618149", "mimeType": "application/pdf", "variants": { "full": { "ecarUrl": "https://sunbirdstagingpublic.blob.core.windows.net/sunbird-content-staging/content/do_21353901951355289618149/aparna-87_1652722745251_do_21353901951355289618149_1.ecar", "size": "193301" }, "spine": { "ecarUrl": "https://sunbirdstagingpublic.blob.core.windows.net/sunbird-content-staging/content/do_21353901951355289618149/aparna-87_1652722745316_do_21353901951355289618149_1_SPINE.ecar", "size": "10093" } }, "objectType": "Content", "se_mediums": [ "English" ], "gradeLevel": [ "Class 3" ], "appIcon": "https://sunbirdstagingpublic.blob.core.windows.net/sunbird-content-staging/content/do_21353901951355289618149/artifact/rose.thumb.jpg", "primaryCategory": "eTextbook", "contentEncoding": "identity", "artifactUrl": "https://sunbirdstagingpublic.blob.core.windows.net/sunbird-content-staging/content/do_21353901951355289618149/artifact/cbse-copy-89.pdf", "contentType": "eTextBook", "se_gradeLevels": [ "Class 3" ], "trackable": { "enabled": "No", "autoBatch": "No" }, "identifier": "do_21353901951355289618149", "audience": [ "Student" ], "se_boardIds": [ "ekstep_ncert_k-12_board_cbse" ], "subjectIds": [ "ekstep_ncert_k-12_subject_english" ], "visibility": "Default", "author": "Aparna", "discussionForum": { "enabled": "No" }, "index": 1, "mediaType": "content", "osId": "org.ekstep.quiz.app", "languageCode": [ "en" ], "lastPublishedBy": "56c84dee-7149-47af-902d-0138e080cec0", "version": 2, "pragma": [ "external" ], "se_subjects": [ "English" ], "license": "CC BY 4.0", "prevState": "Review", "size": 209850.0, "lastPublishedOn": "2022-05-16T17:39:05.200+0000", "name": "Aparna 87", "topic": [ "Water" ], "mediumIds": [ "ekstep_ncert_k-12_medium_english" ], "attributions": [ "Nadiya Anusha" ], "status": "Live", "topicsIds": [ "ekstep_ncert_k-12_topic_environmentalstudies_l1con_3" ], "code": "2bf23df3-3fa3-c5ad-e1d0-6784a96cb2b8", "interceptionPoints": {}, "credentials": { "enabled": "No" }, "prevStatus": "Processing", "origin": "do_21353901951355289618149", "description": "about water", "streamingUrl": "https://sunbirdstagingpublic.blob.core.windows.net/sunbird-content-staging/content/do_21353901951355289618149/artifact/cbse-copy-89.pdf", "medium": [ "English" ], "posterImage": "https://stagingdock.blob.core.windows.net/sunbird-content-dock/content/do_21353901951355289618149/artifact/rose.jpg", "idealScreenSize": "normal", "createdOn": "2022-05-16T17:39:01.560+0000", "se_boards": [ "CBSE" ], "se_mediumIds": [ "ekstep_ncert_k-12_medium_english" ], "processId": "497335f0-51d2-4cec-a3ba-6eb81309aa87", "contentDisposition": "inline", "lastUpdatedOn": "2022-05-16T17:39:05.366+0000", "originData": { "identifier": "do_21353901951355289618149", "repository": "https://dockstaging.sunbirded.org/api/content/v1/read/do_21353901951355289618149" }, "se_topicIds": [ "ekstep_ncert_k-12_topic_environmentalstudies_l1con_3" ], "dialcodeRequired": "No", "lastStatusChangedOn": "2022-05-16T17:39:05.366+0000", "createdFor": [ "01345815127107174426" ], "creator": "27aprilindividual@yopmail.com", "os": [ "All" ], "se_subjectIds": [ "ekstep_ncert_k-12_subject_english" ], "se_FWIds": [ "ekstep_ncert_k-12" ], "pkgVersion": 1, "versionKey": "1652722743949", "idealScreenDensity": "hdpi", "framework": "ekstep_ncert_k-12", "depth": 2, "boardIds": [ "ekstep_ncert_k-12_board_cbse" ], "lastSubmittedOn": "2022-05-16T17:39:03.621+0000", "createdBy": "c6698266-f5d8-4e72-a831-5da9b3205d16", "se_topics": [ "Water" ], "compatibilityLevel": 4, "gradeLevelIds": [ "ekstep_ncert_k-12_gradelevel_class3" ], "board": "CBSE", "programId": "9ae297d0-d51a-11ec-907e-e7dd0cd587df" } ], "contentDisposition": "inline", "lastUpdatedOn": "2022-05-18T11:01:38.794+0000", "contentEncoding": "gzip", "generateDIALCodes": "No", "contentType": "TextBookUnit", "dialcodeRequired": "Yes", "identifier": "do_21354032495652044812326", "lastStatusChangedOn": "2022-05-18T11:01:38.794+0000", "audience": [ "Student" ], "os": [ "All" ], "visibility": "Parent", "discussionForum": { "enabled": "No" }, "index": 4, "mediaType": "content", "osId": "org.ekstep.launcher", "languageCode": [ "en" ], "version": 2, "versionKey": "1652871698794", "license": "CC BY 4.0", "idealScreenDensity": "hdpi", "dialcodes": [ "F6I6S6" ], "depth": 1, "compatibilityLevel": 1, "name": "Ch4", "attributions": [], "status": "Draft" } ], "appId": "staging.sunbird.portal", "contentEncoding": "gzip", "lockKey": "3e222f39-d917-4582-96ee-458f1ab80f30", "generateDIALCodes": "Yes", "totalCompressedSize": 209850, "mimeTypesCount": "{\"application/pdf\":1,\"application/vnd.ekstep.content-collection\":8,\"\":1}", "sYS_INTERNAL_LAST_UPDATED_ON": "2022-05-18T11:02:51.732+0000", "contentType": "TextBook", "se_gradeLevels": [ "Class 1" ], "trackable": { "enabled": "No", "autoBatch": "No" }, "identifier": "do_21354027142511820812318", "audience": [ "Student" ], "se_boardIds": [ "tn_k-12_5_board_statetamilnadu" ], "subjectIds": [ "tn_k-12_5_subject_english" ], "toc_url": "https://sunbirdstagingpublic.blob.core.windows.net/sunbird-content-staging/content/do_21354027142511820812318/artifact/do_21354027142511820812318_toc.json", "visibility": "Default", "contentTypesCount": "{\"TextBookUnit\":6,\"eTextBook\":1,\"Course\":1,\"CourseUnit\":1,\"\":1}", "author": "ContentcreatorTN", "consumerId": "cb069f8d-e4e1-46c5-831f-d4a83b323ada", "childNodes": [ "do_21354022673739776012312", "do_21354031968951500812320", "do_21353947184001843212121", "do_21354031968955596812322", "do_21353901750772531217809", "do_21354032495648768012324", "do_21353901951355289618149", "do_21354032495652044812326" ], "discussionForum": { "enabled": "No" }, "mediaType": "content", "osId": "org.ekstep.quiz.app", "lastPublishedBy": "08631a74-4b94-4cf7-a818-831135248a4a", "languageCode": [ "en" ], "version": 2, "se_subjects": [ "English" ], "license": "CC BY 4.0", "prevState": "Review", "qrCodeProcessId": "40c3e296-ccdc-487d-8cda-9f0a88c071d2", "size": 16926, "lastPublishedOn": "2022-05-18T10:53:41.260+0000", "name": "DialCodeHierarchy", "mediumIds": [ "tn_k-12_5_medium_english" ], "status": "Draft", "code": "org.sunbird.D3eL5z", "credentials": { "enabled": "No" }, "prevStatus": "Processing", "description": "Enter description for TextBook", "medium": [ "English" ], "idealScreenSize": "normal", "createdOn": "2022-05-18T09:12:44.203+0000", "reservedDialcodes": { "A3S8L6": 2, "R8C7G2": 1, "D1D5H9": 4, "V2A8N1": 0, "L5L9A2": 5, "F6I6S6": 3 }, "se_boards": [ "State (Tamil Nadu)" ], "se_mediumIds": [ "tn_k-12_5_medium_english" ], "copyrightYear": 2022, "contentDisposition": "inline", "additionalCategories": [ "Textbook" ], "lastUpdatedOn": "2022-05-18T10:53:41.611+0000", "dialcodeRequired": "Yes", "lastStatusChangedOn": "2022-05-18T11:01:39.892+0000", "createdFor": [ "01269878797503692810" ], "creator": "ContentcreatorTN", "os": [ "All" ], "se_subjectIds": [ "tn_k-12_5_subject_english" ], "se_FWIds": [ "tn_k-12_5" ], "pkgVersion": 1, "versionKey": "1652871771396", "idealScreenDensity": "hdpi", "framework": "tn_k-12_5", "dialcodes": [ "V2A8N1" ], "depth": 0, "s3Key": "content/do_21354027142511820812318/artifact/do_21354027142511820812318_toc.json", "boardIds": [ "tn_k-12_5_board_statetamilnadu" ], "lastSubmittedOn": "2022-05-18T10:53:03.017+0000", "createdBy": "4cd4c690-eab6-4938-855a-447c7b1b8ea9", "compatibilityLevel": 1, "leafNodesCount": 1, "userConsent": "Yes", "gradeLevelIds": [ "tn_k-12_5_gradelevel_class1" ], "board": "State (Tamil Nadu)", "resourceType": "Book" }' +); + +INSERT INTO dev_hierarchy_store.content_hierarchy(identifier, hierarchy) VALUES ( +'do_21354027142511820812318', +'{"ownershipType": [ "createdBy"],"publish_type": "public","copyright": "tn","se_gradeLevelIds": [ "tn_k-12_5_gradelevel_class1"],"keywords": [ "வாக்கியங்கள்", "വളരെ", "बतख़!", "Drop", "മലയാളം"],"subject": [ "English"],"channel": "01269878797503692810","downloadUrl": "https://sunbirdstagingpublic.blob.core.windows.net/sunbird-content-staging/content/do_21354027142511820812318/dialcodehierarchy_1652871221390_do_21354027142511820812318_1_SPINE.ecar","organisation": [ "Tamil Nadu"],"language": [ "English"],"mimeType": "application/vnd.ekstep.content-collection","variants": "{\"spine\":{\"ecarUrl\":\"https://sunbirdstagingpublic.blob.core.windows.net/sunbird-content-staging/content/do_21354027142511820812318/dialcodehierarchy_1652871221390_do_21354027142511820812318_1_SPINE.ecar\",\"size\":\"16926\"},\"online\":{\"ecarUrl\":\"https://sunbirdstagingpublic.blob.core.windows.net/sunbird-content-staging/content/do_21354027142511820812318/dialcodehierarchy_1652871221531_do_21354027142511820812318_1_ONLINE.ecar\",\"size\":\"8313\"}}","body": null,"leafNodes": [ "do_21353901951355289618149"],"objectType": "Content","se_mediums": [ "English"],"gradeLevel": [ "Class 1"],"appIcon": "","children": [ { "lastStatusChangedOn": "2022-05-18T10:50:55.849+0000", "parent": "do_21354027142511820812318", "children": [{ "copyright": "tn", "lastStatusChangedOn": "2022-05-18T07:41:49.160+0000", "publish_type": "public", "parent": "do_21354031968951500812320", "author": "Guest name changed", "se_mediumIds": [ "tn_k-12_5_medium_english" ], "organisation": [ "Tamil Nadu", "MPPS GYARAGONDANAHALLI" ], "children": [ {"lastStatusChangedOn": "2022-05-18T07:43:11.157+0000","parent": "do_21354022673739776012312","children": [ { "copyright": "2013", "lastStatusChangedOn": "2022-05-16T17:39:05.366+0000", "originData": "{\"identifier\":\"do_21353901951355289618149\",\"repository\":\"https://dockstaging.sunbirded.org/api/content/v1/read/do_21353901951355289618149\"}", "parent": "do_21354022740915814412313", "author": "Aparna", "se_mediumIds": ["ekstep_ncert_k-12_medium_english" ], "mediaType": "content", "name": "Aparna 87", "se_topicIds": ["ekstep_ncert_k-12_topic_environmentalstudies_l1con_3" ], "discussionForum": "{\"enabled\":\"No\"}", "createdOn": "2022-05-16T17:39:01.560+0000", "createdFor": ["01345815127107174426" ], "channel": "01345815127107174426", "source": "https://dockstaging.sunbirded.org/api/content/v1/read/do_21353901951355289618149", "lastUpdatedOn": "2022-05-16T17:39:05.366+0000", "subject": ["English" ], "size": 209850.0, "se_topics": ["Water" ], "streamingUrl": "https://sunbirdstagingpublic.blob.core.windows.net/sunbird-content-staging/content/do_21353901951355289618149/artifact/cbse-copy-89.pdf", "identifier": "do_21353901951355289618149", "se_gradeLevelIds": ["ekstep_ncert_k-12_gradelevel_class3" ], "description": "about water", "gradeLevel": ["Class 3" ], "ownershipType": ["createdBy" ], "mediumIds": ["ekstep_ncert_k-12_medium_english" ], "compatibilityLevel": 4, "audience": ["Student" ], "trackable": "{\"enabled\":\"No\",\"autoBatch\":\"No\"}", "se_boards": ["CBSE" ], "os": ["All" ], "primaryCategory": "eTextbook", "appIcon": "https://sunbirdstagingpublic.blob.core.windows.net/sunbird-content-staging/content/do_21353901951355289618149/artifact/rose.thumb.jpg", "languageCode": ["en" ], "se_mediums": ["English" ], "se_subjectIds": ["ekstep_ncert_k-12_subject_english" ], "downloadUrl": "https://sunbirdstagingpublic.blob.core.windows.net/sunbird-content-staging/content/do_21353901951355289618149/aparna-87_1652722745251_do_21353901951355289618149_1.ecar", "se_subjects": ["English" ], "medium": ["English" ], "attributions": ["Nadiya Anusha" ], "framework": "ekstep_ncert_k-12", "posterImage": "https://stagingdock.blob.core.windows.net/sunbird-content-dock/content/do_21353901951355289618149/artifact/rose.jpg", "creator": "27aprilindividual@yopmail.com", "versionKey": "1652722743949", "mimeType": "application/pdf", "code": "2bf23df3-3fa3-c5ad-e1d0-6784a96cb2b8", "se_boardIds": ["ekstep_ncert_k-12_board_cbse" ], "license": "CC BY 4.0", "version": 2, "prevStatus": "Processing", "contentType": "eTextBook", "prevState": "Review", "language": ["English" ], "board": "CBSE", "lastPublishedOn": "2022-05-16T17:39:05.200+0000", "objectType": "Content", "origin": "do_21353901951355289618149", "subjectIds": ["ekstep_ncert_k-12_subject_english" ], "status": "Live", "pragma": ["external" ], "programId": "9ae297d0-d51a-11ec-907e-e7dd0cd587df", "createdBy": "c6698266-f5d8-4e72-a831-5da9b3205d16", "dialcodeRequired": "No", "lastSubmittedOn": "2022-05-16T17:39:03.621+0000", "interceptionPoints": "{}", "keywords": ["Drop" ], "idealScreenSize": "normal", "contentEncoding": "identity", "depth": 2, "lastPublishedBy": "56c84dee-7149-47af-902d-0138e080cec0", "topic": ["Water" ], "topicsIds": ["ekstep_ncert_k-12_topic_environmentalstudies_l1con_3" ], "se_gradeLevels": ["Class 3" ], "osId": "org.ekstep.quiz.app", "se_FWIds": ["ekstep_ncert_k-12" ], "contentDisposition": "inline", "previewUrl": "https://sunbirdstagingpublic.blob.core.windows.net/sunbird-content-staging/content/do_21353901951355289618149/artifact/cbse-copy-89.pdf", "boardIds": ["ekstep_ncert_k-12_board_cbse" ], "artifactUrl": "https://sunbirdstagingpublic.blob.core.windows.net/sunbird-content-staging/content/do_21353901951355289618149/artifact/cbse-copy-89.pdf", "visibility": "Default", "credentials": "{\"enabled\":\"No\"}", "processId": "497335f0-51d2-4cec-a3ba-6eb81309aa87", "variants": "{\"full\":{\"ecarUrl\":\"https://sunbirdstagingpublic.blob.core.windows.net/sunbird-content-staging/content/do_21353901951355289618149/aparna-87_1652722745251_do_21353901951355289618149_1.ecar\",\"size\":\"193301\"},\"spine\":{\"ecarUrl\":\"https://sunbirdstagingpublic.blob.core.windows.net/sunbird-content-staging/content/do_21353901951355289618149/aparna-87_1652722745316_do_21353901951355289618149_1_SPINE.ecar\",\"size\":\"10093\"}}", "gradeLevelIds": ["ekstep_ncert_k-12_gradelevel_class3" ], "index": 1, "pkgVersion": 1, "idealScreenDensity": "hdpi" }],"mediaType": "content","name": "Course Unit1","discussionForum": { "enabled": "No"},"createdOn": "2022-05-18T07:43:11.157+0000","channel": "01269878797503692810","generateDIALCodes": "No","lastUpdatedOn": "2022-05-18T07:43:47.944+0000","identifier": "do_21354022740915814412313","ownershipType": [ "createdBy"],"compatibilityLevel": 1,"audience": [ "Student"],"trackable": { "enabled": "No", "autoBatch": "No"},"os": [ "All"],"primaryCategory": "Course Unit","languageCode": [ "en"],"downloadUrl": "https://sunbirdstagingpublic.blob.core.windows.net/sunbird-content-staging/content/do_21354022673739776012312/newcert_1652859862583_do_21354022673739776012312_1_SPINE.ecar","attributions": [],"versionKey": "1652859791157","mimeType": "application/vnd.ekstep.content-collection","code": "8335cfaa-74bc-171d-5c8d-4e895f8fb701","license": "CC BY 4.0","leafNodes": [ "do_21353901951355289618149"],"version": 2,"contentType": "CourseUnit","language": [ "English"],"lastPublishedOn": "2022-05-18T07:44:22.530+0000","objectType": "Content","status": "Live","dialcodeRequired": "No","idealScreenSize": "normal","contentEncoding": "gzip","leafNodesCount": 1,"depth": 1,"osId": "org.ekstep.launcher","contentDisposition": "inline","visibility": "Parent","credentials": { "enabled": "No"},"variants": "{\"spine\":{\"ecarUrl\":\"https://sunbirdstagingpublic.blob.core.windows.net/sunbird-content-staging/content/do_21354022673739776012312/newcert_1652859862583_do_21354022673739776012312_1_SPINE.ecar\",\"size\":\"13797\"},\"online\":{\"ecarUrl\":\"https://sunbirdstagingpublic.blob.core.windows.net/sunbird-content-staging/content/do_21354022673739776012312/newcert_1652859862667_do_21354022673739776012312_1_ONLINE.ecar\",\"size\":\"5181\"}}","index": 1,"pkgVersion": 1,"idealScreenDensity": "hdpi" } ], "body": null, "mediaType": "content", "name": "newcert", "toc_url": "https://sunbirdstagingpublic.blob.core.windows.net/sunbird-content-staging/content/do_21354022673739776012312/artifact/do_21354022673739776012312_toc.json", "batches": [ {"name": "nc","createdFor": [ "01269878797503692810", "01275630040485068839017"],"enrollmentType": "open","endDate": "2022-05-20","enrollmentEndDate": "2022-05-19","status": 1,"batchId": "013540229187338240118","startDate": "2022-05-18" } ], "discussionForum": { "enabled": "Yes" }, "createdOn": "2022-05-18T07:41:49.160+0000", "createdFor": [ "01269878797503692810" ], "channel": "01269878797503692810", "generateDIALCodes": "No", "lastUpdatedOn": "2022-05-18T07:43:47.944+0000", "subject": [ "Accountancy" ], "size": 13797, "publishError": null, "targetMediumIds": [ "tn_k-12_5_medium_english" ], "identifier": "do_21354022673739776012312", "se_gradeLevelIds": [ "tn_k-12_5_gradelevel_class1" ], "description": "Enter description for Course", "resourceType": "Course", "ownershipType": [ "createdBy" ], "compatibilityLevel": 4, "targetBoardIds": [ "tn_k-12_5_board_statetamilnadu" ], "audience": [ "Student" ], "trackable": { "enabled": "Yes", "autoBatch": "No" }, "se_boards": [ "State (Tamil Nadu)" ], "os": [ "All" ], "primaryCategory": "Course", "se_mediums": [ "English" ], "se_subjectIds": [ "tn_k-12_5_subject_accountancy", "tn_k-12_5_subject_english" ], "downloadUrl": "https://sunbirdstagingpublic.blob.core.windows.net/sunbird-content-staging/content/do_21354022673739776012312/newcert_1652859862583_do_21354022673739776012312_1_SPINE.ecar", "se_subjects": [ "Accountancy" ], "lockKey": "3efeaad1-490b-4044-8185-52a421057844", "attributions": [], "framework": "tn_k-12_5", "creator": "Guest name changed", "totalCompressedSize": 209850, "versionKey": "1652859827944", "mimeType": "application/vnd.ekstep.content-collection", "sYS_INTERNAL_LAST_UPDATED_ON": "2022-05-18T07:44:22.582+0000", "code": "org.sunbird.11SAbB", "se_boardIds": [ "tn_k-12_5_board_statetamilnadu" ], "license": "CC BY 4.0", "leafNodes": [ "do_21353901951355289618149" ], "version": 2, "contentType": "Course", "language": [ "English" ], "lastPublishedOn": "2022-05-18T07:44:22.530+0000", "contentTypesCount": "{\"eTextBook\":1,\"CourseUnit\":1}", "objectType": "Content", "subjectIds": [ "tn_k-12_5_subject_accountancy" ], "status": "Live", "reservedDialcodes": { "U5X4C5": 0 }, "targetFWIds": [ "tn_k-12_5" ], "createdBy": "fca2925f-1eee-4654-9177-fece3fd6afc9", "dialcodeRequired": "No", "lastSubmittedOn": "2022-05-18T07:43:47.614+0000", "keywords": [ "Drop" ], "dialcodes": null, "userConsent": "Yes", "idealScreenSize": "normal", "contentEncoding": "gzip", "leafNodesCount": 1, "depth": 2, "consumerId": "cb069f8d-e4e1-46c5-831f-d4a83b323ada", "lastPublishedBy": "08631a74-4b94-4cf7-a818-831135248a4a", "flagReasons": null, "targetSubjectIds": [ "tn_k-12_5_subject_english" ], "mimeTypesCount": "{\"application/pdf\":1,\"application/vnd.ekstep.content-collection\":1}", "se_gradeLevels": [ "Class 1" ], "osId": "org.ekstep.quiz.app", "copyrightYear": 2022, "se_FWIds": [ "tn_k-12_5" ], "s3Key": "content/do_21354022673739776012312/artifact/do_21354022673739776012312_toc.json", "contentDisposition": "inline", "additionalCategories": [ "Textbook" ], "childNodes": [ "do_21353901951355289618149", "do_21354022740915814412313" ], "visibility": "Default", "credentials": { "enabled": "Yes" }, "targetGradeLevelIds": [ "tn_k-12_5_gradelevel_class1" ], "variants": "{\"spine\":{\"ecarUrl\":\"https://sunbirdstagingpublic.blob.core.windows.net/sunbird-content-staging/content/do_21354022673739776012312/newcert_1652859862583_do_21354022673739776012312_1_SPINE.ecar\",\"size\":\"13797\"},\"online\":{\"ecarUrl\":\"https://sunbirdstagingpublic.blob.core.windows.net/sunbird-content-staging/content/do_21354022673739776012312/newcert_1652859862667_do_21354022673739776012312_1_ONLINE.ecar\",\"size\":\"5181\"}}", "index": 1, "pkgVersion": 1, "idealScreenDensity": "hdpi"} ], "mediaType": "content", "name": "Ch1 ", "discussionForum": {"enabled": "No" }, "createdOn": "2022-05-18T10:50:55.849+0000", "channel": "01269878797503692810", "generateDIALCodes": "No", "lastUpdatedOn": "2022-05-18T10:50:56.492+0000", "identifier": "do_21354031968951500812320", "ownershipType": ["createdBy" ], "compatibilityLevel": 1, "audience": ["Student" ], "os": ["All" ], "primaryCategory": "Textbook Unit", "languageCode": ["en" ], "downloadUrl": "https://sunbirdstagingpublic.blob.core.windows.net/sunbird-content-staging/content/do_21354027142511820812318/dialcodehierarchy_1652871221390_do_21354027142511820812318_1_SPINE.ecar", "attributions": [], "versionKey": "1652871055849", "mimeType": "application/vnd.ekstep.content-collection", "code": "80986053-eb44-0c35-1c3b-c84806d4775a", "license": "CC BY 4.0", "leafNodes": ["do_21353901951355289618149" ], "version": 2, "contentType": "TextBookUnit", "language": ["English" ], "lastPublishedOn": "2022-05-18T10:53:41.260+0000", "objectType": "Content", "status": "Live", "dialcodeRequired": "Yes", "dialcodes": ["R8C7G2" ], "idealScreenSize": "normal", "contentEncoding": "gzip", "leafNodesCount": 1, "depth": 1, "osId": "org.ekstep.launcher", "contentDisposition": "inline", "visibility": "Parent", "credentials": {"enabled": "No" }, "variants": "{\"spine\":{\"ecarUrl\":\"https://sunbirdstagingpublic.blob.core.windows.net/sunbird-content-staging/content/do_21354027142511820812318/dialcodehierarchy_1652871221390_do_21354027142511820812318_1_SPINE.ecar\",\"size\":\"16926\"},\"online\":{\"ecarUrl\":\"https://sunbirdstagingpublic.blob.core.windows.net/sunbird-content-staging/content/do_21354027142511820812318/dialcodehierarchy_1652871221531_do_21354027142511820812318_1_ONLINE.ecar\",\"size\":\"8313\"}}", "index": 1, "pkgVersion": 1, "idealScreenDensity": "hdpi" }, { "lastStatusChangedOn": "2022-05-18T10:50:55.854+0000", "parent": "do_21354027142511820812318", "children": [{ "parent": "do_21354031968955596812322", "children": [ {"lastStatusChangedOn": "2022-05-17T06:08:04.909+0000","parent": "do_21353947184001843212121","children": [ { "lastStatusChangedOn": "2022-05-17T06:08:47.990+0000", "parent": "do_21353947287477452812131", "mediaType": "content", "name": "വളരെ", "discussionForum": {"enabled": "No" }, "createdOn": "2022-05-17T06:08:47.990+0000", "channel": "01269878797503692810", "generateDIALCodes": "No", "lastUpdatedOn": "2022-05-17T06:12:02.265+0000", "identifier": "do_21353947322769408012133", "description": "വളരെ", "ownershipType": ["createdBy" ], "compatibilityLevel": 1, "audience": ["Student" ], "os": ["All" ], "primaryCategory": "Textbook Unit", "languageCode": ["en" ], "downloadUrl": "https://sunbirdstagingpublic.blob.core.windows.net/sunbird-content-staging/content/do_21353947184001843212121/knndd_1652767948813_do_21353947184001843212121_1_SPINE.ecar", "attributions": [], "versionKey": "1652767727990", "mimeType": "application/vnd.ekstep.content-collection", "code": "f00aabd6-ca25-00de-97c7-44aa0d08b803", "license": "CC BY 4.0", "leafNodes": [], "version": 2, "contentType": "TextBookUnit", "language": ["English" ], "lastPublishedOn": "2022-05-17T06:12:27.957+0000", "objectType": "Content", "status": "Draft", "dialcodeRequired": "No", "keywords": ["വളരെ" ], "idealScreenSize": "normal", "contentEncoding": "gzip", "leafNodesCount": 0, "depth": 2, "osId": "org.ekstep.launcher", "contentDisposition": "inline", "visibility": "Parent", "credentials": {"enabled": "No" }, "variants": {"spine": { "ecarUrl": "https://sunbirdstagingpublic.blob.core.windows.net/sunbird-content-staging/content/do_21353947184001843212121/knndd_1652767948813_do_21353947184001843212121_1_SPINE.ecar", "size": "4019"},"online": { "ecarUrl": "https://sunbirdstagingpublic.blob.core.windows.net/sunbird-content-staging/content/do_21353947184001843212121/knndd_1652767948873_do_21353947184001843212121_1_ONLINE.ecar", "size": "4018"} }, "index": 1, "pkgVersion": 1, "idealScreenDensity": "hdpi" }],"mediaType": "content","name": "வாக்கியங்கள்","discussionForum": { "enabled": "No"},"createdOn": "2022-05-17T06:08:04.909+0000","channel": "01269878797503692810","generateDIALCodes": "No","lastUpdatedOn": "2022-05-17T06:12:02.265+0000","identifier": "do_21353947287477452812131","description": "வாக்கியங்கள்","ownershipType": [ "createdBy"],"compatibilityLevel": 1,"audience": [ "Student"],"os": [ "All"],"primaryCategory": "Textbook Unit","languageCode": [ "en"],"downloadUrl": "https://sunbirdstagingpublic.blob.core.windows.net/sunbird-content-staging/content/do_21353947184001843212121/knndd_1652767948813_do_21353947184001843212121_1_SPINE.ecar","attributions": [],"versionKey": "1652767684909","mimeType": "application/vnd.ekstep.content-collection","code": "a1e342e1-a00b-d708-598a-1c52c4fc2cc7","license": "CC BY 4.0","leafNodes": [],"version": 2,"contentType": "TextBookUnit","language": [ "English"],"lastPublishedOn": "2022-05-17T06:12:27.957+0000","objectType": "Content","status": "Draft","dialcodeRequired": "No","keywords": [ "வாக்கியங்கள்"],"idealScreenSize": "normal","contentEncoding": "gzip","leafNodesCount": 0,"depth": 1,"osId": "org.ekstep.launcher","contentDisposition": "inline","visibility": "Parent","credentials": { "enabled": "No"},"variants": { "spine": { "ecarUrl": "https://sunbirdstagingpublic.blob.core.windows.net/sunbird-content-staging/content/do_21353947184001843212121/knndd_1652767948813_do_21353947184001843212121_1_SPINE.ecar", "size": "4019" }, "online": { "ecarUrl": "https://sunbirdstagingpublic.blob.core.windows.net/sunbird-content-staging/content/do_21353947184001843212121/knndd_1652767948873_do_21353947184001843212121_1_ONLINE.ecar", "size": "4018" }},"index": 1,"pkgVersion": 1,"idealScreenDensity": "hdpi" }, {"lastStatusChangedOn": "2022-05-17T06:09:30.081+0000","parent": "do_21353947184001843212121","mediaType": "content","name": "बतख़","discussionForum": { "enabled": "No"},"createdOn": "2022-05-17T06:09:30.081+0000","channel": "01269878797503692810","generateDIALCodes": "No","lastUpdatedOn": "2022-05-17T06:12:02.265+0000","identifier": "do_21353947357250355212135","description": "बतख़","ownershipType": [ "createdBy"],"compatibilityLevel": 1,"audience": [ "Student"],"os": [ "All"],"primaryCategory": "Textbook Unit","languageCode": [ "en"],"downloadUrl": "https://sunbirdstagingpublic.blob.core.windows.net/sunbird-content-staging/content/do_21353947184001843212121/knndd_1652767948813_do_21353947184001843212121_1_SPINE.ecar","attributions": [],"versionKey": "1652767770081","mimeType": "application/vnd.ekstep.content-collection","code": "710a43da-9a04-1d67-7d51-17bdce7fc1aa","license": "CC BY 4.0","leafNodes": [],"version": 2,"contentType": "TextBookUnit","language": [ "English"],"lastPublishedOn": "2022-05-17T06:12:27.957+0000","objectType": "Content","status": "Draft","dialcodeRequired": "No","keywords": [ "बतख़!"],"idealScreenSize": "normal","contentEncoding": "gzip","leafNodesCount": 0,"depth": 1,"osId": "org.ekstep.launcher","contentDisposition": "inline","visibility": "Parent","credentials": { "enabled": "No"},"variants": { "spine": { "ecarUrl": "https://sunbirdstagingpublic.blob.core.windows.net/sunbird-content-staging/content/do_21353947184001843212121/knndd_1652767948813_do_21353947184001843212121_1_SPINE.ecar", "size": "4019" }, "online": { "ecarUrl": "https://sunbirdstagingpublic.blob.core.windows.net/sunbird-content-staging/content/do_21353947184001843212121/knndd_1652767948873_do_21353947184001843212121_1_ONLINE.ecar", "size": "4018" }},"index": 2,"pkgVersion": 1,"idealScreenDensity": "hdpi" }, {"lastStatusChangedOn": "2022-05-17T06:13:07.924+0000","parent": "do_21353947184001843212121","mediaType": "content","name": "മലയാളം","discussionForum": { "enabled": "No"},"createdOn": "2022-05-17T06:13:07.924+0000","channel": "01269878797503692810","generateDIALCodes": "No","lastUpdatedOn": "2022-05-17T06:13:07.924+0000","identifier": "do_21353947535707340812137","description": "മലയാളം","ownershipType": [ "createdBy"],"compatibilityLevel": 1,"audience": [ "Student"],"os": [ "All"],"primaryCategory": "Textbook Unit","languageCode": [ "en"],"attributions": [],"versionKey": "1652767987924","mimeType": "application/vnd.ekstep.content-collection","code": "3e976c16-cefa-f237-f444-d25f2fa8f915","license": "CC BY 4.0","version": 2,"contentType": "TextBookUnit","language": [ "English"],"objectType": "Content","status": "Draft","dialcodeRequired": "No","keywords": [ "മലയാളം"],"idealScreenSize": "normal","contentEncoding": "gzip","depth": 1,"osId": "org.ekstep.launcher","contentDisposition": "inline","visibility": "Parent","credentials": { "enabled": "No"},"index": 3,"idealScreenDensity": "hdpi" } ], "identifier": "do_21353947184001843212121", "objectType": "Content", "depth": 2, "index": 1, "contentType": null, "primaryCategory": null} ], "mediaType": "content", "name": "Ch2", "discussionForum": {"enabled": "No" }, "createdOn": "2022-05-18T10:50:55.854+0000", "channel": "01269878797503692810", "generateDIALCodes": "No", "lastUpdatedOn": "2022-05-18T10:50:56.492+0000", "identifier": "do_21354031968955596812322", "ownershipType": ["createdBy" ], "compatibilityLevel": 1, "audience": ["Student" ], "os": ["All" ], "primaryCategory": "Textbook Unit", "languageCode": ["en" ], "downloadUrl": "https://sunbirdstagingpublic.blob.core.windows.net/sunbird-content-staging/content/do_21354027142511820812318/dialcodehierarchy_1652871221390_do_21354027142511820812318_1_SPINE.ecar", "attributions": [], "versionKey": "1652871055854", "mimeType": "application/vnd.ekstep.content-collection", "code": "22c05b75-ae2a-6280-c192-2e2a4157d141", "license": "CC BY 4.0", "leafNodes": [], "version": 2, "contentType": "TextBookUnit", "language": ["English" ], "lastPublishedOn": "2022-05-18T10:53:41.260+0000", "objectType": "Content", "status": "Live", "dialcodeRequired": "Yes", "dialcodes": ["A3S8L6" ], "idealScreenSize": "normal", "contentEncoding": "gzip", "leafNodesCount": 0, "depth": 1, "osId": "org.ekstep.launcher", "contentDisposition": "inline", "visibility": "Parent", "credentials": {"enabled": "No" }, "variants": "{\"spine\":{\"ecarUrl\":\"https://sunbirdstagingpublic.blob.core.windows.net/sunbird-content-staging/content/do_21354027142511820812318/dialcodehierarchy_1652871221390_do_21354027142511820812318_1_SPINE.ecar\",\"size\":\"16926\"},\"online\":{\"ecarUrl\":\"https://sunbirdstagingpublic.blob.core.windows.net/sunbird-content-staging/content/do_21354027142511820812318/dialcodehierarchy_1652871221531_do_21354027142511820812318_1_ONLINE.ecar\",\"size\":\"8313\"}}", "index": 2, "pkgVersion": 1, "idealScreenDensity": "hdpi" }],"primaryCategory": "Digital Textbook","appId": "staging.sunbird.portal","contentEncoding": "gzip","lockKey": "3e222f39-d917-4582-96ee-458f1ab80f30","generateDIALCodes": "Yes","totalCompressedSize": 209850,"mimeTypesCount": "{\"application/pdf\":1,\"application/vnd.ekstep.content-collection\":8,\"\":1}","sYS_INTERNAL_LAST_UPDATED_ON": "2022-05-18T10:53:41.389+0000","contentType": "TextBook","se_gradeLevels": [ "Class 1"],"trackable": { "enabled": "No", "autoBatch": "No"},"identifier": "do_21354027142511820812318","audience": [ "Student"],"se_boardIds": [ "tn_k-12_5_board_statetamilnadu"],"subjectIds": [ "tn_k-12_5_subject_english"],"toc_url": "https://sunbirdstagingpublic.blob.core.windows.net/sunbird-content-staging/content/do_21354027142511820812318/artifact/do_21354027142511820812318_toc.json","visibility": "Default","contentTypesCount": "{\"TextBookUnit\":6,\"eTextBook\":1,\"Course\":1,\"CourseUnit\":1,\"\":1}","author": "ContentcreatorTN","consumerId": "cb069f8d-e4e1-46c5-831f-d4a83b323ada","childNodes": [ "do_21354022673739776012312", "do_21354031968951500812320", "do_21353947184001843212121", "do_21354031968955596812322", "do_21353901951355289618149", "do_21354022740915814412313"],"discussionForum": { "enabled": "No"},"mediaType": "content","osId": "org.ekstep.quiz.app","lastPublishedBy": "08631a74-4b94-4cf7-a818-831135248a4a","version": 2,"se_subjects": [ "English"],"license": "CC BY 4.0","qrCodeProcessId": "40c3e296-ccdc-487d-8cda-9f0a88c071d2","size": 16926,"lastPublishedOn": "2022-05-18T10:53:41.260+0000","name": "DialCodeHierarchy","mediumIds": [ "tn_k-12_5_medium_english"],"attributions": [],"status": "Live","code": "org.sunbird.D3eL5z","publishError": null,"credentials": { "enabled": "No"},"description": "Enter description for TextBook","medium": [ "English"],"idealScreenSize": "normal","createdOn": "2022-05-18T09:12:44.203+0000","reservedDialcodes": "{\"A3S8L6\":2,\"R8C7G2\":1,\"D1D5H9\":4,\"V2A8N1\":0,\"L5L9A2\":5,\"F6I6S6\":3}","se_boards": [ "State (Tamil Nadu)"],"se_mediumIds": [ "tn_k-12_5_medium_english"],"copyrightYear": 2022,"contentDisposition": "inline","additionalCategories": [ "Textbook"],"lastUpdatedOn": "2022-05-18T10:50:56.492+0000","SYS_INTERNAL_LAST_UPDATED_ON": "2022-05-18T10:52:07.401+0000","dialcodeRequired": "Yes","lastStatusChangedOn": "2022-05-18T09:12:44.203+0000","createdFor": [ "01269878797503692810"],"creator": "ContentcreatorTN","os": [ "All"],"flagReasons": null,"se_subjectIds": [ "tn_k-12_5_subject_english"],"se_FWIds": [ "tn_k-12_5"],"pkgVersion": 1,"versionKey": "1652871183414","idealScreenDensity": "hdpi","framework": "tn_k-12_5","dialcodes": [ "V2A8N1"],"depth": 0,"s3Key": "content/do_21354027142511820812318/artifact/do_21354027142511820812318_toc.json","boardIds": [ "tn_k-12_5_board_statetamilnadu"],"lastSubmittedOn": "2022-05-18T10:53:03.017+0000","createdBy": "4cd4c690-eab6-4938-855a-447c7b1b8ea9","compatibilityLevel": 1,"leafNodesCount": 1,"userConsent": "Yes","gradeLevelIds": [ "tn_k-12_5_gradelevel_class1"],"board": "State (Tamil Nadu)","resourceType": "Book" }' +); + +INSERT INTO dev_hierarchy_store.content_hierarchy(identifier, hierarchy) VALUES ( +'do_1234', +'{"identifier":"do_1234","children":[{"ownershipType":["createdBy"],"parent":"do_123","code":"7cf20ea47763d420865bcc713def7a7b","keywords":["UnitKW1","UnitKW2"],"credentials":{"enabled":"No"},"channel":"0126825293972439041","description":"","language":["English"],"mimeType":"application/vnd.ekstep.content-collection","idealScreenSize":"normal","createdOn":"2021-12-21T19:54:31.617+0530","objectType":"Collection","primaryCategory":"Textbook Unit","children":[{"ownershipType":["createdBy"],"parent":"do_11340511137112064018","code":"9e1862f6518a7c87ee693cebb4fec278","keywords":["UnitKW1L2","UnitKW2L2"],"credentials":{"enabled":"No"},"channel":"0126825293972439041","description":"This section describes about various part of the body such as head, hands, legs etc.","language":["English"],"mimeType":"application/vnd.ekstep.content-collection","idealScreenSize":"normal","createdOn":"2021-12-21T19:54:31.660+0530","objectType":"Collection","primaryCategory":"Textbook Unit","children":[{"ownershipType":["createdBy"],"parent":"do_11343567193423872014","previewUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/assets/do_1134009478823116801129/chapter_1.pdf","keywords":["CPPDFContent1","CPPDFContent2","CollectionKW1"],"channel":"0126825293972439041","downloadUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11340096165525094411/collection-publishing-pdf-content_1635860615969_do_11340096165525094411_1.ecar","language":["English"],"mimeType":"application/pdf","variants":{"full":{"ecarUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11340096165525094411/collection-publishing-pdf-content_1635860615969_do_11340096165525094411_1.ecar","size":"256918"},"spine":{"ecarUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11340096165525094411/collection-publishing-pdf-content_1635860619148_do_11340096165525094411_1_SPINE.ecar","size":"6378"}},"objectType":"Content","appIcon":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11340094790233292811/artifact/033019_sz_reviews_feat_1564126718632.thumb.jpg","primaryCategory":"Explanation Content","contentEncoding":"identity","artifactUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/assets/do_1134009478823116801129/chapter_1.pdf","contentType":"Resource","identifier":"do_11340096165525094411","audience":["Student"],"visibility":"Default","discussionForum":{"enabled":"No"},"index":1,"mediaType":"content","osId":"org.ekstep.quiz.app","languageCode":["en"],"lastPublishedBy":"","version":2,"pragma":["external"],"license":"CC BY 4.0","prevState":"Draft","lastPublishedOn":"2021-11-02T19:13:35.589+0530","name":"Collection Publishing PDF Content","status":"Live","code":"c9ce1ce0-b9b4-402e-a9c3-556701070838","interceptionPoints":{},"credentials":{"enabled":"No"},"prevStatus":"Processing","streamingUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/assets/do_1134009478823116801129/chapter_1.pdf","idealScreenSize":"normal","createdOn":"2021-11-02T18:56:17.917+0530","copyrightYear":2021,"contentDisposition":"inline","lastUpdatedOn":"2021-11-02T19:13:39.729+0530","dialcodeRequired":"No","lastStatusChangedOn":"2021-11-02T19:13:39.729+0530","createdFor":["01309282781705830427"],"creator":"N131","os":["All"],"se_FWIds":["ekstep_ncert_k-12"],"pkgVersion":1,"versionKey":"1635859577917","idealScreenDensity":"hdpi","framework":"ekstep_ncert_k-12","depth":3,"createdBy":"0b71985d-fcb0-4018-ab14-83f10c3b0426","compatibilityLevel":4,"resourceType":"Learn"}],"contentDisposition":"inline","lastUpdatedOn":"2021-12-21T19:55:11.840+0530","contentEncoding":"gzip","generateDIALCodes":"No","contentType":"TextBookUnit","dialcodeRequired":"No","identifier":"do_11343567193423872014","lastStatusChangedOn":"2021-12-21T19:54:31.661+0530","audience":["Student"],"os":["All"],"visibility":"Parent","discussionForum":{"enabled":"No"},"index":1,"mediaType":"content","osId":"org.ekstep.launcher","languageCode":["en"],"version":2,"versionKey":"1640096671660","license":"CC BY 4.0","idealScreenDensity":"hdpi","framework":"ncert_k-12","depth":2,"compatibilityLevel":1,"name":"L2 Folder","attributions":[],"status":"Draft"},{"ownershipType":["createdBy"],"parent":"do_11340511137112064018","previewUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/assets/do_1134009478823116801129/chapter_1.pdf","keywords":["CPPDFContent1","CPPDFContent2","CollectionKW1"],"channel":"0126825293972439041","downloadUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11340096165525094411/collection-publishing-pdf-content_1635860615969_do_11340096165525094411_1.ecar","language":["English"],"mimeType":"application/pdf","variants":{"full":{"ecarUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11340096165525094411/collection-publishing-pdf-content_1635860615969_do_11340096165525094411_1.ecar","size":"256918"},"spine":{"ecarUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11340096165525094411/collection-publishing-pdf-content_1635860619148_do_11340096165525094411_1_SPINE.ecar","size":"6378"}},"objectType":"Content","appIcon":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11340094790233292811/artifact/033019_sz_reviews_feat_1564126718632.thumb.jpg","primaryCategory":"Explanation Content","contentEncoding":"identity","artifactUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/assets/do_1134009478823116801129/chapter_1.pdf","contentType":"Resource","identifier":"do_11340096165525094411","audience":["Student"],"visibility":"Default","discussionForum":{"enabled":"No"},"index":2,"mediaType":"content","osId":"org.ekstep.quiz.app","languageCode":["en"],"lastPublishedBy":"","version":2,"pragma":["external"],"license":"CC BY 4.0","prevState":"Draft","lastPublishedOn":"2021-11-02T19:13:35.589+0530","name":"Collection Publishing PDF Content","status":"Live","code":"c9ce1ce0-b9b4-402e-a9c3-556701070838","interceptionPoints":{},"credentials":{"enabled":"No"},"prevStatus":"Processing","streamingUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/assets/do_1134009478823116801129/chapter_1.pdf","idealScreenSize":"normal","createdOn":"2021-11-02T18:56:17.917+0530","copyrightYear":2021,"contentDisposition":"inline","lastUpdatedOn":"2021-11-02T19:13:39.729+0530","dialcodeRequired":"No","lastStatusChangedOn":"2021-11-02T19:13:39.729+0530","createdFor":["01309282781705830427"],"creator":"N131","os":["All"],"se_FWIds":["ekstep_ncert_k-12"],"pkgVersion":1,"versionKey":"1635859577917","idealScreenDensity":"hdpi","framework":"ekstep_ncert_k-12","depth":2,"createdBy":"0b71985d-fcb0-4018-ab14-83f10c3b0426","compatibilityLevel":4,"resourceType":"Learn"}],"contentDisposition":"inline","lastUpdatedOn":"2021-12-21T19:55:11.837+0530","contentEncoding":"gzip","generateDIALCodes":"No","contentType":"TextBookUnit","dialcodeRequired":"No","identifier":"do_11340511137112064018","lastStatusChangedOn":"2021-12-21T19:54:31.617+0530","audience":["Student"],"os":["All"],"visibility":"Parent","discussionForum":{"enabled":"No"},"index":1,"mediaType":"content","osId":"org.ekstep.launcher","languageCode":["en"],"version":2,"versionKey":"1640096671617","license":"CC BY 4.0","idealScreenDensity":"hdpi","framework":"ncert_k-12","depth":1,"compatibilityLevel":1,"name":"Collection Parent","attributions":[],"status":"Draft"}]}' +); + +INSERT INTO dev_hierarchy_store.content_hierarchy(identifier, hierarchy, relational_metadata) VALUES ( +'do_12345', +'{"identifier":"do_12345","children":[{"ownershipType":["createdBy"],"parent":"do_123","code":"7cf20ea47763d420865bcc713def7a7b","keywords":["UnitKW1","UnitKW2"],"credentials":{"enabled":"No"},"channel":"0126825293972439041","description":"","language":["English"],"mimeType":"application/vnd.ekstep.content-collection","idealScreenSize":"normal","createdOn":"2021-12-21T19:54:31.617+0530","objectType":"Collection","primaryCategory":"Textbook Unit","children":[{"ownershipType":["createdBy"],"parent":"do_11340511137112064018","code":"9e1862f6518a7c87ee693cebb4fec278","keywords":["UnitKW1L2","UnitKW2L2"],"credentials":{"enabled":"No"},"channel":"0126825293972439041","description":"This section describes about various part of the body such as head, hands, legs etc.","language":["English"],"mimeType":"application/vnd.ekstep.content-collection","idealScreenSize":"normal","createdOn":"2021-12-21T19:54:31.660+0530","objectType":"Collection","primaryCategory":"Textbook Unit","children":[{"ownershipType":["createdBy"],"parent":"do_11343567193423872014","previewUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/assets/do_1134009478823116801129/chapter_1.pdf","keywords":["CPPDFContent1","CPPDFContent2","CollectionKW1"],"channel":"0126825293972439041","downloadUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11340096165525094411/collection-publishing-pdf-content_1635860615969_do_11340096165525094411_1.ecar","language":["English"],"mimeType":"application/pdf","variants":{"full":{"ecarUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11340096165525094411/collection-publishing-pdf-content_1635860615969_do_11340096165525094411_1.ecar","size":"256918"},"spine":{"ecarUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11340096165525094411/collection-publishing-pdf-content_1635860619148_do_11340096165525094411_1_SPINE.ecar","size":"6378"}},"objectType":"Content","appIcon":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11340094790233292811/artifact/033019_sz_reviews_feat_1564126718632.thumb.jpg","primaryCategory":"Explanation Content","contentEncoding":"identity","artifactUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/assets/do_1134009478823116801129/chapter_1.pdf","contentType":"Resource","identifier":"do_11340096165525094411","audience":["Student"],"visibility":"Default","discussionForum":{"enabled":"No"},"index":1,"mediaType":"content","osId":"org.ekstep.quiz.app","languageCode":["en"],"lastPublishedBy":"","version":2,"pragma":["external"],"license":"CC BY 4.0","prevState":"Draft","lastPublishedOn":"2021-11-02T19:13:35.589+0530","name":"Collection Publishing PDF Content","status":"Live","code":"c9ce1ce0-b9b4-402e-a9c3-556701070838","interceptionPoints":{},"credentials":{"enabled":"No"},"prevStatus":"Processing","streamingUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/assets/do_1134009478823116801129/chapter_1.pdf","idealScreenSize":"normal","createdOn":"2021-11-02T18:56:17.917+0530","copyrightYear":2021,"contentDisposition":"inline","lastUpdatedOn":"2021-11-02T19:13:39.729+0530","dialcodeRequired":"No","lastStatusChangedOn":"2021-11-02T19:13:39.729+0530","createdFor":["01309282781705830427"],"creator":"N131","os":["All"],"se_FWIds":["ekstep_ncert_k-12"],"pkgVersion":1,"versionKey":"1635859577917","idealScreenDensity":"hdpi","framework":"ekstep_ncert_k-12","depth":3,"createdBy":"0b71985d-fcb0-4018-ab14-83f10c3b0426","compatibilityLevel":4,"resourceType":"Learn"}],"contentDisposition":"inline","lastUpdatedOn":"2021-12-21T19:55:11.840+0530","contentEncoding":"gzip","generateDIALCodes":"No","contentType":"TextBookUnit","dialcodeRequired":"No","identifier":"do_11343567193423872014","lastStatusChangedOn":"2021-12-21T19:54:31.661+0530","audience":["Student"],"os":["All"],"visibility":"Parent","discussionForum":{"enabled":"No"},"index":1,"mediaType":"content","osId":"org.ekstep.launcher","languageCode":["en"],"version":2,"versionKey":"1640096671660","license":"CC BY 4.0","idealScreenDensity":"hdpi","framework":"ncert_k-12","depth":2,"compatibilityLevel":1,"name":"L2 Folder","attributions":[],"status":"Draft"},{"ownershipType":["createdBy"],"parent":"do_11340511137112064018","previewUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/assets/do_1134009478823116801129/chapter_1.pdf","keywords":["CPPDFContent1","CPPDFContent2","CollectionKW1"],"channel":"0126825293972439041","downloadUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11340096165525094411/collection-publishing-pdf-content_1635860615969_do_11340096165525094411_1.ecar","language":["English"],"mimeType":"application/pdf","variants":{"full":{"ecarUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11340096165525094411/collection-publishing-pdf-content_1635860615969_do_11340096165525094411_1.ecar","size":"256918"},"spine":{"ecarUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11340096165525094411/collection-publishing-pdf-content_1635860619148_do_11340096165525094411_1_SPINE.ecar","size":"6378"}},"objectType":"Content","appIcon":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11340094790233292811/artifact/033019_sz_reviews_feat_1564126718632.thumb.jpg","primaryCategory":"Explanation Content","contentEncoding":"identity","artifactUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/assets/do_1134009478823116801129/chapter_1.pdf","contentType":"Resource","identifier":"do_11340096165525094411","audience":["Student"],"visibility":"Default","discussionForum":{"enabled":"No"},"index":2,"mediaType":"content","osId":"org.ekstep.quiz.app","languageCode":["en"],"lastPublishedBy":"","version":2,"pragma":["external"],"license":"CC BY 4.0","prevState":"Draft","lastPublishedOn":"2021-11-02T19:13:35.589+0530","name":"Collection Publishing PDF Content","status":"Live","code":"c9ce1ce0-b9b4-402e-a9c3-556701070838","interceptionPoints":{},"credentials":{"enabled":"No"},"prevStatus":"Processing","streamingUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/assets/do_1134009478823116801129/chapter_1.pdf","idealScreenSize":"normal","createdOn":"2021-11-02T18:56:17.917+0530","copyrightYear":2021,"contentDisposition":"inline","lastUpdatedOn":"2021-11-02T19:13:39.729+0530","dialcodeRequired":"No","lastStatusChangedOn":"2021-11-02T19:13:39.729+0530","createdFor":["01309282781705830427"],"creator":"N131","os":["All"],"se_FWIds":["ekstep_ncert_k-12"],"pkgVersion":1,"versionKey":"1635859577917","idealScreenDensity":"hdpi","framework":"ekstep_ncert_k-12","depth":2,"createdBy":"0b71985d-fcb0-4018-ab14-83f10c3b0426","compatibilityLevel":4,"resourceType":"Learn"}],"contentDisposition":"inline","lastUpdatedOn":"2021-12-21T19:55:11.837+0530","contentEncoding":"gzip","generateDIALCodes":"No","contentType":"TextBookUnit","dialcodeRequired":"No","identifier":"do_11340511137112064018","lastStatusChangedOn":"2021-12-21T19:54:31.617+0530","audience":["Student"],"os":["All"],"visibility":"Parent","discussionForum":{"enabled":"No"},"index":1,"mediaType":"content","osId":"org.ekstep.launcher","languageCode":["en"],"version":2,"versionKey":"1640096671617","license":"CC BY 4.0","idealScreenDensity":"hdpi","framework":"ncert_k-12","depth":1,"compatibilityLevel":1,"name":"Collection Parent","attributions":[],"status":"Draft"}]}', +'' +); + +INSERT INTO dev_hierarchy_store.content_hierarchy(identifier, hierarchy, relational_metadata) VALUES ( +'do_123456', +'{"identifier":"do_123456","children":[{"ownershipType":["createdBy"],"parent":"do_123","code":"7cf20ea47763d420865bcc713def7a7b","keywords":["UnitKW1","UnitKW2"],"credentials":{"enabled":"No"},"channel":"0126825293972439041","description":"","language":["English"],"mimeType":"application/vnd.ekstep.content-collection","idealScreenSize":"normal","createdOn":"2021-12-21T19:54:31.617+0530","objectType":"Collection","primaryCategory":"Textbook Unit","children":[{"ownershipType":["createdBy"],"parent":"do_11340511137112064018","code":"9e1862f6518a7c87ee693cebb4fec278","keywords":["UnitKW1L2","UnitKW2L2"],"credentials":{"enabled":"No"},"channel":"0126825293972439041","description":"This section describes about various part of the body such as head, hands, legs etc.","language":["English"],"mimeType":"application/vnd.ekstep.content-collection","idealScreenSize":"normal","createdOn":"2021-12-21T19:54:31.660+0530","objectType":"Collection","primaryCategory":"Textbook Unit","children":[{"ownershipType":["createdBy"],"parent":"do_11343567193423872014","previewUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/assets/do_1134009478823116801129/chapter_1.pdf","keywords":["CPPDFContent1","CPPDFContent2","CollectionKW1"],"channel":"0126825293972439041","downloadUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11340096165525094411/collection-publishing-pdf-content_1635860615969_do_11340096165525094411_1.ecar","language":["English"],"mimeType":"application/pdf","variants":{"full":{"ecarUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11340096165525094411/collection-publishing-pdf-content_1635860615969_do_11340096165525094411_1.ecar","size":"256918"},"spine":{"ecarUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11340096165525094411/collection-publishing-pdf-content_1635860619148_do_11340096165525094411_1_SPINE.ecar","size":"6378"}},"objectType":"Content","appIcon":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11340094790233292811/artifact/033019_sz_reviews_feat_1564126718632.thumb.jpg","primaryCategory":"Explanation Content","contentEncoding":"identity","artifactUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/assets/do_1134009478823116801129/chapter_1.pdf","contentType":"Resource","identifier":"do_11340096165525094411","audience":["Student"],"visibility":"Default","discussionForum":{"enabled":"No"},"index":1,"mediaType":"content","osId":"org.ekstep.quiz.app","languageCode":["en"],"lastPublishedBy":"","version":2,"pragma":["external"],"license":"CC BY 4.0","prevState":"Draft","lastPublishedOn":"2021-11-02T19:13:35.589+0530","name":"Collection Publishing PDF Content","status":"Live","code":"c9ce1ce0-b9b4-402e-a9c3-556701070838","interceptionPoints":{},"credentials":{"enabled":"No"},"prevStatus":"Processing","streamingUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/assets/do_1134009478823116801129/chapter_1.pdf","idealScreenSize":"normal","createdOn":"2021-11-02T18:56:17.917+0530","copyrightYear":2021,"contentDisposition":"inline","lastUpdatedOn":"2021-11-02T19:13:39.729+0530","dialcodeRequired":"No","lastStatusChangedOn":"2021-11-02T19:13:39.729+0530","createdFor":["01309282781705830427"],"creator":"N131","os":["All"],"se_FWIds":["ekstep_ncert_k-12"],"pkgVersion":1,"versionKey":"1635859577917","idealScreenDensity":"hdpi","framework":"ekstep_ncert_k-12","depth":3,"createdBy":"0b71985d-fcb0-4018-ab14-83f10c3b0426","compatibilityLevel":4,"resourceType":"Learn"}],"contentDisposition":"inline","lastUpdatedOn":"2021-12-21T19:55:11.840+0530","contentEncoding":"gzip","generateDIALCodes":"No","contentType":"TextBookUnit","dialcodeRequired":"No","identifier":"do_11343567193423872014","lastStatusChangedOn":"2021-12-21T19:54:31.661+0530","audience":["Student"],"os":["All"],"visibility":"Parent","discussionForum":{"enabled":"No"},"index":1,"mediaType":"content","osId":"org.ekstep.launcher","languageCode":["en"],"version":2,"versionKey":"1640096671660","license":"CC BY 4.0","idealScreenDensity":"hdpi","framework":"ncert_k-12","depth":2,"compatibilityLevel":1,"name":"L2 Folder","attributions":[],"status":"Draft"},{"ownershipType":["createdBy"],"parent":"do_11340511137112064018","previewUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/assets/do_1134009478823116801129/chapter_1.pdf","keywords":["CPPDFContent1","CPPDFContent2","CollectionKW1"],"channel":"0126825293972439041","downloadUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11340096165525094411/collection-publishing-pdf-content_1635860615969_do_11340096165525094411_1.ecar","language":["English"],"mimeType":"application/pdf","variants":{"full":{"ecarUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11340096165525094411/collection-publishing-pdf-content_1635860615969_do_11340096165525094411_1.ecar","size":"256918"},"spine":{"ecarUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11340096165525094411/collection-publishing-pdf-content_1635860619148_do_11340096165525094411_1_SPINE.ecar","size":"6378"}},"objectType":"Content","appIcon":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11340094790233292811/artifact/033019_sz_reviews_feat_1564126718632.thumb.jpg","primaryCategory":"Explanation Content","contentEncoding":"identity","artifactUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/assets/do_1134009478823116801129/chapter_1.pdf","contentType":"Resource","identifier":"do_11340096165525094411","audience":["Student"],"visibility":"Default","discussionForum":{"enabled":"No"},"index":2,"mediaType":"content","osId":"org.ekstep.quiz.app","languageCode":["en"],"lastPublishedBy":"","version":2,"pragma":["external"],"license":"CC BY 4.0","prevState":"Draft","lastPublishedOn":"2021-11-02T19:13:35.589+0530","name":"Collection Publishing PDF Content","status":"Live","code":"c9ce1ce0-b9b4-402e-a9c3-556701070838","interceptionPoints":{},"credentials":{"enabled":"No"},"prevStatus":"Processing","streamingUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/assets/do_1134009478823116801129/chapter_1.pdf","idealScreenSize":"normal","createdOn":"2021-11-02T18:56:17.917+0530","copyrightYear":2021,"contentDisposition":"inline","lastUpdatedOn":"2021-11-02T19:13:39.729+0530","dialcodeRequired":"No","lastStatusChangedOn":"2021-11-02T19:13:39.729+0530","createdFor":["01309282781705830427"],"creator":"N131","os":["All"],"se_FWIds":["ekstep_ncert_k-12"],"pkgVersion":1,"versionKey":"1635859577917","idealScreenDensity":"hdpi","framework":"ekstep_ncert_k-12","depth":2,"createdBy":"0b71985d-fcb0-4018-ab14-83f10c3b0426","compatibilityLevel":4,"resourceType":"Learn"}],"contentDisposition":"inline","lastUpdatedOn":"2021-12-21T19:55:11.837+0530","contentEncoding":"gzip","generateDIALCodes":"No","contentType":"TextBookUnit","dialcodeRequired":"No","identifier":"do_11340511137112064018","lastStatusChangedOn":"2021-12-21T19:54:31.617+0530","audience":["Student"],"os":["All"],"visibility":"Parent","discussionForum":{"enabled":"No"},"index":1,"mediaType":"content","osId":"org.ekstep.launcher","languageCode":["en"],"version":2,"versionKey":"1640096671617","license":"CC BY 4.0","idealScreenDensity":"hdpi","framework":"ncert_k-12","depth":1,"compatibilityLevel":1,"name":"Collection Parent","attributions":[],"status":"Draft"}]}', +'{}' +); diff --git a/publish-pipeline/live-node-publisher/src/test/scala/org/sunbird/job/livenodepublisher/domain/EventSpec.scala b/publish-pipeline/live-node-publisher/src/test/scala/org/sunbird/job/livenodepublisher/domain/EventSpec.scala new file mode 100644 index 000000000..62c25675c --- /dev/null +++ b/publish-pipeline/live-node-publisher/src/test/scala/org/sunbird/job/livenodepublisher/domain/EventSpec.scala @@ -0,0 +1,21 @@ +package org.sunbird.job.livenodepublisher.domain + +import com.typesafe.config.{Config, ConfigFactory} +import org.scalatest.{FlatSpec, Matchers} +import org.scalatestplus.mockito.MockitoSugar +import org.sunbird.job.livenodepublisher.publish.domain.Event +import org.sunbird.job.livenodepublisher.task.LiveNodePublisherConfig +import org.sunbird.job.util.JSONUtil + +class EventSpec extends FlatSpec with Matchers with MockitoSugar { + + val config: Config = ConfigFactory.load("test.conf").withFallback(ConfigFactory.systemEnvironment()) + val jobConfig: LiveNodePublisherConfig = new LiveNodePublisherConfig(config) + + "isValid" should "return true for a valid event" in { + val sunbirdEvent = "{\"eid\":\"BE_JOB_REQUEST\",\"ets\":1619527882745,\"mid\":\"LP.1619527882745.32dc378a-430f-49f6-83b5-bd73b767ad36\",\"actor\":{\"id\":\"collection-publish\",\"type\":\"System\"},\"context\":{\"channel\":\"ORG_001\",\"pdata\":{\"id\":\"org.sunbird.platform\",\"ver\":\"1.0\"},\"env\":\"dev\"},\"object\":{\"id\":\"do_11361948306824396811\",\"ver\":\"1619153418829\"},\"edata\":{\"publish_type\":\"public\",\"metadata\":{\"identifier\":\"do_11361948306824396811\",\"mimeType\":\"application/vnd.ekstep.content-collection\",\"objectType\":\"Collection\",\"lastPublishedBy\":\"\",\"pkgVersion\":1},\"action\":\"republish\",\"iteration\":1}}" + val event = new Event(JSONUtil.deserialize[java.util.Map[String, Any]](sunbirdEvent),0,1) + + assert(event.validEvent(jobConfig)) + } +} \ No newline at end of file diff --git a/publish-pipeline/live-node-publisher/src/test/scala/org/sunbird/job/livenodepublisher/fixture/EventFixture.scala b/publish-pipeline/live-node-publisher/src/test/scala/org/sunbird/job/livenodepublisher/fixture/EventFixture.scala new file mode 100644 index 000000000..27ecd7fac --- /dev/null +++ b/publish-pipeline/live-node-publisher/src/test/scala/org/sunbird/job/livenodepublisher/fixture/EventFixture.scala @@ -0,0 +1,9 @@ +package org.sunbird.job.livenodepublisher.fixture + +object EventFixture { + + val PDF_EVENT1: String = + """ + |{"eid":"BE_JOB_REQUEST","ets":1619527882745,"mid":"LP.1619527882745.32dc378a-430f-49f6-83b5-bd73b767ad36","actor":{"id":"content-publish","type":"System"},"context":{"channel":"","pdata":{"id":"org.sunbird.platform","ver":"1.0"}},"object":{"id":"do_11329603741667328018","ver":"1619153418829"},"edata":{"publish_type":"public","metadata":{"identifier":"do_11329603741667328018","mimeType":"application/pdf","objectType":"Content","lastPublishedBy":"sample-last-published-by","pkgVersion":1},"action":"publish","iteration":1}} + |""".stripMargin +} diff --git a/publish-pipeline/live-node-publisher/src/test/scala/org/sunbird/job/livenodepublisher/publish/helpers/spec/ExtractableMimeTypeHelperSpec.scala b/publish-pipeline/live-node-publisher/src/test/scala/org/sunbird/job/livenodepublisher/publish/helpers/spec/ExtractableMimeTypeHelperSpec.scala new file mode 100644 index 000000000..88cfad14f --- /dev/null +++ b/publish-pipeline/live-node-publisher/src/test/scala/org/sunbird/job/livenodepublisher/publish/helpers/spec/ExtractableMimeTypeHelperSpec.scala @@ -0,0 +1,165 @@ +package org.sunbird.job.livenodepublisher.publish.helpers.spec + +import akka.dispatch.ExecutionContexts +import com.typesafe.config.{Config, ConfigFactory} +import org.scalatest.{BeforeAndAfterAll, FlatSpec, Matchers} +import org.scalatestplus.mockito.MockitoSugar +import org.sunbird.job.livenodepublisher.publish.helpers.ExtractableMimeTypeHelper +import org.sunbird.job.livenodepublisher.task.LiveNodePublisherConfig +import org.sunbird.job.exception.InvalidInputException +import org.sunbird.job.publish.core.ObjectData +import org.sunbird.job.util.CloudStorageUtil + +class ExtractableMimeTypeHelperSpec extends FlatSpec with BeforeAndAfterAll with Matchers with MockitoSugar { + + implicit val ec = ExecutionContexts.global + val config: Config = ConfigFactory.load("test.conf").withFallback(ConfigFactory.systemEnvironment()) + val jobConfig: LiveNodePublisherConfig = new LiveNodePublisherConfig(config) + implicit val cloudStorageUtil = new CloudStorageUtil(jobConfig) + + "processECMLBody with xml " should " throw exception Error! Invalid Media ('id' is required.) in ... if media id is blank and type is other than js and css" in { + val obj: ObjectData = new ObjectData("do_113188615625731", + Map[String, AnyRef]("identifier" -> "do_113188615625731", "objectType" -> "Content", "mimeType" -> "application/vnd.ekstep.ecml-archive", "primaryCategory" -> "some category", "name" -> "Some Content", "code" -> "some code"), + Some(Map[String, AnyRef]("body" -> "")) + ) + + assertThrows[InvalidInputException] { + ExtractableMimeTypeHelper.processECMLBody(obj, jobConfig)(ec, cloudStorageUtil) + } + } + + "processECMLBody with xml " should " throw exception Error! Invalid Media ('type' is required.) in ... if media type is blank" in { + val obj: ObjectData = new ObjectData("do_113188615625731", + Map[String, AnyRef]("identifier" -> "do_113188615625731", "objectType" -> "Content", "mimeType" -> "application/vnd.ekstep.ecml-archive", "primaryCategory" -> "some category", "name" -> "Some Content", "code" -> "some code"), + Some(Map[String, AnyRef]("body" -> "")) + ) + + assertThrows[InvalidInputException] { + ExtractableMimeTypeHelper.processECMLBody(obj, jobConfig)(ec, cloudStorageUtil) + } + } + + "processECMLBody with xml " should " throw exception Error! Invalid Media ('src' is required.) in ... if media src is blank" in { + val obj: ObjectData = new ObjectData("do_113188615625731", + Map[String, AnyRef]("identifier" -> "do_113188615625731", "objectType" -> "Content", "mimeType" -> "application/vnd.ekstep.ecml-archive", "primaryCategory" -> "some category", "name" -> "Some Content", "code" -> "some code"), + Some(Map[String, AnyRef]("body" -> "")) + ) + + assertThrows[InvalidInputException] { + ExtractableMimeTypeHelper.processECMLBody(obj, jobConfig)(ec, cloudStorageUtil) + } + } + + "processECMLBody with xml " should " process the ecml body" in { + val obj: ObjectData = new ObjectData("do_113188615625731", + Map[String, AnyRef]("identifier" -> "do_113188615625731", "objectType" -> "Content", "mimeType" -> "application/vnd.ekstep.ecml-archive", "primaryCategory" -> "some category", "name" -> "Some Content", "code" -> "some code"), + Some(Map[String, AnyRef]("body" -> "

A person having less ‘haemoglobin’ is suffering from:

Jaundice

Anaemia

Malaria

Chikungunya

\",\"media\":[],\"responseDeclaration\":{\"responseValue\":{\"cardinality\":\"single\",\"type\":\"integer\",\"correct_response\":{\"value\":\"2\"}}},\"options\":[{\"answer\":false,\"value\":{\"type\":\"text\",\"body\":\"

Jaundice

\",\"resvalue\":0,\"resindex\":0}},{\"answer\":false,\"value\":{\"type\":\"text\",\"body\":\"

Anaemia

\",\"resvalue\":1,\"resindex\":1}},{\"answer\":true,\"value\":{\"type\":\"text\",\"body\":\"

Malaria

\",\"resvalue\":2,\"resindex\":2}},{\"answer\":false,\"value\":{\"type\":\"text\",\"body\":\"

Chikungunya

\",\"resvalue\":3,\"resindex\":3}}],\"questionCount\":0}]]>
")) + ) + val result: Map[String, AnyRef] = ExtractableMimeTypeHelper.processECMLBody(obj, jobConfig)(ec, cloudStorageUtil) + result.contains("artifactUrl") shouldBe true + result.get("artifactUrl") shouldNot be(null) + result.contains("cloudStorageKey") shouldBe true + result.get("cloudStorageKey") shouldNot be(null) + } + + "processECMLBody with json " should " throw exception if no body is available" in { + val obj: ObjectData = new ObjectData("do_113188615625731", + Map[String, AnyRef]("identifier" -> "do_113188615625731", "objectType" -> "Content", "mimeType" -> "application/vnd.ekstep.ecml-archive", "primaryCategory" -> "some category", "name" -> "Some Content", "code" -> "some code"), + Some(Map[String, AnyRef]()) + ) + + assertThrows[InvalidInputException] { + val result: Map[String, AnyRef] = ExtractableMimeTypeHelper.processECMLBody(obj, jobConfig)(ec, cloudStorageUtil) + } + } + + "processECMLBody with json " should " throw exception Invalid Content Body if not a valid body" in { + val obj: ObjectData = new ObjectData("do_113188615625731", + Map[String, AnyRef]("identifier" -> "do_113188615625731", "objectType" -> "Content", "mimeType" -> "application/vnd.ekstep.ecml-archive", "primaryCategory" -> "some category", "name" -> "Some Content", "code" -> "some code"), + Some(Map[String, AnyRef]("body" -> "Invalid body")) + ) + + assertThrows[InvalidInputException] { + ExtractableMimeTypeHelper.processECMLBody(obj, jobConfig)(ec, cloudStorageUtil) + } + } + + "processECMLBody with json " should " throw exception Error! Invalid Controller ('id' is required.)" in { + val obj: ObjectData = new ObjectData("do_113188615625731", + Map[String, AnyRef]("identifier" -> "do_113188615625731", "objectType" -> "Content", "mimeType" -> "application/vnd.ekstep.ecml-archive", "primaryCategory" -> "some category", "name" -> "Some Content", "code" -> "some code"), + Some(Map[String, AnyRef]("body" -> "{\"theme\":{\"controller\":[{\"name\":\"dictionary\",\"type\":\"data\",\"id\":\"\",\"__cdata\":{}}]}}")) + ) + + assertThrows[InvalidInputException] { + ExtractableMimeTypeHelper.processECMLBody(obj, jobConfig)(ec, cloudStorageUtil) + } + } + + "processECMLBody with json " should " throw exception Error! Invalid Controller ('type' is required.)" in { + val obj: ObjectData = new ObjectData("do_113188615625731", + Map[String, AnyRef]("identifier" -> "do_113188615625731", "objectType" -> "Content", "mimeType" -> "application/vnd.ekstep.ecml-archive", "primaryCategory" -> "some category", "name" -> "Some Content", "code" -> "some code"), + Some(Map[String, AnyRef]("body" -> "{\"theme\":{\"controller\":[{\"name\":\"dictionary\",\"type\":\"\",\"id\":\"dictionary\",\"__cdata\":{}}]}}")) + ) + + assertThrows[InvalidInputException] { + ExtractableMimeTypeHelper.processECMLBody(obj, jobConfig)(ec, cloudStorageUtil) + } + } + + "processECMLBody with json " should " throw exception Error! Invalid Controller ('type' should be either 'items' or 'data')" in { + val obj: ObjectData = new ObjectData("do_113188615625731", + Map[String, AnyRef]("identifier" -> "do_113188615625731", "objectType" -> "Content", "mimeType" -> "application/vnd.ekstep.ecml-archive", "primaryCategory" -> "some category", "name" -> "Some Content", "code" -> "some code"), + Some(Map[String, AnyRef]("body" -> "{\"theme\":{\"controller\":[{\"name\":\"dictionary\",\"type\":\"some type\",\"id\":\"dictionary\",\"__cdata\":{}}]}}")) + ) + + assertThrows[InvalidInputException] { + ExtractableMimeTypeHelper.processECMLBody(obj, jobConfig)(ec, cloudStorageUtil) + } + } + + "processECMLBody with json " should " throw exception Error! Invalid Media ('id' is required.) if media id is blank and type is other than js and css" in { + val obj: ObjectData = new ObjectData("do_113188615625731", + Map[String, AnyRef]("identifier" -> "do_113188615625731", "objectType" -> "Content", "mimeType" -> "application/vnd.ekstep.ecml-archive", "primaryCategory" -> "some category", "name" -> "Some Content", "code" -> "some code"), + Some(Map[String, AnyRef]("body" -> "{\"theme\":{\"controller\":[{\"name\":\"dictionary\",\"type\":\"data\",\"id\":\"dictionary\",\"__cdata\":{}}],\"manifest\":{\"media\":[{\"id\":\"\",\"src\":\"/content-plugins/org.ekstep.questionset-1.0/editor/assets/quizimage.png\",\"assetId\":\"QuizImage\",\"type\":\"image\",\"preload\":true}]}}}")) + ) + + assertThrows[InvalidInputException] { + ExtractableMimeTypeHelper.processECMLBody(obj, jobConfig)(ec, cloudStorageUtil) + } + } + + "processECMLBody with json " should " throw exception Error! Invalid Media ('type' is required.) if media type is blank" in { + val obj: ObjectData = new ObjectData("do_113188615625731", + Map[String, AnyRef]("identifier" -> "do_113188615625731", "objectType" -> "Content", "mimeType" -> "application/vnd.ekstep.ecml-archive", "primaryCategory" -> "some category", "name" -> "Some Content", "code" -> "some code"), + Some(Map[String, AnyRef]("body" -> "{\"theme\":{\"controller\":[{\"name\":\"dictionary\",\"type\":\"data\",\"id\":\"dictionary\",\"__cdata\":{}}],\"manifest\":{\"media\":[{\"id\":\"QuizImage\",\"src\":\"/content-plugins/org.ekstep.questionset-1.0/editor/assets/quizimage.png\",\"assetId\":\"QuizImage\",\"type\":\"\",\"preload\":true}]}}}")) + ) + + assertThrows[InvalidInputException] { + ExtractableMimeTypeHelper.processECMLBody(obj, jobConfig)(ec, cloudStorageUtil) + } + } + + "processECMLBody with json " should " throw exception Error! Invalid Media ('src' is required.) if media src is blank" in { + val obj: ObjectData = new ObjectData("do_113188615625731", + Map[String, AnyRef]("identifier" -> "do_113188615625731", "objectType" -> "Content", "mimeType" -> "application/vnd.ekstep.ecml-archive", "primaryCategory" -> "some category", "name" -> "Some Content", "code" -> "some code"), + Some(Map[String, AnyRef]("body" -> "{\"theme\":{\"controller\":[{\"name\":\"dictionary\",\"type\":\"data\",\"id\":\"dictionary\",\"__cdata\":{}}],\"manifest\":{\"media\":[{\"id\":\"QuizImage\",\"src\":\"\",\"assetId\":\"QuizImage\",\"type\":\"image\",\"preload\":true}]}}}")) + ) + + assertThrows[InvalidInputException] { + ExtractableMimeTypeHelper.processECMLBody(obj, jobConfig)(ec, cloudStorageUtil) + } + } + + "processECMLBody with json " should " process the ecml body" in { + val obj: ObjectData = new ObjectData("do_113188615625731", + Map[String, AnyRef]("identifier" -> "do_113188615625731", "objectType" -> "Content", "mimeType" -> "application/vnd.ekstep.ecml-archive", "primaryCategory" -> "some category", "name" -> "Some Content", "code" -> "some code"), + Some(Map[String, AnyRef]("body" -> "{\"theme\":{\"manifest\":{\"media\":[{\"id\":253452185,\"plugin\":\"org.ekstep.navigation\",\"ver\":\"1.0\",\"src\":\"/content-plugins/org.ekstep.navigation-1.0/renderer/controller/navigation_ctrl.js\",\"type\":\"js\"},{\"id\":\"c264cc74-39dc-40e6-8e6a-04b1b379bd52\",\"plugin\":\"org.ekstep.navigation\",\"ver\":\"1.0\",\"src\":\"/content-plugins/org.ekstep.navigation-1.0/renderer/templates/navigation.html\",\"type\":\"js\"},{\"id\":\"org.ekstep.navigation\",\"plugin\":\"org.ekstep.navigation\",\"ver\":\"1.0\",\"src\":\"/content-plugins/org.ekstep.navigation-1.0/renderer/plugin.js\",\"type\":\"plugin\"},{\"id\":\"org.ekstep.navigation_manifest\",\"plugin\":\"org.ekstep.navigation\",\"ver\":\"1.0\",\"src\":\"/content-plugins/org.ekstep.navigation-1.0/manifest.json\",\"type\":\"json\"},{\"id\":\"org.ekstep.questionunit.renderer.audioicon\",\"plugin\":\"org.ekstep.questionunit\",\"ver\":\"1.0\",\"src\":\"/content-plugins/org.ekstep.questionunit-1.0/renderer/assets/audio-icon.png\",\"type\":\"image\"},{\"id\":\"org.ekstep.questionunit.renderer.downarrow\",\"plugin\":\"org.ekstep.questionunit\",\"ver\":\"1.0\",\"src\":\"/content-plugins/org.ekstep.questionunit-1.0/renderer/assets/down_arrow.png\",\"type\":\"image\"},{\"id\":\"489ca96e-de6d-41c2-a346-3e13a5e81cd1\",\"plugin\":\"org.ekstep.questionunit\",\"ver\":\"1.0\",\"src\":\"/content-plugins/org.ekstep.questionunit-1.0/renderer/components/js/components.js\",\"type\":\"js\"},{\"id\":\"d3eab6b4-eb5a-4739-b985-ca85caaae11e\",\"plugin\":\"org.ekstep.questionunit\",\"ver\":\"1.0\",\"src\":\"/content-plugins/org.ekstep.questionunit-1.0/renderer/components/css/components.css\",\"type\":\"css\"},{\"id\":\"4152517e-7698-4813-840c-ab0d4bb1a48a\",\"plugin\":\"org.ekstep.questionunit\",\"ver\":\"1.0\",\"src\":\"/content-plugins/org.ekstep.questionunit-1.0/renderer/libs/katex/katex.min.js\",\"type\":\"js\"},{\"id\":\"d9a519d7-e450-4b31-be54-5cec9d21d5ce\",\"plugin\":\"org.ekstep.questionunit\",\"ver\":\"1.0\",\"src\":\"/content-plugins/org.ekstep.questionunit-1.0/renderer/libs/katex/katex.min.css\",\"type\":\"css\"},{\"id\":\"org.ekstep.questionunit\",\"plugin\":\"org.ekstep.questionunit\",\"ver\":\"1.0\",\"src\":\"/content-plugins/org.ekstep.questionunit-1.0/renderer/plugin.js\",\"type\":\"plugin\"},{\"id\":\"org.ekstep.questionunit_manifest\",\"plugin\":\"org.ekstep.questionunit\",\"ver\":\"1.0\",\"src\":\"/content-plugins/org.ekstep.questionunit-1.0/manifest.json\",\"type\":\"json\"},{\"id\":\"7139ec8e-dc5d-437b-a7c4-62d0be5a28c5\",\"plugin\":\"org.ekstep.questionunit.mcq\",\"ver\":\"1.1\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/styles/style.css\",\"type\":\"css\"},{\"id\":\"bdd3831b-95ca-4398-a6e6-6a61984452a5\",\"plugin\":\"org.ekstep.questionunit.mcq\",\"ver\":\"1.1\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/styles/horizontal_and_vertical.css\",\"type\":\"css\"},{\"id\":\"09bd898a-6132-4ebd-936c-5437a2134229\",\"plugin\":\"org.ekstep.questionunit.mcq\",\"ver\":\"1.1\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/template/mcq-layouts.js\",\"type\":\"js\"},{\"id\":\"a46352ba-dccb-4c5e-bc1a-84beeb85037c\",\"plugin\":\"org.ekstep.questionunit.mcq\",\"ver\":\"1.1\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/template/template_controller.js\",\"type\":\"js\"},{\"id\":\"38886295-0bac-4654-bded-6d032cb37431\",\"plugin\":\"org.ekstep.questionunit.mcq\",\"ver\":\"1.1\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/tick_icon.png\",\"type\":\"image\"},{\"id\":\"37aabee2-1538-4391-8d1d-5d406722b084\",\"plugin\":\"org.ekstep.questionunit.mcq\",\"ver\":\"1.1\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/audio-icon2.png\",\"type\":\"image\"},{\"id\":\"73d9743a-9202-4e68-8797-ad873b67cdb6\",\"plugin\":\"org.ekstep.questionunit.mcq\",\"ver\":\"1.1\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/assets/music-blue.png\",\"type\":\"image\"},{\"id\":\"org.ekstep.questionunit.mcq\",\"plugin\":\"org.ekstep.questionunit.mcq\",\"ver\":\"1.1\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/renderer/plugin.js\",\"type\":\"plugin\"},{\"id\":\"org.ekstep.questionunit.mcq_manifest\",\"plugin\":\"org.ekstep.questionunit.mcq\",\"ver\":\"1.1\",\"src\":\"/content-plugins/org.ekstep.questionunit.mcq-1.1/manifest.json\",\"type\":\"json\"},{\"id\":\"org.ekstep.questionset.quiz\",\"plugin\":\"org.ekstep.questionset.quiz\",\"ver\":\"1.0\",\"src\":\"/content-plugins/org.ekstep.questionset.quiz-1.0/renderer/plugin.js\",\"type\":\"plugin\"},{\"id\":\"org.ekstep.questionset.quiz_manifest\",\"plugin\":\"org.ekstep.questionset.quiz\",\"ver\":\"1.0\",\"src\":\"/content-plugins/org.ekstep.questionset.quiz-1.0/manifest.json\",\"type\":\"json\"},{\"id\":\"org.ekstep.iterator\",\"plugin\":\"org.ekstep.iterator\",\"ver\":\"1.0\",\"src\":\"/content-plugins/org.ekstep.iterator-1.0/renderer/plugin.js\",\"type\":\"plugin\"},{\"id\":\"org.ekstep.iterator_manifest\",\"plugin\":\"org.ekstep.iterator\",\"ver\":\"1.0\",\"src\":\"/content-plugins/org.ekstep.iterator-1.0/manifest.json\",\"type\":\"json\"},{\"id\":\"cb4d4669-9291-4583-906c-685ab9a76994\",\"plugin\":\"org.ekstep.questionset\",\"ver\":\"1.0\",\"src\":\"/content-plugins/org.ekstep.questionset-1.0/renderer/utils/telemetry_logger.js\",\"type\":\"js\"},{\"id\":\"dd12429d-d861-4d54-b80d-12047823b058\",\"plugin\":\"org.ekstep.questionset\",\"ver\":\"1.0\",\"src\":\"/content-plugins/org.ekstep.questionset-1.0/renderer/utils/html_audio_plugin.js\",\"type\":\"js\"},{\"id\":\"56254ade-8e4e-4090-b97b-8b9b7bc12620\",\"plugin\":\"org.ekstep.questionset\",\"ver\":\"1.0\",\"src\":\"/content-plugins/org.ekstep.questionset-1.0/renderer/utils/qs_feedback_popup.js\",\"type\":\"js\"},{\"id\":\"org.ekstep.questionset\",\"plugin\":\"org.ekstep.questionset\",\"ver\":\"1.0\",\"src\":\"/content-plugins/org.ekstep.questionset-1.0/renderer/plugin.js\",\"type\":\"plugin\"},{\"id\":\"org.ekstep.questionset_manifest\",\"plugin\":\"org.ekstep.questionset\",\"ver\":\"1.0\",\"src\":\"/content-plugins/org.ekstep.questionset-1.0/manifest.json\",\"type\":\"json\"},{\"id\":\"QuizImage\",\"src\":\"/content-plugins/org.ekstep.questionset-1.0/editor/assets/quizimage.png\",\"assetId\":\"QuizImage\",\"type\":\"image\",\"preload\":true}]},\"id\":\"theme\",\"startStage\":\"scene55138843-9f04-487d-bf33-1920769f2c96\",\"ver\":0.2,\"controller\":[{\"name\":\"assessment_185d692a-1841-4a35-96c1-67cebcde42a2\",\"type\":\"items\",\"id\":\"assessment_185d692a-1841-4a35-96c1-67cebcde42a2\",\"__cdata\":{\"total_items\":1,\"subject\":\"domain\",\"code\":\"ItemSet_921ee047-c676-4e0b-9a71-42f371f7b843\",\"type\":\"materialised\",\"lastUpdatedOn\":\"2016-06-15T09:42:19.585+0000\",\"showImmediateFeedback\":true,\"SET_TYPE\":\"MATERIALISED_SET\",\"createdOn\":\"2016-06-15T09:42:19.575+0000\",\"title\":\"Assessment Title\",\"items\":{\"domain_11824\":[{\"model\":{\"keys\":\"र,रा,रि,री,रु,रू,रे,रै,रो,रौ,रं,रः,ट,टा,टि,टी,टु,टू,टे,टै,टो,टौ,टं,टः\"},\"question_audio\":\"domain_10561\",\"subject\":\"domain\",\"template_id\":\"domain_4171\",\"type\":\"ftb\",\"feedback\":\"\",\"qlevel\":\"EASY\",\"title\":\"Story5Q1\",\"question_image\":\"domain_10624\",\"name\":\"Story5Q1\",\"domain\":\"literacy\",\"max_score\":1,\"question\":\"\",\"template\":\"org.ekstep.ftb.barakhadi\",\"answer\":{\"ans1\":{\"value\":\"रोटी\",\"score\":1}},\"code\":\"org.ekstep.assessmentitem.domain_4533\",\"lastUpdatedOn\":\"2016-06-15T09:40:54.289+0000\",\"concepts\":[{\"identifier\":\"LO1\",\"name\":\"Receptive Vocabulary\",\"objectType\":\"Concept\",\"relation\":\"associatedTo\",\"description\":null,\"index\":null}],\"createdOn\":\"2016-05-23T10:09:03.976+0000\",\"lastUpdatedBy\":\"323\",\"used_for\":\"worksheet\",\"owner\":\"323\",\"gradeLevel\":[\"Other\"],\"language\":[\"Hindi\"],\"identifier\":\"domain_4533\",\"media\":[{\"id\":\"domain_10624\",\"type\":\"image\",\"src\":\"https://ekstep-public.s3-ap-southeast-1.amazonaws.com/content/roti_323_1465971255_1465971255545.jpg\",\"asset_id\":\"domain_10624\",\"preload\":true},{\"id\":\"domain_10561\",\"type\":\"audio\",\"src\":\"https://ekstep-public.s3-ap-southeast-1.amazonaws.com/content/story11q4a1_141_1465970351_1465970351272.mp3\",\"asset_id\":\"domain_10561\",\"preload\":true}]}]},\"SET_OBJECT_TYPE_KEY\":\"AssessmentItem\",\"shuffle\":false,\"lastUpdatedBy\":\"323\",\"owner\":\"Ilimi\",\"used_for\":\"assessment\",\"max_score\":1,\"gradeLevel\":[\"Grade 1\"],\"item_sets\":[{\"id\":\"domain_11824\",\"count\":1}],\"language\":[\"English\"],\"identifier\":\"domain_11824\"}},{\"name\":\"dictionary\",\"type\":\"data\",\"id\":\"dictionary\",\"__cdata\":{}}],\"template\":[{\"text\":[{\"align\":\"center\",\"color\":\"black\",\"font\":\"Verdana\",\"fontsize\":70,\"model\":\"item.title\",\"w\":80,\"x\":10,\"y\":6,\"z-index\":101},{\"align\":\"center\",\"color\":\"black\",\"font\":\"Verdana\",\"fontsize\":100,\"h\":15,\"id\":\"newText\",\"model\":\"item.ans1\",\"valign\":\"middle\",\"w\":35,\"x\":58,\"y\":68,\"z-index\":100}],\"shape\":[{\"event\":{\"type\":\"click\"},\"h\":15,\"hitArea\":true,\"opacity\":1,\"w\":80,\"x\":10,\"y\":6,\"z-index\":99},{\"event\":{\"action\":{\"asset\":\"bKeyboard\",\"command\":\"custom\",\"id\":\"newText\",\"invoke\":\"switchTarget\",\"type\":\"command\"},\"type\":\"click\"},\"h\":15,\"hitArea\":true,\"stroke\":\"black\",\"stroke-width\":5,\"w\":35,\"x\":58,\"y\":67,\"z-index\":99},{\"event\":{\"action\":{\"asset_model\":\"item.question_audio\",\"command\":\"play\",\"type\":\"command\"},\"type\":\"click\"},\"h\":40,\"hitArea\":true,\"stroke-width\":5,\"w\":20,\"x\":10,\"y\":58,\"z-index\":99}],\"keyboard\":{\"id\":\"bKeyboard\",\"keys\":\"item.keys\",\"limit\":10,\"target\":\"newText\",\"type\":\"custom\",\"x\":5,\"y\":15},\"g\":{\"image\":{\"h\":75,\"model\":\"item.question_image\",\"w\":100,\"x\":0,\"y\":0,\"z-index\":102},\"text\":{\"align\":\"center\",\"color\":\"black\",\"font\":\"Verdana\",\"fontsize\":\"2em\",\"h\":25,\"model\":\"item.question\",\"valign\":\"middle\",\"w\":100,\"x\":0,\"y\":75,\"z-index\":103},\"h\":40,\"w\":20,\"x\":10,\"y\":58},\"id\":\"org.ekstep.ftb.barakhadi\"},{\"text\":{\"event\":{\"action\":[{\"command\":\"stop\",\"sound\":true,\"type\":\"command\"},{\"asset_model\":\"item.question_audio\",\"command\":\"play\",\"type\":\"command\"}],\"type\":\"click\"},\"align\":\"center\",\"color\":\"black\",\"font\":\"Verdana\",\"fontsize\":\"2em\",\"lineHeight\":2,\"model\":\"item.question\",\"valign\":\"middle\",\"w\":100,\"x\":0,\"y\":15},\"shape\":{\"event\":{\"action\":[{\"command\":\"stop\",\"sound\":true,\"type\":\"command\"},{\"asset_model\":\"item.question_audio\",\"command\":\"play\",\"type\":\"command\"}],\"type\":\"click\"},\"h\":10,\"hitArea\":true,\"type\":\"rect\",\"w\":100,\"x\":0,\"y\":15},\"mcq\":{\"options\":{\"shape\":{\"h\":100,\"stroke\":\"black\",\"type\":\"roundrect\",\"w\":100,\"x\":0,\"y\":0},\"text\":{\"align\":\"center\",\"font\":\"Verdana\",\"fontsize\":\"1.6em\",\"h\":100,\"model\":\"option.value.text\",\"valign\":\"middle\",\"w\":100,\"x\":0,\"y\":0},\"event\":{\"action\":[{\"command\":\"stop\",\"sound\":true,\"type\":\"command\"},{\"asset_model\":\"option.value.audio\",\"command\":\"play\",\"type\":\"command\"}],\"type\":\"click\"},\"cols\":4,\"h\":30,\"highlight\":\"yellow\",\"layout\":\"table\",\"marginX\":5,\"marginY\":5,\"options\":\"options\",\"w\":97,\"x\":1.5,\"y\":50},\"model\":\"item\",\"multi_select\":false,\"shadow\":\"#56B1F7\"},\"id\":\"org.ekstep.mcq.ta\"},{\"text\":{\"model\":\"item.title\",\"x\":9,\"y\":7,\"w\":86,\"h\":4,\"font\":\"Georgia\",\"fontsize\":42},\"mcq\":{\"options\":{\"layout\":\"table\",\"x\":20,\"y\":15,\"w\":70,\"h\":85,\"cols\":2,\"marginX\":10,\"marginY\":5,\"options\":\"options\"},\"multi_select\":false,\"model\":\"item\"},\"g\":{\"image\":{\"event\":{\"action\":[{\"type\":\"command\",\"command\":\"stop\",\"asset_model\":\"item.hints[0].asset\"},{\"type\":\"command\",\"command\":\"toggleShow\",\"asset\":\"hint\"}],\"type\":\"click\"},\"asset\":\"speech_bubble\",\"x\":0,\"y\":0,\"w\":100,\"h\":100},\"text\":[{\"x\":10,\"y\":20,\"w\":80,\"h\":80,\"font\":\"Georgia\",\"weight\":\"bold\",\"fontsize\":150,\"__text\":\"Hint\"},{\"x\":10,\"y\":40,\"w\":80,\"h\":80,\"font\":\"Georgia\",\"fontsize\":120,\"model\":\"item.hints[1].asset\"}],\"x\":9,\"y\":17,\"w\":20,\"h\":20,\"id\":\"hint\",\"visible\":false},\"image\":{\"event\":{\"action\":[{\"type\":\"command\",\"command\":\"togglePlay\",\"asset_model\":\"item.hints[0].asset\"},{\"type\":\"command\",\"command\":\"toggleShow\",\"asset\":\"hint\"}],\"type\":\"click\"},\"asset\":\"icon_hint\",\"x\":5,\"y\":35},\"id\":\"mcq_template_1\"},{\"image\":[{\"event\":{\"action\":{\"type\":\"command\",\"command\":\"show\",\"asset\":\"retryDialog\"},\"type\":\"click\"},\"asset\":\"popupTint\",\"x\":-100,\"y\":-150,\"w\":550,\"h\":600,\"visible\":true,\"id\":\"popup-Tint\"},{\"asset\":\"retryBg\",\"x\":0,\"y\":0,\"w\":150,\"h\":150,\"visible\":true,\"id\":\"right\"}],\"shape\":[{\"event\":{\"action\":[{\"type\":\"command\",\"command\":\"hide\",\"asset\":\"retryDialog\"},{\"type\":\"command\",\"command\":\"SHOWHTMLELEMENTS\",\"asset\":\"retry\"}],\"type\":\"click\"},\"type\":\"roundrect\",\"x\":72,\"y\":25,\"w\":50,\"h\":65,\"visible\":true,\"id\":\"retry\",\"hitArea\":true},{\"event\":{\"action\":{\"type\":\"command\",\"command\":\"transitionTo\",\"asset\":\"theme\",\"param\":\"next\",\"effect\":\"fadein\",\"direction\":\"left\",\"ease\":\"linear\",\"duration\":100},\"type\":\"click\"},\"type\":\"roundrect\",\"x\":110,\"y\":100,\"w\":25,\"h\":35,\"visible\":true,\"id\":\"continue\",\"hitArea\":true}],\"id\":\"retry\"},{\"g\":{\"image\":[{\"asset\":\"popupTint\",\"x\":0,\"y\":0,\"w\":100,\"h\":100,\"visible\":true,\"id\":\"popup-Tint\"}],\"text\":[{\"x\":25,\"y\":25,\"w\":50,\"h\":9,\"visible\":true,\"editable\":true,\"model\":\"word.lemma\",\"weight\":\"normal\",\"font\":\"helvetica\",\"color\":\"rgb(0,0,0)\",\"fontstyle\":\"\",\"fontsize\":75,\"align\":\"left\",\"z-index\":1,\"id\":\"lemma\"},{\"x\":25,\"y\":35,\"w\":50,\"h\":40,\"visible\":true,\"editable\":true,\"model\":\"word.gloss\",\"weight\":\"normal\",\"font\":\"helvetica\",\"color\":\"rgb(0,0,0)\",\"fontstyle\":\"\",\"fontsize\":43,\"align\":\"left\",\"z-index\":2,\"id\":\"gloss\"}],\"shape\":[{\"x\":20,\"y\":20,\"w\":60,\"h\":60,\"visible\":true,\"editable\":true,\"type\":\"roundrect\",\"radius\":10,\"opacity\":1,\"fill\":\"#45b3a5\",\"stroke-width\":1,\"z-index\":0,\"id\":\"textBg\"}],\"x\":0,\"y\":0,\"w\":100,\"h\":100,\"event\":{\"action\":[{\"type\":\"command\",\"command\":\"SHOWHTMLELEMENTS\",\"asset\":\"textBg\"},{\"type\":\"command\",\"command\":\"hide\",\"parent\":true}],\"type\":\"click\"}},\"id\":\"infoTemplate\"},{\"image\":[{\"event\":{\"action\":{\"type\":\"command\",\"command\":\"show\",\"asset\":\"\"},\"type\":\"click\"},\"asset\":\"popupTint\",\"x\":-100,\"y\":-150,\"w\":550,\"h\":600,\"visible\":true,\"id\":\"popup-Tint\"},{\"event\":{\"action\":[{\"type\":\"command\",\"command\":\"transitionTo\",\"asset\":\"theme\",\"param\":\"next\",\"effect\":\"fadein\",\"direction\":\"left\",\"ease\":\"linear\",\"duration\":500}],\"type\":\"click\"},\"asset\":\"goodjobBg\",\"x\":0,\"y\":0,\"w\":150,\"h\":150,\"visible\":true,\"id\":\"continue\"}],\"id\":\"goodjob\"}],\"stage\":[{\"id\":\"recordAudio7\",\"x\":0,\"y\":0,\"w\":100,\"h\":100,\"param\":[{\"name\":\"previousScreen\",\"value\":\"recordAudio6\"},{\"name\":\"nextScreen\",\"value\":\"recordAudio8\"},{\"name\":\"next\",\"value\":\"scene7b2e5492-b134-4945-9cde-803a094fb430\"},{\"name\":\"previous\",\"value\":\"recordAudio6\"}],\"events\":{\"event\":[{\"action\":[{\"asset\":\"startrec\",\"command\":\"hide\",\"type\":\"command\"},{\"asset\":\"stoprec\",\"command\":\"show\",\"type\":\"command\"},{\"asset\":\"mover\",\"command\":\"show\",\"type\":\"command\"},{\"tween\":{\"to\":[{\"duration\":0,\"ease\":\"linear\",\"__cdata\":{\"x\":47,\"y\":45}},{\"duration\":1000,\"ease\":\"sineInOut\",\"__cdata\":{\"x\":62,\"y\":45}},{\"duration\":1000,\"ease\":\"sineInOut\",\"__cdata\":{\"x\":47,\"y\":45}}],\"id\":\"mover\",\"loop\":true},\"asset\":\"mover\",\"type\":\"animation\"}],\"type\":\"rec_started\"},{\"action\":[{\"asset\":\"stoprec\",\"command\":\"hide\",\"type\":\"command\"},{\"asset\":\"mover\",\"command\":\"hide\",\"type\":\"command\"},{\"asset\":\"playback\",\"command\":\"show\",\"type\":\"command\"}],\"type\":\"rec_stopped\"},{\"action\":{\"type\":\"command\",\"command\":\"play\",\"asset\":\"domain_4002\",\"loop\":1},\"type\":\"enter\"},{\"action\":{\"type\":\"command\",\"command\":\"stop\",\"asset\":\"domain_4002\",\"loop\":1},\"type\":\"exit\"}]},\"image\":[{\"x\":16.38888888888889,\"y\":20.22222222222222,\"w\":69.44444444444444,\"h\":77.77777777777779,\"visible\":true,\"editable\":true,\"asset\":\"domain_3126\",\"z-index\":0},{\"x\":45,\"y\":80,\"w\":10,\"h\":18,\"visible\":true,\"editable\":true,\"asset\":\"mic\",\"event\":{\"action\":{\"asset\":\"recorder\",\"command\":\"toggleShow\",\"type\":\"command\"},\"type\":\"click\"},\"z-index\":6},{\"event\":{\"action\":{\"type\":\"command\",\"command\":\"transitionTo\",\"asset\":\"theme\",\"param\":\"next\",\"effect\":\"fadein\",\"direction\":\"left\",\"ease\":\"linear\",\"duration\":500},\"type\":\"click\"},\"asset\":\"next\",\"x\":93,\"y\":3,\"w\":5,\"h\":8.3,\"id\":\"next\",\"visible\":true,\"editable\":true,\"z-index\":100},{\"event\":{\"action\":{\"type\":\"command\",\"command\":\"transitionTo\",\"asset\":\"theme\",\"param\":\"previous\",\"effect\":\"fadein\",\"direction\":\"right\",\"ease\":\"linear\",\"duration\":100},\"type\":\"click\"},\"asset\":\"previous\",\"x\":2,\"y\":3,\"w\":5,\"h\":8.3,\"id\":\"previous\",\"visible\":true,\"editable\":true,\"z-index\":100}],\"text\":[{\"x\":25.27777777777778,\"y\":6.222222222222222,\"w\":71.11111111111111,\"h\":9.322222222222223,\"visible\":true,\"editable\":true,\"__text\":\"बिल्लियाँ देखती ही रह गई |\",\"weight\":\"normal\",\"font\":\"Helvetica\",\"color\":\"rgb(0,0,0)\",\"fontstyle\":\"\",\"fontsize\":85,\"lineHeight\":1.3,\"align\":\"left\",\"z-index\":7}],\"shape\":[],\"hotspot\":[{\"x\":25.208333333333332,\"y\":4.888888888888889,\"w\":14.722222222222223,\"h\":8.444444444444445,\"visible\":true,\"editable\":true,\"type\":\"roundrect\",\"radius\":1,\"fill\":\"red\",\"stroke-width\":1,\"keyword\":\"\",\"event\":[{\"action\":{\"type\":\"command\",\"command\":\"play\",\"asset\":\"domain_7637\"},\"type\":\"click\"}],\"hitArea\":true,\"z-index\":1},{\"x\":40.97222222222222,\"y\":5.111111111111112,\"w\":10.277777777777777,\"h\":8,\"visible\":true,\"editable\":true,\"type\":\"roundrect\",\"radius\":1,\"fill\":\"red\",\"stroke-width\":1,\"keyword\":\"\",\"event\":[{\"action\":{\"type\":\"command\",\"command\":\"play\",\"asset\":\"domain_7638\"},\"type\":\"click\"}],\"hitArea\":true,\"z-index\":2},{\"x\":52.361111111111114,\"y\":5.111111111111112,\"w\":3.75,\"h\":8.666666666666668,\"visible\":true,\"editable\":true,\"type\":\"roundrect\",\"radius\":1,\"fill\":\"red\",\"stroke-width\":1,\"keyword\":\"\",\"event\":[{\"action\":{\"type\":\"command\",\"command\":\"play\",\"asset\":\"domain_7639\"},\"type\":\"click\"}],\"hitArea\":true,\"z-index\":3},{\"x\":57.36111111111111,\"y\":7.333333333333333,\"w\":4.861111111111112,\"h\":6.888888888888889,\"visible\":true,\"editable\":true,\"type\":\"roundrect\",\"radius\":1,\"fill\":\"red\",\"stroke-width\":1,\"keyword\":\"\",\"event\":[{\"action\":{\"type\":\"command\",\"command\":\"play\",\"asset\":\"domain_7640\"},\"type\":\"click\"}],\"hitArea\":true,\"z-index\":4},{\"x\":63.47222222222222,\"y\":4.888888888888889,\"w\":5.555555555555555,\"h\":9.777777777777779,\"visible\":true,\"editable\":true,\"type\":\"roundrect\",\"radius\":1,\"fill\":\"red\",\"stroke-width\":1,\"keyword\":\"\",\"event\":[{\"action\":{\"type\":\"command\",\"command\":\"play\",\"asset\":\"domain_7641\"},\"type\":\"click\"}],\"hitArea\":true,\"z-index\":5}],\"embed\":[],\"div\":[],\"audio\":[{\"asset\":\"domain_4002\"},{\"asset\":\"domain_7637\"},{\"asset\":\"domain_7638\"},{\"asset\":\"domain_7639\"},{\"asset\":\"domain_7640\"},{\"asset\":\"domain_7641\"}],\"scribble\":[],\"g\":[{\"g\":[{\"image\":{\"event\":{\"action\":{\"asset\":\"recorder\",\"command\":\"hide\",\"type\":\"command\"},\"type\":\"click\"},\"asset\":\"blacktint\",\"h\":100,\"w\":100,\"x\":0,\"y\":0},\"h\":100,\"w\":100,\"x\":0,\"y\":0},{\"shape\":[{\"fill\":\"gray\",\"h\":20,\"type\":\"roundrect\",\"w\":30,\"x\":36,\"y\":37},{\"event\":{\"action\":{\"asset\":\"startrec\",\"command\":\"startRecord\",\"failure\":\"rec_start_fail\",\"success\":\"rec_started\",\"type\":\"command\"},\"type\":\"click\"},\"fill\":\"#34e941\",\"h\":20,\"type\":\"roundrect\",\"w\":30,\"x\":35,\"y\":35}],\"image\":{\"asset\":\"record\",\"h\":10,\"w\":6,\"x\":38,\"y\":40},\"text\":{\"color\":\"darkgreen\",\"font\":\"sans-serif\",\"fontsize\":50,\"h\":5,\"w\":20,\"x\":47,\"y\":43,\"__text\":\"Start\"},\"h\":100,\"id\":\"startrec\",\"w\":100,\"x\":0,\"y\":0},{\"shape\":[{\"fill\":\"gray\",\"h\":20,\"type\":\"roundrect\",\"w\":30,\"x\":36,\"y\":37},{\"event\":{\"action\":{\"asset\":\"stoprec\",\"command\":\"stopRecord\",\"failure\":\"rec_stop_failed\",\"success\":\"rec_stopped\",\"type\":\"command\"},\"type\":\"click\"},\"fill\":\"#e93441\",\"h\":20,\"type\":\"roundrect\",\"w\":30,\"x\":35,\"y\":35},{\"fill\":\"#e98888\",\"h\":2,\"id\":\"mover\",\"type\":\"circle\",\"visible\":false,\"w\":2,\"x\":47,\"y\":45}],\"image\":{\"asset\":\"stop_record\",\"h\":10,\"w\":6,\"x\":38,\"y\":40},\"h\":100,\"id\":\"stoprec\",\"visible\":false,\"w\":100,\"x\":0,\"y\":0},{\"shape\":[{\"fill\":\"gray\",\"h\":20,\"type\":\"roundrect\",\"w\":30,\"x\":36,\"y\":37},{\"event\":{\"action\":[{\"asset\":\"current_rec\",\"command\":\"TOGGLEPLAY\",\"type\":\"command\"},{\"asset\":\"recorder\",\"command\":\"hide\",\"type\":\"command\"},{\"asset\":\"playback\",\"command\":\"hide\",\"type\":\"command\"},{\"asset\":\"startrec\",\"command\":\"show\",\"type\":\"command\"}],\"type\":\"click\"},\"fill\":\"#34e941\",\"h\":20,\"type\":\"roundrect\",\"w\":30,\"x\":35,\"y\":35}],\"image\":{\"asset\":\"icon_sound\",\"h\":10,\"id\":\"story_audio_button\",\"w\":6,\"x\":38,\"y\":40},\"text\":{\"color\":\"darkgreen\",\"font\":\"sans-serif\",\"fontsize\":50,\"h\":5,\"w\":20,\"x\":47,\"y\":43,\"__text\":\"Play\"},\"h\":100,\"id\":\"playback\",\"visible\":false,\"w\":100,\"x\":0,\"y\":0}],\"h\":100,\"id\":\"recorder\",\"z-index\":200,\"visible\":false,\"w\":100,\"x\":0,\"y\":0}],\"appEvents\":{\"list\":\"rec_started,rec_start_fail,rec_stopped,rec_stop_failed\"}},{\"id\":\"scene185d692a-1841-4a35-96c1-67cebcde42a2\",\"x\":0,\"y\":0,\"w\":100,\"h\":100,\"param\":[{\"name\":\"next\",\"value\":\"scenea3cc8a52-833a-4a9d-b6e6-3be6b266b70f\"},{\"name\":\"previous\",\"value\":\"scene5de719ed-207c-4f01-a7e8-e1eb4abf77ed\"}],\"events\":{\"event\":[{\"action\":{\"type\":\"command\",\"command\":\"play\",\"asset\":\"domain_11825\",\"loop\":1},\"type\":\"enter\"},{\"action\":{\"type\":\"command\",\"command\":\"stop\",\"asset\":\"domain_11825\",\"loop\":1},\"type\":\"exit\"},{\"action\":[{\"type\":\"command\",\"command\":\"play\",\"asset\":\"retry_audio\"},{\"type\":\"command\",\"command\":\"show\",\"asset\":\"retryDialog\"},{\"tween\":{\"to\":{\"ease\":\"bounceOut \",\"duration\":1500,\"__cdata\":{\"x\":20,\"y\":20,\"w\":40,\"h\":30}},\"id\":\"retryShowAnim\"},\"type\":\"animation\",\"asset\":\"retryDialog\"}],\"type\":\"wrong_response\"},{\"action\":[{\"type\":\"command\",\"command\":\"play\",\"asset\":\"goodjob_audio\"},{\"type\":\"command\",\"command\":\"show\",\"asset\":\"goodjobDialog\"},{\"tween\":{\"to\":{\"ease\":\"bounceOut\",\"duration\":1500,\"__cdata\":{\"x\":20,\"y\":20,\"w\":40,\"h\":30}},\"id\":\"gjShowAnim\"},\"type\":\"animation\",\"asset\":\"goodjobDialog\"}],\"type\":\"correct_response\"}]},\"image\":[{\"event\":{\"action\":{\"type\":\"command\",\"command\":\"transitionTo\",\"asset\":\"theme\",\"param\":\"next\",\"effect\":\"fadein\",\"direction\":\"left\",\"ease\":\"linear\",\"duration\":500},\"type\":\"click\"},\"asset\":\"next\",\"x\":93,\"y\":3,\"w\":5,\"h\":8.3,\"id\":\"next\",\"visible\":true,\"editable\":true,\"z-index\":100},{\"event\":{\"action\":{\"type\":\"command\",\"command\":\"transitionTo\",\"asset\":\"theme\",\"param\":\"previous\",\"effect\":\"fadein\",\"direction\":\"right\",\"ease\":\"linear\",\"duration\":100},\"type\":\"click\"},\"asset\":\"previous\",\"x\":2,\"y\":3,\"w\":5,\"h\":8.3,\"id\":\"previous\",\"visible\":true,\"editable\":true,\"z-index\":100},{\"event\":{\"action\":[{\"type\":\"command\",\"command\":\"eval\",\"asset\":\"scene185d692a-1841-4a35-96c1-67cebcde42a2\",\"success\":\"correct_response\",\"failure\":\"wrong_response\"},{\"type\":\"command\",\"command\":\"HIDEHTMLELEMENTS\",\"asset\":\"scene185d692a-1841-4a35-96c1-67cebcde42a2\"}],\"type\":\"click\"},\"asset\":\"validate\",\"x\":91,\"y\":15,\"h\":15}],\"text\":[],\"shape\":[],\"hotspot\":[],\"embed\":[],\"div\":[],\"audio\":[{\"asset\":\"domain_11825\"}],\"scribble\":[],\"g\":[{\"embed\":{\"template\":\"item\",\"var-item\":\"item\"},\"x\":10,\"y\":0,\"w\":80,\"h\":90},{\"embed\":{\"template-name\":\"retry\"},\"x\":30,\"y\":-40,\"w\":40,\"h\":48,\"id\":\"retryDialog\",\"visible\":false},{\"embed\":{\"template-name\":\"goodjob\"},\"x\":30,\"y\":-40,\"w\":40,\"h\":48,\"id\":\"goodjobDialog\",\"visible\":false}],\"iterate\":\"assessment_185d692a-1841-4a35-96c1-67cebcde42a2\",\"preload\":true,\"var\":\"item\",\"appEvents\":{\"list\":\"next_item,correct_response,wrong_response\"}},{\"id\":\"scenea3cc8a52-833a-4a9d-b6e6-3be6b266b70f\",\"x\":0,\"y\":0,\"w\":100,\"h\":100,\"param\":[{\"name\":\"next\",\"value\":\"endScreen\"},{\"name\":\"previous\",\"value\":\"scene185d692a-1841-4a35-96c1-67cebcde42a2\"}],\"events\":{\"event\":[{\"action\":{\"type\":\"command\",\"command\":\"play\",\"asset\":\"domain_8313\",\"loop\":1},\"type\":\"enter\"},{\"action\":{\"type\":\"command\",\"command\":\"stop\",\"asset\":\"domain_8313\",\"loop\":1},\"type\":\"exit\"},{\"action\":[{\"type\":\"command\",\"command\":\"play\",\"asset\":\"retry_audio\"},{\"type\":\"command\",\"command\":\"show\",\"asset\":\"retryDialog\"},{\"tween\":{\"to\":{\"ease\":\"bounceOut \",\"duration\":1500,\"__cdata\":{\"x\":20,\"y\":20,\"w\":40,\"h\":30}},\"id\":\"retryShowAnim\"},\"type\":\"animation\",\"asset\":\"retryDialog\"}],\"type\":\"wrong_response\"},{\"action\":[{\"type\":\"command\",\"command\":\"play\",\"asset\":\"goodjob_audio\"},{\"type\":\"command\",\"command\":\"show\",\"asset\":\"goodjobDialog\"},{\"tween\":{\"to\":{\"ease\":\"bounceOut\",\"duration\":1500,\"__cdata\":{\"x\":20,\"y\":20,\"w\":40,\"h\":30}},\"id\":\"gjShowAnim\"},\"type\":\"animation\",\"asset\":\"goodjobDialog\"}],\"type\":\"correct_response\"}]},\"image\":[{\"event\":{\"action\":{\"type\":\"command\",\"command\":\"transitionTo\",\"asset\":\"theme\",\"param\":\"next\",\"effect\":\"fadein\",\"direction\":\"left\",\"ease\":\"linear\",\"duration\":500},\"type\":\"click\"},\"asset\":\"next\",\"x\":93,\"y\":3,\"w\":5,\"h\":8.3,\"id\":\"next\",\"visible\":true,\"editable\":true,\"z-index\":100},{\"event\":{\"action\":{\"type\":\"command\",\"command\":\"transitionTo\",\"asset\":\"theme\",\"param\":\"previous\",\"effect\":\"fadein\",\"direction\":\"right\",\"ease\":\"linear\",\"duration\":100},\"type\":\"click\"},\"asset\":\"previous\",\"x\":2,\"y\":3,\"w\":5,\"h\":8.3,\"id\":\"previous\",\"visible\":true,\"editable\":true,\"z-index\":100},{\"event\":{\"action\":[{\"type\":\"command\",\"command\":\"eval\",\"asset\":\"scenea3cc8a52-833a-4a9d-b6e6-3be6b266b70f\",\"success\":\"correct_response\",\"failure\":\"wrong_response\"},{\"type\":\"command\",\"command\":\"HIDEHTMLELEMENTS\",\"asset\":\"scenea3cc8a52-833a-4a9d-b6e6-3be6b266b70f\"}],\"type\":\"click\"},\"asset\":\"validate\",\"x\":91,\"y\":15,\"h\":15}],\"text\":[],\"shape\":[],\"hotspot\":[],\"embed\":[],\"div\":[],\"audio\":[{\"asset\":\"domain_8313\"}],\"scribble\":[],\"g\":[{\"embed\":{\"template\":\"item\",\"var-item\":\"item\"},\"x\":10,\"y\":0,\"w\":80,\"h\":90},{\"embed\":{\"template-name\":\"retry\"},\"x\":30,\"y\":-40,\"w\":40,\"h\":48,\"id\":\"retryDialog\",\"visible\":false},{\"embed\":{\"template-name\":\"goodjob\"},\"x\":30,\"y\":-40,\"w\":40,\"h\":48,\"id\":\"goodjobDialog\",\"visible\":false}],\"iterate\":\"assessment_a3cc8a52-833a-4a9d-b6e6-3be6b266b70f\",\"preload\":true,\"var\":\"item\",\"appEvents\":{\"list\":\"next_item,correct_response,wrong_response\"}},{\"id\":\"endScreen\",\"x\":0,\"y\":0,\"w\":100,\"h\":100,\"param\":[{\"name\":\"previousScreen\",\"value\":\"recordAudio9\"},{\"name\":\"nextScreen\",\"value\":\"splash\"},{\"name\":\"previous\",\"value\":\"scenea3cc8a52-833a-4a9d-b6e6-3be6b266b70f\"}],\"events\":{\"event\":[]},\"image\":[{\"x\":0,\"y\":0,\"w\":100,\"h\":100,\"visible\":true,\"editable\":true,\"asset\":\"endScreen\",\"z-index\":0},{\"x\":40,\"y\":50,\"w\":8,\"h\":13,\"visible\":false,\"editable\":true,\"asset\":\"previous\",\"event\":{\"action\":{\"asset\":\"theme\",\"command\":\"transitionTo\",\"direction\":\"right\",\"duration\":600,\"ease\":\"linear\",\"effect\":\"scroll\",\"param\":\"previousScreen\",\"type\":\"command\"},\"type\":\"click\"},\"z-index\":1},{\"x\":50,\"y\":50,\"w\":8,\"h\":13,\"visible\":false,\"editable\":true,\"asset\":\"next\",\"event\":{\"action\":{\"asset\":\"theme\",\"command\":\"transitionTo\",\"direction\":\"right\",\"duration\":600,\"ease\":\"linear\",\"effect\":\"scroll\",\"param\":\"nextScreen\",\"type\":\"command\"},\"type\":\"click\"},\"z-index\":2},{\"event\":{\"action\":{\"type\":\"command\",\"command\":\"transitionTo\",\"asset\":\"theme\",\"param\":\"previous\",\"effect\":\"fadein\",\"direction\":\"right\",\"ease\":\"linear\",\"duration\":100},\"type\":\"click\"},\"asset\":\"previous\",\"x\":2,\"y\":3,\"w\":5,\"h\":8.3,\"id\":\"previous\",\"visible\":true,\"editable\":true,\"z-index\":100}],\"text\":[],\"shape\":[],\"hotspot\":[],\"embed\":[],\"div\":[],\"audio\":[],\"scribble\":[],\"g\":[]}]}}")) + ) + val result: Map[String, AnyRef] = ExtractableMimeTypeHelper.processECMLBody(obj, jobConfig)(ec, cloudStorageUtil) + result.contains("artifactUrl") shouldBe true + result.get("artifactUrl") shouldNot be(null) + result.contains("cloudStorageKey") shouldBe true + result.get("cloudStorageKey") shouldNot be(null) + } + +} diff --git a/publish-pipeline/live-node-publisher/src/test/scala/org/sunbird/job/livenodepublisher/publish/helpers/spec/LiveCollectionPublisherSpec.scala b/publish-pipeline/live-node-publisher/src/test/scala/org/sunbird/job/livenodepublisher/publish/helpers/spec/LiveCollectionPublisherSpec.scala new file mode 100644 index 000000000..1f419ee4d --- /dev/null +++ b/publish-pipeline/live-node-publisher/src/test/scala/org/sunbird/job/livenodepublisher/publish/helpers/spec/LiveCollectionPublisherSpec.scala @@ -0,0 +1,226 @@ +package org.sunbird.job.livenodepublisher.publish.helpers.spec + +import akka.dispatch.ExecutionContexts +import com.typesafe.config.{Config, ConfigFactory} +import org.apache.commons.lang3.StringUtils +import org.cassandraunit.CQLDataLoader +import org.cassandraunit.dataset.cql.FileCQLDataSet +import org.cassandraunit.utils.EmbeddedCassandraServerHelper +import org.mockito.ArgumentMatchers.{any, anyString} +import org.mockito.Mockito +import org.mockito.Mockito.{doNothing, when} +import org.scalatest.{BeforeAndAfterAll, FlatSpec, Matchers} +import org.scalatestplus.mockito.MockitoSugar +import org.sunbird.job.domain.`object`.{DefinitionCache, ObjectDefinition} +import org.sunbird.job.livenodepublisher.publish.helpers.LiveCollectionPublisher +import org.sunbird.job.livenodepublisher.task.LiveNodePublisherConfig +import org.sunbird.job.publish.config.PublishConfig +import org.sunbird.job.publish.core.{DefinitionConfig, ExtDataConfig, ObjectData} +import org.sunbird.job.publish.helpers.EcarPackageType +import org.sunbird.job.util._ + +import java.text.SimpleDateFormat +import java.util +import java.util.Date +import scala.concurrent.ExecutionContextExecutor + +class LiveCollectionPublisherSpec extends FlatSpec with BeforeAndAfterAll with Matchers with MockitoSugar { + + implicit val mockNeo4JUtil: Neo4JUtil = mock[Neo4JUtil](Mockito.withSettings().serializable()) + implicit var cassandraUtil: CassandraUtil = _ + val config: Config = ConfigFactory.load("test.conf").withFallback(ConfigFactory.systemEnvironment()) + val jobConfig: LiveNodePublisherConfig = new LiveNodePublisherConfig(config) + implicit val cloudStorageUtil: CloudStorageUtil = new CloudStorageUtil(jobConfig) + implicit val ec: ExecutionContextExecutor = ExecutionContexts.global + implicit val defCache: DefinitionCache = new DefinitionCache() + implicit val defConfig: DefinitionConfig = DefinitionConfig(jobConfig.schemaSupportVersionMap, jobConfig.definitionBasePath) + implicit val publishConfig: PublishConfig = jobConfig.asInstanceOf[PublishConfig] + implicit val httpUtil: HttpUtil = new HttpUtil + val mockElasticUtil: ElasticSearchUtil = mock[ElasticSearchUtil](Mockito.withSettings().serializable()) + var definitionCache = new DefinitionCache() + implicit val definition: ObjectDefinition = definitionCache.getDefinition("Collection", jobConfig.schemaSupportVersionMap.getOrElse("collection", "1.0").asInstanceOf[String], jobConfig.definitionBasePath) + implicit val readerConfig: ExtDataConfig = ExtDataConfig(jobConfig.hierarchyKeyspaceName, jobConfig.hierarchyTableName, definition.getExternalPrimaryKey, definition.getExternalProps) + + def getTimeStamp: String = { + val sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ"); + sdf.format(new Date()) + } + + override protected def beforeAll(): Unit = { + super.beforeAll() + EmbeddedCassandraServerHelper.startEmbeddedCassandra(80000L) + cassandraUtil = new CassandraUtil(jobConfig.cassandraHost, jobConfig.cassandraPort, jobConfig) + val session = cassandraUtil.session + val dataLoader = new CQLDataLoader(session) + dataLoader.load(new FileCQLDataSet(getClass.getResource("/test.cql").getPath, true, true)) + } + + override protected def afterAll(): Unit = { + super.afterAll() + try { + EmbeddedCassandraServerHelper.cleanEmbeddedCassandra() + delay(10000) + } catch { + case ex: Exception => + } + } + + def delay(time: Long): Unit = { + try { + Thread.sleep(time) + } catch { + case ex: Exception => print("") + } + } + + "enrichObjectMetadata" should "enrich the Content pkgVersion metadata" in { + val data = new ObjectData("do_2133950809948078081503", Map[String, AnyRef]("name" -> "Content Name", "identifier" -> "do_2133950809948078081503", "pkgVersion" -> 0.0.asInstanceOf[AnyRef], "mimeType" -> "application/vnd.ekstep.content-collection", "keywords" -> Array[String]("test keyword"))) + val result: ObjectData = new TestCollectionPublisher().enrichObjectMetadata(data).getOrElse(data) + result.metadata.getOrElse("pkgVersion", 0.0.asInstanceOf[Number]).asInstanceOf[Number] should be(1.0.asInstanceOf[Number]) + } + + // "validateMetadata with invalid external data" should "return exception messages" in { + // val data = new ObjectData("do_123", Map[String, AnyRef]("name" -> "Content Name", "identifier" -> "do_123", "pkgVersion" -> 0.0.asInstanceOf[AnyRef]), Some(Map[String, AnyRef]("artifactUrl" -> "artifactUrl"))) + // val result: List[String] = new TestCollectionPublisher().validateMetadata(data, data.identifier) + // result.size should be(1) + // } + + "saveExternalData " should "save external data to cassandra table" in { + val data = new ObjectData("do_1234", Map[String, AnyRef](), Some(Map[String, AnyRef]("body" -> "body", "answer" -> "answer"))) + new TestCollectionPublisher().saveExternalData(data, readerConfig) + } + + "getHierarchy " should "do nothing " in { + val identifier = "do_11329603741667328018" + new TestCollectionPublisher().getHierarchy(identifier, readerConfig) + } + + "getExtDatas " should "do nothing " in { + val identifier = "do_11329603741667328018" + new TestCollectionPublisher().getExtDatas(List(identifier), readerConfig) + } + + "getHierarchies " should "do nothing " in { + val identifier = "do_11329603741667328018" + new TestCollectionPublisher().getHierarchies(List(identifier), readerConfig) + } + + "getDataForEcar" should "return one element in list" in { + val data = new ObjectData("do_123", Map("objectType" -> "Content"), Some(Map("responseDeclaration" -> "test")), Some(Map())) + val result: Option[List[Map[String, AnyRef]]] = new TestCollectionPublisher().getDataForEcar(data) + result.size should be(1) + } + + "getObjectWithEcar" should "return object with ecar url" in { + val unpublishedChildrenObj: List[Map[String, AnyRef]] = ScalaJsonUtil.deserialize[List[Map[String, AnyRef]]](unpublishedChildrenData) + val data = new ObjectData("do_123", Map("objectType" -> "Collection", "identifier" -> "do_123", "name" -> "Test Collection", "lastPublishedOn" -> getTimeStamp, "lastUpdatedOn" -> getTimeStamp, "status" -> "Draft", "downloadUrl" -> "downloadUrl", "variants" -> Map.empty[String,AnyRef]), Some(Map()), Some(Map("children" -> unpublishedChildrenObj))) + val result = new TestCollectionPublisher().getObjectWithEcar(data, List(EcarPackageType.SPINE, EcarPackageType.ONLINE))(ec, mockNeo4JUtil, cassandraUtil, readerConfig, cloudStorageUtil, jobConfig, defCache, defConfig, httpUtil) + StringUtils.isNotBlank(result.metadata.getOrElse("downloadUrl", "").asInstanceOf[String]) + } + + "syncNodes" should "sync Child Nodes into ElasticSearch" in { + Mockito.reset(mockElasticUtil) + doNothing().when(mockElasticUtil).addDocument(anyString(), anyString()) + doNothing().when(mockElasticUtil).bulkIndexWithIndexId(anyString(), anyString(), any()) + + val do_113405023736512512114Json = """{"identifier":"do_113405023736512512114","graph_id":"domain","node_id":0,"collections":["do_11340502373639782416", "do_113405023736479744112"],"objectType":"Collection","nodeType":"DATA_NODE"}""" + when(mockElasticUtil.getDocumentAsString("do_113405023736512512114")).thenReturn(do_113405023736512512114Json) + + val do_11340502373639782416Json = """{"identifier":"do_11340502373639782416","graph_id":"domain","node_id":0,"collections":["do_11340502373642240018", "do_11340502373608652812"],"objectType":"Collection","nodeType":"DATA_NODE"}""" + when(mockElasticUtil.getDocumentAsString("do_11340502373639782416")).thenReturn(do_11340502373639782416Json) + + val do_11340502373642240018Json = """{"identifier":"do_11340502373642240018","graph_id":"domain","node_id":0,"collections":["do_11336831941257625611"],"objectType":"Collection","nodeType":"DATA_NODE"}""" + when(mockElasticUtil.getDocumentAsString("do_11340502373642240018")).thenReturn(do_11340502373642240018Json) + + val do_11340502373608652812Json = """{"identifier":"do_11340502373608652812","graph_id":"domain","node_id":0,"collections":["do_11340096165525094411"],"objectType":"Collection","nodeType":"DATA_NODE"}""" + when(mockElasticUtil.getDocumentAsString("do_11340502373608652812")).thenReturn(do_11340502373608652812Json) + + val do_113405023736479744112Json = """{"identifier":"do_113405023736479744112","graph_id":"domain","node_id":0,"collections":["do_11340502373638144014"],"objectType":"Collection","nodeType":"DATA_NODE"}""" + when(mockElasticUtil.getDocumentAsString("do_113405023736479744112")).thenReturn(do_113405023736479744112Json) + + val do_11340502373638144014Json = """{"identifier":"do_11340502373638144014","graph_id":"domain","node_id":0,"collections":["do_113405023736455168110"],"objectType":"Collection","nodeType":"DATA_NODE"}""" + when(mockElasticUtil.getDocumentAsString("do_11340502373638144014")).thenReturn(do_11340502373638144014Json) + + val do_113405023736455168110Json = """{"identifier":"do_113405023736455168110","graph_id":"domain","node_id":0,"collections":["do_11340096293585715212"],"objectType":"Collection","nodeType":"DATA_NODE"}""" + when(mockElasticUtil.getDocumentAsString("do_113405023736455168110")).thenReturn(do_113405023736455168110Json) + + val publishedCollectionNodeMetadataObj: Map[String,AnyRef] = ScalaJsonUtil.deserialize[Map[String,AnyRef]](publishedCollectionNodeMetadata) + val data = new ObjectData("do_123", publishedCollectionNodeMetadataObj, Some(Map.empty[String, AnyRef])) + val syncChildrenData = ScalaJsonUtil.deserialize[List[Map[String, AnyRef]]](publishedChildrenData) + val messages: Map[String, Map[String, AnyRef]] = new TestCollectionPublisher().syncNodes(data, syncChildrenData, List.empty)(mockElasticUtil, mockNeo4JUtil, cassandraUtil, readerConfig, definition, jobConfig) + + assert(messages != null && messages.size > 0) + } + + "updateHierarchyMetadata" should "update child nodes with published object metadata" in { + val unpublishedChildrenObj: List[Map[String, AnyRef]] = ScalaJsonUtil.deserialize[List[Map[String, AnyRef]]](unpublishedChildrenData) + val publishedCollectionNodeMetadataObj: Map[String,AnyRef] = ScalaJsonUtil.deserialize[Map[String,AnyRef]](publishedCollectionNodeMetadata) + + val collRelationalMetadata = new TestCollectionPublisher().getRelationalMetadata("do_123", readerConfig)(cassandraUtil).get + // Collection - update and publish children - line 418 in PublishFinalizer + val updatedChildren: List[Map[String, AnyRef]] = new TestCollectionPublisher().updateHierarchyMetadata(unpublishedChildrenObj, publishedCollectionNodeMetadataObj, collRelationalMetadata)(jobConfig) + + assert(updatedChildren.nonEmpty) + + updatedChildren.map(child => { + assert(child.contains("pkgVersion")) + assert(child("pkgVersion").toString.equalsIgnoreCase("1")) + }) + } + + "getRelationalMetadata" should "return empty Map when there is no entry in relational_metadata column" in { + val collRelationalMetadata = new TestCollectionPublisher().getRelationalMetadata("do_1234", readerConfig)(cassandraUtil).get + assert(collRelationalMetadata != null && collRelationalMetadata.isEmpty) + } + + "getRelationalMetadata" should "return empty Map when there is empty entry in relational_metadata column" in { + val collRelationalMetadata = new TestCollectionPublisher().getRelationalMetadata("do_12345", readerConfig)(cassandraUtil).get + assert(collRelationalMetadata != null && collRelationalMetadata.isEmpty) + } + + "getRelationalMetadata" should "return empty Map when there is empty object entry in relational_metadata column" in { + val collRelationalMetadata = new TestCollectionPublisher().getRelationalMetadata("do_123456", readerConfig)(cassandraUtil).get + assert(collRelationalMetadata != null && collRelationalMetadata.isEmpty) + } + + "publishHierarchy " should "save hierarchy data to cassandra table" in { + val publishChildrenData = ScalaJsonUtil.deserialize[List[Map[String, AnyRef]]](publishedChildrenData) + val publishedCollectionNodeMetadataObj: Map[String,AnyRef] = ScalaJsonUtil.deserialize[Map[String,AnyRef]](publishedCollectionNodeMetadata) + val data = new ObjectData("do_123", publishedCollectionNodeMetadataObj, Some(Map.empty[String, AnyRef])) + val result = new TestCollectionPublisher().publishHierarchy(publishChildrenData, data, readerConfig, jobConfig) + assert(result) + } + + "getUnitsFromLiveContent" should "return object hierarchy" in { + val data = new ObjectData("do_2133950809948078081503", Map("identifier" -> "do_2133950809948078081503"), Some(Map.empty[String, AnyRef])) + val fetchedChildren = new TestCollectionPublisher().getUnitsFromLiveContent(data)(cassandraUtil,readerConfig,jobConfig) + assert(fetchedChildren.nonEmpty) + } + + "updateOriginPkgVersion" should "return origin node Data" in { + val metaData = new java.util.HashMap[String, AnyRef]() { + { + put("IL_UNIQUE_ID", "do_11300581751853056018") + put("identifier", "do_11300581751853056018") + put("name", "Origin Content") + put("createdBy", "874ed8a5-782e-4f6c-8f36-e0288455901e") + put("channel", "b00bc992ef25f1a9a8d63291e20efc8d") + put("trackable", "{\"enabled\":\"Yes\",\"autoBatch\":\"Yes\"}") + put("createdFor", util.Arrays.asList("ORG_001")) + put("pkgVersion", 3.asInstanceOf[AnyRef]) + } + } + val collectionObj = new ObjectData("do_123", Map("objectType" -> "Collection", "identifier" -> "do_123", "name" -> "Test Collection","origin" -> "do_456", "originData" -> Map("name" -> "Contemporary India I", "copyType" -> "deep", "license" -> "CC BY 4.0", "organisation" -> Array("NCERT"))), Some(Map()), Some(Map())) + when(mockNeo4JUtil.getNodeProperties(anyString())).thenReturn(metaData) + val originObj = new TestCollectionPublisher().updateOriginPkgVersion(collectionObj) + assert(originObj.metadata("originData").asInstanceOf[Map[String,AnyRef]]("pkgVersion") == 3) + } + + val collRelationalMetadataStr = "{\"do_123\":{\"name\":\"Collection Publish T21\",\"children\":[\"do_11340511137112064018\",\"do_11340511137080934412\"],\"root\":true},\"do_11340511137112064018\":{\"name\":\"Collection Parent\",\"children\":[\"do_11340096165525094411\"],\"root\":false,\"relationalMetadata\":{\"do_11340096165525094411\":{\"name\":\"Test Name RM L1 - R1\",\"keywords\":[\"Overwriting content KW1\"]}}},\"do_11340511137080934412\":{\"name\":\"Collection Parent\",\"children\":[\"do_11340096165525094411\"],\"root\":false,\"relationalMetadata\":{\"do_11340096165525094411\":{\"name\":\"Test Name RM L1 - R1\",\"keywords\":[\"Overwriting content KW1\"]}}},\"do_11340096165525094411\":{\"name\":\"PDF Content\",\"children\":[],\"root\":false},\"do_113405111371145216110\":{\"name\":\"test\",\"children\":[], \"root\":false}}" + val publishedChildrenData = "[{\"lastStatusChangedOn\":\"2021-11-08T12:40:36.586+0530\",\"parent\":\"do_11340502356035174411\",\"children\":[{\"lastStatusChangedOn\":\"2021-11-08T12:40:36.572+0530\",\"parent\":\"do_113405023736512512114\",\"children\":[{\"lastStatusChangedOn\":\"2021-11-08T12:40:36.575+0530\",\"parent\":\"do_11340502373639782416\",\"children\":[{\"copyright\":\"J H S BHARKHOKHA, Tamil Nadu\",\"lastStatusChangedOn\":\"2021-09-17T16:22:50.404+0530\",\"parent\":\"do_11340502373642240018\",\"licenseterms\":\"By creating any type of content (resources, books, courses etc.) on DIKSHA, you consent to publish it under the Creative Commons License Framework. Please choose the applicable creative commons license you wish to apply to your content.\",\"organisation\":[\"J H S BHARKHOKHA\",\"Tamil Nadu\"],\"mediaType\":\"content\",\"name\":\"jaga Aug 25th more than 200mp mp4 update 1\",\"discussionForum\":{\"enabled\":\"No\"},\"createdOn\":\"2021-09-17T16:05:29.019+0530\",\"channel\":\"0126825293972439041\",\"lastUpdatedOn\":\"2021-09-17T16:22:50.404+0530\",\"size\":363062652,\"identifier\":\"do_11336831941257625611\",\"resourceType\":\"Learn\",\"ownershipType\":[\"createdBy\"],\"compatibilityLevel\":1,\"audience\":[\"Student\"],\"os\":[\"All\"],\"primaryCategory\":\"eTextbook\",\"appIcon\":\"https://stagingdock.blob.core.windows.net/sunbird-content-dock/content/do_2134462034258575361402/artifact/rhinocerous.thumb.jpg\",\"languageCode\":[\"en\"],\"downloadUrl\":\"\",\"framework\":\"tn_k-12_5\",\"creator\":\"सामग्री निर्माता TN\",\"versionKey\":\"1631875539805\",\"mimeType\":\"video/mp4\",\"code\":\"3bd7411e-c03c-4997-a247-4d43a5cc820b\",\"license\":\"CC BY 4.0\",\"version\":2,\"prevStatus\":\"Live\",\"contentType\":\"Resource\",\"prevState\":\"Draft\",\"language\":[\"English\"],\"lastPublishedOn\":\"2021-09-17T16:22:15.047+0530\",\"objectType\":\"Content\",\"status\":\"Live\",\"createdBy\":\"fca2925f-1eee-4654-9177-fece3fd6afc9\",\"dialcodeRequired\":\"No\",\"interceptionPoints\":{},\"idealScreenSize\":\"normal\",\"contentEncoding\":\"identity\",\"depth\":4,\"consumerId\":\"2eaff3db-cdd1-42e5-a611-bebbf906e6cf\",\"lastPublishedBy\":\"\",\"osId\":\"org.ekstep.quiz.app\",\"copyrightYear\":2021,\"se_FWIds\":[\"tn_k-12_5\"],\"contentDisposition\":\"online-only\",\"previewUrl\":\"https://preprodall.blob.core.windows.net/ntp-content-preprod/content/assets/do_2133520666218741761984/como-kids-tv-_-the-story-of-comos-family-_-30min-_-cartoon-video-for-kids.mp4\",\"artifactUrl\":\"https://preprodall.blob.core.windows.net/ntp-content-preprod/content/assets/do_2133520666218741761984/como-kids-tv-_-the-story-of-comos-family-_-30min-_-cartoon-video-for-kids.mp4\",\"visibility\":\"Default\",\"credentials\":{\"enabled\":\"No\"},\"variants\":{\"spine\":{\"ecarUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11336831941257625611/jaga-aug-25th-more-than-200mp-mp4-update-1_1631875935857_do_11336831941257625611_3_SPINE.ecar\",\"size\":\"2172\"}},\"index\":1,\"pkgVersion\":3,\"idealScreenDensity\":\"hdpi\"}],\"mediaType\":\"content\",\"name\":\"5.1.1 Key parts in the head\",\"discussionForum\":{\"enabled\":\"No\"},\"createdOn\":\"2021-11-08T12:40:36.575+0530\",\"channel\":\"0126825293972439041\",\"generateDIALCodes\":\"No\",\"lastUpdatedOn\":\"2021-11-08T12:46:52.715+0530\",\"identifier\":\"do_11340502373642240018\",\"description\":\"xyz\",\"ownershipType\":[\"createdBy\"],\"compatibilityLevel\":1,\"audience\":[\"Student\"],\"os\":[\"All\"],\"primaryCategory\":\"Textbook Unit\",\"languageCode\":[\"en\"],\"downloadUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/collection/do_11340502356035174411/collection-publish-t20_1636356202227_do_11340502356035174411_1_SPINE.ecar\",\"framework\":\"ncert_k-12\",\"versionKey\":\"1636355436575\",\"mimeType\":\"application/vnd.ekstep.content-collection\",\"code\":\"7991af5c2e51e4d3d7b83167aaac8829\",\"license\":\"CC BY 4.0\",\"leafNodes\":[\"do_11336831941257625611\"],\"version\":2,\"contentType\":\"TextBookUnit\",\"language\":[\"English\"],\"lastPublishedOn\":\"2021-11-08T12:53:09.398+0530\",\"objectType\":\"Collection\",\"status\":\"Live\",\"dialcodeRequired\":\"No\",\"idealScreenSize\":\"normal\",\"contentEncoding\":\"gzip\",\"leafNodesCount\":1,\"depth\":3,\"osId\":\"org.ekstep.launcher\",\"contentDisposition\":\"inline\",\"visibility\":\"Parent\",\"credentials\":{\"enabled\":\"No\"},\"variants\":\"{\\\"spine\\\":{\\\"ecarUrl\\\":\\\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/collection/do_11340502356035174411/collection-publish-t20_1636356202227_do_11340502356035174411_1_SPINE.ecar\\\",\\\"size\\\":\\\"12044\\\"},\\\"online\\\":{\\\"ecarUrl\\\":\\\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/collection/do_11340502356035174411/collection-publish-t20_1636356202412_do_11340502356035174411_1_ONLINE.ecar\\\",\\\"size\\\":\\\"5074\\\"}}\",\"index\":1,\"pkgVersion\":1,\"idealScreenDensity\":\"hdpi\"},{\"lastStatusChangedOn\":\"2021-11-08T12:40:36.534+0530\",\"parent\":\"do_11340502373639782416\",\"children\":[{\"lastStatusChangedOn\":\"2021-11-02T19:13:39.729+0530\",\"parent\":\"do_11340502373608652812\",\"mediaType\":\"content\",\"name\":\"Collection Publishing PDF Content\",\"discussionForum\":{\"enabled\":\"No\"},\"createdOn\":\"2021-11-02T18:56:17.917+0530\",\"createdFor\":[\"01309282781705830427\"],\"channel\":\"0126825293972439041\",\"lastUpdatedOn\":\"2021-11-02T19:13:39.729+0530\",\"streamingUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/assets/do_1134009478823116801129/chapter_1.pdf\",\"identifier\":\"do_11340096165525094411\",\"resourceType\":\"Learn\",\"ownershipType\":[\"createdBy\"],\"compatibilityLevel\":4,\"audience\":[\"Student\"],\"os\":[\"All\"],\"primaryCategory\":\"Explanation Content\",\"appIcon\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11340094790233292811/artifact/033019_sz_reviews_feat_1564126718632.thumb.jpg\",\"languageCode\":[\"en\"],\"downloadUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11340096165525094411/collection-publishing-pdf-content_1635860615969_do_11340096165525094411_1.ecar\",\"framework\":\"ekstep_ncert_k-12\",\"creator\":\"N131\",\"versionKey\":\"1635859577917\",\"mimeType\":\"application/pdf\",\"code\":\"c9ce1ce0-b9b4-402e-a9c3-556701070838\",\"license\":\"CC BY 4.0\",\"version\":2,\"prevStatus\":\"Processing\",\"contentType\":\"Resource\",\"prevState\":\"Draft\",\"language\":[\"English\"],\"lastPublishedOn\":\"2021-11-02T19:13:35.589+0530\",\"objectType\":\"Content\",\"status\":\"Live\",\"pragma\":[\"external\"],\"createdBy\":\"0b71985d-fcb0-4018-ab14-83f10c3b0426\",\"dialcodeRequired\":\"No\",\"interceptionPoints\":{},\"keywords\":[\"CPPDFContent1\",\"CPPDFContent2\",\"CollectionKW1\"],\"idealScreenSize\":\"normal\",\"contentEncoding\":\"identity\",\"depth\":4,\"lastPublishedBy\":\"\",\"osId\":\"org.ekstep.quiz.app\",\"copyrightYear\":2021,\"se_FWIds\":[\"ekstep_ncert_k-12\"],\"contentDisposition\":\"inline\",\"previewUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/assets/do_1134009478823116801129/chapter_1.pdf\",\"artifactUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/assets/do_1134009478823116801129/chapter_1.pdf\",\"visibility\":\"Default\",\"credentials\":{\"enabled\":\"No\"},\"variants\":{\"full\":{\"ecarUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11340096165525094411/collection-publishing-pdf-content_1635860615969_do_11340096165525094411_1.ecar\",\"size\":\"256918\"},\"spine\":{\"ecarUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11340096165525094411/collection-publishing-pdf-content_1635860619148_do_11340096165525094411_1_SPINE.ecar\",\"size\":\"6378\"}},\"index\":1,\"pkgVersion\":1,\"idealScreenDensity\":\"hdpi\"}],\"mediaType\":\"content\",\"name\":\"5.1.2 Other parts\",\"discussionForum\":{\"enabled\":\"No\"},\"createdOn\":\"2021-11-08T12:40:36.534+0530\",\"channel\":\"0126825293972439041\",\"generateDIALCodes\":\"No\",\"lastUpdatedOn\":\"2021-11-08T12:46:52.715+0530\",\"identifier\":\"do_11340502373608652812\",\"description\":\"\",\"ownershipType\":[\"createdBy\"],\"compatibilityLevel\":1,\"audience\":[\"Student\"],\"os\":[\"All\"],\"primaryCategory\":\"Textbook Unit\",\"languageCode\":[\"en\"],\"downloadUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/collection/do_11340502356035174411/collection-publish-t20_1636356202227_do_11340502356035174411_1_SPINE.ecar\",\"framework\":\"ncert_k-12\",\"versionKey\":\"1636355436534\",\"mimeType\":\"application/vnd.ekstep.content-collection\",\"code\":\"3bf70f06d3e8dba010d8806fd94259b1\",\"license\":\"CC BY 4.0\",\"leafNodes\":[\"do_11340096165525094411\"],\"version\":2,\"contentType\":\"TextBookUnit\",\"language\":[\"English\"],\"lastPublishedOn\":\"2021-11-08T12:53:09.398+0530\",\"objectType\":\"Collection\",\"status\":\"Live\",\"dialcodeRequired\":\"No\",\"idealScreenSize\":\"normal\",\"contentEncoding\":\"gzip\",\"leafNodesCount\":1,\"depth\":3,\"osId\":\"org.ekstep.launcher\",\"contentDisposition\":\"inline\",\"visibility\":\"Parent\",\"credentials\":{\"enabled\":\"No\"},\"variants\":\"{\\\"spine\\\":{\\\"ecarUrl\\\":\\\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/collection/do_11340502356035174411/collection-publish-t20_1636356202227_do_11340502356035174411_1_SPINE.ecar\\\",\\\"size\\\":\\\"12044\\\"},\\\"online\\\":{\\\"ecarUrl\\\":\\\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/collection/do_11340502356035174411/collection-publish-t20_1636356202412_do_11340502356035174411_1_ONLINE.ecar\\\",\\\"size\\\":\\\"5074\\\"}}\",\"index\":2,\"pkgVersion\":1,\"idealScreenDensity\":\"hdpi\"}],\"mediaType\":\"content\",\"name\":\"5.1 Parts of Body\",\"discussionForum\":{\"enabled\":\"No\"},\"createdOn\":\"2021-11-08T12:40:36.572+0530\",\"channel\":\"0126825293972439041\",\"generateDIALCodes\":\"No\",\"lastUpdatedOn\":\"2021-11-08T12:46:52.715+0530\",\"identifier\":\"do_11340502373639782416\",\"description\":\"This section describes about various part of the body such as head, hands, legs etc.\",\"ownershipType\":[\"createdBy\"],\"compatibilityLevel\":1,\"audience\":[\"Student\"],\"os\":[\"All\"],\"primaryCategory\":\"Textbook Unit\",\"languageCode\":[\"en\"],\"downloadUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/collection/do_11340502356035174411/collection-publish-t20_1636356202227_do_11340502356035174411_1_SPINE.ecar\",\"framework\":\"ncert_k-12\",\"versionKey\":\"1636355436572\",\"mimeType\":\"application/vnd.ekstep.content-collection\",\"code\":\"20cc1f31e62f924c6e47bf04c994376b\",\"license\":\"CC BY 4.0\",\"leafNodes\":[\"do_11336831941257625611\",\"do_11340096165525094411\"],\"version\":2,\"contentType\":\"TextBookUnit\",\"language\":[\"English\"],\"lastPublishedOn\":\"2021-11-08T12:53:09.398+0530\",\"objectType\":\"Collection\",\"status\":\"Live\",\"dialcodeRequired\":\"No\",\"idealScreenSize\":\"normal\",\"contentEncoding\":\"gzip\",\"leafNodesCount\":2,\"depth\":2,\"osId\":\"org.ekstep.launcher\",\"contentDisposition\":\"inline\",\"visibility\":\"Parent\",\"credentials\":{\"enabled\":\"No\"},\"variants\":\"{\\\"spine\\\":{\\\"ecarUrl\\\":\\\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/collection/do_11340502356035174411/collection-publish-t20_1636356202227_do_11340502356035174411_1_SPINE.ecar\\\",\\\"size\\\":\\\"12044\\\"},\\\"online\\\":{\\\"ecarUrl\\\":\\\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/collection/do_11340502356035174411/collection-publish-t20_1636356202412_do_11340502356035174411_1_ONLINE.ecar\\\",\\\"size\\\":\\\"5074\\\"}}\",\"index\":1,\"pkgVersion\":1,\"idealScreenDensity\":\"hdpi\"},{\"lastStatusChangedOn\":\"2021-11-08T12:40:36.582+0530\",\"parent\":\"do_113405023736512512114\",\"children\":[{\"lastStatusChangedOn\":\"2021-11-08T12:40:36.570+0530\",\"parent\":\"do_113405023736479744112\",\"children\":[{\"lastStatusChangedOn\":\"2021-11-08T12:40:36.579+0530\",\"parent\":\"do_11340502373638144014\",\"children\":[{\"lastStatusChangedOn\":\"2021-11-02T19:16:10.667+0530\",\"parent\":\"do_113405023736455168110\",\"mediaType\":\"content\",\"name\":\"Collection Publish MP4 content\",\"discussionForum\":{\"enabled\":\"No\"},\"createdOn\":\"2021-11-02T18:58:53.445+0530\",\"channel\":\"0126825293972439041\",\"lastUpdatedOn\":\"2021-11-02T19:16:10.667+0530\",\"identifier\":\"do_11340096293585715212\",\"resourceType\":\"Learn\",\"ownershipType\":[\"createdBy\"],\"compatibilityLevel\":1,\"audience\":[\"Student\"],\"os\":[\"All\"],\"primaryCategory\":\"Explanation Content\",\"appIcon\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_1134009488766730241130/artifact/033019_sz_reviews_feat_1564126718632.thumb.jpg\",\"languageCode\":[\"en\"],\"downloadUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11340096293585715212/collection-publish-mp4-content_1635860769119_do_11340096293585715212_1.ecar\",\"framework\":\"ekstep_ncert_k-12\",\"versionKey\":\"1635859733445\",\"mimeType\":\"video/mp4\",\"code\":\"e0b58864-3dc5-484a-b194-38c3eddcbce1\",\"license\":\"CC BY 4.0\",\"version\":2,\"prevStatus\":\"Draft\",\"contentType\":\"Resource\",\"prevState\":\"Draft\",\"language\":[\"English\"],\"lastPublishedOn\":\"2021-11-02T19:16:08.789+0530\",\"objectType\":\"Content\",\"status\":\"Live\",\"createdBy\":\"0b71985d-fcb0-4018-ab14-83f10c3b0426\",\"dialcodeRequired\":\"No\",\"interceptionPoints\":{},\"keywords\":[\"CPMP4ContentKW1\",\"CPMP4ContentKW2\"],\"idealScreenSize\":\"normal\",\"contentEncoding\":\"identity\",\"depth\":5,\"lastPublishedBy\":\"\",\"osId\":\"org.ekstep.quiz.app\",\"se_FWIds\":[\"ekstep_ncert_k-12\"],\"contentDisposition\":\"inline\",\"previewUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/assets/do_1134009488766730241130/amoeba-eat.mp4\",\"artifactUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/assets/do_1134009488766730241130/amoeba-eat.mp4\",\"visibility\":\"Default\",\"credentials\":{\"enabled\":\"No\"},\"variants\":{\"full\":{\"ecarUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11340096293585715212/collection-publish-mp4-content_1635860769119_do_11340096293585715212_1.ecar\",\"size\":\"2692101\"},\"spine\":{\"ecarUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11340096293585715212/collection-publish-mp4-content_1635860770277_do_11340096293585715212_1_SPINE.ecar\",\"size\":\"6275\"}},\"index\":1,\"pkgVersion\":1,\"idealScreenDensity\":\"hdpi\"}],\"mediaType\":\"content\",\"name\":\"dsffgdg\",\"discussionForum\":{\"enabled\":\"No\"},\"createdOn\":\"2021-11-08T12:40:36.579+0530\",\"channel\":\"0126825293972439041\",\"generateDIALCodes\":\"No\",\"lastUpdatedOn\":\"2021-11-08T12:46:52.715+0530\",\"identifier\":\"do_113405023736455168110\",\"description\":\"\",\"ownershipType\":[\"createdBy\"],\"compatibilityLevel\":1,\"audience\":[\"Student\"],\"os\":[\"All\"],\"primaryCategory\":\"Textbook Unit\",\"languageCode\":[\"en\"],\"downloadUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/collection/do_11340502356035174411/collection-publish-t20_1636356202227_do_11340502356035174411_1_SPINE.ecar\",\"framework\":\"ncert_k-12\",\"versionKey\":\"1636355436579\",\"mimeType\":\"application/vnd.ekstep.content-collection\",\"code\":\"9cf84ff2fb08f9af4c23eb09df9b2520\",\"license\":\"CC BY 4.0\",\"leafNodes\":[\"do_11340096293585715212\"],\"version\":2,\"contentType\":\"TextBookUnit\",\"language\":[\"English\"],\"lastPublishedOn\":\"2021-11-08T12:53:09.398+0530\",\"objectType\":\"Collection\",\"status\":\"Live\",\"dialcodeRequired\":\"No\",\"idealScreenSize\":\"normal\",\"contentEncoding\":\"gzip\",\"leafNodesCount\":1,\"depth\":4,\"osId\":\"org.ekstep.launcher\",\"contentDisposition\":\"inline\",\"visibility\":\"Parent\",\"credentials\":{\"enabled\":\"No\"},\"variants\":\"{\\\"spine\\\":{\\\"ecarUrl\\\":\\\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/collection/do_11340502356035174411/collection-publish-t20_1636356202227_do_11340502356035174411_1_SPINE.ecar\\\",\\\"size\\\":\\\"12044\\\"},\\\"online\\\":{\\\"ecarUrl\\\":\\\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/collection/do_11340502356035174411/collection-publish-t20_1636356202412_do_11340502356035174411_1_ONLINE.ecar\\\",\\\"size\\\":\\\"5074\\\"}}\",\"index\":1,\"pkgVersion\":1,\"idealScreenDensity\":\"hdpi\"}],\"mediaType\":\"content\",\"name\":\"5.2.1 Respiratory System\",\"discussionForum\":{\"enabled\":\"No\"},\"createdOn\":\"2021-11-08T12:40:36.570+0530\",\"channel\":\"0126825293972439041\",\"generateDIALCodes\":\"No\",\"lastUpdatedOn\":\"2021-11-08T12:46:52.715+0530\",\"identifier\":\"do_11340502373638144014\",\"description\":\"\",\"ownershipType\":[\"createdBy\"],\"compatibilityLevel\":1,\"audience\":[\"Student\"],\"os\":[\"All\"],\"primaryCategory\":\"Textbook Unit\",\"languageCode\":[\"en\"],\"downloadUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/collection/do_11340502356035174411/collection-publish-t20_1636356202227_do_11340502356035174411_1_SPINE.ecar\",\"attributions\":[],\"framework\":\"ncert_k-12\",\"versionKey\":\"1636355436570\",\"mimeType\":\"application/vnd.ekstep.content-collection\",\"code\":\"b186b1bbcc9c58db865f75e34345179e\",\"license\":\"CC BY 4.0\",\"leafNodes\":[\"do_11340096293585715212\"],\"version\":2,\"contentType\":\"TextBookUnit\",\"language\":[\"English\"],\"lastPublishedOn\":\"2021-11-08T12:53:09.398+0530\",\"objectType\":\"Collection\",\"status\":\"Live\",\"dialcodeRequired\":\"No\",\"keywords\":[\"UnitKW1\",\"UnitKW2\"],\"idealScreenSize\":\"normal\",\"contentEncoding\":\"gzip\",\"leafNodesCount\":1,\"depth\":3,\"osId\":\"org.ekstep.launcher\",\"contentDisposition\":\"inline\",\"visibility\":\"Parent\",\"credentials\":{\"enabled\":\"No\"},\"variants\":\"{\\\"spine\\\":{\\\"ecarUrl\\\":\\\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/collection/do_11340502356035174411/collection-publish-t20_1636356202227_do_11340502356035174411_1_SPINE.ecar\\\",\\\"size\\\":\\\"12044\\\"},\\\"online\\\":{\\\"ecarUrl\\\":\\\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/collection/do_11340502356035174411/collection-publish-t20_1636356202412_do_11340502356035174411_1_ONLINE.ecar\\\",\\\"size\\\":\\\"5074\\\"}}\",\"index\":1,\"pkgVersion\":1,\"idealScreenDensity\":\"hdpi\"}],\"mediaType\":\"content\",\"name\":\"5.2 Organ Systems\",\"discussionForum\":{\"enabled\":\"No\"},\"createdOn\":\"2021-11-08T12:40:36.582+0530\",\"channel\":\"0126825293972439041\",\"generateDIALCodes\":\"No\",\"lastUpdatedOn\":\"2021-11-08T12:46:52.715+0530\",\"identifier\":\"do_113405023736479744112\",\"description\":\"\",\"ownershipType\":[\"createdBy\"],\"compatibilityLevel\":1,\"audience\":[\"Student\"],\"os\":[\"All\"],\"primaryCategory\":\"Textbook Unit\",\"languageCode\":[\"en\"],\"downloadUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/collection/do_11340502356035174411/collection-publish-t20_1636356202227_do_11340502356035174411_1_SPINE.ecar\",\"framework\":\"ncert_k-12\",\"versionKey\":\"1636355436582\",\"mimeType\":\"application/vnd.ekstep.content-collection\",\"code\":\"40a1ed37e0fad94eca76b2a96fe086ab\",\"license\":\"CC BY 4.0\",\"leafNodes\":[\"do_11340096293585715212\"],\"version\":2,\"contentType\":\"TextBookUnit\",\"language\":[\"English\"],\"lastPublishedOn\":\"2021-11-08T12:53:09.398+0530\",\"objectType\":\"Collection\",\"status\":\"Live\",\"dialcodeRequired\":\"No\",\"idealScreenSize\":\"normal\",\"contentEncoding\":\"gzip\",\"leafNodesCount\":1,\"depth\":2,\"osId\":\"org.ekstep.launcher\",\"contentDisposition\":\"inline\",\"visibility\":\"Parent\",\"credentials\":{\"enabled\":\"No\"},\"variants\":\"{\\\"spine\\\":{\\\"ecarUrl\\\":\\\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/collection/do_11340502356035174411/collection-publish-t20_1636356202227_do_11340502356035174411_1_SPINE.ecar\\\",\\\"size\\\":\\\"12044\\\"},\\\"online\\\":{\\\"ecarUrl\\\":\\\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/collection/do_11340502356035174411/collection-publish-t20_1636356202412_do_11340502356035174411_1_ONLINE.ecar\\\",\\\"size\\\":\\\"5074\\\"}}\",\"index\":2,\"pkgVersion\":1,\"idealScreenDensity\":\"hdpi\"}],\"mediaType\":\"content\",\"name\":\"5. Human Body\",\"discussionForum\":{\"enabled\":\"No\"},\"createdOn\":\"2021-11-08T12:40:36.586+0530\",\"channel\":\"0126825293972439041\",\"generateDIALCodes\":\"No\",\"lastUpdatedOn\":\"2021-11-08T12:46:52.715+0530\",\"identifier\":\"do_113405023736512512114\",\"description\":\"This chapter describes about human body\",\"ownershipType\":[\"createdBy\"],\"compatibilityLevel\":1,\"audience\":[\"Student\"],\"os\":[\"All\"],\"primaryCategory\":\"Textbook Unit\",\"languageCode\":[\"en\"],\"downloadUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/collection/do_11340502356035174411/collection-publish-t20_1636356202227_do_11340502356035174411_1_SPINE.ecar\",\"framework\":\"ncert_k-12\",\"versionKey\":\"1636355436586\",\"mimeType\":\"application/vnd.ekstep.content-collection\",\"code\":\"76abafa2a0c2cfef90b52db1ef41fb82\",\"license\":\"CC BY 4.0\",\"leafNodes\":[\"do_11340096293585715212\",\"do_11336831941257625611\",\"do_11340096165525094411\"],\"version\":2,\"contentType\":\"TextBookUnit\",\"language\":[\"English\"],\"lastPublishedOn\":\"2021-11-08T12:53:09.398+0530\",\"objectType\":\"Collection\",\"status\":\"Live\",\"dialcodeRequired\":\"No\",\"idealScreenSize\":\"normal\",\"contentEncoding\":\"gzip\",\"leafNodesCount\":3,\"depth\":1,\"osId\":\"org.ekstep.launcher\",\"contentDisposition\":\"inline\",\"visibility\":\"Parent\",\"credentials\":{\"enabled\":\"No\"},\"variants\":\"{\\\"spine\\\":{\\\"ecarUrl\\\":\\\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/collection/do_11340502356035174411/collection-publish-t20_1636356202227_do_11340502356035174411_1_SPINE.ecar\\\",\\\"size\\\":\\\"12044\\\"},\\\"online\\\":{\\\"ecarUrl\\\":\\\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/collection/do_11340502356035174411/collection-publish-t20_1636356202412_do_11340502356035174411_1_ONLINE.ecar\\\",\\\"size\\\":\\\"5074\\\"}}\",\"index\":1,\"pkgVersion\":1,\"idealScreenDensity\":\"hdpi\"}]" + val unpublishedChildrenData = "[{\"lastStatusChangedOn\":\"2021-11-08T15:38:54.180+0530\",\"parent\":\"do_11340511118032076811\",\"children\":[{\"lastStatusChangedOn\":\"2021-11-08T15:38:54.166+0530\",\"parent\":\"do_113405111371202560114\",\"children\":[{\"lastStatusChangedOn\":\"2021-11-08T15:38:54.170+0530\",\"parent\":\"do_11340511137108787216\",\"children\":[{\"copyright\":\"J H S BHARKHOKHA, Tamil Nadu\",\"lastStatusChangedOn\":\"2021-09-17T16:22:50.404+0530\",\"parent\":\"do_11340511137112064018\",\"licenseterms\":\"By creating any type of content (resources, books, courses etc.) on DIKSHA, you consent to publish it under the Creative Commons License Framework. Please choose the applicable creative commons license you wish to apply to your content.\",\"organisation\":[\"J H S BHARKHOKHA\",\"Tamil Nadu\"],\"mediaType\":\"content\",\"name\":\"jaga Aug 25th more than 200mp mp4 update 1\",\"discussionForum\":{\"enabled\":\"No\"},\"createdOn\":\"2021-09-17T16:05:29.019+0530\",\"channel\":\"0126825293972439041\",\"lastUpdatedOn\":\"2021-09-17T16:22:50.404+0530\",\"size\":363062652,\"identifier\":\"do_11336831941257625611\",\"resourceType\":\"Learn\",\"ownershipType\":[\"createdBy\"],\"compatibilityLevel\":1,\"audience\":[\"Student\"],\"os\":[\"All\"],\"primaryCategory\":\"eTextbook\",\"appIcon\":\"https://stagingdock.blob.core.windows.net/sunbird-content-dock/content/do_2134462034258575361402/artifact/rhinocerous.thumb.jpg\",\"languageCode\":[\"en\"],\"downloadUrl\":\"\",\"framework\":\"tn_k-12_5\",\"creator\":\"सामग्री निर्माता TN\",\"versionKey\":\"1631875539805\",\"mimeType\":\"video/mp4\",\"code\":\"3bd7411e-c03c-4997-a247-4d43a5cc820b\",\"license\":\"CC BY 4.0\",\"version\":2,\"prevStatus\":\"Live\",\"contentType\":\"Resource\",\"prevState\":\"Draft\",\"language\":[\"English\"],\"lastPublishedOn\":\"2021-09-17T16:22:15.047+0530\",\"objectType\":\"Content\",\"status\":\"Live\",\"createdBy\":\"fca2925f-1eee-4654-9177-fece3fd6afc9\",\"dialcodeRequired\":\"No\",\"interceptionPoints\":{},\"idealScreenSize\":\"normal\",\"contentEncoding\":\"identity\",\"depth\":4,\"consumerId\":\"2eaff3db-cdd1-42e5-a611-bebbf906e6cf\",\"lastPublishedBy\":\"\",\"osId\":\"org.ekstep.quiz.app\",\"copyrightYear\":2021,\"se_FWIds\":[\"tn_k-12_5\"],\"contentDisposition\":\"online-only\",\"previewUrl\":\"https://preprodall.blob.core.windows.net/ntp-content-preprod/content/assets/do_2133520666218741761984/como-kids-tv-_-the-story-of-comos-family-_-30min-_-cartoon-video-for-kids.mp4\",\"artifactUrl\":\"https://preprodall.blob.core.windows.net/ntp-content-preprod/content/assets/do_2133520666218741761984/como-kids-tv-_-the-story-of-comos-family-_-30min-_-cartoon-video-for-kids.mp4\",\"visibility\":\"Default\",\"credentials\":{\"enabled\":\"No\"},\"variants\":{\"spine\":{\"ecarUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11336831941257625611/jaga-aug-25th-more-than-200mp-mp4-update-1_1631875935857_do_11336831941257625611_3_SPINE.ecar\",\"size\":\"2172\"}},\"index\":1,\"pkgVersion\":3,\"idealScreenDensity\":\"hdpi\"}],\"mediaType\":\"content\",\"name\":\"5.1.1 Key parts in the head\",\"discussionForum\":{\"enabled\":\"No\"},\"createdOn\":\"2021-11-08T15:38:54.170+0530\",\"channel\":\"0126825293972439041\",\"generateDIALCodes\":\"No\",\"lastUpdatedOn\":\"2021-11-08T15:38:54.170+0530\",\"identifier\":\"do_11340511137112064018\",\"description\":\"xyz\",\"ownershipType\":[\"createdBy\"],\"compatibilityLevel\":1,\"audience\":[\"Student\"],\"os\":[\"All\"],\"primaryCategory\":\"Textbook Unit\",\"languageCode\":[\"en\"],\"framework\":\"ncert_k-12\",\"versionKey\":\"1636366134170\",\"mimeType\":\"application/vnd.ekstep.content-collection\",\"code\":\"7991af5c2e51e4d3d7b83167aaac8829\",\"license\":\"CC BY 4.0\",\"version\":2,\"contentType\":\"TextBookUnit\",\"language\":[\"English\"],\"objectType\":\"Collection\",\"status\":\"Draft\",\"dialcodeRequired\":\"No\",\"idealScreenSize\":\"normal\",\"contentEncoding\":\"gzip\",\"depth\":3,\"osId\":\"org.ekstep.launcher\",\"contentDisposition\":\"inline\",\"visibility\":\"Parent\",\"credentials\":{\"enabled\":\"No\"},\"index\":1,\"idealScreenDensity\":\"hdpi\"},{\"lastStatusChangedOn\":\"2021-11-08T15:38:54.133+0530\",\"parent\":\"do_11340511137108787216\",\"children\":[{\"lastStatusChangedOn\":\"2021-11-02T19:13:39.729+0530\",\"parent\":\"do_11340511137080934412\",\"mediaType\":\"content\",\"name\":\"Collection Publishing PDF Content\",\"discussionForum\":{\"enabled\":\"No\"},\"createdOn\":\"2021-11-02T18:56:17.917+0530\",\"createdFor\":[\"01309282781705830427\"],\"channel\":\"0126825293972439041\",\"lastUpdatedOn\":\"2021-11-02T19:13:39.729+0530\",\"streamingUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/assets/do_1134009478823116801129/chapter_1.pdf\",\"identifier\":\"do_11340096165525094411\",\"resourceType\":\"Learn\",\"ownershipType\":[\"createdBy\"],\"compatibilityLevel\":4,\"audience\":[\"Student\"],\"os\":[\"All\"],\"primaryCategory\":\"Explanation Content\",\"appIcon\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11340094790233292811/artifact/033019_sz_reviews_feat_1564126718632.thumb.jpg\",\"languageCode\":[\"en\"],\"downloadUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11340096165525094411/collection-publishing-pdf-content_1635860615969_do_11340096165525094411_1.ecar\",\"framework\":\"ekstep_ncert_k-12\",\"creator\":\"N131\",\"versionKey\":\"1635859577917\",\"mimeType\":\"application/pdf\",\"code\":\"c9ce1ce0-b9b4-402e-a9c3-556701070838\",\"license\":\"CC BY 4.0\",\"version\":2,\"prevStatus\":\"Processing\",\"contentType\":\"Resource\",\"prevState\":\"Draft\",\"language\":[\"English\"],\"lastPublishedOn\":\"2021-11-02T19:13:35.589+0530\",\"objectType\":\"Content\",\"status\":\"Live\",\"pragma\":[\"external\"],\"createdBy\":\"0b71985d-fcb0-4018-ab14-83f10c3b0426\",\"dialcodeRequired\":\"No\",\"interceptionPoints\":{},\"keywords\":[\"CPPDFContent1\",\"CPPDFContent2\",\"CollectionKW1\"],\"idealScreenSize\":\"normal\",\"contentEncoding\":\"identity\",\"depth\":4,\"lastPublishedBy\":\"\",\"osId\":\"org.ekstep.quiz.app\",\"copyrightYear\":2021,\"se_FWIds\":[\"ekstep_ncert_k-12\"],\"contentDisposition\":\"inline\",\"previewUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/assets/do_1134009478823116801129/chapter_1.pdf\",\"artifactUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/assets/do_1134009478823116801129/chapter_1.pdf\",\"visibility\":\"Default\",\"credentials\":{\"enabled\":\"No\"},\"variants\":{\"full\":{\"ecarUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11340096165525094411/collection-publishing-pdf-content_1635860615969_do_11340096165525094411_1.ecar\",\"size\":\"256918\"},\"spine\":{\"ecarUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11340096165525094411/collection-publishing-pdf-content_1635860619148_do_11340096165525094411_1_SPINE.ecar\",\"size\":\"6378\"}},\"index\":1,\"pkgVersion\":1,\"idealScreenDensity\":\"hdpi\"}],\"mediaType\":\"content\",\"name\":\"5.1.2 Other parts\",\"discussionForum\":{\"enabled\":\"No\"},\"createdOn\":\"2021-11-08T15:38:54.133+0530\",\"channel\":\"0126825293972439041\",\"generateDIALCodes\":\"No\",\"lastUpdatedOn\":\"2021-11-08T15:38:54.132+0530\",\"identifier\":\"do_11340511137080934412\",\"description\":\"\",\"ownershipType\":[\"createdBy\"],\"compatibilityLevel\":1,\"audience\":[\"Student\"],\"os\":[\"All\"],\"primaryCategory\":\"Textbook Unit\",\"languageCode\":[\"en\"],\"framework\":\"ncert_k-12\",\"versionKey\":\"1636366134133\",\"mimeType\":\"application/vnd.ekstep.content-collection\",\"code\":\"3bf70f06d3e8dba010d8806fd94259b1\",\"license\":\"CC BY 4.0\",\"version\":2,\"contentType\":\"TextBookUnit\",\"language\":[\"English\"],\"objectType\":\"Collection\",\"status\":\"Draft\",\"dialcodeRequired\":\"No\",\"idealScreenSize\":\"normal\",\"contentEncoding\":\"gzip\",\"depth\":3,\"osId\":\"org.ekstep.launcher\",\"contentDisposition\":\"inline\",\"visibility\":\"Parent\",\"credentials\":{\"enabled\":\"No\"},\"index\":2,\"idealScreenDensity\":\"hdpi\"}],\"mediaType\":\"content\",\"name\":\"5.1 Parts of Body\",\"discussionForum\":{\"enabled\":\"No\"},\"createdOn\":\"2021-11-08T15:38:54.166+0530\",\"channel\":\"0126825293972439041\",\"generateDIALCodes\":\"No\",\"lastUpdatedOn\":\"2021-11-08T15:38:54.165+0530\",\"identifier\":\"do_11340511137108787216\",\"description\":\"This section describes about various part of the body such as head, hands, legs etc.\",\"ownershipType\":[\"createdBy\"],\"compatibilityLevel\":1,\"audience\":[\"Student\"],\"os\":[\"All\"],\"primaryCategory\":\"Textbook Unit\",\"languageCode\":[\"en\"],\"framework\":\"ncert_k-12\",\"versionKey\":\"1636366134166\",\"mimeType\":\"application/vnd.ekstep.content-collection\",\"code\":\"20cc1f31e62f924c6e47bf04c994376b\",\"license\":\"CC BY 4.0\",\"version\":2,\"contentType\":\"TextBookUnit\",\"language\":[\"English\"],\"objectType\":\"Collection\",\"status\":\"Draft\",\"dialcodeRequired\":\"No\",\"idealScreenSize\":\"normal\",\"contentEncoding\":\"gzip\",\"depth\":2,\"osId\":\"org.ekstep.launcher\",\"contentDisposition\":\"inline\",\"visibility\":\"Parent\",\"credentials\":{\"enabled\":\"No\"},\"index\":1,\"idealScreenDensity\":\"hdpi\"},{\"lastStatusChangedOn\":\"2021-11-08T15:38:54.177+0530\",\"parent\":\"do_113405111371202560114\",\"children\":[{\"lastStatusChangedOn\":\"2021-11-08T15:38:54.162+0530\",\"parent\":\"do_113405111371169792112\",\"children\":[{\"lastStatusChangedOn\":\"2021-11-08T15:38:54.173+0530\",\"parent\":\"do_11340511137105510414\",\"children\":[{\"lastStatusChangedOn\":\"2021-11-02T19:16:10.667+0530\",\"parent\":\"do_113405111371145216110\",\"mediaType\":\"content\",\"name\":\"Collection Publish MP4 content\",\"discussionForum\":{\"enabled\":\"No\"},\"createdOn\":\"2021-11-02T18:58:53.445+0530\",\"channel\":\"0126825293972439041\",\"lastUpdatedOn\":\"2021-11-02T19:16:10.667+0530\",\"identifier\":\"do_11340096293585715212\",\"resourceType\":\"Learn\",\"ownershipType\":[\"createdBy\"],\"compatibilityLevel\":1,\"audience\":[\"Student\"],\"os\":[\"All\"],\"primaryCategory\":\"Explanation Content\",\"appIcon\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_1134009488766730241130/artifact/033019_sz_reviews_feat_1564126718632.thumb.jpg\",\"languageCode\":[\"en\"],\"downloadUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11340096293585715212/collection-publish-mp4-content_1635860769119_do_11340096293585715212_1.ecar\",\"framework\":\"ekstep_ncert_k-12\",\"versionKey\":\"1635859733445\",\"mimeType\":\"video/mp4\",\"code\":\"e0b58864-3dc5-484a-b194-38c3eddcbce1\",\"license\":\"CC BY 4.0\",\"version\":2,\"prevStatus\":\"Draft\",\"contentType\":\"Resource\",\"prevState\":\"Draft\",\"language\":[\"English\"],\"lastPublishedOn\":\"2021-11-02T19:16:08.789+0530\",\"objectType\":\"Content\",\"status\":\"Live\",\"createdBy\":\"0b71985d-fcb0-4018-ab14-83f10c3b0426\",\"dialcodeRequired\":\"No\",\"interceptionPoints\":{},\"keywords\":[\"CPMP4ContentKW1\",\"CPMP4ContentKW2\"],\"idealScreenSize\":\"normal\",\"contentEncoding\":\"identity\",\"depth\":5,\"lastPublishedBy\":\"\",\"osId\":\"org.ekstep.quiz.app\",\"se_FWIds\":[\"ekstep_ncert_k-12\"],\"contentDisposition\":\"inline\",\"previewUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/assets/do_1134009488766730241130/amoeba-eat.mp4\",\"artifactUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/assets/do_1134009488766730241130/amoeba-eat.mp4\",\"visibility\":\"Default\",\"credentials\":{\"enabled\":\"No\"},\"variants\":{\"full\":{\"ecarUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11340096293585715212/collection-publish-mp4-content_1635860769119_do_11340096293585715212_1.ecar\",\"size\":\"2692101\"},\"spine\":{\"ecarUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11340096293585715212/collection-publish-mp4-content_1635860770277_do_11340096293585715212_1_SPINE.ecar\",\"size\":\"6275\"}},\"index\":1,\"pkgVersion\":1,\"idealScreenDensity\":\"hdpi\"}],\"mediaType\":\"content\",\"name\":\"dsffgdg\",\"discussionForum\":{\"enabled\":\"No\"},\"createdOn\":\"2021-11-08T15:38:54.173+0530\",\"channel\":\"0126825293972439041\",\"generateDIALCodes\":\"No\",\"lastUpdatedOn\":\"2021-11-08T15:38:54.173+0530\",\"identifier\":\"do_113405111371145216110\",\"description\":\"\",\"ownershipType\":[\"createdBy\"],\"compatibilityLevel\":1,\"audience\":[\"Student\"],\"os\":[\"All\"],\"primaryCategory\":\"Textbook Unit\",\"languageCode\":[\"en\"],\"framework\":\"ncert_k-12\",\"versionKey\":\"1636366134173\",\"mimeType\":\"application/vnd.ekstep.content-collection\",\"code\":\"9cf84ff2fb08f9af4c23eb09df9b2520\",\"license\":\"CC BY 4.0\",\"version\":2,\"contentType\":\"TextBookUnit\",\"language\":[\"English\"],\"objectType\":\"Collection\",\"status\":\"Draft\",\"dialcodeRequired\":\"No\",\"idealScreenSize\":\"normal\",\"contentEncoding\":\"gzip\",\"depth\":4,\"osId\":\"org.ekstep.launcher\",\"contentDisposition\":\"inline\",\"visibility\":\"Parent\",\"credentials\":{\"enabled\":\"No\"},\"index\":1,\"idealScreenDensity\":\"hdpi\"}],\"mediaType\":\"content\",\"name\":\"5.2.1 Respiratory System\",\"discussionForum\":{\"enabled\":\"No\"},\"createdOn\":\"2021-11-08T15:38:54.162+0530\",\"channel\":\"0126825293972439041\",\"generateDIALCodes\":\"No\",\"lastUpdatedOn\":\"2021-11-08T15:53:32.894+0530\",\"identifier\":\"do_11340511137105510414\",\"description\":\"\",\"ownershipType\":[\"createdBy\"],\"compatibilityLevel\":1,\"audience\":[\"Student\"],\"os\":[\"All\"],\"primaryCategory\":\"Textbook Unit\",\"languageCode\":[\"en\"],\"attributions\":[],\"framework\":\"ncert_k-12\",\"versionKey\":\"1636366134162\",\"mimeType\":\"application/vnd.ekstep.content-collection\",\"code\":\"b186b1bbcc9c58db865f75e34345179e\",\"license\":\"CC BY 4.0\",\"version\":2,\"contentType\":\"TextBookUnit\",\"language\":[\"English\"],\"objectType\":\"Collection\",\"status\":\"Draft\",\"dialcodeRequired\":\"No\",\"keywords\":[\"UnitKW1\",\"UnitKW2\"],\"idealScreenSize\":\"normal\",\"contentEncoding\":\"gzip\",\"depth\":3,\"osId\":\"org.ekstep.launcher\",\"contentDisposition\":\"inline\",\"visibility\":\"Parent\",\"credentials\":{\"enabled\":\"No\"},\"index\":1,\"idealScreenDensity\":\"hdpi\"}],\"mediaType\":\"content\",\"name\":\"5.2 Organ Systems\",\"discussionForum\":{\"enabled\":\"No\"},\"createdOn\":\"2021-11-08T15:38:54.176+0530\",\"channel\":\"0126825293972439041\",\"generateDIALCodes\":\"No\",\"lastUpdatedOn\":\"2021-11-08T15:38:54.176+0530\",\"identifier\":\"do_113405111371169792112\",\"description\":\"\",\"ownershipType\":[\"createdBy\"],\"compatibilityLevel\":1,\"audience\":[\"Student\"],\"os\":[\"All\"],\"primaryCategory\":\"Textbook Unit\",\"languageCode\":[\"en\"],\"framework\":\"ncert_k-12\",\"versionKey\":\"1636366134176\",\"mimeType\":\"application/vnd.ekstep.content-collection\",\"code\":\"40a1ed37e0fad94eca76b2a96fe086ab\",\"license\":\"CC BY 4.0\",\"version\":2,\"contentType\":\"TextBookUnit\",\"language\":[\"English\"],\"objectType\":\"Collection\",\"status\":\"Draft\",\"dialcodeRequired\":\"No\",\"idealScreenSize\":\"normal\",\"contentEncoding\":\"gzip\",\"depth\":2,\"osId\":\"org.ekstep.launcher\",\"contentDisposition\":\"inline\",\"visibility\":\"Parent\",\"credentials\":{\"enabled\":\"No\"},\"index\":2,\"idealScreenDensity\":\"hdpi\"}],\"mediaType\":\"content\",\"name\":\"5. Human Body\",\"discussionForum\":{\"enabled\":\"No\"},\"createdOn\":\"2021-11-08T15:38:54.180+0530\",\"channel\":\"0126825293972439041\",\"generateDIALCodes\":\"No\",\"lastUpdatedOn\":\"2021-11-08T15:38:54.179+0530\",\"identifier\":\"do_113405111371202560114\",\"description\":\"This chapter describes about human body\",\"ownershipType\":[\"createdBy\"],\"compatibilityLevel\":1,\"audience\":[\"Student\"],\"os\":[\"All\"],\"primaryCategory\":\"Textbook Unit\",\"languageCode\":[\"en\"],\"framework\":\"ncert_k-12\",\"versionKey\":\"1636366134180\",\"mimeType\":\"application/vnd.ekstep.content-collection\",\"code\":\"76abafa2a0c2cfef90b52db1ef41fb82\",\"license\":\"CC BY 4.0\",\"version\":2,\"contentType\":\"TextBookUnit\",\"language\":[\"English\"],\"objectType\":\"Collection\",\"status\":\"Draft\",\"dialcodeRequired\":\"No\",\"idealScreenSize\":\"normal\",\"contentEncoding\":\"gzip\",\"depth\":1,\"osId\":\"org.ekstep.launcher\",\"contentDisposition\":\"inline\",\"visibility\":\"Parent\",\"credentials\":{\"enabled\":\"No\"},\"index\":1,\"idealScreenDensity\":\"hdpi\"}]" + val publishedCollectionNodeMetadata = "{\"copyright\":\"tn\",\"lastStatusChangedOn\":\"2021-11-08T15:38:31.391+0530\",\"publish_type\":\"public\",\"author\":\"ContentcreatorTN\",\"children\":[{\"name\":\"5. Human Body\",\"identifier\":\"do_113405111371202560114\",\"description\":\"This chapter describes about human body\",\"objectType\":\"Collection\",\"index\":1}],\"body\":null,\"mediaType\":\"content\",\"name\":\"Collection Publish T20\",\"toc_url\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11340511118032076811/artifact/do_11340511118032076811_toc.json\",\"discussionForum\":\"{\\\"enabled\\\":\\\"No\\\"}\",\"createdOn\":\"2021-11-08T15:38:31.391+0530\",\"createdFor\":[\"0125196274181898243\"],\"channel\":\"0126825293972439041\",\"generateDIALCodes\":\"No\",\"lastUpdatedOn\":\"2021-11-08T15:53:33.587+0530\",\"size\":12048,\"publishError\":null,\"identifier\":\"do_11340511118032076811\",\"description\":\"Collection Publish\",\"resourceType\":\"Book\",\"ownershipType\":[\"createdBy\"],\"compatibilityLevel\":1,\"audience\":[\"Student\"],\"trackable\":\"{\\\"enabled\\\":\\\"No\\\",\\\"autoBatch\\\":\\\"No\\\"}\",\"os\":[\"All\"],\"primaryCategory\":\"Digital Textbook\",\"appIcon\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/collection/do_11340511118032076811/artifact/16_bvnvrokht6hn97eqcklwk2fs6ppx2z.thumb.png\",\"downloadUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/collection/do_11340511118032076811/collection-publish-t20_1636367178646_do_11340511118032076811_1_SPINE.ecar\",\"attributions\":[],\"framework\":\"ncert_k-12\",\"posterImage\":\"https://ntpproductionall.blob.core.windows.net/ntp-content-production/content/assets/do_31321903538537267212904/16_bvnvrokht6hn97eqcklwk2fs6ppx2z.png\",\"creator\":\"NCERT\",\"totalCompressedSize\":363062652,\"versionKey\":\"1636367013587\",\"mimeType\":\"application/vnd.ekstep.content-collection\",\"code\":\"0125196274181898243\",\"license\":\"CC BY 4.0\",\"leafNodes\":[\"do_11340096293585715212\",\"do_11336831941257625611\",\"do_11340096165525094411\"],\"version\":2,\"contentType\":\"TextBook\",\"language\":[\"English\"],\"lastPublishedOn\":\"2021-11-08T15:56:15.604+0530\",\"contentTypesCount\":\"{\\\"TextBookUnit\\\":7,\\\"Resource\\\":3}\",\"objectType\":\"Collection\",\"status\":\"Live\",\"createdBy\":\"220d4745-6764-498d-ad37-5e49b8cce716\",\"dialcodeRequired\":\"No\",\"keywords\":[\"CPPDFContent1\",\"UnitKW2\",\"CollectionKW1\",\"CPPDFContent2\",\"UnitKW1\",\"CPMP4ContentKW1\",\"CPMP4ContentKW2\"],\"userConsent\":\"Yes\",\"idealScreenSize\":\"normal\",\"contentEncoding\":\"gzip\",\"leafNodesCount\":3,\"depth\":0,\"flagReasons\":null,\"mimeTypesCount\":\"{\\\"application/pdf\\\":1,\\\"video/mp4\\\":2,\\\"application/vnd.ekstep.content-collection\\\":7}\",\"osId\":\"org.ekstep.quiz.app\",\"copyrightYear\":2021,\"se_FWIds\":[\"ncert_k-12\"],\"contentDisposition\":\"inline\",\"additionalCategories\":[],\"childNodes\":[\"do_11336831941257625611\",\"do_11340511137112064018\",\"do_11340511137108787216\",\"do_113405111371202560114\",\"do_11340096165525094411\",\"do_11340511137080934412\",\"do_11340096293585715212\",\"do_113405111371145216110\",\"do_11340511137105510414\",\"do_113405111371169792112\"],\"visibility\":\"Default\",\"credentials\":\"{\\\"enabled\\\":\\\"No\\\"}\",\"variants\":\"{\\\"spine\\\":{\\\"ecarUrl\\\":\\\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/collection/do_11340511118032076811/collection-publish-t20_1636367178646_do_11340511118032076811_1_SPINE.ecar\\\",\\\"size\\\":\\\"12048\\\"},\\\"online\\\":{\\\"ecarUrl\\\":\\\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/collection/do_11340511118032076811/collection-publish-t20_1636367178792_do_11340511118032076811_1_ONLINE.ecar\\\",\\\"size\\\":\\\"5081\\\"}}\",\"pkgVersion\":1,\"idealScreenDensity\":\"hdpi\"}" +} + + +class TestCollectionPublisher extends LiveCollectionPublisher {} \ No newline at end of file diff --git a/publish-pipeline/live-node-publisher/src/test/scala/org/sunbird/job/livenodepublisher/publish/helpers/spec/LiveContentPublisherSpec.scala b/publish-pipeline/live-node-publisher/src/test/scala/org/sunbird/job/livenodepublisher/publish/helpers/spec/LiveContentPublisherSpec.scala new file mode 100644 index 000000000..1b61077eb --- /dev/null +++ b/publish-pipeline/live-node-publisher/src/test/scala/org/sunbird/job/livenodepublisher/publish/helpers/spec/LiveContentPublisherSpec.scala @@ -0,0 +1,243 @@ +package org.sunbird.job.livenodepublisher.publish.helpers.spec + +import akka.dispatch.ExecutionContexts +import com.typesafe.config.{Config, ConfigFactory} +import org.apache.commons.lang3.StringUtils +import org.cassandraunit.CQLDataLoader +import org.cassandraunit.dataset.cql.FileCQLDataSet +import org.cassandraunit.utils.EmbeddedCassandraServerHelper +import org.mockito.Mockito +import org.scalatest.{BeforeAndAfterAll, FlatSpec, Matchers} +import org.scalatestplus.mockito.MockitoSugar +import org.sunbird.job.livenodepublisher.publish.helpers.LiveContentPublisher +import org.sunbird.job.livenodepublisher.task.LiveNodePublisherConfig +import org.sunbird.job.domain.`object`.DefinitionCache +import org.sunbird.job.exception.InvalidInputException +import org.sunbird.job.publish.config.PublishConfig +import org.sunbird.job.publish.core.{DefinitionConfig, ExtDataConfig, ObjectData, ObjectExtData} +import org.sunbird.job.publish.helpers.EcarPackageType +import org.sunbird.job.util.{CassandraUtil, CloudStorageUtil, HttpUtil, Neo4JUtil} + +import scala.concurrent.ExecutionContextExecutor + +class LiveContentPublisherSpec extends FlatSpec with BeforeAndAfterAll with Matchers with MockitoSugar { + + implicit val mockNeo4JUtil: Neo4JUtil = mock[Neo4JUtil](Mockito.withSettings().serializable()) + implicit var cassandraUtil: CassandraUtil = _ + val config: Config = ConfigFactory.load("test.conf").withFallback(ConfigFactory.systemEnvironment()) + val jobConfig: LiveNodePublisherConfig = new LiveNodePublisherConfig(config) + implicit val readerConfig: ExtDataConfig = ExtDataConfig(jobConfig.contentKeyspaceName, jobConfig.contentTableName) + implicit val cloudStorageUtil: CloudStorageUtil = new CloudStorageUtil(jobConfig) + implicit val ec: ExecutionContextExecutor = ExecutionContexts.global + implicit val defCache: DefinitionCache = new DefinitionCache() + implicit val defConfig: DefinitionConfig = DefinitionConfig(jobConfig.schemaSupportVersionMap, jobConfig.definitionBasePath) + implicit val publishConfig: PublishConfig = jobConfig.asInstanceOf[PublishConfig] + implicit val httpUtil: HttpUtil = new HttpUtil + + override protected def beforeAll(): Unit = { + super.beforeAll() + EmbeddedCassandraServerHelper.startEmbeddedCassandra(80000L) + cassandraUtil = new CassandraUtil(jobConfig.cassandraHost, jobConfig.cassandraPort, jobConfig) + val session = cassandraUtil.session + val dataLoader = new CQLDataLoader(session) + dataLoader.load(new FileCQLDataSet(getClass.getResource("/test.cql").getPath, true, true)) + } + + override protected def afterAll(): Unit = { + super.afterAll() + try { + EmbeddedCassandraServerHelper.cleanEmbeddedCassandra() + delay(10000) + } catch { + case ex: Exception => + } + } + + def delay(time: Long): Unit = { + try { + Thread.sleep(time) + } catch { + case ex: Exception => print("") + } + } + + "enrichObjectMetadata" should "enrich the Content pkgVersion metadata" in { + val data = new ObjectData("do_123", Map[String, AnyRef]("name" -> "Content Name", "identifier" -> "do_123", "pkgVersion" -> 0.0.asInstanceOf[AnyRef], "mimeType" -> "application/pdf")) + val result: ObjectData = new TestLiveContentPublisher().enrichObjectMetadata(data).getOrElse(data) + result.metadata.getOrElse("pkgVersion", 0.0.asInstanceOf[Number]).asInstanceOf[Number] should be(1.0.asInstanceOf[Number]) + } + + ignore should "enrich the Content metadata for application/vnd.ekstep.html-archive should through exception in artifactUrl is not available" in { + val data = new ObjectData("do_123", Map[String, AnyRef]("name" -> "Content Name", "identifier" -> "do_123", "pkgVersion" -> 0.0.asInstanceOf[AnyRef], "mimeType" -> "application/vnd.ekstep.html-archive")) + val result: ObjectData = new TestLiveContentPublisher().enrichObjectMetadata(data).getOrElse(data) + result.metadata.getOrElse("pkgVersion", 0.0.asInstanceOf[Number]).asInstanceOf[Number] should be(1.0.asInstanceOf[Number]) + } + + "enrichObjectMetadata" should "enrich the Content metadata for application/vnd.ekstep.html-archive" in { + val data = new ObjectData("do_1132167819505500161297", Map[String, AnyRef]("name" -> "Content Name", "identifier" -> "do_1132167819505500161297", "pkgVersion" -> 0.0.asInstanceOf[AnyRef], "mimeType" -> "application/vnd.ekstep.html-archive", "artifactUrl" -> "artifactUrl.zip")) + val result: ObjectData = new TestLiveContentPublisher().enrichObjectMetadata(data).getOrElse(data) + result.metadata.getOrElse("pkgVersion", 0.0.asInstanceOf[Number]).asInstanceOf[Number] should be(1.0.asInstanceOf[Number]) + } + + "validateMetadata with invalid external data" should "return exception messages" in { + val data = new ObjectData("do_123", Map[String, AnyRef]("name" -> "Content Name", "identifier" -> "do_123", "pkgVersion" -> 0.0.asInstanceOf[AnyRef]), Some(Map[String, AnyRef]("artifactUrl" -> "artifactUrl"))) + val result: List[String] = new TestLiveContentPublisher().validateMetadata(data, data.identifier, jobConfig) + result.size should be(1) + } + + "validateMetadata with mimeType application/vnd.ekstep.ecml-archive " should " return exception messages if extData is set as None" in { + val data = new ObjectData("do_123", Map[String, AnyRef]("name" -> "Content Name", "identifier" -> "do_123", "pkgVersion" -> 0.0.asInstanceOf[AnyRef], "mimeType" -> "application/vnd.ekstep.ecml-archive"), None) + val result: List[String] = new TestLiveContentPublisher().validateMetadata(data, data.identifier, jobConfig) + result.size should be(1) + result.contains("Either 'body' or 'artifactUrl' are required for processing of ECML content for : do_123") shouldBe true + } + + "validateMetadata with mimeType application/vnd.ekstep.ecml-archive " should " return exception messages if is having body=\"\"" in { + val data = new ObjectData("do_123", Map[String, AnyRef]("name" -> "Content Name", "identifier" -> "do_123", "pkgVersion" -> 0.0.asInstanceOf[AnyRef], "mimeType" -> "application/vnd.ekstep.ecml-archive"), Some(Map[String, AnyRef]("body" -> ""))) + val result: List[String] = new TestLiveContentPublisher().validateMetadata(data, data.identifier, jobConfig) + result.size should be(1) + result.contains("Either 'body' or 'artifactUrl' are required for processing of ECML content for : do_123") shouldBe true + } + + "validateMetadata with mimeType application/vnd.ekstep.ecml-archive " should " return exception messages if is having body=null" in { + val data = new ObjectData("do_123", Map[String, AnyRef]("name" -> "Content Name", "identifier" -> "do_123", "pkgVersion" -> 0.0.asInstanceOf[AnyRef], "mimeType" -> "application/vnd.ekstep.ecml-archive"), Some(Map[String, AnyRef]("body" -> null))) + val result: List[String] = new TestLiveContentPublisher().validateMetadata(data, data.identifier, jobConfig) + result.size should be(1) + result.contains("Either 'body' or 'artifactUrl' are required for processing of ECML content for : do_123") shouldBe true + } + + "validateMetadata with mimeType application/vnd.ekstep.ecml-archive " should " not return exception messages if is having body=null but artifactUrl is available" in { + val data = new ObjectData("do_123", Map[String, AnyRef]("name" -> "Content Name", "identifier" -> "do_123", "pkgVersion" -> 0.0.asInstanceOf[AnyRef], "mimeType" -> "application/vnd.ekstep.ecml-archive", "artifactUrl" -> "sampleUrl"), Some(Map[String, AnyRef]("body" -> null))) + val result: List[String] = new TestLiveContentPublisher().validateMetadata(data, data.identifier, jobConfig) + result.size should be(0) + } + + "validateMetadata with mimeType application/vnd.ekstep.ecml-archive " should " not return exception messages if is having valid body" in { + val data = new ObjectData("do_123", Map[String, AnyRef]("name" -> "Content Name", "identifier" -> "do_123", "pkgVersion" -> 0.0.asInstanceOf[AnyRef], "mimeType" -> "application/vnd.ekstep.ecml-archive"), Some(Map[String, AnyRef]("body" -> "{\"theme\":{\"id\":\"theme\",\"version\":\"1.0\",\"startStage\":\"8ab605cc-b26d-4d0d-a827-2112b0330c3a\",\"stage\":[{\"x\":0,\"y\":0,\"w\":100,\"h\":100,\"id\":\"8ab605cc-b26d-4d0d-a827-2112b0330c3a\",\"rotate\":null,\"config\":{\"__cdata\":\"{\\\"opacity\\\":100,\\\"strokeWidth\\\":1,\\\"stroke\\\":\\\"rgba(255, 255, 255, 0)\\\",\\\"autoplay\\\":false,\\\"visible\\\":true,\\\"color\\\":\\\"#FFFFFF\\\",\\\"genieControls\\\":false,\\\"instructions\\\":\\\"\\\"}\"},\"manifest\":{\"media\":[{\"assetId\":\"do_113303238321799168110\"},{\"assetId\":\"do_113303315360907264114\"}]},\"image\":[{\"asset\":\"do_113303238321799168110\",\"x\":20,\"y\":20,\"w\":49.51,\"h\":14.29,\"rotate\":0,\"z-index\":0,\"id\":\"a62c82d4-e497-424a-b9a5-45b585572e20\",\"config\":{\"__cdata\":\"{\\\"opacity\\\":100,\\\"strokeWidth\\\":1,\\\"stroke\\\":\\\"rgba(255, 255, 255, 0)\\\",\\\"autoplay\\\":false,\\\"visible\\\":true}\"}},{\"asset\":\"do_113303315360907264114\",\"x\":20,\"y\":20,\"w\":49.51,\"h\":14.29,\"rotate\":0,\"z-index\":1,\"id\":\"dcc3d9e7-e4e0-409c-b099-434523556c10\",\"config\":{\"__cdata\":\"{\\\"opacity\\\":100,\\\"strokeWidth\\\":1,\\\"stroke\\\":\\\"rgba(255, 255, 255, 0)\\\",\\\"autoplay\\\":false,\\\"visible\\\":true}\"}}]}],\"manifest\":{\"media\":[{\"id\":\"1b34ae41-4c56-4c4d-a0e4-be56169cb7e6\",\"plugin\":\"org.ekstep.navigation\",\"ver\":\"1.0\",\"src\":\"/content-plugins/org.ekstep.navigation-1.0/renderer/controller/navigation_ctrl.js\",\"type\":\"js\"},{\"id\":\"e65308d4-8420-4533-b74d-87c1909e9b12\",\"plugin\":\"org.ekstep.navigation\",\"ver\":\"1.0\",\"src\":\"/content-plugins/org.ekstep.navigation-1.0/renderer/templates/navigation.html\",\"type\":\"js\"},{\"id\":\"org.ekstep.navigation\",\"plugin\":\"org.ekstep.navigation\",\"ver\":\"1.0\",\"src\":\"/content-plugins/org.ekstep.navigation-1.0/renderer/plugin.js\",\"type\":\"plugin\"},{\"id\":\"org.ekstep.navigation_manifest\",\"plugin\":\"org.ekstep.navigation\",\"ver\":\"1.0\",\"src\":\"/content-plugins/org.ekstep.navigation-1.0/manifest.json\",\"type\":\"json\"},{\"name\":\"Screenshot 2021-06-17 at 5 17 12 PM\",\"id\":\"do_113303238321799168110\",\"src\":\"/assets/public/content/do_113303238321799168110/artifact/do_113303238321799168110_1623930459486_screenshot-2021-06-17-at-5.17.12-pm.png\",\"type\":\"image\"},{\"name\":\"Screenshot 2021-06-17 at 5 17 12 PM\",\"id\":\"do_113303315360907264114\",\"src\":\"/assets/public/content/do_113303315360907264114/artifact/do_113303315360907264114_1623939863955_screenshot-2021-06-17-at-5.17.12-pm.png\",\"type\":\"image\"}]},\"plugin-manifest\":{\"plugin\":[{\"id\":\"org.ekstep.navigation\",\"ver\":\"1.0\",\"type\":\"plugin\",\"depends\":\"\"}]},\"compatibilityVersion\":2}}"))) + val result: List[String] = new TestLiveContentPublisher().validateMetadata(data, data.identifier, jobConfig) + result.size should be(0) + } + + "validateMetadata with mimeType video/x-youtube or video/youtube " should " return exception messages if content is having invalid artifactUrl" in { + val data = new ObjectData("do_123", Map[String, AnyRef]("name" -> "Content Name", "identifier" -> "do_123", "pkgVersion" -> 0.0.asInstanceOf[AnyRef], "mimeType" -> "video/x-youtube", "artifactUrl" -> "https://www.youtube.com/"), None) + val result: List[String] = new TestLiveContentPublisher().validateMetadata(data, data.identifier, jobConfig) + result.size should be(1) + result.contains("Invalid youtube Url = https://www.youtube.com/ for : do_123") shouldBe true + } + + "validateMetadata with mimeType video/x-youtube or video/youtube " should " not return exception messages if content is having valid artifactUrl = https://www.youtube.com/embed/watch?" in { + val data = new ObjectData("do_123", Map[String, AnyRef]("name" -> "Content Name", "identifier" -> "do_123", "pkgVersion" -> 0.0.asInstanceOf[AnyRef], "mimeType" -> "video/x-youtube", "artifactUrl" -> "https://www.youtube.com/embed/watch?"), None) + val result: List[String] = new TestLiveContentPublisher().validateMetadata(data, data.identifier, jobConfig) + result.size should be(0) + } + + "validateMetadata with mimeType video/x-youtube or video/youtube " should " not return exception messages if content is having valid artifactUrl = https://www.youtube.com/watch?v=6Js8tBCfbWk" in { + val data = new ObjectData("do_123", Map[String, AnyRef]("name" -> "Content Name", "identifier" -> "do_123", "pkgVersion" -> 0.0.asInstanceOf[AnyRef], "mimeType" -> "video/x-youtube", "artifactUrl" -> "https://www.youtube.com/watch?v=6Js8tBCfbWk"), None) + val result: List[String] = new TestLiveContentPublisher().validateMetadata(data, data.identifier, jobConfig) + result.size should be(0) + } + + "validateMetadata with mimeType video/x-youtube or video/youtube " should " not return exception messages if content is having valid artifactUrl = https://youtu.be/6Js8tBCfbWk" in { + val data = new ObjectData("do_123", Map[String, AnyRef]("name" -> "Content Name", "identifier" -> "do_123", "pkgVersion" -> 0.0.asInstanceOf[AnyRef], "mimeType" -> "video/x-youtube", "artifactUrl" -> "https://youtu.be/6Js8tBCfbWk"), None) + val result: List[String] = new TestLiveContentPublisher().validateMetadata(data, data.identifier, jobConfig) + result.size should be(0) + } + + "validateMetadata with mimeType application/pdf " should " throw InvalidInputException invalid artifactUrl" in { + val data = new ObjectData("do_123", Map[String, AnyRef]("name" -> "Content Name", "identifier" -> "do_123", "pkgVersion" -> 0.0.asInstanceOf[AnyRef], "mimeType" -> "application/pdf", "artifactUrl" -> "https://www.youtube.com/"), None) + assertThrows[InvalidInputException] { + new TestLiveContentPublisher().validateMetadata(data, data.identifier, jobConfig) + } + } + + "validateMetadata with mimeType application/pdf " should " exception messages if content is having other fileType in artifactUrl" in { + val data = new ObjectData("do_123", Map[String, AnyRef]("name" -> "Content Name", "identifier" -> "do_123", "pkgVersion" -> 0.0.asInstanceOf[AnyRef], "mimeType" -> "application/pdf", "artifactUrl" -> "https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_1130958930694553601102/artifact/index.epub"), None) + val result: List[String] = new TestLiveContentPublisher().validateMetadata(data, data.identifier, jobConfig) + result.size should be(1) + result.contains("Error! Invalid File Extension. Uploaded file https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_1130958930694553601102/artifact/index.epub is not a pdf file for : do_123") shouldBe true + } + + "validateMetadata with mimeType application/pdf " should " not return exception messages if content is having valid artifactUrl" in { + val data = new ObjectData("do_123", Map[String, AnyRef]("name" -> "Content Name", "identifier" -> "do_123", "pkgVersion" -> 0.0.asInstanceOf[AnyRef], "mimeType" -> "application/pdf", "artifactUrl" -> "https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11329603741667328018/artifact/do_11329603741667328018_1623058698775_intellijidea_referencecard.pdf"), None) + val result: List[String] = new TestLiveContentPublisher().validateMetadata(data, data.identifier, jobConfig) + result.size should be(0) + } + + "validateMetadata with mimeType application/epub " should " return exception messages if content is having invalid artifactUrl" in { + val data = new ObjectData("do_123", Map[String, AnyRef]("name" -> "Content Name", "identifier" -> "do_123", "pkgVersion" -> 0.0.asInstanceOf[AnyRef], "mimeType" -> "application/epub", "artifactUrl" -> "https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11329603741667328018/artifact/do_11329603741667328018_1623058698775_intellijidea_referencecard.pdf"), None) + val result: List[String] = new TestLiveContentPublisher().validateMetadata(data, data.identifier, jobConfig) + result.size should be(1) + result.contains("Error! Invalid File Extension. Uploaded file https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11329603741667328018/artifact/do_11329603741667328018_1623058698775_intellijidea_referencecard.pdf is not a epub file for : do_123") shouldBe true + } + + "validateMetadata with mimeType application/epub " should " not return exception messages if content is having valid artifactUrl" in { + val data = new ObjectData("do_123", Map[String, AnyRef]("name" -> "Content Name", "identifier" -> "do_123", "pkgVersion" -> 0.0.asInstanceOf[AnyRef], "mimeType" -> "application/epub", "artifactUrl" -> "https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_1130958930694553601102/artifact/index.epub"), None) + val result: List[String] = new TestLiveContentPublisher().validateMetadata(data, data.identifier, jobConfig) + result.size should be(0) + } + + "validateMetadata with mimeType application/msword " should " return exception messages if content is having invalid artifactUrl" in { + val data = new ObjectData("do_123", Map[String, AnyRef]("name" -> "Content Name", "identifier" -> "do_123", "pkgVersion" -> 0.0.asInstanceOf[AnyRef], "mimeType" -> "application/msword", "artifactUrl" -> "https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11329603741667328018/artifact/do_11329603741667328018_1623058698775_intellijidea_referencecard.pdf"), None) + val result: List[String] = new TestLiveContentPublisher().validateMetadata(data, data.identifier, jobConfig) + result.size should be(1) + result.contains("Error! Invalid File Extension. | Uploaded file https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11329603741667328018/artifact/do_11329603741667328018_1623058698775_intellijidea_referencecard.pdf should be among the Allowed_file_extensions for mimeType doc [doc, docx, ppt, pptx, key, odp, pps, odt, wpd, wps, wks] for : do_123") shouldBe true + } + + "validateMetadata with mimeType application/msword and .pptx " should " not return exception messages if content is having valid artifactUrl" in { + val data = new ObjectData("do_123", Map[String, AnyRef]("name" -> "Content Name", "identifier" -> "do_123", "pkgVersion" -> 0.0.asInstanceOf[AnyRef], "mimeType" -> "application/msword", "artifactUrl" -> "https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_112216616320983040129/artifact/performance_out_1491286194831.pptx"), None) + val result: List[String] = new TestLiveContentPublisher().validateMetadata(data, data.identifier, jobConfig) + result.size should be(0) + } + + "validateMetadata with mimeType application/msword and .docx " should " not return exception messages if content is having valid artifactUrl" in { + val data = new ObjectData("do_123", Map[String, AnyRef]("name" -> "Content Name", "identifier" -> "do_123", "pkgVersion" -> 0.0.asInstanceOf[AnyRef], "mimeType" -> "application/msword", "artifactUrl" -> "https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_112216615190192128128/artifact/prdassetstagging-2_1491286084107.docx"), None) + val result: List[String] = new TestLiveContentPublisher().validateMetadata(data, data.identifier, jobConfig) + result.size should be(0) + } + + "saveExternalData " should "save external data to cassandra table" in { + val data = new ObjectData("do_123", Map[String, AnyRef](), Some(Map[String, AnyRef]("body" -> "body", "answer" -> "answer"))) + new TestLiveContentPublisher().saveExternalData(data, readerConfig) + } + + "getExtData " should " get content body for application/vnd.ekstep.ecml-archive mimeType " in { + val identifier = "do_11321328578759884811663" + val result: Option[ObjectExtData] = new TestLiveContentPublisher().getExtData(identifier, "application/vnd.ekstep.ecml-archive", readerConfig) + result.getOrElse(new ObjectExtData).data.getOrElse(Map()).contains("body") shouldBe true + } + + "getExtData " should " not get content body for other than application/pdf mimeType " in { + val identifier = "do_11321328578759884811663" + val result: Option[ObjectExtData] = new TestLiveContentPublisher().getExtData(identifier, "application/pdf", readerConfig) + result.getOrElse(new ObjectExtData).data.getOrElse(Map()).contains("body") shouldBe false + } + + "getHierarchy " should "do nothing " in { + val identifier = "do_11329603741667328018" + new TestLiveContentPublisher().getHierarchy(identifier, readerConfig) + } + + "getExtDatas " should "do nothing " in { + val identifier = "do_11329603741667328018" + new TestLiveContentPublisher().getExtDatas(List(identifier), readerConfig) + } + + "getHierarchies " should "do nothing " in { + val identifier = "do_11329603741667328018" + new TestLiveContentPublisher().getHierarchies(List(identifier), readerConfig) + } + + "getDataForEcar" should "return one element in list" in { + val data = new ObjectData("do_123", Map("objectType" -> "Content"), Some(Map("responseDeclaration" -> "test")), Some(Map())) + val result: Option[List[Map[String, AnyRef]]] = new TestLiveContentPublisher().getDataForEcar(data) + result.size should be(1) + } + + "getObjectWithEcar" should "return object with ecar url" in { + val data = new ObjectData("do_123", Map("objectType" -> "Content", "identifier" -> "do_123", "name" -> "Test PDF Content"), Some(Map("responseDeclaration" -> "test", "media" -> "[{\"id\":\"do_1127129497561497601326\",\"type\":\"image\",\"src\":\"/content/do_1127129497561497601326.img/artifact/sunbird_1551961194254.jpeg\",\"baseUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev\"}]")), Some(Map())) + val result = new TestLiveContentPublisher().getObjectWithEcar(data, List(EcarPackageType.FULL.toString, EcarPackageType.ONLINE))(ec, mockNeo4JUtil, cloudStorageUtil, jobConfig, defCache, defConfig, httpUtil) + StringUtils.isNotBlank(result.metadata.getOrElse("downloadUrl", "").asInstanceOf[String]) + } +} + +class TestLiveContentPublisher extends LiveContentPublisher {} diff --git a/publish-pipeline/live-node-publisher/src/test/scala/org/sunbird/job/livenodepublisher/publish/helpers/spec/LiveObjectReaderTestSpec.scala b/publish-pipeline/live-node-publisher/src/test/scala/org/sunbird/job/livenodepublisher/publish/helpers/spec/LiveObjectReaderTestSpec.scala new file mode 100644 index 000000000..67248f9cc --- /dev/null +++ b/publish-pipeline/live-node-publisher/src/test/scala/org/sunbird/job/livenodepublisher/publish/helpers/spec/LiveObjectReaderTestSpec.scala @@ -0,0 +1,53 @@ +package org.sunbird.job.livenodepublisher.publish.helpers.spec + +import com.typesafe.config.{Config, ConfigFactory} +import org.mockito.Mockito +import org.mockito.Mockito._ +import org.scalatest.{BeforeAndAfterAll, FlatSpec, Matchers} +import org.scalatestplus.mockito.MockitoSugar +import org.sunbird.job.livenodepublisher.publish.helpers.LiveObjectReader +import org.sunbird.job.livenodepublisher.task.LiveNodePublisherConfig +import org.sunbird.job.publish.config.PublishConfig +import org.sunbird.job.publish.core.{ExtDataConfig, ObjectExtData} +import org.sunbird.job.util.{CassandraUtil, Neo4JUtil} + +import scala.collection.JavaConverters._ + +class LiveObjectReaderTestSpec extends FlatSpec with BeforeAndAfterAll with Matchers with MockitoSugar { + + override protected def beforeAll(): Unit = { + super.beforeAll() + } + + override protected def afterAll(): Unit = { + super.afterAll() + } + + implicit val mockNeo4JUtil: Neo4JUtil = mock[Neo4JUtil](Mockito.withSettings().serializable()) + implicit val mockCassandraUtil: CassandraUtil = mock[CassandraUtil](Mockito.withSettings().serializable()) + val config: Config = ConfigFactory.load("test.conf").withFallback(ConfigFactory.systemEnvironment()) + implicit val jobConfig: LiveNodePublisherConfig = new LiveNodePublisherConfig(config) + + "Object Reader " should " read the metadata " in { + when(mockNeo4JUtil.getNodeProperties("do_123.img")).thenReturn(Map[String, AnyRef]("name" -> "Content Name", "identifier" -> "do_123.img", "IL_UNIQUE_ID" -> "do_123.img", "pkgVersion" -> 2.0.asInstanceOf[AnyRef]).asJava) + val objectReader = new TestObjectReader() + val readerConfig = ExtDataConfig("test", "test") + val obj = objectReader.getObject("do_123", 2, "", "Public", readerConfig) + val metadata = obj.metadata.asJava + metadata.isEmpty should be(false) + obj.extData should be(None) + obj.hierarchy should be(None) + } +} + +class TestObjectReader extends LiveObjectReader { + + + override def getExtDatas(identifiers: List[String], readerConfig: ExtDataConfig)(implicit cassandraUtil: CassandraUtil): Option[Map[String, AnyRef]] = None + + override def getHierarchies(identifiers: List[String], readerConfig: ExtDataConfig)(implicit cassandraUtil: CassandraUtil): Option[Map[String, AnyRef]] = None + + override def getExtData(identifier: String, mimeType: String, readerConfig: ExtDataConfig)(implicit cassandraUtil: CassandraUtil, jobConfig: PublishConfig): Option[ObjectExtData] = None + + override def getHierarchy(identifier: String, readerConfig: ExtDataConfig)(implicit cassandraUtil: CassandraUtil, jobConfig: PublishConfig): Option[Map[String, AnyRef]] = None +} diff --git a/publish-pipeline/live-node-publisher/src/test/scala/org/sunbird/job/livenodepublisher/publish/helpers/spec/LiveObjectUpdaterSpec.scala b/publish-pipeline/live-node-publisher/src/test/scala/org/sunbird/job/livenodepublisher/publish/helpers/spec/LiveObjectUpdaterSpec.scala new file mode 100644 index 000000000..fb1cd4dbc --- /dev/null +++ b/publish-pipeline/live-node-publisher/src/test/scala/org/sunbird/job/livenodepublisher/publish/helpers/spec/LiveObjectUpdaterSpec.scala @@ -0,0 +1,80 @@ +package org.sunbird.job.livenodepublisher.publish.helpers.spec + +import com.typesafe.config.{Config, ConfigFactory} +import org.mockito.ArgumentMatchers.{any, anyString} +import org.mockito.Mockito +import org.mockito.Mockito.when +import org.scalatest.{BeforeAndAfterAll, FlatSpec, Matchers} +import org.scalatestplus.mockito.MockitoSugar +import org.sunbird.job.domain.`object`.DefinitionCache +import org.sunbird.job.livenodepublisher.publish.helpers.LiveObjectUpdater +import org.sunbird.job.livenodepublisher.task.LiveNodePublisherConfig +import org.sunbird.job.publish.config.PublishConfig +import org.sunbird.job.publish.core.{DefinitionConfig, ExtDataConfig, ObjectData} +import org.sunbird.job.util.{CassandraUtil, Neo4JUtil} + +import java.util + +class LiveObjectUpdaterSpec extends FlatSpec with BeforeAndAfterAll with Matchers with MockitoSugar { + + implicit val mockNeo4JUtil: Neo4JUtil = mock[Neo4JUtil](Mockito.withSettings().serializable()) + implicit val mockCassandraUtil: CassandraUtil = mock[CassandraUtil](Mockito.withSettings().serializable()) + implicit val readerConfig = ExtDataConfig("test", "test") + implicit lazy val defCache: DefinitionCache = new DefinitionCache() + implicit val definitionConfig: DefinitionConfig = DefinitionConfig(Map("itemset" -> "2.0"), "https://sunbirddev.blob.core.windows.net/sunbird-content-dev/schemas/local") + val config: Config = ConfigFactory.load("test.conf").withFallback(ConfigFactory.systemEnvironment()) + implicit val jobConfig: LiveNodePublisherConfig = new LiveNodePublisherConfig(config) + + + override protected def beforeAll(): Unit = { + super.beforeAll() + } + + override protected def afterAll(): Unit = { + super.afterAll() + } + + "ObjectUpdater saveOnSuccess" should " update the status for successfully published data " in { + + when(mockNeo4JUtil.executeQuery(anyString())).thenReturn(any()); + + val hierarchy = Map("identifier" -> "do_123", "children" -> List(Map("identifier" -> "do_234", "name" -> "Children-1"), Map("identifier" -> "do_345", "name" -> "Children-2"))) + val metadata = Map("objectType" -> "QuestionSet", "identifier" -> "do_123","publish_type" -> "Public", "IL_UNIQUE_ID" -> "do_123", "IL_FUNC_OBJECT_TYPE" -> "QuestionSet", "name" -> "Test QuestionSet", "status" -> "Live") + val objData = new ObjectData("do_123", metadata, None, Some(hierarchy)) + + val obj = new TestObjectUpdater() + obj.saveOnSuccess(objData) + } + + "ObjectUpdater saveOnFailure" should " update the status for failed published data " in { + + when(mockNeo4JUtil.executeQuery(anyString())).thenReturn(any()) + + val hierarchy = Map("identifier" -> "do_123", "children" -> List(Map("identifier" -> "do_234", "name" -> "Children-1"), Map("identifier" -> "do_345", "name" -> "Children-2"))) + val metadata = Map("objectType" -> "QuestionSet","identifier" -> "do_123","publish_type" -> "Public", "IL_UNIQUE_ID" -> "do_123", "IL_FUNC_OBJECT_TYPE" -> "QuestionSet", "name" -> "Test QuestionSet", "status" -> "Live") + val objData = new ObjectData("do_123", metadata, None, Some(hierarchy)) + + val obj = new TestObjectUpdater() + obj.saveOnFailure(objData, List("Testing Save on Publish Failure"), 1) + } + + "ObjectUpdater metaDataQuery" should " give the update query " in { + val hierarchy = Map("identifier" -> "do_123", "children" -> List(Map("identifier" -> "do_234", "name" -> "Children-1"), Map("identifier" -> "do_345", "name" -> "Children-2"))) + val variants = new util.HashMap[String, String] {{ + put("spine","https://sunbirddev.blob.core.windows.net/sunbird-content-dev/questionset/do_1132380439842324481319/hindi-questionset-17_1615972827743_do_1132380439842324481319_1_SPINE.ecar") + put("online","https://sunbirddev.blob.core.windows.net/sunbird-content-dev/questionset/do_1132380439842324481319/hindi-questionset-17_1615972829357_do_1132380439842324481319_1_ONLINE.ecar") + }} + val outcomeProcessing = Map("name" -> "abc") + val metadata: Map[String, AnyRef] = Map("description" -> "Hello \"World\"", "keywords" -> List("anusha"),"channel" -> "01309282781705830427","mimeType" -> "application/vnd.sunbird.questionset","showHints" -> "No","objectType" -> "QuestionSet","primaryCategory" -> "Practice Question Set","contentEncoding" -> "gzip","showSolutions" -> "No","identifier" -> "do_1132421849134858241686","visibility" -> "Default","showTimer" -> "Yes","author" -> "anusha","childNodes" -> util.Arrays.asList("do_1132421859856875521687"),"consumerId" -> "273f3b18-5dda-4a27-984a-060c7cd398d3","version" -> 1.asInstanceOf[AnyRef],"prevState" -> "Draft","IL_FUNC_OBJECT_TYPE" -> "QuestionSet","name" -> "Timer","timeLimits" -> "{\"maxTime\":\"3541\",\"warningTime\":\"2401\"}","IL_UNIQUE_ID" -> "do_1132421849134858241686","board" -> "CBSE", "variants" -> variants, "outcomeProcessing" -> outcomeProcessing) + val objData = new ObjectData("do_123", metadata, None, Some(hierarchy)) + val obj = new TestObjectUpdater() + println(obj.metaDataQuery(objData)(defCache, definitionConfig)) + } +} + +class TestObjectUpdater extends LiveObjectUpdater { + override def saveExternalData(obj: ObjectData, readerConfig: ExtDataConfig)(implicit cassandraUtil: CassandraUtil): Unit = None + + override def deleteExternalData(obj: ObjectData, readerConfig: ExtDataConfig)(implicit cassandraUtil: CassandraUtil): Unit = None +} + diff --git a/publish-pipeline/live-node-publisher/src/test/scala/org/sunbird/job/livenodepublisher/spec/LiveNodePublisherStreamTaskSpec.scala b/publish-pipeline/live-node-publisher/src/test/scala/org/sunbird/job/livenodepublisher/spec/LiveNodePublisherStreamTaskSpec.scala new file mode 100644 index 000000000..11c5540aa --- /dev/null +++ b/publish-pipeline/live-node-publisher/src/test/scala/org/sunbird/job/livenodepublisher/spec/LiveNodePublisherStreamTaskSpec.scala @@ -0,0 +1,108 @@ +package org.sunbird.job.livenodepublisher.spec + +import com.google.gson.Gson +import com.typesafe.config.{Config, ConfigFactory} +import org.apache.flink.api.common.typeinfo.TypeInformation +import org.apache.flink.api.java.typeutils.TypeExtractor +import org.apache.flink.runtime.testutils.MiniClusterResourceConfiguration +import org.apache.flink.streaming.api.functions.source.SourceFunction +import org.apache.flink.streaming.api.functions.source.SourceFunction.SourceContext +import org.apache.flink.test.util.MiniClusterWithClientResource +import org.cassandraunit.CQLDataLoader +import org.cassandraunit.dataset.cql.FileCQLDataSet +import org.cassandraunit.utils.EmbeddedCassandraServerHelper +import org.mockito.ArgumentMatchers.anyString +import org.mockito.Mockito +import org.mockito.Mockito.when +import org.sunbird.job.connector.FlinkKafkaConnector +import org.sunbird.job.domain.`object`.{DefinitionCache, ObjectDefinition} +import org.sunbird.job.livenodepublisher.fixture.EventFixture +import org.sunbird.job.livenodepublisher.publish.domain.Event +import org.sunbird.job.livenodepublisher.task.{LiveNodePublisherConfig, LiveNodePublisherStreamTask} +import org.sunbird.job.publish.config.PublishConfig +import org.sunbird.job.publish.core.ExtDataConfig +import org.sunbird.job.util.{CassandraUtil, CloudStorageUtil, HttpUtil, Neo4JUtil} +import org.sunbird.spec.{BaseMetricsReporter, BaseTestSpec} + +import java.text.SimpleDateFormat +import java.util +import java.util.Date + +class LiveNodePublisherStreamTaskSpec extends BaseTestSpec { + + implicit val mapTypeInfo: TypeInformation[java.util.Map[String, AnyRef]] = TypeExtractor.getForClass(classOf[java.util.Map[String, AnyRef]]) + implicit val strTypeInfo: TypeInformation[String] = TypeExtractor.getForClass(classOf[String]) + + val flinkCluster = new MiniClusterWithClientResource(new MiniClusterResourceConfiguration.Builder() + .setConfiguration(testConfiguration()) + .setNumberSlotsPerTaskManager(1) + .setNumberTaskManagers(1) + .build) + val mockKafkaUtil: FlinkKafkaConnector = mock[FlinkKafkaConnector](Mockito.withSettings().serializable()) + val config: Config = ConfigFactory.load("test.conf").withFallback(ConfigFactory.systemEnvironment()) + implicit val jobConfig: LiveNodePublisherConfig = new LiveNodePublisherConfig(config) + var definitionCache = new DefinitionCache() + implicit val definition: ObjectDefinition = definitionCache.getDefinition("Collection", jobConfig.schemaSupportVersionMap.getOrElse("collection", "1.0").asInstanceOf[String], jobConfig.definitionBasePath) + implicit val readerConfig: ExtDataConfig = ExtDataConfig(jobConfig.hierarchyKeyspaceName, jobConfig.hierarchyTableName, definition.getExternalPrimaryKey, definition.getExternalProps) + + val mockHttpUtil = mock[HttpUtil](Mockito.withSettings().serializable()) + implicit val mockNeo4JUtil: Neo4JUtil = mock[Neo4JUtil](Mockito.withSettings().serializable()) + var cassandraUtil: CassandraUtil = _ + val publishConfig: PublishConfig = new PublishConfig(config, "") + val cloudStorageUtil: CloudStorageUtil = new CloudStorageUtil(publishConfig) + + override protected def beforeAll(): Unit = { + super.beforeAll() + EmbeddedCassandraServerHelper.startEmbeddedCassandra(80000L) + cassandraUtil = new CassandraUtil(jobConfig.cassandraHost, jobConfig.cassandraPort, jobConfig) + val session = cassandraUtil.session + val dataLoader = new CQLDataLoader(session) + dataLoader.load(new FileCQLDataSet(getClass.getResource("/test.cql").getPath, true, true)) + flinkCluster.before() + } + + override protected def afterAll(): Unit = { + super.afterAll() + try { + EmbeddedCassandraServerHelper.cleanEmbeddedCassandra() + } catch { + case ex: Exception => { + } + } + flinkCluster.after() + } + + def initialize(): Unit = { + when(mockKafkaUtil.kafkaJobRequestSource[Event](jobConfig.kafkaInputTopic)).thenReturn(new ContentPublishEventSource) + } + + def getTimeStamp: String = { + val sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ"); + sdf.format(new Date()) + } + + ignore should " publish the content " in { + when(mockNeo4JUtil.getNodeProperties(anyString())).thenReturn(new util.HashMap[String, AnyRef]) + initialize + new LiveNodePublisherStreamTask(jobConfig, mockKafkaUtil, mockHttpUtil).process() + BaseMetricsReporter.gaugeMetrics(s"${jobConfig.jobName}.${jobConfig.totalEventsCount}").getValue() should be(1) + BaseMetricsReporter.gaugeMetrics(s"${jobConfig.jobName}.${jobConfig.contentPublishEventCount}").getValue() should be(1) + } +} + +private class ContentPublishEventSource extends SourceFunction[Event] { + + override def run(ctx: SourceContext[Event]) { + ctx.collect(jsonToEvent(EventFixture.PDF_EVENT1)) + } + + override def cancel() = {} + + def jsonToEvent(json: String): Event = { + val gson = new Gson() + val data = gson.fromJson(json, new util.LinkedHashMap[String, Any]().getClass).asInstanceOf[util.Map[String, Any]] + val metadataMap = data.get("edata").asInstanceOf[util.Map[String, Any]].get("metadata").asInstanceOf[util.Map[String, Any]] + metadataMap.put("pkgVersion", metadataMap.get("pkgVersion").asInstanceOf[Double].toInt) + new Event(data, 0, 10) + } +} diff --git a/publish-pipeline/pom.xml b/publish-pipeline/pom.xml index 860a93853..8a0824a30 100644 --- a/publish-pipeline/pom.xml +++ b/publish-pipeline/pom.xml @@ -15,8 +15,9 @@ publish-core - questionset-publish + content-publish + live-node-publisher diff --git a/publish-pipeline/publish-core/pom.xml b/publish-pipeline/publish-core/pom.xml index 125f8b529..0c8270be9 100644 --- a/publish-pipeline/publish-core/pom.xml +++ b/publish-pipeline/publish-core/pom.xml @@ -62,8 +62,9 @@ + org.apache.maven.plugins maven-surefire-plugin - 2.20 + 2.22.2 true diff --git a/publish-pipeline/publish-core/src/main/scala/org/sunbird/job/publish/helpers/ObjectBundle.scala b/publish-pipeline/publish-core/src/main/scala/org/sunbird/job/publish/helpers/ObjectBundle.scala index d8c591877..7230f8d5c 100644 --- a/publish-pipeline/publish-core/src/main/scala/org/sunbird/job/publish/helpers/ObjectBundle.scala +++ b/publish-pipeline/publish-core/src/main/scala/org/sunbird/job/publish/helpers/ObjectBundle.scala @@ -36,17 +36,15 @@ trait ObjectBundle { Slug.makeSlug(contentName, isTransliterate = true) + "_" + System.currentTimeMillis() + "_" + identifier + "_" + metadata.getOrElse("pkgVersion", "") + (if (StringUtils.equals(EcarPackageType.FULL, pkgType)) ".ecar" else "_" + pkgType + ".ecar") } - def getManifestData(objIdentifier: String, pkgType: String, objList: List[Map[String, AnyRef]])(implicit defCache: DefinitionCache, neo4JUtil: Neo4JUtil, defConfig: DefinitionConfig, config: PublishConfig): (List[Map[String, AnyRef]], List[Map[AnyRef, String]]) = { + def getManifestData(objIdentifier: String, rootObjectType: String, pkgType: String, objList: List[Map[String, AnyRef]])(implicit defCache: DefinitionCache, neo4JUtil: Neo4JUtil, defConfig: DefinitionConfig, config: PublishConfig): (List[Map[String, AnyRef]], List[Map[AnyRef, String]]) = { objList.map(data => { val identifier = data.getOrElse("identifier", "").asInstanceOf[String].replaceAll(".img", "") val mimeType = data.getOrElse("mimeType", "").asInstanceOf[String] val visibility = data.getOrElse("visibility", "").asInstanceOf[String] val objectType: String = if(!data.contains("objectType") || data.getOrElse("objectType", "").asInstanceOf[String].isBlank || data.getOrElse("objectType", "").asInstanceOf[String].isEmpty) { - if(visibility.equalsIgnoreCase("Parent") && mimeType.equalsIgnoreCase("application/vnd.ekstep.content-collection")) "Collection" - else { - val metaData = Option(neo4JUtil.getNodeProperties(identifier)).getOrElse(neo4JUtil.getNodeProperties(identifier)).asScala.toMap - metaData.getOrElse("IL_FUNC_OBJECT_TYPE", "").asInstanceOf[String] - } + val metaData = Option(neo4JUtil.getNodeProperties(identifier)).getOrElse(neo4JUtil.getNodeProperties(identifier)).asScala.toMap + logger.info("ObjectBundle:: getManifestData:: if objectType does not exist identifier:: " + identifier) + if (metaData == null || metaData.isEmpty) rootObjectType else metaData.getOrElse("IL_FUNC_OBJECT_TYPE", "").asInstanceOf[String] } else data.getOrElse("objectType", "").asInstanceOf[String] .replaceAll("Image", "") val contentDisposition = data.getOrElse("contentDisposition", "").asInstanceOf[String] logger.info("ObjectBundle:: getManifestData:: identifier:: " + identifier + " || objectType:: " + objectType) @@ -98,7 +96,7 @@ trait ObjectBundle { val objType = if(obj.getString("objectType", "").replaceAll("Image", "").equalsIgnoreCase("collection")) "content" else obj.getString("objectType", "").replaceAll("Image", "") logger.info("ObjectBundle ::: getObjectBundle ::: input objList :::: " + objList) // create manifest data - val (updatedObjList, dUrls) = getManifestData(obj.identifier, pkgType, objList) + val (updatedObjList, dUrls) = getManifestData(obj.identifier, objType, pkgType, objList) logger.info("ObjectBundle ::: getObjectBundle ::: updatedObjList :::: " + updatedObjList) val downloadUrls: Map[AnyRef, List[String]] = dUrls.flatten.groupBy(_._1).map { case (k, v) => k -> v.map(_._2) } logger.info("ObjectBundle ::: getObjectBundle ::: downloadUrls :::: " + downloadUrls) diff --git a/publish-pipeline/publish-core/src/main/scala/org/sunbird/job/publish/helpers/ObjectReader.scala b/publish-pipeline/publish-core/src/main/scala/org/sunbird/job/publish/helpers/ObjectReader.scala index bd194e142..760360f58 100644 --- a/publish-pipeline/publish-core/src/main/scala/org/sunbird/job/publish/helpers/ObjectReader.scala +++ b/publish-pipeline/publish-core/src/main/scala/org/sunbird/job/publish/helpers/ObjectReader.scala @@ -1,6 +1,7 @@ package org.sunbird.job.publish.helpers import org.slf4j.LoggerFactory +import org.sunbird.job.publish.config.PublishConfig import org.sunbird.job.publish.core.{ExtDataConfig, ObjectData, ObjectExtData} import org.sunbird.job.util.{CassandraUtil, Neo4JUtil} @@ -10,7 +11,7 @@ trait ObjectReader { private[this] val logger = LoggerFactory.getLogger(classOf[ObjectReader]) - def getObject(identifier: String, pkgVersion: Double, mimeType: String, publishType: String, readerConfig: ExtDataConfig)(implicit neo4JUtil: Neo4JUtil, cassandraUtil: CassandraUtil): ObjectData = { + def getObject(identifier: String, pkgVersion: Double, mimeType: String, publishType: String, readerConfig: ExtDataConfig)(implicit neo4JUtil: Neo4JUtil, cassandraUtil: CassandraUtil, config: PublishConfig): ObjectData = { logger.info("Reading editable object data for: " + identifier + " with pkgVersion: " + pkgVersion) val metadata = getMetadata(identifier, mimeType, publishType, pkgVersion) logger.info("Reading metadata for: " + identifier + " with metadata: " + metadata) @@ -31,9 +32,9 @@ trait ObjectReader { metaData ++ Map[String, AnyRef]("identifier" -> id, "objectType" -> objType, "publish_type" -> publishType) - ("IL_UNIQUE_ID", "IL_FUNC_OBJECT_TYPE", "IL_SYS_NODE_TYPE") } - def getExtData(identifier: String, pkgVersion: Double, mimeType: String, readerConfig: ExtDataConfig)(implicit cassandraUtil: CassandraUtil): Option[ObjectExtData] + def getExtData(identifier: String, pkgVersion: Double, mimeType: String, readerConfig: ExtDataConfig)(implicit cassandraUtil: CassandraUtil, config: PublishConfig): Option[ObjectExtData] - def getHierarchy(identifier: String, pkgVersion: Double, readerConfig: ExtDataConfig)(implicit cassandraUtil: CassandraUtil): Option[Map[String, AnyRef]] + def getHierarchy(identifier: String, pkgVersion: Double, readerConfig: ExtDataConfig)(implicit cassandraUtil: CassandraUtil, config: PublishConfig): Option[Map[String, AnyRef]] def getEditableObjId(identifier: String, pkgVersion: Double): String = { if (pkgVersion > 0) identifier + ".img" else identifier diff --git a/publish-pipeline/publish-core/src/main/scala/org/sunbird/job/publish/helpers/ObjectUpdater.scala b/publish-pipeline/publish-core/src/main/scala/org/sunbird/job/publish/helpers/ObjectUpdater.scala index c47765af6..f5f99eb6e 100644 --- a/publish-pipeline/publish-core/src/main/scala/org/sunbird/job/publish/helpers/ObjectUpdater.scala +++ b/publish-pipeline/publish-core/src/main/scala/org/sunbird/job/publish/helpers/ObjectUpdater.scala @@ -6,8 +6,9 @@ import org.neo4j.driver.v1.StatementResult import org.slf4j.LoggerFactory import org.sunbird.job.domain.`object`.DefinitionCache import org.sunbird.job.exception.InvalidInputException +import org.sunbird.job.publish.config.PublishConfig import org.sunbird.job.publish.core.{DefinitionConfig, ExtDataConfig, ObjectData} -import org.sunbird.job.util.{CassandraUtil, JSONUtil, Neo4JUtil, ScalaJsonUtil} +import org.sunbird.job.util.{CSPMetaUtil, CassandraUtil, JSONUtil, Neo4JUtil, ScalaJsonUtil} import java.text.SimpleDateFormat import java.util @@ -18,12 +19,12 @@ trait ObjectUpdater { private[this] val logger = LoggerFactory.getLogger(classOf[ObjectUpdater]) @throws[Exception] - def saveOnSuccess(obj: ObjectData)(implicit neo4JUtil: Neo4JUtil, cassandraUtil: CassandraUtil, readerConfig: ExtDataConfig, definitionCache: DefinitionCache, config: DefinitionConfig): Unit = { + def saveOnSuccess(obj: ObjectData)(implicit neo4JUtil: Neo4JUtil, cassandraUtil: CassandraUtil, readerConfig: ExtDataConfig, definitionCache: DefinitionCache, definitionConfig: DefinitionConfig, config: PublishConfig): Unit = { val publishType = obj.getString("publish_type", "Public") val status = if (StringUtils.equalsIgnoreCase("Unlisted", publishType)) "Unlisted" else "Live" val editId = obj.dbId val identifier = obj.identifier - val metadataUpdateQuery = metaDataQuery(obj)(definitionCache, config) + val metadataUpdateQuery = metaDataQuery(obj)(definitionCache, definitionConfig) val query = s"""MATCH (n:domain{IL_UNIQUE_ID:"$identifier"}) SET n.status="$status",n.pkgVersion=${obj.pkgVersion},n.prevStatus="Processing",$metadataUpdateQuery,$auditPropsUpdateQuery;""" logger.info("ObjectUpdater:: saveOnSuccess:: Query: " + query) logger.info(s"ObjectUpdater:: saveOnSuccess:: DB ID for ${obj.identifier} is : ${obj.dbId} | pkgVersion : ${obj.pkgVersion}" ) @@ -118,23 +119,35 @@ trait ObjectUpdater { s"""n.lastUpdatedOn="$updatedOn",n.lastStatusChangedOn="$updatedOn"""" } - def getContentBody(identifier: String, readerConfig: ExtDataConfig)(implicit cassandraUtil: CassandraUtil): String = { + def getContentBody(identifier: String, readerConfig: ExtDataConfig)(implicit cassandraUtil: CassandraUtil, config: PublishConfig): String = { // fetch content body from cassandra val select = QueryBuilder.select() select.fcall("blobAsText", QueryBuilder.column("body")).as("body") val selectWhere: Select.Where = select.from(readerConfig.keyspace, readerConfig.table).where().and(QueryBuilder.eq("content_id", identifier + ".img")) logger.info("ObjectUpdater:: getContentBody:: Cassandra Fetch Query for image:: " + selectWhere.toString) val row = cassandraUtil.findOne(selectWhere.toString) - if (null != row) row.getString("body") else { + if (null != row) { + val body = row.getString("body") + val updatedBody = if (isrRelativePathEnabled(config)) CSPMetaUtil.updateAbsolutePath(body) else body + updatedBody + } else { val selectId = QueryBuilder.select() selectId.fcall("blobAsText", QueryBuilder.column("body")).as("body") val selectWhereId: Select.Where = selectId.from(readerConfig.keyspace, readerConfig.table).where().and(QueryBuilder.eq("content_id", identifier)) logger.info("ObjectUpdater:: getContentBody:: Cassandra Fetch Query :: " + selectWhereId.toString) val rowId = cassandraUtil.findOne(selectWhereId.toString) - if (null != rowId) rowId.getString("body") else "" + if (null != rowId) { + val body = rowId.getString("body") + val updatedBody = if (isrRelativePathEnabled(config)) CSPMetaUtil.updateAbsolutePath(body) else body + updatedBody + } else "" } } + private def isrRelativePathEnabled(config: PublishConfig): Boolean = { + config.getBoolean("cloudstorage.metadata.replace_absolute_path", false) + } + def updateContentBody(identifier: String, ecmlBody: String, readerConfig: ExtDataConfig)(implicit cassandraUtil: CassandraUtil): Unit = { val updateQuery = QueryBuilder.update(readerConfig.keyspace, readerConfig.table) .where(QueryBuilder.eq("content_id", identifier)) diff --git a/publish-pipeline/publish-core/src/test/resources/test.conf b/publish-pipeline/publish-core/src/test/resources/test.conf index 29026d686..d8264900f 100644 --- a/publish-pipeline/publish-core/src/test/resources/test.conf +++ b/publish-pipeline/publish-core/src/test/resources/test.conf @@ -37,4 +37,4 @@ neo4j { //azure_storage_container="dummy_secret" //aws_storage_key="dev" //aws_storage_secret="dummy_secret" -//aws_storage_container="dev" +//aws_storage_container="dev" \ No newline at end of file diff --git a/publish-pipeline/publish-core/src/test/scala/org/sunbird/job/publish/spec/EcarGeneratorSpec.scala b/publish-pipeline/publish-core/src/test/scala/org/sunbird/job/publish/spec/EcarGeneratorSpec.scala index 052f996f7..e62da72e5 100644 --- a/publish-pipeline/publish-core/src/test/scala/org/sunbird/job/publish/spec/EcarGeneratorSpec.scala +++ b/publish-pipeline/publish-core/src/test/scala/org/sunbird/job/publish/spec/EcarGeneratorSpec.scala @@ -35,7 +35,7 @@ class EcarGeneratorSpec extends FlatSpec with BeforeAndAfterAll with Matchers { "Object Ecar Generator generateEcar" should "return a Map containing Packaging Type and its url after uploading it to cloud" in { val hierarchy = Map("identifier" -> "do_123", "children" -> List(Map("identifier" -> "do_234", "name" -> "Children-1", "objectType" -> "Question"), Map("identifier" -> "do_345", "name" -> "Children-2", "objectType" -> "Question"))) - val metadata = Map("identifier" -> "do_123", "appIcon" -> "https://dev.sunbirded.org/assets/images/sunbird_logo.png", "identifier" -> "do_123", "objectType" -> "QuestionSet", "name" -> "Test QuestionSet", "status" -> "Live") + val metadata = Map("identifier" -> "do_123", "appIcon" -> "https://dev.knowlg.sunbird.org/content/preview/assets/icons/avatar_anonymous.png", "identifier" -> "do_123", "objectType" -> "QuestionSet", "name" -> "Test QuestionSet", "status" -> "Live") val objData = new ObjectData("do_123", metadata, None, Some(hierarchy)) val obj = new TestEcarGenerator() val result = obj.generateEcar(objData,List("SPINE")) @@ -50,6 +50,6 @@ class TestEcarGenerator extends EcarGenerator { "src" -> "somepath/sunbird_1551961194254.jpeg", "baseUrl" -> "some_base_url" ) - val testObj = List(Map("children" -> List(Map("identifier" -> "do_234", "name" -> "Children-1", "objectType" -> "Question"), Map("identifier" -> "do_345", "name" -> "Children-2", "objectType" -> "Question")), "name" -> "Test QuestionSet", "appIcon" -> "https://dev.sunbirded.org/assets/images/sunbird_logo.png", "objectType" -> "QuestionSet", "identifier" -> "do_123", "status" -> "Live", "identifier" -> "do_123"), Map("identifier" -> "do_234", "name" -> "Children-1", "objectType" -> "Question", "media" -> ScalaJsonUtil.serialize(List(media))), Map("identifier" -> "do_345", "name" -> "Children-2", "objectType" -> "Question")) + val testObj = List(Map("children" -> List(Map("identifier" -> "do_234", "name" -> "Children-1", "objectType" -> "Question"), Map("identifier" -> "do_345", "name" -> "Children-2", "objectType" -> "Question")), "name" -> "Test QuestionSet", "appIcon" -> "https://dev.knowlg.sunbird.org/content/preview/assets/icons/avatar_anonymous.png", "objectType" -> "QuestionSet", "identifier" -> "do_123", "status" -> "Live", "identifier" -> "do_123"), Map("identifier" -> "do_234", "name" -> "Children-1", "objectType" -> "Question", "media" -> ScalaJsonUtil.serialize(List(media))), Map("identifier" -> "do_345", "name" -> "Children-2", "objectType" -> "Question")) override def getDataForEcar(obj: ObjectData): Option[List[Map[String, AnyRef]]] = Some(testObj) } diff --git a/publish-pipeline/publish-core/src/test/scala/org/sunbird/job/publish/spec/ObjectEnrichmentSpec.scala b/publish-pipeline/publish-core/src/test/scala/org/sunbird/job/publish/spec/ObjectEnrichmentSpec.scala index b5aeca620..d66422709 100644 --- a/publish-pipeline/publish-core/src/test/scala/org/sunbird/job/publish/spec/ObjectEnrichmentSpec.scala +++ b/publish-pipeline/publish-core/src/test/scala/org/sunbird/job/publish/spec/ObjectEnrichmentSpec.scala @@ -37,7 +37,7 @@ class ObjectEnrichmentSpec extends FlatSpec with BeforeAndAfterAll with Matchers when(mockNeo4JUtil.getNodesName(List("NCERT"))).thenReturn(Map("NCERT"-> "NCERT")) val hierarchy = Map("identifier" -> "do_123", "children" -> List(Map("identifier" -> "do_234", "name" -> "Children-1"), Map("identifier" -> "do_345", "name" -> "Children-2"))) - val metadata = Map("identifier" -> "do_123", "targetFWIds" -> List("NCERT"), "boardIds" -> List("NCERT"), "appIcon" -> "https://dev.sunbirded.org/assets/images/sunbird_logo.png", "IL_UNIQUE_ID" -> "do_123", "IL_FUNC_OBJECT_TYPE" -> "QuestionSet", "name" -> "Test QuestionSet", "status" -> "Live") + val metadata = Map("identifier" -> "do_123", "targetFWIds" -> List("NCERT"), "boardIds" -> List("NCERT"), "appIcon" -> "https://dev.knowlg.sunbird.org/content/preview/assets/icons/avatar_anonymous.png", "IL_UNIQUE_ID" -> "do_123", "IL_FUNC_OBJECT_TYPE" -> "QuestionSet", "name" -> "Test QuestionSet", "status" -> "Live") val objData = new ObjectData("do_123", metadata, None, Some(hierarchy)) val objectEnrichment = new TestObjectEnrichment() diff --git a/publish-pipeline/publish-core/src/test/scala/org/sunbird/job/publish/spec/ObjectReaderTestSpec.scala b/publish-pipeline/publish-core/src/test/scala/org/sunbird/job/publish/spec/ObjectReaderTestSpec.scala index 0eb0c7cd7..217ed6420 100644 --- a/publish-pipeline/publish-core/src/test/scala/org/sunbird/job/publish/spec/ObjectReaderTestSpec.scala +++ b/publish-pipeline/publish-core/src/test/scala/org/sunbird/job/publish/spec/ObjectReaderTestSpec.scala @@ -4,6 +4,7 @@ import org.mockito.Mockito import org.mockito.Mockito._ import org.scalatest.{BeforeAndAfterAll, FlatSpec, Matchers} import org.scalatestplus.mockito.MockitoSugar +import org.sunbird.job.publish.config.PublishConfig import org.sunbird.job.publish.core.{ExtDataConfig, ObjectExtData} import org.sunbird.job.publish.helpers.ObjectReader import org.sunbird.job.util.{CassandraUtil, Neo4JUtil} @@ -22,6 +23,7 @@ class ObjectReaderTestSpec extends FlatSpec with BeforeAndAfterAll with Matchers implicit val mockNeo4JUtil: Neo4JUtil = mock[Neo4JUtil](Mockito.withSettings().serializable()) implicit val mockCassandraUtil: CassandraUtil = mock[CassandraUtil](Mockito.withSettings().serializable()) + implicit val config: PublishConfig = mock[PublishConfig](Mockito.withSettings().serializable()) "Object Reader " should " read the metadata " in { when(mockNeo4JUtil.getNodeProperties("do_123.img")).thenReturn(Map[String, AnyRef]("name" -> "Content Name", "identifier" -> "do_123.img", "IL_UNIQUE_ID" -> "do_123.img", "pkgVersion" -> 2.0.asInstanceOf[AnyRef]).asJava) @@ -37,9 +39,9 @@ class ObjectReaderTestSpec extends FlatSpec with BeforeAndAfterAll with Matchers class TestObjectReader extends ObjectReader { - override def getExtData(identifier: String, pkgVersion: Double, mimeType: String, readerConfig: ExtDataConfig)(implicit cassandraUtil: CassandraUtil): Option[ObjectExtData] = None + override def getExtData(identifier: String, pkgVersion: Double, mimeType: String, readerConfig: ExtDataConfig)(implicit cassandraUtil: CassandraUtil, config: PublishConfig): Option[ObjectExtData] = None - override def getHierarchy(identifier: String, pkgVersion: Double, readerConfig: ExtDataConfig)(implicit cassandraUtil: CassandraUtil): Option[Map[String, AnyRef]] = None + override def getHierarchy(identifier: String, pkgVersion: Double, readerConfig: ExtDataConfig)(implicit cassandraUtil: CassandraUtil, config: PublishConfig): Option[Map[String, AnyRef]] = None override def getExtDatas(identifiers: List[String], readerConfig: ExtDataConfig)(implicit cassandraUtil: CassandraUtil): Option[Map[String, AnyRef]] = None diff --git a/publish-pipeline/publish-core/src/test/scala/org/sunbird/job/publish/spec/ObjectUpdaterSpec.scala b/publish-pipeline/publish-core/src/test/scala/org/sunbird/job/publish/spec/ObjectUpdaterSpec.scala index d41da385b..413c6a203 100644 --- a/publish-pipeline/publish-core/src/test/scala/org/sunbird/job/publish/spec/ObjectUpdaterSpec.scala +++ b/publish-pipeline/publish-core/src/test/scala/org/sunbird/job/publish/spec/ObjectUpdaterSpec.scala @@ -6,6 +6,7 @@ import org.mockito.Mockito.when import org.scalatest.{BeforeAndAfterAll, FlatSpec, Matchers} import org.scalatestplus.mockito.MockitoSugar import org.sunbird.job.domain.`object`.DefinitionCache +import org.sunbird.job.publish.config.PublishConfig import org.sunbird.job.publish.core.{DefinitionConfig, ExtDataConfig, ObjectData} import org.sunbird.job.publish.helpers.ObjectUpdater import org.sunbird.job.util.{CassandraUtil, Neo4JUtil} @@ -16,6 +17,7 @@ class ObjectUpdaterSpec extends FlatSpec with BeforeAndAfterAll with Matchers wi implicit val mockNeo4JUtil: Neo4JUtil = mock[Neo4JUtil](Mockito.withSettings().serializable()) implicit val mockCassandraUtil: CassandraUtil = mock[CassandraUtil](Mockito.withSettings().serializable()) + implicit val config: PublishConfig = mock[PublishConfig](Mockito.withSettings().serializable()) implicit val readerConfig = ExtDataConfig("test", "test") implicit lazy val defCache: DefinitionCache = new DefinitionCache() implicit val definitionConfig: DefinitionConfig = DefinitionConfig(Map("itemset" -> "2.0"), "https://sunbirddev.blob.core.windows.net/sunbird-content-dev/schemas/local") @@ -31,7 +33,7 @@ class ObjectUpdaterSpec extends FlatSpec with BeforeAndAfterAll with Matchers wi "ObjectUpdater saveOnSuccess" should " update the status for successfully published data " in { - when(mockNeo4JUtil.executeQuery(anyString())).thenReturn(any()); + when(mockNeo4JUtil.executeQuery(anyString())).thenReturn(any()) val hierarchy = Map("identifier" -> "do_123", "children" -> List(Map("identifier" -> "do_234", "name" -> "Children-1"), Map("identifier" -> "do_345", "name" -> "Children-2"))) val metadata = Map("objectType" -> "QuestionSet", "identifier" -> "do_123","publish_type" -> "Public", "IL_UNIQUE_ID" -> "do_123", "IL_FUNC_OBJECT_TYPE" -> "QuestionSet", "name" -> "Test QuestionSet", "status" -> "Live") diff --git a/publish-pipeline/publish-core/src/test/scala/org/sunbird/job/publish/spec/ThumbnailGeneratorSpec.scala b/publish-pipeline/publish-core/src/test/scala/org/sunbird/job/publish/spec/ThumbnailGeneratorSpec.scala index a2ab960c6..a5767806f 100644 --- a/publish-pipeline/publish-core/src/test/scala/org/sunbird/job/publish/spec/ThumbnailGeneratorSpec.scala +++ b/publish-pipeline/publish-core/src/test/scala/org/sunbird/job/publish/spec/ThumbnailGeneratorSpec.scala @@ -25,7 +25,7 @@ class ThumbnailGeneratorSpec extends FlatSpec with BeforeAndAfterAll with Matche "Object Thumbnail Generator generateThumbnail" should "add the thumbnail to ObjectData" in { val hierarchy = Map("identifier" -> "do_123", "children" -> List(Map("identifier" -> "do_234", "name" -> "Children-1"), Map("identifier" -> "do_345", "name" -> "Children-2"))) - val metadata = Map("identifier" -> "do_123", "appIcon" -> "https://dev.sunbirded.org/assets/images/sunbird_logo.png", "IL_UNIQUE_ID" -> "do_123", "objectType" -> "QuestionSet", "name" -> "Test QuestionSet", "status" -> "Live") + val metadata = Map("identifier" -> "do_123", "appIcon" -> "https://dev.knowlg.sunbird.org/content/preview/assets/icons/avatar_anonymous.png", "IL_UNIQUE_ID" -> "do_123", "objectType" -> "QuestionSet", "name" -> "Test QuestionSet", "status" -> "Live") val objData = new ObjectData("do_123", metadata, None, Some(hierarchy)) val thumbnailGenerator = new TestThumbnailGenerator() @@ -35,8 +35,8 @@ class ThumbnailGeneratorSpec extends FlatSpec with BeforeAndAfterAll with Matche resultMetadata.isEmpty should be(false) resultMetadata.getOrElse("posterImage", "").asInstanceOf[String].isEmpty should be(false) resultMetadata.getOrElse("appIcon", "").asInstanceOf[String].isEmpty should be(false) - resultMetadata.getOrElse("posterImage", "").asInstanceOf[String] shouldBe "https://dev.sunbirded.org/assets/images/sunbird_logo.png" - resultMetadata.getOrElse("appIcon", "").asInstanceOf[String] shouldBe "https://sunbirddev.blob.core.windows.net/sunbird-content-dev/questionset/do_123/artifact/sunbird_logo.thumb.png" + resultMetadata.getOrElse("posterImage", "").asInstanceOf[String] shouldBe "https://dev.knowlg.sunbird.org/content/preview/assets/icons/avatar_anonymous.png" + resultMetadata.getOrElse("appIcon", "").asInstanceOf[String] shouldBe "https://sunbirddev.blob.core.windows.net/sunbird-content-dev/questionset/do_123/artifact/avatar_anonymous.thumb.png" } "Object Thumbnail Generator generateThumbnail with google drive link" should "add the thumbnail to ObjectData" in { diff --git a/publish-pipeline/questionset-publish/src/main/resources/questionset-publish.conf b/publish-pipeline/questionset-publish/src/main/resources/questionset-publish.conf index cc0b3d61a..dcd68c01d 100644 --- a/publish-pipeline/questionset-publish/src/main/resources/questionset-publish.conf +++ b/publish-pipeline/questionset-publish/src/main/resources/questionset-publish.conf @@ -36,4 +36,16 @@ redis { database { qsCache.id = 0 } -} \ No newline at end of file +} + +cloudstorage.metadata.replace_absolute_path=false +cloudstorage.relative_path_prefix= "CONTENT_STORAGE_BASE_PATH" +cloudstorage.read_base_path="https://sunbirddev.blob.core.windows.net" +cloudstorage.write_base_path=["https://sunbirddev.blob.core.windows.net","https://obj.dev.sunbird.org"] +cloudstorage.metadacloudstorage.metadata.list=["appIcon","posterImage","artifactUrl","downloadUrl","variants","previewUrl","pdfUrl", "streamingUrl", "toc_url"] + +cloud_storage_type="" +cloud_storage_key="" +cloud_storage_secret="" +cloud_storage_container="" +cloud_storage_endpoint="" \ No newline at end of file diff --git a/publish-pipeline/questionset-publish/src/main/scala/org/sunbird/job/questionset/function/QuestionPublishFunction.scala b/publish-pipeline/questionset-publish/src/main/scala/org/sunbird/job/questionset/function/QuestionPublishFunction.scala index a5f5b245b..502be2ad5 100644 --- a/publish-pipeline/questionset-publish/src/main/scala/org/sunbird/job/questionset/function/QuestionPublishFunction.scala +++ b/publish-pipeline/questionset-publish/src/main/scala/org/sunbird/job/questionset/function/QuestionPublishFunction.scala @@ -40,8 +40,8 @@ class QuestionPublishFunction(config: QuestionSetPublishConfig, httpUtil: HttpUt override def open(parameters: Configuration): Unit = { super.open(parameters) - cassandraUtil = new CassandraUtil(config.cassandraHost, config.cassandraPort) - neo4JUtil = new Neo4JUtil(config.graphRoutePath, config.graphName) + cassandraUtil = new CassandraUtil(config.cassandraHost, config.cassandraPort, config) + neo4JUtil = new Neo4JUtil(config.graphRoutePath, config.graphName, config) cloudStorageUtil = new CloudStorageUtil(config) ec = ExecutionContexts.global definitionCache = new DefinitionCache() @@ -63,7 +63,7 @@ class QuestionPublishFunction(config: QuestionSetPublishConfig, httpUtil: HttpUt override def processElement(data: PublishMetadata, context: ProcessFunction[PublishMetadata, String]#Context, metrics: Metrics): Unit = { logger.info("Question publishing started for : " + data.identifier) metrics.incCounter(config.questionPublishEventCount) - val objData = getObject(data.identifier, data.pkgVersion, data.mimeType, data.publishType, readerConfig)(neo4JUtil, cassandraUtil) + val objData = getObject(data.identifier, data.pkgVersion, data.mimeType, data.publishType, readerConfig)(neo4JUtil, cassandraUtil, config) val obj = if (StringUtils.isNotBlank(data.lastPublishedBy)) { val newMeta = objData.metadata ++ Map("lastPublishedBy" -> data.lastPublishedBy) new ObjectData(objData.identifier, newMeta, objData.extData, objData.hierarchy) @@ -74,7 +74,7 @@ class QuestionPublishFunction(config: QuestionSetPublishConfig, httpUtil: HttpUt val enrichedObj = enrichObject(obj)(neo4JUtil, cassandraUtil, readerConfig, cloudStorageUtil, config, definitionCache, definitionConfig) val objWithEcar = getObjectWithEcar(enrichedObj, pkgTypes)(ec, neo4JUtil, cloudStorageUtil, config, definitionCache, definitionConfig, httpUtil) logger.info("Ecar generation done for Question: " + objWithEcar.identifier) - saveOnSuccess(objWithEcar)(neo4JUtil, cassandraUtil, readerConfig, definitionCache, definitionConfig) + saveOnSuccess(objWithEcar)(neo4JUtil, cassandraUtil, readerConfig, definitionCache, definitionConfig, config) metrics.incCounter(config.questionPublishSuccessEventCount) logger.info("Question publishing completed successfully for : " + data.identifier) } else { diff --git a/publish-pipeline/questionset-publish/src/main/scala/org/sunbird/job/questionset/function/QuestionSetPublishFunction.scala b/publish-pipeline/questionset-publish/src/main/scala/org/sunbird/job/questionset/function/QuestionSetPublishFunction.scala index d4c98a25e..da6936983 100644 --- a/publish-pipeline/questionset-publish/src/main/scala/org/sunbird/job/questionset/function/QuestionSetPublishFunction.scala +++ b/publish-pipeline/questionset-publish/src/main/scala/org/sunbird/job/questionset/function/QuestionSetPublishFunction.scala @@ -44,8 +44,8 @@ class QuestionSetPublishFunction(config: QuestionSetPublishConfig, httpUtil: Htt override def open(parameters: Configuration): Unit = { super.open(parameters) - cassandraUtil = new CassandraUtil(config.cassandraHost, config.cassandraPort) - neo4JUtil = new Neo4JUtil(config.graphRoutePath, config.graphName) + cassandraUtil = new CassandraUtil(config.cassandraHost, config.cassandraPort, config) + neo4JUtil = new Neo4JUtil(config.graphRoutePath, config.graphName, config) cloudStorageUtil = new CloudStorageUtil(config) ec = ExecutionContexts.global definitionCache = new DefinitionCache() @@ -71,7 +71,7 @@ class QuestionSetPublishFunction(config: QuestionSetPublishConfig, httpUtil: Htt val readerConfig = ExtDataConfig(config.questionSetKeyspaceName, config.questionSetTableName, definition.getExternalPrimaryKey, definition.getExternalProps) val qDef: ObjectDefinition = definitionCache.getDefinition("Question", config.schemaSupportVersionMap.getOrElse("question", "1.0").asInstanceOf[String], config.definitionBasePath) val qReaderConfig = ExtDataConfig(config.questionKeyspaceName, qDef.getExternalTable, qDef.getExternalPrimaryKey, qDef.getExternalProps) - val objData = getObject(data.identifier, data.pkgVersion, data.mimeType, data.publishType, readerConfig)(neo4JUtil, cassandraUtil) + val objData = getObject(data.identifier, data.pkgVersion, data.mimeType, data.publishType, readerConfig)(neo4JUtil, cassandraUtil, config) val obj = if (StringUtils.isNotBlank(data.lastPublishedBy)) { val newMeta = objData.metadata ++ Map("lastPublishedBy" -> data.lastPublishedBy) new ObjectData(objData.identifier, newMeta, objData.extData, objData.hierarchy) @@ -103,7 +103,7 @@ class QuestionSetPublishFunction(config: QuestionSetPublishConfig, httpUtil: Htt val objWithEcar = generateECAR(enrichedObj, pkgTypes)(ec, neo4JUtil, cloudStorageUtil, config, definitionCache, definitionConfig, httpUtil) // Generate PDF URL val updatedObj = generatePreviewUrl(objWithEcar, qList)(httpUtil, cloudStorageUtil) - saveOnSuccess(updatedObj)(neo4JUtil, cassandraUtil, readerConfig, definitionCache, definitionConfig) + saveOnSuccess(updatedObj)(neo4JUtil, cassandraUtil, readerConfig, definitionCache, definitionConfig, config) logger.info("QuestionSet publishing completed successfully for : " + data.identifier) metrics.incCounter(config.questionSetPublishSuccessEventCount) } else { @@ -123,7 +123,7 @@ class QuestionSetPublishFunction(config: QuestionSetPublishConfig, httpUtil: Htt val messages = ListBuffer[String]() children.foreach(q => { val id = q.identifier.replace(".img", "") - val obj = getObject(id, 0, q.mimeType, publishType, readerConfig)(neo4JUtil, cassandraUtil) + val obj = getObject(id, 0, q.mimeType, publishType, readerConfig)(neo4JUtil, cassandraUtil, config) logger.info(s"question metadata for $id : ${obj.metadata}") if (!List("Live", "Unlisted").contains(obj.getString("status", ""))) { logger.info("Question publishing failed for : " + id) diff --git a/publish-pipeline/questionset-publish/src/main/scala/org/sunbird/job/questionset/publish/helpers/QuestionPublisher.scala b/publish-pipeline/questionset-publish/src/main/scala/org/sunbird/job/questionset/publish/helpers/QuestionPublisher.scala index 653325df2..5150f2c2b 100644 --- a/publish-pipeline/questionset-publish/src/main/scala/org/sunbird/job/questionset/publish/helpers/QuestionPublisher.scala +++ b/publish-pipeline/questionset-publish/src/main/scala/org/sunbird/job/questionset/publish/helpers/QuestionPublisher.scala @@ -25,7 +25,7 @@ trait QuestionPublisher extends ObjectReader with ObjectValidator with ObjectEnr private[this] val logger = LoggerFactory.getLogger(classOf[QuestionPublisher]) val extProps = List("body", "editorState", "answer", "solutions", "instructions", "hints", "media", "responseDeclaration", "interactions") - override def getHierarchy(identifier: String, pkgVersion: Double, readerConfig: ExtDataConfig)(implicit cassandraUtil: CassandraUtil): Option[Map[String, AnyRef]] = None + override def getHierarchy(identifier: String, pkgVersion: Double, readerConfig: ExtDataConfig)(implicit cassandraUtil: CassandraUtil, config: PublishConfig): Option[Map[String, AnyRef]] = None override def enrichObjectMetadata(obj: ObjectData)(implicit neo4JUtil: Neo4JUtil, cassandraUtil: CassandraUtil, readerConfig: ExtDataConfig, cloudStorageUtil: CloudStorageUtil, config: PublishConfig, definitionCache: DefinitionCache, definitionConfig: DefinitionConfig): Option[ObjectData] = { val pkgVersion = obj.metadata.getOrElse("pkgVersion", 0.0.asInstanceOf[Number]).asInstanceOf[Number].intValue() + 1 @@ -50,7 +50,7 @@ trait QuestionPublisher extends ObjectReader with ObjectValidator with ObjectEnr messages.toList } - override def getExtData(identifier: String, pkgVersion: Double, mimeType: String, readerConfig: ExtDataConfig)(implicit cassandraUtil: CassandraUtil): Option[ObjectExtData] = { + override def getExtData(identifier: String, pkgVersion: Double, mimeType: String, readerConfig: ExtDataConfig)(implicit cassandraUtil: CassandraUtil, config: PublishConfig): Option[ObjectExtData] = { val row: Row = Option(getQuestionData(getEditableObjId(identifier, pkgVersion), readerConfig)).getOrElse(getQuestionData(identifier, readerConfig)) val data = if (null != row) Option(extProps.map(prop => prop -> row.getString(prop.toLowerCase())).toMap.filter(p => StringUtils.isNotBlank(p._2))) else Option(Map[String, AnyRef]()) Option(ObjectExtData(data)) @@ -124,7 +124,7 @@ trait QuestionPublisher extends ObjectReader with ObjectValidator with ObjectEnr try { val objType = obj.getString("objectType", "") val objList = getDataForEcar(obj).getOrElse(List()) - val (updatedObjList, dUrls) = getManifestData(obj.identifier, pkgType, objList) + val (updatedObjList, dUrls) = getManifestData(obj.identifier, objType, pkgType, objList) val downloadUrls: Map[AnyRef, List[String]] = dUrls.flatten.groupBy(_._1).map { case (k, v) => k -> v.map(_._2) } logger.info("QuestionPublisher ::: updateArtifactUrl ::: downloadUrls :::: " + downloadUrls) val duration: String = config.getString("media_download_duration", "300 seconds") diff --git a/publish-pipeline/questionset-publish/src/main/scala/org/sunbird/job/questionset/publish/helpers/QuestionSetPublisher.scala b/publish-pipeline/questionset-publish/src/main/scala/org/sunbird/job/questionset/publish/helpers/QuestionSetPublisher.scala index abd84e383..118f0618c 100644 --- a/publish-pipeline/questionset-publish/src/main/scala/org/sunbird/job/questionset/publish/helpers/QuestionSetPublisher.scala +++ b/publish-pipeline/questionset-publish/src/main/scala/org/sunbird/job/questionset/publish/helpers/QuestionSetPublisher.scala @@ -19,7 +19,7 @@ trait QuestionSetPublisher extends ObjectReader with ObjectValidator with Object private[this] val logger = LoggerFactory.getLogger(classOf[QuestionSetPublisher]) val extProps = List("body", "editorState", "answer", "solutions", "instructions", "hints", "media", "responseDeclaration", "interactions", "identifier") - override def getExtData(identifier: String, pkgVersion: Double, mimeType: String, readerConfig: ExtDataConfig)(implicit cassandraUtil: CassandraUtil): Option[ObjectExtData] = { + override def getExtData(identifier: String, pkgVersion: Double, mimeType: String, readerConfig: ExtDataConfig)(implicit cassandraUtil: CassandraUtil, config: PublishConfig): Option[ObjectExtData] = { val row: Row = Option(getQuestionSetData(getEditableObjId(identifier, pkgVersion), readerConfig)).getOrElse(getQuestionSetData(identifier, readerConfig)) val data: Map[String, AnyRef] = if (null != row) readerConfig.propsMapping.keySet.map(prop => prop -> row.getString(prop.toLowerCase())).toMap.filter(p => StringUtils.isNotBlank(p._2.asInstanceOf[String])) else Map[String, AnyRef]() val hierarchy: Map[String, AnyRef] = if(data.contains("hierarchy")) ScalaJsonUtil.deserialize[Map[String, AnyRef]](data.getOrElse("hierarchy", "{}").asInstanceOf[String]) else Map[String, AnyRef]() @@ -57,7 +57,7 @@ trait QuestionSetPublisher extends ObjectReader with ObjectValidator with Object messages.toList } - override def getHierarchy(identifier: String, pkgVersion: Double, readerConfig: ExtDataConfig)(implicit cassandraUtil: CassandraUtil): Option[Map[String, AnyRef]] = { + override def getHierarchy(identifier: String, pkgVersion: Double, readerConfig: ExtDataConfig)(implicit cassandraUtil: CassandraUtil, config: PublishConfig): Option[Map[String, AnyRef]] = { val row: Row = Option(getQuestionSetHierarchy(getEditableObjId(identifier, pkgVersion), readerConfig)).getOrElse(getQuestionSetHierarchy(identifier, readerConfig)) if (null != row) { val data: Map[String, AnyRef] = ScalaJsonUtil.deserialize[Map[String, AnyRef]](row.getString("hierarchy")) @@ -163,12 +163,12 @@ trait QuestionSetPublisher extends ObjectReader with ObjectValidator with Object Some(new ObjectData(obj.identifier, newMetadata, obj.extData, hierarchy = Some(Map("identifier" -> obj.identifier, "children" -> enrichChildren(children))))) } - def enrichChildren(children: List[Map[String, AnyRef]])(implicit neo4JUtil: Neo4JUtil, cassandraUtil: CassandraUtil, readerConfig: ExtDataConfig, definitionCache: DefinitionCache, definitionConfig: DefinitionConfig): List[Map[String, AnyRef]] = { + def enrichChildren(children: List[Map[String, AnyRef]])(implicit neo4JUtil: Neo4JUtil, cassandraUtil: CassandraUtil, readerConfig: ExtDataConfig, definitionCache: DefinitionCache, definitionConfig: DefinitionConfig, config: PublishConfig): List[Map[String, AnyRef]] = { val newChildren = children.map(element => enrichMetadata(element)) newChildren } - def enrichMetadata(element: Map[String, AnyRef])(implicit neo4JUtil: Neo4JUtil, cassandraUtil: CassandraUtil, readerConfig: ExtDataConfig, definitionCache: DefinitionCache, definitionConfig: DefinitionConfig): Map[String, AnyRef] = { + def enrichMetadata(element: Map[String, AnyRef])(implicit neo4JUtil: Neo4JUtil, cassandraUtil: CassandraUtil, readerConfig: ExtDataConfig, definitionCache: DefinitionCache, definitionConfig: DefinitionConfig, config: PublishConfig): Map[String, AnyRef] = { if (StringUtils.equalsIgnoreCase(element.getOrElse("objectType", "").asInstanceOf[String], "QuestionSet") && StringUtils.equalsIgnoreCase(element.getOrElse("visibility", "").asInstanceOf[String], "Parent")) { val children: List[Map[String, AnyRef]] = element.getOrElse("children", List()).asInstanceOf[List[Map[String, AnyRef]]] diff --git a/publish-pipeline/questionset-publish/src/main/scala/org/sunbird/job/questionset/publish/util/QuestionPublishUtil.scala b/publish-pipeline/questionset-publish/src/main/scala/org/sunbird/job/questionset/publish/util/QuestionPublishUtil.scala index bc9a3b434..fc7c18151 100644 --- a/publish-pipeline/questionset-publish/src/main/scala/org/sunbird/job/questionset/publish/util/QuestionPublishUtil.scala +++ b/publish-pipeline/questionset-publish/src/main/scala/org/sunbird/job/questionset/publish/util/QuestionPublishUtil.scala @@ -21,7 +21,7 @@ object QuestionPublishUtil extends QuestionPublisher { logger.info("QuestionPublishUtil :::: publishing child question for questionset : " + identifier) objList.map(qData => { logger.info("QuestionPublishUtil :::: publishing child question : " + qData.identifier) - val objData = getObject(qData.identifier, qData.pkgVersion, qData.mimeType, qData.metadata.getOrElse("publish_type", "Public").toString, readerConfig)(neo4JUtil, cassandraUtil) + val objData = getObject(qData.identifier, qData.pkgVersion, qData.mimeType, qData.metadata.getOrElse("publish_type", "Public").toString, readerConfig)(neo4JUtil, cassandraUtil, config) val obj = if (StringUtils.isNotBlank(lastPublishedBy)) { val newMeta = objData.metadata ++ Map("lastPublishedBy" -> lastPublishedBy) new ObjectData(objData.identifier, newMeta, objData.extData, objData.hierarchy) @@ -35,7 +35,7 @@ object QuestionPublishUtil extends QuestionPublisher { } else enrichedObj val objWithEcar = getObjectWithEcar(objWithArtifactUrl, pkgTypes)(ec, neo4JUtil, cloudStorageUtil, config, definitionCache, definitionConfig, httpUtil) logger.info("Ecar generation done for Question: " + objWithEcar.identifier) - saveOnSuccess(objWithEcar)(neo4JUtil, cassandraUtil, readerConfig, definitionCache, definitionConfig) + saveOnSuccess(objWithEcar)(neo4JUtil, cassandraUtil, readerConfig, definitionCache, definitionConfig, config) logger.info("Question publishing completed successfully for : " + qData.identifier) objWithEcar } else { diff --git a/publish-pipeline/questionset-publish/src/test/scala/org/sunbird/job/publish/helpers/spec/QuestionPublisherSpec.scala b/publish-pipeline/questionset-publish/src/test/scala/org/sunbird/job/publish/helpers/spec/QuestionPublisherSpec.scala index 9f501f89d..eafdf38af 100644 --- a/publish-pipeline/questionset-publish/src/test/scala/org/sunbird/job/publish/helpers/spec/QuestionPublisherSpec.scala +++ b/publish-pipeline/questionset-publish/src/test/scala/org/sunbird/job/publish/helpers/spec/QuestionPublisherSpec.scala @@ -36,7 +36,7 @@ class QuestionPublisherSpec extends FlatSpec with BeforeAndAfterAll with Matcher override protected def beforeAll(): Unit = { super.beforeAll() EmbeddedCassandraServerHelper.startEmbeddedCassandra(80000L) - cassandraUtil = new CassandraUtil(jobConfig.cassandraHost, jobConfig.cassandraPort) + cassandraUtil = new CassandraUtil(jobConfig.cassandraHost, jobConfig.cassandraPort, jobConfig) val session = cassandraUtil.session val dataLoader = new CQLDataLoader(session) dataLoader.load(new FileCQLDataSet(getClass.getResource("/test.cql").getPath, true, true)) diff --git a/publish-pipeline/questionset-publish/src/test/scala/org/sunbird/job/publish/helpers/spec/QuestionSetPublisherSpec.scala b/publish-pipeline/questionset-publish/src/test/scala/org/sunbird/job/publish/helpers/spec/QuestionSetPublisherSpec.scala index 66873db50..e2e31efb2 100644 --- a/publish-pipeline/questionset-publish/src/test/scala/org/sunbird/job/publish/helpers/spec/QuestionSetPublisherSpec.scala +++ b/publish-pipeline/questionset-publish/src/test/scala/org/sunbird/job/publish/helpers/spec/QuestionSetPublisherSpec.scala @@ -28,7 +28,7 @@ class QuestionSetPublisherSpec extends FlatSpec with BeforeAndAfterAll with Matc override protected def beforeAll(): Unit = { super.beforeAll() EmbeddedCassandraServerHelper.startEmbeddedCassandra(80000L) - cassandraUtil = new CassandraUtil(jobConfig.cassandraHost, jobConfig.cassandraPort) + cassandraUtil = new CassandraUtil(jobConfig.cassandraHost, jobConfig.cassandraPort, jobConfig) val session = cassandraUtil.session val dataLoader = new CQLDataLoader(session) dataLoader.load(new FileCQLDataSet(getClass.getResource("/test.cql").getPath, true, true)) diff --git a/publish-pipeline/questionset-publish/src/test/scala/org/sunbird/job/publish/util/spec/QuestionPublishUtilSpec.scala b/publish-pipeline/questionset-publish/src/test/scala/org/sunbird/job/publish/util/spec/QuestionPublishUtilSpec.scala index 29eb5e7b5..1e518ebfa 100644 --- a/publish-pipeline/questionset-publish/src/test/scala/org/sunbird/job/publish/util/spec/QuestionPublishUtilSpec.scala +++ b/publish-pipeline/questionset-publish/src/test/scala/org/sunbird/job/publish/util/spec/QuestionPublishUtilSpec.scala @@ -36,7 +36,7 @@ class QuestionPublishUtilSpec extends FlatSpec with BeforeAndAfterAll with Match super.beforeAll() EmbeddedCassandraServerHelper.startEmbeddedCassandra(80000L) - cassandraUtil = new CassandraUtil(jobConfig.cassandraHost, jobConfig.cassandraPort) + cassandraUtil = new CassandraUtil(jobConfig.cassandraHost, jobConfig.cassandraPort, jobConfig) val session = cassandraUtil.session val dataLoader = new CQLDataLoader(session) dataLoader.load(new FileCQLDataSet(getClass.getResource("/test.cql").getPath, true, true)) diff --git a/publish-pipeline/questionset-publish/src/test/scala/org/sunbird/job/spec/QuestionSetPublishStreamTaskSpec.scala b/publish-pipeline/questionset-publish/src/test/scala/org/sunbird/job/spec/QuestionSetPublishStreamTaskSpec.scala index 89c4b2bdf..5606b56be 100644 --- a/publish-pipeline/questionset-publish/src/test/scala/org/sunbird/job/spec/QuestionSetPublishStreamTaskSpec.scala +++ b/publish-pipeline/questionset-publish/src/test/scala/org/sunbird/job/spec/QuestionSetPublishStreamTaskSpec.scala @@ -46,7 +46,7 @@ class QuestionSetPublishStreamTaskSpec extends BaseTestSpec { override protected def beforeAll(): Unit = { super.beforeAll() EmbeddedCassandraServerHelper.startEmbeddedCassandra(80000L) - cassandraUtil = new CassandraUtil(jobConfig.cassandraHost, jobConfig.cassandraPort) + cassandraUtil = new CassandraUtil(jobConfig.cassandraHost, jobConfig.cassandraPort, jobConfig) val session = cassandraUtil.session val dataLoader = new CQLDataLoader(session) dataLoader.load(new FileCQLDataSet(getClass.getResource("/test.cql").getPath, true, true)) diff --git a/qrcode-image-generator/src/main/resources/qrcode-image-generator.conf b/qrcode-image-generator/src/main/resources/qrcode-image-generator.conf index 27ac29f5a..7ad19a6cb 100644 --- a/qrcode-image-generator/src/main/resources/qrcode-image-generator.conf +++ b/qrcode-image-generator/src/main/resources/qrcode-image-generator.conf @@ -33,3 +33,15 @@ lms-cassandra { # Default value is 120 max_allowed_character_for_file_name = 120 + +cloudstorage.metadata.replace_absolute_path=false +cloudstorage.relative_path_prefix= "DIAL_STORAGE_BASE_PATH" +cloudstorage.read_base_path="https://sunbirddev.blob.core.windows.net" +cloudstorage.write_base_path=["https://sunbirddev.blob.core.windows.net/dial","https://obj.dev.sunbird.org/dial"] +cloudstorage.metadata.list=["appIcon","posterImage","artifactUrl","downloadUrl","variants","previewUrl","pdfUrl", "streamingUrl", "toc_url"] + +cloud_storage_type="" +cloud_storage_key="" +cloud_storage_secret="" +cloud_storage_container="" +cloud_storage_endpoint="" diff --git a/qrcode-image-generator/src/main/scala/org/sunbird/job/qrimagegenerator/functions/QRCodeImageGeneratorFunction.scala b/qrcode-image-generator/src/main/scala/org/sunbird/job/qrimagegenerator/functions/QRCodeImageGeneratorFunction.scala index 6326dd0ef..348ab95f2 100644 --- a/qrcode-image-generator/src/main/scala/org/sunbird/job/qrimagegenerator/functions/QRCodeImageGeneratorFunction.scala +++ b/qrcode-image-generator/src/main/scala/org/sunbird/job/qrimagegenerator/functions/QRCodeImageGeneratorFunction.scala @@ -8,7 +8,7 @@ import org.sunbird.job.exception.InvalidEventException import org.sunbird.job.qrimagegenerator.domain.{Event, ImageConfig, QRCodeImageGeneratorRequest} import org.sunbird.job.qrimagegenerator.task.QRCodeImageGeneratorConfig import org.sunbird.job.qrimagegenerator.util.QRCodeImageGeneratorUtil -import org.sunbird.job.util.{CassandraUtil, CloudStorageUtil, FileUtils} +import org.sunbird.job.util.{CassandraUtil, CloudStorageUtil, ElasticSearchUtil, FileUtils} import org.sunbird.job.{BaseProcessFunction, Metrics} import java.io.File @@ -17,6 +17,7 @@ import scala.collection.mutable.ListBuffer class QRCodeImageGeneratorFunction(config: QRCodeImageGeneratorConfig, @transient var cassandraUtil: CassandraUtil = null, @transient var cloudStorageUtil: CloudStorageUtil = null, + @transient var esUtil: ElasticSearchUtil = null, @transient var qRCodeImageGeneratorUtil: QRCodeImageGeneratorUtil = null) (implicit val stringTypeInfo: TypeInformation[String]) extends BaseProcessFunction[Event, String](config) { @@ -24,9 +25,10 @@ class QRCodeImageGeneratorFunction(config: QRCodeImageGeneratorConfig, private val logger = LoggerFactory.getLogger(classOf[QRCodeImageGeneratorFunction]) override def open(parameters: Configuration): Unit = { - cassandraUtil = new CassandraUtil(config.cassandraHost, config.cassandraPort) + cassandraUtil = new CassandraUtil(config.cassandraHost, config.cassandraPort, config) cloudStorageUtil = new CloudStorageUtil(config) - qRCodeImageGeneratorUtil = new QRCodeImageGeneratorUtil(config, cassandraUtil, cloudStorageUtil) + esUtil = new ElasticSearchUtil(config.esConnectionInfo, config.dialcodeExternalIndex, config.dialcodeExternalIndexType) + qRCodeImageGeneratorUtil = new QRCodeImageGeneratorUtil(config, cassandraUtil, cloudStorageUtil, esUtil) super.open(parameters) } @@ -77,30 +79,48 @@ class QRCodeImageGeneratorFunction(config: QRCodeImageGeneratorConfig, val dialCodes: List[Map[String, AnyRef]] = event.dialCodes.filter(dialcode => dialcode.getOrElse("location", "").asInstanceOf[String].isEmpty) val qrGenRequest: QRCodeImageGeneratorRequest = QRCodeImageGeneratorRequest(dialCodes, imageConfig, config.lpTempFileLocation) - logger.info("QRCodeImageGeneratorRequest: " + qrGenRequest) + logger.info("QRCodeImageGeneratorRequest: " + qrGenRequest + " storageContainer: "+event.storageContainer+" storagePath: "+event.storagePath) val generatedImages: ListBuffer[File] = qRCodeImageGeneratorUtil.createQRImages(qrGenRequest, event.storageContainer, event.storagePath, metrics) if (!event.processId.isBlank) { val maxAllowedCharacter: Int = config.getInt("max_allowed_character_for_file_name", 120) logger.info("QRCodeImageGeneratorService:processMessage: Generating zip for QR codes with processId " + event.processId) val storageFileName = if (event.storageFileName.isBlank) event.processId else event.storageFileName + logger.info("QRCodeImageGeneratorService:processMessage: storageFileName " + storageFileName) val qrZipFileName = if (storageFileName.length > maxAllowedCharacter) storageFileName.substring(0, maxAllowedCharacter) else storageFileName + logger.info("QRCodeImageGeneratorService:processMessage: qrZipFileName - " + qrZipFileName + " tempFilePath - "+tempFilePath) // Merge available and generated image list generatedImages.foreach(f => availableImages += f) val zipFileName: String = tempFilePath + File.separator + qrZipFileName + ".zip" + logger.info("QRCodeImageGeneratorService:processMessage: zipFileName - " + zipFileName) + val fileList: List[String] = availableImages.map(f => f.getName).toList FileUtils.zipIt(zipFileName, fileList, tempFilePath) zipFile = new File(zipFileName) - val zipDownloadUrl = cloudStorageUtil.uploadFile(event.storagePath, zipFile, Some(false), container = event.storageContainer) + logger.info("QRCodeImageGeneratorService:processMessage: event.storagePath - " + event.storagePath + " event.storageContainer - "+ event.storageContainer) + val zipDownloadUrl = cloudStorageUtil.uploadFile(event.storagePath.replace("/", ""), zipFile, Some(false), container = event.storageContainer) + logger.info("QRCodeImageGeneratorService:processMessage: zipDownloadUrl - " + zipDownloadUrl(1)) + logger.info("QRCodeImageGeneratorService:processMessage: config.cloudStorageEndpoint - " + config.cloudStorageEndpoint+" config.cloudStorageProxyHost - "+config.cloudStorageProxyHost) metrics.incCounter(config.cloudDbHitCount) - qRCodeImageGeneratorUtil.updateCassandra(config.cassandraDialCodeBatchTable, 2, zipDownloadUrl(1), "processid", event.processId, metrics) + if(config.cloudStorageEndpoint.nonEmpty){ + var newDownloadUrl = zipDownloadUrl(1).replaceAll(config.cloudStorageEndpoint, config.cloudStorageProxyHost) + logger.info("QRCodeImageGeneratorService:processMessage: newDownloadUrl - " + newDownloadUrl) + logger.info("QRCodeImageGeneratorService:processMessage: event - " + event) + qRCodeImageGeneratorUtil.updateCassandra(config.cassandraDialCodeBatchTable, 2, newDownloadUrl, "processid", event.processId, metrics) + } else { + qRCodeImageGeneratorUtil.updateCassandra(config.cassandraDialCodeBatchTable, 2, zipDownloadUrl(1), "processid", event.processId, metrics) + } + } else { logger.info("QRCodeImageGeneratorService:processMessage: Skipping zip creation due to missing processId.") } + + if(config.indexImageURL) + context.output(config.indexImageUrlOutTag, event) logger.info("QRCodeImageGeneratorService:processMessage: Message processed successfully at " + System.currentTimeMillis) } else { logger.info("QRCodeImageGeneratorService: Eid other than BE_QR_IMAGE_GENERATOR or Dialcodes not present") diff --git a/qrcode-image-generator/src/main/scala/org/sunbird/job/qrimagegenerator/functions/QRCodeIndexImageUrlFunction.scala b/qrcode-image-generator/src/main/scala/org/sunbird/job/qrimagegenerator/functions/QRCodeIndexImageUrlFunction.scala new file mode 100644 index 000000000..c5d2a4001 --- /dev/null +++ b/qrcode-image-generator/src/main/scala/org/sunbird/job/qrimagegenerator/functions/QRCodeIndexImageUrlFunction.scala @@ -0,0 +1,57 @@ +package org.sunbird.job.qrimagegenerator.functions + +import org.apache.flink.api.common.typeinfo.TypeInformation +import org.apache.flink.configuration.Configuration +import org.apache.flink.streaming.api.functions.ProcessFunction +import org.slf4j.LoggerFactory +import org.sunbird.job.exception.InvalidEventException +import org.sunbird.job.qrimagegenerator.domain.Event +import org.sunbird.job.qrimagegenerator.task.QRCodeImageGeneratorConfig +import org.sunbird.job.qrimagegenerator.util.QRCodeImageGeneratorUtil +import org.sunbird.job.util.{CassandraUtil, CloudStorageUtil, ElasticSearchUtil, ScalaJsonUtil} +import org.sunbird.job.{BaseProcessFunction, Metrics} + +import scala.collection.mutable + +class QRCodeIndexImageUrlFunction(config: QRCodeImageGeneratorConfig, + @transient var cassandraUtil: CassandraUtil = null, + @transient var cloudStorageUtil: CloudStorageUtil = null, + @transient var esUtil: ElasticSearchUtil = null, + @transient var qRCodeImageGeneratorUtil: QRCodeImageGeneratorUtil = null) + (implicit val stringTypeInfo: TypeInformation[String]) extends BaseProcessFunction[Event, String](config) { + + private val logger = LoggerFactory.getLogger(classOf[QRCodeIndexImageUrlFunction]) + + override def open(parameters: Configuration): Unit = { + cassandraUtil = new CassandraUtil(config.cassandraHost, config.cassandraPort, config) + esUtil = new ElasticSearchUtil(config.esConnectionInfo, config.dialcodeExternalIndex, config.dialcodeExternalIndexType) + qRCodeImageGeneratorUtil = new QRCodeImageGeneratorUtil(config, cassandraUtil, cloudStorageUtil, esUtil) + super.open(parameters) + } + + override def close(): Unit = { + cassandraUtil.close() + super.close() + } + + override def metricsList(): List[String] = { + List() + } + + @throws(classOf[InvalidEventException]) + override def processElement(event: Event, + context: ProcessFunction[Event, String]#Context, + metrics: Metrics): Unit = { + event.dialCodes.foreach { dialcode => + try { + val text = dialcode("text").asInstanceOf[String] + qRCodeImageGeneratorUtil.indexImageInDocument(text)(esUtil, cassandraUtil) + } catch { + case e: Exception => e.printStackTrace() + throw new InvalidEventException(e.getMessage) + } + } + + } + +} diff --git a/qrcode-image-generator/src/main/scala/org/sunbird/job/qrimagegenerator/task/QRCodeImageGeneratorConfig.scala b/qrcode-image-generator/src/main/scala/org/sunbird/job/qrimagegenerator/task/QRCodeImageGeneratorConfig.scala index 8c8cfab4e..3fe4f5b10 100644 --- a/qrcode-image-generator/src/main/scala/org/sunbird/job/qrimagegenerator/task/QRCodeImageGeneratorConfig.scala +++ b/qrcode-image-generator/src/main/scala/org/sunbird/job/qrimagegenerator/task/QRCodeImageGeneratorConfig.scala @@ -1,7 +1,12 @@ package org.sunbird.job.qrimagegenerator.task import com.typesafe.config.Config +import org.apache.commons.lang3.StringUtils +import org.apache.flink.api.common.typeinfo.TypeInformation +import org.apache.flink.api.java.typeutils.TypeExtractor +import org.apache.flink.streaming.api.scala.OutputTag import org.sunbird.job.BaseJobConfig +import org.sunbird.job.qrimagegenerator.domain.Event class QRCodeImageGeneratorConfig(override val config: Config) extends BaseJobConfig(config, "qrcode-image-generator") { @@ -12,6 +17,7 @@ class QRCodeImageGeneratorConfig(override val config: Config) extends BaseJobCon val kafkaInputTopic: String = config.getString("kafka.input.topic") override val kafkaConsumerParallelism: Int = config.getInt("task.consumer.parallelism") override val parallelism: Int = config.getInt("task.parallelism") + implicit val qrImageTypeInfo: TypeInformation[Event] = TypeExtractor.getForClass(classOf[Event]) // Metric List val totalEventsCount = "total-events-count" @@ -23,6 +29,15 @@ class QRCodeImageGeneratorConfig(override val config: Config) extends BaseJobCon val cloudDbHitCount = "cloud-db-hit-events-count" val cloudDbFailCount = "cloud-db-hit-failure-count" + //Tags + val indexImageUrlOutTag: OutputTag[Event] = OutputTag[Event]("index-imageUrl") + + // ES Configs + val esConnectionInfo = config.getString("es.basePath") + + val dialcodeExternalIndex: String = if (config.hasPath("dialcode.index.name")) config.getString("dialcode.index.name") else "dialcode" + val dialcodeExternalIndexType: String = "dc" + // Consumers val eventConsumer = "qrcode-image-generator-consumer" val qrCodeImageGeneratorFunction = "qrcode-image-generator-function" @@ -43,4 +58,8 @@ class QRCodeImageGeneratorConfig(override val config: Config) extends BaseJobCon val cassandraKeyspace: String = config.getString("lms-cassandra.keyspace") val cassandraDialCodeImageTable: String = config.getString("lms-cassandra.table.image") val cassandraDialCodeBatchTable: String = config.getString("lms-cassandra.table.batch") + + val cloudStorageEndpoint: String = if (config.hasPath("cloud_storage_endpoint")) config.getString("cloud_storage_endpoint") else "" + val cloudStorageProxyHost: String = if (config.hasPath("cloud_storage_proxy_host")) config.getString("cloud_storage_endpoint") else "" + val indexImageURL: Boolean = if (config.hasPath("qr.image.indexImageUrl")) config.getBoolean("qr.image.indexImageUrl") else true } diff --git a/qrcode-image-generator/src/main/scala/org/sunbird/job/qrimagegenerator/task/QRCodeImageGeneratorTask.scala b/qrcode-image-generator/src/main/scala/org/sunbird/job/qrimagegenerator/task/QRCodeImageGeneratorTask.scala index 5750a4e95..694f81e16 100644 --- a/qrcode-image-generator/src/main/scala/org/sunbird/job/qrimagegenerator/task/QRCodeImageGeneratorTask.scala +++ b/qrcode-image-generator/src/main/scala/org/sunbird/job/qrimagegenerator/task/QRCodeImageGeneratorTask.scala @@ -7,7 +7,7 @@ import org.apache.flink.api.java.utils.ParameterTool import org.apache.flink.streaming.api.scala.StreamExecutionEnvironment import org.sunbird.job.connector.FlinkKafkaConnector import org.sunbird.job.qrimagegenerator.domain.Event -import org.sunbird.job.qrimagegenerator.functions.QRCodeImageGeneratorFunction +import org.sunbird.job.qrimagegenerator.functions.{QRCodeImageGeneratorFunction, QRCodeIndexImageUrlFunction} import org.sunbird.job.util.FlinkUtil import java.io.File @@ -20,7 +20,7 @@ class QRCodeImageGeneratorTask(config: QRCodeImageGeneratorConfig, kafkaConnecto implicit val stringTypeInfo: TypeInformation[String] = TypeExtractor.getForClass(classOf[String]) val source = kafkaConnector.kafkaJobRequestSource[Event](config.kafkaInputTopic) - env.addSource(source) + val streamTask = env.addSource(source) .name(config.eventConsumer) .uid(config.eventConsumer) .rebalance @@ -30,6 +30,10 @@ class QRCodeImageGeneratorTask(config: QRCodeImageGeneratorConfig, kafkaConnecto .uid(config.qrCodeImageGeneratorFunction) .setParallelism(config.parallelism) + if(config.indexImageURL) + streamTask.getSideOutput(config.indexImageUrlOutTag).process(new QRCodeIndexImageUrlFunction(config)) + .name("index-imageUrl-process").uid("index-imageUrl-process").setParallelism(config.parallelism) + env.execute(config.jobName) } } diff --git a/qrcode-image-generator/src/main/scala/org/sunbird/job/qrimagegenerator/util/QRCodeImageGeneratorUtil.scala b/qrcode-image-generator/src/main/scala/org/sunbird/job/qrimagegenerator/util/QRCodeImageGeneratorUtil.scala index 8088bfa89..22aa842db 100644 --- a/qrcode-image-generator/src/main/scala/org/sunbird/job/qrimagegenerator/util/QRCodeImageGeneratorUtil.scala +++ b/qrcode-image-generator/src/main/scala/org/sunbird/job/qrimagegenerator/util/QRCodeImageGeneratorUtil.scala @@ -1,5 +1,6 @@ package org.sunbird.job.qrimagegenerator.util +import com.datastax.driver.core.Row import com.datastax.driver.core.querybuilder.QueryBuilder import com.google.zxing.client.j2se.BufferedImageLuminanceSource import com.google.zxing.common.{BitMatrix, HybridBinarizer} @@ -8,6 +9,7 @@ import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel import com.google.zxing.{BarcodeFormat, EncodeHintType, NotFoundException, WriterException} import org.slf4j.LoggerFactory import org.sunbird.job.Metrics +import org.sunbird.job.exception.InvalidInputException import org.sunbird.job.qrimagegenerator.domain.{ImageConfig, QRCodeImageGeneratorRequest} import org.sunbird.job.qrimagegenerator.task.QRCodeImageGeneratorConfig import org.sunbird.job.util._ @@ -18,12 +20,14 @@ import java.awt.{Color, Font, FontFormatException, Graphics2D, RenderingHints} import java.io.{File, IOException, InputStream} import java.util.UUID import javax.imageio.ImageIO +import scala.collection.mutable import scala.collection.mutable.ListBuffer -class QRCodeImageGeneratorUtil(config: QRCodeImageGeneratorConfig, cassandraUtil: CassandraUtil, cloudStorageUtil: CloudStorageUtil) { +class QRCodeImageGeneratorUtil(config: QRCodeImageGeneratorConfig, cassandraUtil: CassandraUtil, cloudStorageUtil: CloudStorageUtil, esUtil: ElasticSearchUtil) { private val qrCodeWriter = new QRCodeWriter() private val fontStore: java.util.Map[String, Font] = new java.util.HashMap[String, Font]() private val logger = LoggerFactory.getLogger(classOf[QRCodeImageGeneratorUtil]) + val isrRelativePathEnabled = config.getBoolean("cloudstorage.metadata.replace_absolute_path", false) @throws[WriterException] @throws[IOException] @@ -38,22 +42,33 @@ class QRCodeImageGeneratorUtil(config: QRCodeImageGeneratorConfig, cassandraUtil val data = dialcode("data").asInstanceOf[String] val text = dialcode("text").asInstanceOf[String] val fileName = dialcode("id").asInstanceOf[String] - + logger.info("QRCodeImageGeneratorUtil:createQRImages: data - " + data+" text - "+text+" fileName - "+fileName) var qrImage = generateBaseImage(data, imageConfig.errorCorrectionLevel, imageConfig.pixelsPerBlock, imageConfig.qrCodeMargin, imageConfig.colourModel) if (null != text || !text.isBlank) { val textImage = getTextImage(text, imageConfig.textFontName, imageConfig.textFontSize, imageConfig.textCharacterSpacing, imageConfig.colourModel) qrImage = addTextToBaseImage(qrImage, textImage, imageConfig.colourModel, imageConfig.qrCodeMargin, imageConfig.pixelsPerBlock, imageConfig.qrCodeMarginBottom, imageConfig.imageMargin) } + logger.info("QRCodeImageGeneratorUtil:createQRImages: qrImage - " + qrImage) if (imageConfig.imageBorderSize > 0) drawBorder(qrImage, imageConfig.imageBorderSize, imageConfig.imageMargin) val finalImageFile = new File(req.tempFilePath + File.separator + fileName + "." + imageConfig.imageFormat) logger.info("QRCodeImageGeneratorUtil:createQRImages: creating file - " + finalImageFile.getAbsolutePath) finalImageFile.createNewFile - logger.info("QRCodeImageGeneratorUtil:createQRImages: created file - " + finalImageFile.getAbsolutePath) ImageIO.write(qrImage, imageConfig.imageFormat, finalImageFile) fileList += finalImageFile try { - val imageDownloadUrl = cloudStorageUtil.uploadFile(path, finalImageFile, Some(false), container = container) - updateCassandra(config.cassandraDialCodeImageTable, 2, imageDownloadUrl(1), "filename", fileName, metrics) + logger.info("QRCodeImageGeneratorUtil:createQRImages: path before - " + path) + logger.info("QRCodeImageGeneratorUtil:createQRImages: path after - " + path.replace("/", "")) + val imageDownloadUrl = cloudStorageUtil.uploadFile(path.replace("/", ""), finalImageFile, Some(false), container = container) + logger.info("QRCodeImageGeneratorUtil:createQRImages: imageDownloadUrl - " + imageDownloadUrl(1)) + if(config.cloudStorageEndpoint.nonEmpty){ + logger.info("QRCodeImageGeneratorUtil:createQRImages: config.cloudStorageEndpoint - " + config.cloudStorageEndpoint+" config.cloudStorageProxyHost - "+config.cloudStorageProxyHost) + var newDownloadUrl = imageDownloadUrl(1).replaceAll(config.cloudStorageEndpoint, config.cloudStorageProxyHost) + logger.info("QRCodeImageGeneratorService:processMessage: newDownloadUrl before - " + newDownloadUrl) + updateCassandra(config.cassandraDialCodeImageTable, 2, newDownloadUrl, "filename", fileName, metrics) + } else { + updateCassandra(config.cassandraDialCodeImageTable, 2, imageDownloadUrl(1), "filename", fileName, metrics) + } + } catch { case e: Exception => metrics.incCounter(config.dbFailureEventCount) @@ -152,8 +167,8 @@ class QRCodeImageGeneratorUtil(config: QRCodeImageGeneratorConfig, cassandraUtil } private def getImage(bitMatrix: BitMatrix, colorModel: String) = { - val imageWidth = bitMatrix.getWidth() - val imageHeight = bitMatrix.getHeight() + val imageWidth = bitMatrix.getWidth + val imageHeight = bitMatrix.getHeight val image = new BufferedImage(imageWidth, imageHeight, getImageType(colorModel)) image.createGraphics() val graphics = image.getGraphics.asInstanceOf[Graphics2D] @@ -259,5 +274,33 @@ class QRCodeImageGeneratorUtil(config: QRCodeImageGeneratorConfig, cassandraUtil private def getFontFromStore(fontName: String): Font = { fontStore.getOrDefault(fontName, loadFontStore(fontName)) } + + + def indexImageInDocument(id: String)(esUtil: ElasticSearchUtil, cassandraUtil: CassandraUtil): Unit = { + val documentJson: String = esUtil.getDocumentAsString(id) + val indexDocument = if (documentJson != null && documentJson.nonEmpty) ScalaJsonUtil.deserialize[mutable.Map[String, AnyRef]](documentJson) else mutable.Map[String, AnyRef]() + logger.info("QRCodeImageGeneratorUtil::indexImageInDocument:: indexDocument:: " + indexDocument) + if(indexDocument!=null && indexDocument.nonEmpty && !indexDocument.contains("url")) { + val query = QueryBuilder.select("url").from(config.cassandraKeyspace, config.cassandraDialCodeImageTable) + .allowFiltering() + .where(QueryBuilder.eq("dialcode", id)) + logger.info("QRCodeImageGeneratorUtil::indexImageInDocument:: query:: " + query) + val row: Row = cassandraUtil.findOne(query.toString) + if(null != row && !row.isNull("url")) { + val imageUrl = row.getString("url") + logger.info("QRCodeImageGeneratorUtil::indexImageInDocument:: imageUrl:: " + imageUrl) + val absoluteImageUrl = if (isrRelativePathEnabled) CSPMetaUtil.updateAbsolutePath(imageUrl)(config) else imageUrl + logger.info("QRCodeImageGeneratorUtil::indexImageInDocument:: absoluteImageUrl:: " + absoluteImageUrl) + val updatedDocString = ScalaJsonUtil.serialize(indexDocument + ("imageUrl" -> absoluteImageUrl)) + logger.info("QRCodeImageGeneratorUtil:indexImageInDocument: updatedDocString:: " + updatedDocString) + esUtil.updateDocument(id, updatedDocString) + } + } else { + throw new InvalidInputException("ElasticSearch Document not found for " + id) + } + + } + + } diff --git a/qrcode-image-generator/src/test/resources/test.conf b/qrcode-image-generator/src/test/resources/test.conf index 07b16ef17..8816c0472 100644 --- a/qrcode-image-generator/src/test/resources/test.conf +++ b/qrcode-image-generator/src/test/resources/test.conf @@ -28,4 +28,8 @@ qr.image { imageFormat="png" bottomMargin=0 margin=1 -} \ No newline at end of file +} + +cloud_storage_endpoint="https://example.com" │ +cloud_storage_proxy_host="https://example.com" +indexImageUrl = false diff --git a/qrcode-image-generator/src/test/resources/test.cql b/qrcode-image-generator/src/test/resources/test.cql index 1c66b02ec..6bd783335 100644 --- a/qrcode-image-generator/src/test/resources/test.cql +++ b/qrcode-image-generator/src/test/resources/test.cql @@ -1,6 +1,6 @@ -CREATE KEYSPACE dialcodes WITH replication = {'class': 'NetworkTopologyStrategy', 'datacenter1': '2'} AND durable_writes = true; +CREATE KEYSPACE IF NOT EXISTS dialcodes WITH replication = {'class': 'NetworkTopologyStrategy', 'datacenter1': '2'} AND durable_writes = true; -CREATE TABLE dialcodes.dialcode_images ( +CREATE TABLE IF NOT EXISTS dialcodes.dialcode_images ( filename text PRIMARY KEY, channel text, config map, @@ -11,7 +11,7 @@ CREATE TABLE dialcodes.dialcode_images ( url text ); -CREATE TABLE dialcodes.dialcode_batch ( +CREATE TABLE IF NOT EXISTS dialcodes.dialcode_batch ( processid uuid PRIMARY KEY, channel text, config map, @@ -20,4 +20,6 @@ CREATE TABLE dialcodes.dialcode_batch ( publisher text, status int, url text -); \ No newline at end of file +); + +INSERT INTO dialcodes.dialcode_images(filename, channel, dialcode, url) VALUES ('0_Q1I5I3', 'b00bc992ef25f1a9a8d63291e20efc8d', 'Q1I5I3', 'https://sunbirddev.blob.core.windows.net/sunbird-content-dev/in.ekstep/0_Q1I5I3.png') ; diff --git a/qrcode-image-generator/src/test/scala/org/sunbird/job/spec/QRCodeImageGeneratorTaskTestSpec.scala b/qrcode-image-generator/src/test/scala/org/sunbird/job/spec/QRCodeImageGeneratorTaskTestSpec.scala index 96f05870f..19310cc29 100644 --- a/qrcode-image-generator/src/test/scala/org/sunbird/job/spec/QRCodeImageGeneratorTaskTestSpec.scala +++ b/qrcode-image-generator/src/test/scala/org/sunbird/job/spec/QRCodeImageGeneratorTaskTestSpec.scala @@ -1,7 +1,6 @@ package org.sunbird.job.spec import java.util - import com.typesafe.config.{Config, ConfigFactory} import org.apache.flink.api.common.typeinfo.TypeInformation import org.apache.flink.api.java.typeutils.TypeExtractor @@ -18,8 +17,7 @@ import org.sunbird.job.connector.FlinkKafkaConnector import org.sunbird.job.fixture.EventFixture import org.sunbird.job.qrimagegenerator.domain.Event import org.sunbird.job.qrimagegenerator.task.{QRCodeImageGeneratorConfig, QRCodeImageGeneratorTask} -import org.sunbird.job.util.CloudStorageUtil -import org.sunbird.job.util.{CassandraUtil, JSONUtil} +import org.sunbird.job.util.{CassandraUtil, CloudStorageUtil, ElasticSearchUtil, JSONUtil} import org.sunbird.spec.{BaseMetricsReporter, BaseTestSpec} class QRCodeImageGeneratorTaskTestSpec extends BaseTestSpec { @@ -36,13 +34,13 @@ class QRCodeImageGeneratorTaskTestSpec extends BaseTestSpec { val jobConfig: QRCodeImageGeneratorConfig = new QRCodeImageGeneratorConfig(config) val cloudStorageUtil:CloudStorageUtil = new CloudStorageUtil(jobConfig) var cassandraUtils: CassandraUtil = _ - + val mockElasticUtil: ElasticSearchUtil = mock[ElasticSearchUtil](Mockito.withSettings().serializable()) var currentMilliSecond = 1605816926271L override protected def beforeAll(): Unit = { BaseMetricsReporter.gaugeMetrics.clear() EmbeddedCassandraServerHelper.startEmbeddedCassandra(80000L) - cassandraUtils = new CassandraUtil(jobConfig.cassandraHost, jobConfig.cassandraPort) + cassandraUtils = new CassandraUtil(jobConfig.cassandraHost, jobConfig.cassandraPort, jobConfig) val session = cassandraUtils.session val dataLoader = new CQLDataLoader(session) dataLoader.load(new FileCQLDataSet(getClass.getResource("/test.cql").getPath, true, true)) @@ -55,7 +53,20 @@ class QRCodeImageGeneratorTaskTestSpec extends BaseTestSpec { super.afterAll() } - "QRCodeImageGeneratorTask" should "generate event" in { + + ignore should "generate event" in { + + val N3X6Y3Json = """{"identifier":"N3X6Y3", "filename":"2_N3X6Y3", "channel":"b00bc992ef25f1a9a8d63291e20efc8d"}""" + val U3J1J9Json = """{"identifier":"U3J1J9", "filename":"0_U3J1J9", "channel":"b00bc992ef25f1a9a8d63291e20efc8d"}""" + val V2B5A2Json = """{"identifier":"V2B5A2", "filename":"1_V2B5A2", "channel":"b00bc992ef25f1a9a8d63291e20efc8d"}""" + val F6J3E7Json = """{"identifier":"F6J3E7", "filename":"0_F6J3E7", "channel":"b00bc992ef25f1a9a8d63291e20efc8d"}""" + val Q1I5I3Json = """{"identifier":"Q1I5I3", "filename":"0_Q1I5I3", "channel":"b00bc992ef25f1a9a8d63291e20efc8d"}""" + when(mockElasticUtil.getDocumentAsString("Q1I5I3")).thenReturn(Q1I5I3Json) + when(mockElasticUtil.getDocumentAsString("N3X6Y3")).thenReturn(N3X6Y3Json) + when(mockElasticUtil.getDocumentAsString("U3J1J9")).thenReturn(U3J1J9Json) + when(mockElasticUtil.getDocumentAsString("V2B5A2")).thenReturn(V2B5A2Json) + when(mockElasticUtil.getDocumentAsString("F6J3E7")).thenReturn(F6J3E7Json) + when(mockKafkaUtil.kafkaJobRequestSource[Event](jobConfig.kafkaInputTopic)).thenReturn(new QRCodeImageGeneratorMapSource) new QRCodeImageGeneratorTask(jobConfig, mockKafkaUtil).process() // assertThrows[JobExecutionException] { diff --git a/qrcode-image-generator/src/test/scala/org/sunbird/job/util/QRCodeImageGeneratorUtilSpec.scala b/qrcode-image-generator/src/test/scala/org/sunbird/job/util/QRCodeImageGeneratorUtilSpec.scala new file mode 100644 index 000000000..8920cf4f0 --- /dev/null +++ b/qrcode-image-generator/src/test/scala/org/sunbird/job/util/QRCodeImageGeneratorUtilSpec.scala @@ -0,0 +1,55 @@ +package org.sunbird.job.util + +import com.typesafe.config.{Config, ConfigFactory} +import org.cassandraunit.CQLDataLoader +import org.cassandraunit.dataset.cql.FileCQLDataSet +import org.cassandraunit.utils.EmbeddedCassandraServerHelper +import org.mockito.Mockito +import org.mockito.Mockito.when +import org.scalatest.{BeforeAndAfterAll, FlatSpec, Matchers} +import org.scalatestplus.mockito.MockitoSugar +import org.sunbird.job.exception.InvalidInputException +import org.sunbird.job.qrimagegenerator.task.QRCodeImageGeneratorConfig +import org.sunbird.job.qrimagegenerator.util.QRCodeImageGeneratorUtil + +class QRCodeImageGeneratorUtilSpec extends FlatSpec with BeforeAndAfterAll with Matchers with MockitoSugar { + + val config: Config = ConfigFactory.load("test.conf").withFallback(ConfigFactory.systemEnvironment()) + val jobConfig: QRCodeImageGeneratorConfig = new QRCodeImageGeneratorConfig(config) + implicit val mockNeo4JUtil: Neo4JUtil = mock[Neo4JUtil](Mockito.withSettings().serializable()) + var cassandraUtil: CassandraUtil = _ + implicit val mockCloudUtil: CloudStorageUtil = mock[CloudStorageUtil](Mockito.withSettings().serializable()) + val mockElasticUtil: ElasticSearchUtil = mock[ElasticSearchUtil](Mockito.withSettings().serializable()) + + override protected def beforeAll(): Unit = { + EmbeddedCassandraServerHelper.startEmbeddedCassandra(80000L) + cassandraUtil = new CassandraUtil(jobConfig.cassandraHost, jobConfig.cassandraPort, jobConfig) + val session = cassandraUtil.session + val dataLoader = new CQLDataLoader(session) + dataLoader.load(new FileCQLDataSet(getClass.getResource("/test.cql").getPath, true, true)) + + super.beforeAll() + } + + override protected def afterAll(): Unit = { + cassandraUtil.close() + super.afterAll() + + } + + "QRCodeImageGeneratorFunction" should "return QR Code Document" in { + val qrCodeImageGeneratorUtil = new QRCodeImageGeneratorUtil(jobConfig, cassandraUtil, mockCloudUtil, mockElasticUtil) + assertThrows[InvalidInputException] { + qrCodeImageGeneratorUtil.indexImageInDocument("Q1I5I3")(mockElasticUtil, cassandraUtil) + } + val Q1I5I3Json = """{"identifier":"Q1I5I3", "filename":"0_Q1I5I3", "channel":"b00bc992ef25f1a9a8d63291e20efc8d"}""" + when(mockElasticUtil.getDocumentAsString("Q1I5I3")).thenReturn(Q1I5I3Json) + qrCodeImageGeneratorUtil.indexImageInDocument("Q1I5I3")(mockElasticUtil, cassandraUtil) + } + + +} + + + + diff --git a/relation-cache-updater/src/main/resources/relation-cache-updater.conf b/relation-cache-updater/src/main/resources/relation-cache-updater.conf deleted file mode 100644 index 8a3d68ce4..000000000 --- a/relation-cache-updater/src/main/resources/relation-cache-updater.conf +++ /dev/null @@ -1,26 +0,0 @@ -include "base-config.conf" - -kafka { - input.topic = "sunbirddev.content.postpublish.request" - groupId = "sunbirddev-relation-cache-updater-group" -} - -task { - consumer.parallelism = 1 - parallelism = 2 -} - -lms-cassandra { - keyspace = "dev_hierarchy_store" - table = "content_hierarchy" -} - -redis { - database.index = 10 -} - -dp-redis { - host = 11.2.4.22 - port = 6379 - database.index = 5 -} diff --git a/relation-cache-updater/src/main/scala/org/sunbird/job/relationcache/domain/Event.scala b/relation-cache-updater/src/main/scala/org/sunbird/job/relationcache/domain/Event.scala deleted file mode 100644 index 6502199c7..000000000 --- a/relation-cache-updater/src/main/scala/org/sunbird/job/relationcache/domain/Event.scala +++ /dev/null @@ -1,27 +0,0 @@ -package org.sunbird.job.relationcache.domain - -import org.apache.commons.lang3.StringUtils -import org.sunbird.job.domain.reader.JobRequest - -import java.util - -class Event(eventMap: java.util.Map[String, Any], partition: Int, offset: Long) extends JobRequest(eventMap, partition, offset) { - - val jobName = "RelationCacheUpdater" - - def identifier: String = readOrDefault[String]("edata.identifier", "") - - def action: String = readOrDefault[String]("edata.action", "") - - def mimeType: String = readOrDefault[String]("edata.mimeType", "") - - def eData: Map[String, AnyRef] = readOrDefault("edata", new util.HashMap[String, AnyRef]()).asInstanceOf[Map[String, AnyRef]] - - - def isValidEvent(allowedActions:List[String]): Boolean = { - - allowedActions.contains(action) && StringUtils.equalsIgnoreCase(mimeType, "application/vnd.ekstep.content-collection") && - StringUtils.isNotBlank(identifier) - } - -} \ No newline at end of file diff --git a/relation-cache-updater/src/main/scala/org/sunbird/job/relationcache/functions/RelationCacheUpdater.scala b/relation-cache-updater/src/main/scala/org/sunbird/job/relationcache/functions/RelationCacheUpdater.scala deleted file mode 100644 index 79a2cb05a..000000000 --- a/relation-cache-updater/src/main/scala/org/sunbird/job/relationcache/functions/RelationCacheUpdater.scala +++ /dev/null @@ -1,197 +0,0 @@ -package org.sunbird.job.relationcache.functions - -import java.lang.reflect.Type -import com.datastax.driver.core.querybuilder.QueryBuilder -import com.google.gson.reflect.TypeToken -import org.apache.commons.collections.{CollectionUtils, MapUtils} -import org.apache.commons.lang3.StringUtils -import org.apache.flink.api.common.typeinfo.TypeInformation -import org.apache.flink.configuration.Configuration -import org.apache.flink.shaded.jackson2.com.fasterxml.jackson.databind.ObjectMapper -import org.apache.flink.streaming.api.functions.ProcessFunction -import org.slf4j.LoggerFactory -import org.sunbird.job.cache.{DataCache, RedisConnect} -import org.sunbird.job.relationcache.domain.Event -import org.sunbird.job.util.CassandraUtil -import org.sunbird.job.{BaseProcessFunction, Metrics} -import org.sunbird.job.relationcache.task.RelationCacheUpdaterConfig - -import scala.collection.JavaConverters._ - - -class RelationCacheUpdater(config: RelationCacheUpdaterConfig) - (implicit val stringTypeInfo: TypeInformation[String], - @transient var cassandraUtil: CassandraUtil = null) - extends BaseProcessFunction[Event, String](config) { - - private[this] val logger = LoggerFactory.getLogger(classOf[RelationCacheUpdater]) - private var dataCache: DataCache = _ - private var collectionCache: DataCache = _ - lazy private val mapper: ObjectMapper = new ObjectMapper() - private val allowedActions = List("post-publish-process", "relation-cache-update") - - - override def open(parameters: Configuration): Unit = { - super.open(parameters) - cassandraUtil = new CassandraUtil(config.dbHost, config.dbPort) - - // Using LP cache for leafnodes, ancestors cache for the collection. - val lpCacheConnect = new RedisConnect(config) - dataCache = new DataCache(config, lpCacheConnect, config.relationCacheStore, List()) - dataCache.init() - - // Using DP cache to save the collection metadata cache to existing DP redis cache. - // This job pushes only visibility: Parent data to redis. - val dpCacheConnect = new RedisConnect(config, Option(config.dpRedisHost), Option(config.dpRedisPort)) - collectionCache = new DataCache(config, dpCacheConnect, config.collectionCacheStore, List()) - collectionCache.init() - } - - override def close(): Unit = { - cassandraUtil.close() - dataCache.close() - collectionCache.close() - super.close() - } - - override def processElement(event: Event, context: ProcessFunction[Event, String]#Context, metrics: Metrics): Unit = { - if (event.isValidEvent(allowedActions)) { - val rootId = event.identifier - logger.info("Processing - identifier: " + rootId) - val hierarchy = getHierarchy(rootId)(metrics) - if (MapUtils.isNotEmpty(hierarchy)) { - val leafNodesMap = getLeafNodes(rootId, hierarchy) - logger.info("Leaf-nodes cache updating for: " + leafNodesMap.size) - storeDataInCache(rootId, "leafnodes", leafNodesMap, dataCache)(metrics) - val ancestorsMap = getAncestors(rootId, hierarchy) - logger.info("Ancestors cache updating for: "+ ancestorsMap.size) - storeDataInCache(rootId, "ancestors", ancestorsMap, dataCache)(metrics) - val unitsMap = getUnitMaps(hierarchy) - logger.info("Units cache updating for: "+ unitsMap.size) - storeDataInCache("", "", unitsMap, collectionCache)(metrics) - metrics.incCounter(config.successEventCount) - } else { - logger.warn("Hierarchy Empty: " + rootId) - metrics.incCounter(config.skippedEventCount) - } - } else { - metrics.incCounter(config.skippedEventCount) - } - metrics.incCounter(config.totalEventsCount) - } - - override def metricsList(): List[String] = { - List(config.successEventCount, config.failedEventCount, config.skippedEventCount, config.totalEventsCount, config.dbReadCount, config.cacheWrite) - } - - private def getHierarchy(identifier: String)(implicit metrics: Metrics): java.util.Map[String, AnyRef] = { - val hierarchy = readHierarchyFromDb(identifier) - metrics.incCounter(config.dbReadCount) - if (StringUtils.isNotBlank(hierarchy)) - mapper.readValue(hierarchy, classOf[java.util.Map[String, AnyRef]]) - else new java.util.HashMap[String, AnyRef]() - } - - private def getLeafNodes(identifier: String, hierarchy: java.util.Map[String, AnyRef]): Map[String, List[String]] = { - val mimeType = hierarchy.getOrDefault("mimeType", "").asInstanceOf[String] - val leafNodesMap = if (StringUtils.equalsIgnoreCase(mimeType, "application/vnd.ekstep.content-collection")) { - val leafNodes = getOrComposeLeafNodes(hierarchy, false) - val map: Map[String, List[String]] = if (leafNodes.nonEmpty) Map() + (identifier -> leafNodes) else Map() - val children = getChildren(hierarchy) - val childLeafNodesMap = if (CollectionUtils.isNotEmpty(children)) { - children.asScala.map(child => { - val childId = child.get("identifier").asInstanceOf[String] - getLeafNodes(childId, child) - }).flatten.toMap - } else Map() - map ++ childLeafNodesMap - } else Map() - leafNodesMap.filter(m => m._2.nonEmpty).toMap - } - - private def getOrComposeLeafNodes(hierarchy: java.util.Map[String, AnyRef], compose: Boolean = true): List[String] = { - if (hierarchy.containsKey("leafNodes") && !compose) - hierarchy.getOrDefault("leafNodes", java.util.Arrays.asList()).asInstanceOf[java.util.List[String]].asScala.toList - else { - val children = getChildren(hierarchy) - val childCollections = children.asScala.filter(c => isCollection(c)) - val leafList = childCollections.map(coll => getOrComposeLeafNodes(coll, true)).flatten.toList - val ids = children.asScala.filterNot(c => isCollection(c)).map(c => c.getOrDefault("identifier", "").asInstanceOf[String]).filter(id => StringUtils.isNotBlank(id)) - leafList ++ ids - } - } - - private def isCollection(content: java.util.Map[String, AnyRef]): Boolean = { - StringUtils.equalsIgnoreCase(content.getOrDefault("mimeType", "").asInstanceOf[String], "application/vnd.ekstep.content-collection") - } - - private def getAncestors(identifier: String, hierarchy: java.util.Map[String, AnyRef], parents: List[String] = List()): Map[String, List[String]] = { - val mimeType = hierarchy.getOrDefault("mimeType", "").asInstanceOf[String] - val isCollection = (StringUtils.equalsIgnoreCase(mimeType, "application/vnd.ekstep.content-collection")) - val ancestors = if (isCollection) identifier :: parents else parents - val ancestorsMap = if (isCollection) { - getChildren(hierarchy).asScala.map(child => { - val childId = child.get("identifier").asInstanceOf[String] - getAncestors(childId, child, ancestors) - }).filter(m => m.nonEmpty).reduceOption((a,b) => { - // Here we are merging the Resource ancestors where it is used multiple times - Functional. - // convert maps to seq, to keep duplicate keys and concat then group by key - Code explanation. - val grouped = (a.toSeq ++ b.toSeq).groupBy(_._1) - grouped.mapValues(_.map(_._2).toList.flatten.distinct) - }).getOrElse(Map()) - } else { - Map(identifier -> parents) - } - ancestorsMap.filter(m => m._2.nonEmpty) - } - - private def getChildren(hierarchy: java.util.Map[String, AnyRef]) = { - val children = hierarchy.getOrDefault("children", java.util.Arrays.asList()).asInstanceOf[java.util.List[java.util.Map[String, AnyRef]]] - if (CollectionUtils.isEmpty(children)) List().asJava else children - } - - private def storeDataInCache(rootId: String, suffix: String, dataMap: Map[String, AnyRef], cache: DataCache)(implicit metrics: Metrics) = { - val finalSuffix = if (StringUtils.isNotBlank(suffix)) ":" + suffix else "" - val finalPrefix = if (StringUtils.isNoneBlank(rootId)) rootId + ":" else "" - try { - dataMap.foreach(each => each._2 match { - case value: List[String] => - cache.createListWithRetry(finalPrefix + each._1 + finalSuffix, each._2.asInstanceOf[List[String]]) - metrics.incCounter(config.cacheWrite) - case _ => - cache.setWithRetry(finalPrefix + each._1 + finalSuffix, each._2.asInstanceOf[String]) - metrics.incCounter(config.cacheWrite) - }) - } catch { - case e: Throwable => { - metrics.incCounter(config.failedEventCount) - logger.info("Failed to write data for " + suffix + ": " + rootId + " with map: " + dataMap) - throw e - } - } - } - - def readHierarchyFromDb(identifier: String): String = { - val columnName = "hierarchy" - val selectQuery = QueryBuilder.select().column(columnName).from(config.dbKeyspace, config.dbTable) - selectQuery.where.and(QueryBuilder.eq(config.hierarchyPrimaryKey.head, identifier)) - val rows = cassandraUtil.find(selectQuery.toString) - if (CollectionUtils.isNotEmpty(rows)) - rows.asScala.head.getObject("hierarchy").asInstanceOf[String] - else - "" - } - - private def getUnitMaps(hierarchy: java.util.Map[String, AnyRef]): Map[String, String] = { - val mimeType = hierarchy.getOrDefault("mimeType", "").asInstanceOf[String] - if (StringUtils.equalsIgnoreCase(mimeType, "application/vnd.ekstep.content-collection")) { - val children = getChildren(hierarchy).asScala - // TODO - Here the collection with "visibility:Parent" constructed with children in it. May be we should remove children. - if (children.nonEmpty) - (if (StringUtils.equalsIgnoreCase(hierarchy.getOrDefault("visibility", "").asInstanceOf[String], "Parent")) - Map(hierarchy.get("identifier").asInstanceOf[String] -> mapper.writeValueAsString(hierarchy)) - else Map()) ++ children.flatMap(child => getUnitMaps(child)).toMap - else Map() - } else Map() - } -} diff --git a/relation-cache-updater/src/main/scala/org/sunbird/job/relationcache/task/RelationCacheUpdaterConfig.scala b/relation-cache-updater/src/main/scala/org/sunbird/job/relationcache/task/RelationCacheUpdaterConfig.scala deleted file mode 100644 index e84ee109a..000000000 --- a/relation-cache-updater/src/main/scala/org/sunbird/job/relationcache/task/RelationCacheUpdaterConfig.scala +++ /dev/null @@ -1,45 +0,0 @@ -package org.sunbird.job.relationcache.task - -import java.util - -import com.typesafe.config.Config -import org.apache.flink.api.common.typeinfo.TypeInformation -import org.apache.flink.api.java.typeutils.TypeExtractor -import org.sunbird.job.BaseJobConfig - -class RelationCacheUpdaterConfig(override val config: Config) extends BaseJobConfig(config, "relation-cache-updater") { - - private val serialVersionUID = 2905979434303791379L - - implicit val mapTypeInfo: TypeInformation[util.Map[String, AnyRef]] = TypeExtractor.getForClass(classOf[util.Map[String, AnyRef]]) - implicit val stringTypeInfo: TypeInformation[String] = TypeExtractor.getForClass(classOf[String]) - - // Kafka Topics Configuration - val kafkaInputTopic: String = config.getString("kafka.input.topic") - override val kafkaConsumerParallelism: Int = config.getInt("task.consumer.parallelism") - - // Metric List - val totalEventsCount = "total-events-count" - val successEventCount = "success-events-count" - val failedEventCount = "failed-events-count" - val skippedEventCount = "skipped-event-count" - val cacheWrite = "cache-write-count" - val dbReadCount = "db-read-count" - - // Consumers - val relationCacheConsumer = "relation-cache-updater-consumer" - - // Cassandra Configurations - val dbTable: String = config.getString("lms-cassandra.table") - val dbKeyspace: String = config.getString("lms-cassandra.keyspace") - val dbHost: String = config.getString("lms-cassandra.host") - val dbPort: Int = config.getInt("lms-cassandra.port") - val hierarchyPrimaryKey: List[String] = List("identifier") - - // Redis Configurations - val relationCacheStore: Int = config.getInt("redis.database.index") - val dpRedisHost: String = config.getString("dp-redis.host") - val dpRedisPort: Int = config.getInt("dp-redis.port") - val collectionCacheStore: Int = config.getInt("dp-redis.database.index") - -} diff --git a/relation-cache-updater/src/test/resources/logback-test.xml b/relation-cache-updater/src/test/resources/logback-test.xml deleted file mode 100644 index e81294323..000000000 --- a/relation-cache-updater/src/test/resources/logback-test.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n - - - - - - - - - - - - \ No newline at end of file diff --git a/relation-cache-updater/src/test/resources/test.conf b/relation-cache-updater/src/test/resources/test.conf deleted file mode 100644 index 9c94e4cb0..000000000 --- a/relation-cache-updater/src/test/resources/test.conf +++ /dev/null @@ -1,27 +0,0 @@ -include "base-test.conf" - -kafka { - input.topic = "flink.relation.cache.input" - groupId = "flink-relation-cache-updater-group" -} - -task { - consumer.parallelism = 1 -} - -lms-cassandra { - keyspace = "hierarchy_store" - table = "content_hierarchy" - host = "localhost" - port = "9142" -} - -redis { - database.index = 10 -} - -dp-redis { - host = localhost - port = 6340 - database.index = 5 -} diff --git a/relation-cache-updater/src/test/resources/test.cql b/relation-cache-updater/src/test/resources/test.cql deleted file mode 100644 index b54f8bb48..000000000 --- a/relation-cache-updater/src/test/resources/test.cql +++ /dev/null @@ -1,13 +0,0 @@ -CREATE KEYSPACE IF NOT EXISTS hierarchy_store WITH replication = { - 'class': 'SimpleStrategy', - 'replication_factor': '1' -}; - -CREATE TABLE IF NOT EXISTS hierarchy_store.content_hierarchy ( - identifier text, - hierarchy text, - PRIMARY KEY (identifier) -); - -INSERT INTO hierarchy_store.content_hierarchy(identifier, hierarchy) VALUES ('do_11305855864948326411234', '{"ownershipType":["createdBy"],"copyright":"Sunbird","downloadUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11305855864948326411234/course-hierarchy-test-1_1594062643205_do_11305855864948326411234_1.0_spine.ecar","channel":"b00bc992ef25f1a9a8d63291e20efc8d","organisation":["Sunbird"],"language":["English"],"variants":{"online":{"ecarUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11305855864948326411234/course-hierarchy-test-1_1594062643394_do_11305855864948326411234_1.0_online.ecar","size":7399.0},"spine":{"ecarUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11305855864948326411234/course-hierarchy-test-1_1594062643205_do_11305855864948326411234_1.0_spine.ecar","size":40995.0}},"mimeType":"application/vnd.ekstep.content-collection","leafNodes":["do_1130314841730334721104","do_1130314849898332161107","do_1130314857102131201110","do_1130314847650037761106","do_1130314851303178241108","do_1130314845426565121105"],"objectType":"Content","appIcon":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11305855864948326411234/artifact/2a4b8abd789184932399d222d03d9b5c.thumb.jpg","collections":[],"children":[{"ownershipType":["createdBy"],"parent":"do_11305855864948326411234","copyright":"Sunbird","code":"do_11305855931318272011245","channel":"b00bc992ef25f1a9a8d63291e20efc8d","language":["English"],"mimeType":"application/vnd.ekstep.content-collection","idealScreenSize":"normal","createdOn":"2020-07-06T19:06:56.160+0000","objectType":"Content","children":[{"ownershipType":["createdBy"],"parent":"do_11305855931318272011245","copyright":"Sunbird","code":"b99980a2-63dd-4d55-9c35-5c5a70e0c505","channel":"b00bc992ef25f1a9a8d63291e20efc8d","language":["English"],"mimeType":"application/vnd.ekstep.content-collection","idealScreenSize":"normal","createdOn":"2020-07-06T19:06:56.153+0000","objectType":"Content","children":[{"ownershipType":["createdBy"],"parent":"do_11305855931312537611237","copyright":"Sunbird","code":"3e43357b-05ad-455e-bbb3-2911be50cc71","channel":"b00bc992ef25f1a9a8d63291e20efc8d","language":["English"],"mimeType":"application/vnd.ekstep.content-collection","idealScreenSize":"normal","createdOn":"2020-07-06T19:06:56.159+0000","objectType":"Content","children":[{"ownershipType":["createdBy"],"parent":"do_11305855931317452811243","copyright":"Sunbird","code":"82426726-038e-47d1-9403-813a083278d2","channel":"b00bc992ef25f1a9a8d63291e20efc8d","language":["English"],"mimeType":"application/vnd.ekstep.content-collection","idealScreenSize":"normal","createdOn":"2020-07-06T19:06:56.156+0000","objectType":"Content","children":[{"ownershipType":["createdBy"],"parent":"do_11305855931314995211239","copyright":"Sunbird","code":"9e2e3a6e-8a76-433d-85c7-d4ac3d732157","channel":"b00bc992ef25f1a9a8d63291e20efc8d","language":["English"],"mimeType":"application/vnd.ekstep.content-collection","idealScreenSize":"normal","createdOn":"2020-07-06T19:06:56.157+0000","objectType":"Content","children":[{"ownershipType":["createdBy"],"previewUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_1130314841730334721104/artifact/sftbr-04u.pdf","downloadUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_1130314841730334721104/prad-pdf-content-1_1590758122228_do_1130314841730334721104_2.0.ecar","channel":"in.ekstep","questions":[],"language":["English"],"variants":{"spine":{"ecarUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_1130314841730334721104/prad-pdf-content-1_1590758122675_do_1130314841730334721104_2.0_spine.ecar","size":943.0}},"mimeType":"application/pdf","usesContent":[],"artifactUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_1130314841730334721104/artifact/sftbr-04u.pdf","contentEncoding":"identity","contentType":"Resource","identifier":"do_1130314841730334721104","audience":["Learner"],"visibility":"Default","mediaType":"content","itemSets":[],"osId":"org.ekstep.quiz.app","lastPublishedBy":"System","version":2,"pragma":["external"],"prevState":"Live","license":"CC BY 4.0","lastPublishedOn":"2020-05-29T13:15:18.492+0000","size":736557.0,"concepts":[],"name":"prad PDF Content-1","status":"Live","code":"test-Resourcce","prevStatus":"Processing","methods":[],"streamingUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_1130314841730334721104/artifact/sftbr-04u.pdf","idealScreenSize":"normal","createdOn":"2020-05-29T13:02:25.342+0000","contentDisposition":"inline","lastUpdatedOn":"2020-05-29T13:15:17.957+0000","SYS_INTERNAL_LAST_UPDATED_ON":"2020-05-29T13:15:23.067+0000","dialcodeRequired":"No","lastStatusChangedOn":"2020-05-29T13:15:23.061+0000","os":["All"],"cloudStorageKey":"content/do_1130314841730334721104/artifact/sftbr-04u.pdf","libraries":[],"pkgVersion":2.0,"versionKey":"1590758117957","idealScreenDensity":"hdpi","s3Key":"ecar_files/do_1130314841730334721104/prad-pdf-content-1_1590758122228_do_1130314841730334721104_2.0.ecar","framework":"NCF","compatibilityLevel":4,"index":1,"depth":6,"parent":"do_11305855931315814411241"},{"ownershipType":["createdBy"],"previewUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_1130314845426565121105/artifact/sftbr-04u.pdf","downloadUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_1130314845426565121105/prad-pdf-content-2_1590758131556_do_1130314845426565121105_1.0.ecar","channel":"in.ekstep","questions":[],"language":["English"],"variants":{"spine":{"ecarUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_1130314845426565121105/prad-pdf-content-2_1590758132007_do_1130314845426565121105_1.0_spine.ecar","size":859.0}},"mimeType":"application/pdf","usesContent":[],"artifactUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_1130314845426565121105/artifact/sftbr-04u.pdf","contentEncoding":"identity","contentType":"Resource","identifier":"do_1130314845426565121105","audience":["Learner"],"visibility":"Default","mediaType":"content","itemSets":[],"osId":"org.ekstep.quiz.app","lastPublishedBy":"System","version":2,"pragma":["external"],"prevState":"Draft","license":"CC BY 4.0","lastPublishedOn":"2020-05-29T13:15:31.543+0000","size":736471.0,"concepts":[],"name":"prad PDF Content-2","status":"Live","code":"test-Resourcce","prevStatus":"Processing","methods":[],"streamingUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_1130314845426565121105/artifact/sftbr-04u.pdf","idealScreenSize":"normal","createdOn":"2020-05-29T13:03:10.463+0000","contentDisposition":"inline","lastUpdatedOn":"2020-05-29T13:15:31.107+0000","SYS_INTERNAL_LAST_UPDATED_ON":"2020-05-29T13:15:32.306+0000","dialcodeRequired":"No","lastStatusChangedOn":"2020-05-29T13:15:32.302+0000","os":["All"],"cloudStorageKey":"content/do_1130314845426565121105/artifact/sftbr-04u.pdf","libraries":[],"pkgVersion":1.0,"versionKey":"1590758131107","idealScreenDensity":"hdpi","s3Key":"ecar_files/do_1130314845426565121105/prad-pdf-content-2_1590758131556_do_1130314845426565121105_1.0.ecar","framework":"NCF","compatibilityLevel":4,"index":2,"depth":6,"parent":"do_11305855931315814411241"}],"contentDisposition":"inline","lastUpdatedOn":"2020-07-06T19:10:42.581+0000","contentEncoding":"gzip","contentType":"CourseUnit","dialcodeRequired":"No","identifier":"do_11305855931315814411241","lastStatusChangedOn":"2020-07-06T19:06:56.157+0000","audience":["Learner"],"os":["All"],"visibility":"Parent","index":1,"mediaType":"content","osId":"org.ekstep.launcher","languageCode":["en"],"versionKey":"1594062416157","license":"CC BY 4.0","idealScreenDensity":"hdpi","framework":"NCFCOPY","depth":5,"compatibilityLevel":1,"name":"Unit - 1.1.1.1.1","status":"Live","lastPublishedOn":"2020-07-06T19:10:42.996+0000","pkgVersion":1.0,"leafNodesCount":2,"leafNodes":["do_1130314841730334721104","do_1130314845426565121105"],"downloadUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11305855864948326411234/course-hierarchy-test-1_1594062643205_do_11305855864948326411234_1.0_spine.ecar","variants":"{\"online\":{\"ecarUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11305855864948326411234/course-hierarchy-test-1_1594062643394_do_11305855864948326411234_1.0_online.ecar\",\"size\":7399.0},\"spine\":{\"ecarUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11305855864948326411234/course-hierarchy-test-1_1594062643205_do_11305855864948326411234_1.0_spine.ecar\",\"size\":40995.0}}"}],"contentDisposition":"inline","lastUpdatedOn":"2020-07-06T19:10:42.581+0000","contentEncoding":"gzip","contentType":"CourseUnit","dialcodeRequired":"No","identifier":"do_11305855931314995211239","lastStatusChangedOn":"2020-07-06T19:06:56.156+0000","audience":["Learner"],"os":["All"],"visibility":"Parent","index":1,"mediaType":"content","osId":"org.ekstep.launcher","languageCode":["en"],"versionKey":"1594062416156","license":"CC BY 4.0","idealScreenDensity":"hdpi","framework":"NCFCOPY","depth":4,"compatibilityLevel":1,"name":"Unit - 1.1.1.1","status":"Live","lastPublishedOn":"2020-07-06T19:10:42.996+0000","pkgVersion":1.0,"leafNodesCount":2,"leafNodes":["do_1130314841730334721104","do_1130314845426565121105"],"downloadUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11305855864948326411234/course-hierarchy-test-1_1594062643205_do_11305855864948326411234_1.0_spine.ecar","variants":"{\"online\":{\"ecarUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11305855864948326411234/course-hierarchy-test-1_1594062643394_do_11305855864948326411234_1.0_online.ecar\",\"size\":7399.0},\"spine\":{\"ecarUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11305855864948326411234/course-hierarchy-test-1_1594062643205_do_11305855864948326411234_1.0_spine.ecar\",\"size\":40995.0}}"}],"contentDisposition":"inline","lastUpdatedOn":"2020-07-06T19:10:42.581+0000","contentEncoding":"gzip","contentType":"CourseUnit","dialcodeRequired":"No","identifier":"do_11305855931317452811243","lastStatusChangedOn":"2020-07-06T19:06:56.159+0000","audience":["Learner"],"os":["All"],"visibility":"Parent","index":1,"mediaType":"content","osId":"org.ekstep.launcher","languageCode":["en"],"versionKey":"1594062416159","license":"CC BY 4.0","idealScreenDensity":"hdpi","framework":"NCFCOPY","depth":3,"compatibilityLevel":1,"name":"Unit - 1.1.1","status":"Live","lastPublishedOn":"2020-07-06T19:10:42.996+0000","pkgVersion":1.0,"leafNodesCount":2,"leafNodes":["do_1130314841730334721104","do_1130314845426565121105"],"downloadUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11305855864948326411234/course-hierarchy-test-1_1594062643205_do_11305855864948326411234_1.0_spine.ecar","variants":"{\"online\":{\"ecarUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11305855864948326411234/course-hierarchy-test-1_1594062643394_do_11305855864948326411234_1.0_online.ecar\",\"size\":7399.0},\"spine\":{\"ecarUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11305855864948326411234/course-hierarchy-test-1_1594062643205_do_11305855864948326411234_1.0_spine.ecar\",\"size\":40995.0}}"}],"contentDisposition":"inline","lastUpdatedOn":"2020-07-06T19:10:42.581+0000","contentEncoding":"gzip","contentType":"CourseUnit","dialcodeRequired":"No","identifier":"do_11305855931312537611237","lastStatusChangedOn":"2020-07-06T19:06:56.153+0000","audience":["Learner"],"os":["All"],"visibility":"Parent","index":1,"mediaType":"content","osId":"org.ekstep.launcher","languageCode":["en"],"versionKey":"1594062416153","license":"CC BY 4.0","idealScreenDensity":"hdpi","framework":"NCFCOPY","depth":2,"compatibilityLevel":1,"name":"Unit - 1.1","status":"Live","lastPublishedOn":"2020-07-06T19:10:42.996+0000","pkgVersion":1.0,"leafNodesCount":2,"leafNodes":["do_1130314841730334721104","do_1130314845426565121105"],"downloadUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11305855864948326411234/course-hierarchy-test-1_1594062643205_do_11305855864948326411234_1.0_spine.ecar","variants":"{\"online\":{\"ecarUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11305855864948326411234/course-hierarchy-test-1_1594062643394_do_11305855864948326411234_1.0_online.ecar\",\"size\":7399.0},\"spine\":{\"ecarUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11305855864948326411234/course-hierarchy-test-1_1594062643205_do_11305855864948326411234_1.0_spine.ecar\",\"size\":40995.0}}"}],"contentDisposition":"inline","lastUpdatedOn":"2020-07-06T19:10:42.581+0000","contentEncoding":"gzip","contentType":"CourseUnit","dialcodeRequired":"No","identifier":"do_11305855931318272011245","lastStatusChangedOn":"2020-07-06T19:06:56.160+0000","audience":["Learner"],"os":["All"],"visibility":"Parent","index":1,"mediaType":"content","osId":"org.ekstep.launcher","languageCode":["en"],"versionKey":"1594062416160","license":"CC BY 4.0","idealScreenDensity":"hdpi","framework":"NCFCOPY","depth":1,"compatibilityLevel":1,"name":"Unit - 1","status":"Live","lastPublishedOn":"2020-07-06T19:10:42.996+0000","pkgVersion":1.0,"leafNodesCount":2,"leafNodes":["do_1130314841730334721104","do_1130314845426565121105"],"downloadUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11305855864948326411234/course-hierarchy-test-1_1594062643205_do_11305855864948326411234_1.0_spine.ecar","variants":"{\"online\":{\"ecarUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11305855864948326411234/course-hierarchy-test-1_1594062643394_do_11305855864948326411234_1.0_online.ecar\",\"size\":7399.0},\"spine\":{\"ecarUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11305855864948326411234/course-hierarchy-test-1_1594062643205_do_11305855864948326411234_1.0_spine.ecar\",\"size\":40995.0}}"},{"ownershipType":["createdBy"],"parent":"do_11305855864948326411234","copyright":"Sunbird","code":"do_11305856007005798411326","channel":"b00bc992ef25f1a9a8d63291e20efc8d","language":["English"],"mimeType":"application/vnd.ekstep.content-collection","idealScreenSize":"normal","createdOn":"2020-07-06T19:08:28.552+0000","objectType":"Content","children":[{"ownershipType":["createdBy"],"parent":"do_11305856007005798411326","copyright":"Sunbird","code":"6dd56b10-389a-49d1-a413-d11494856293","channel":"b00bc992ef25f1a9a8d63291e20efc8d","language":["English"],"mimeType":"application/vnd.ekstep.content-collection","idealScreenSize":"normal","createdOn":"2020-07-06T19:08:28.555+0000","objectType":"Content","children":[{"ownershipType":["createdBy"],"parent":"do_11305856007008256011330","copyright":"Sunbird","code":"55c16590-ec0e-4c24-83c8-aed18e0b2d8d","channel":"b00bc992ef25f1a9a8d63291e20efc8d","language":["English"],"mimeType":"application/vnd.ekstep.content-collection","idealScreenSize":"normal","createdOn":"2020-07-06T19:08:28.554+0000","objectType":"Content","children":[{"ownershipType":["createdBy"],"parent":"do_11305856007007436811328","copyright":"Sunbird","code":"70a1ac3b-e26c-4d92-9058-674286e597c5","channel":"b00bc992ef25f1a9a8d63291e20efc8d","language":["English"],"mimeType":"application/vnd.ekstep.content-collection","idealScreenSize":"normal","createdOn":"2020-07-06T19:08:28.550+0000","objectType":"Content","children":[{"ownershipType":["createdBy"],"parent":"do_11305856007004160011324","copyright":"Sunbird","code":"ae34bd17-cac9-4198-b84e-244a1312bbd9","channel":"b00bc992ef25f1a9a8d63291e20efc8d","language":["English"],"mimeType":"application/vnd.ekstep.content-collection","idealScreenSize":"normal","createdOn":"2020-07-06T19:08:28.556+0000","objectType":"Content","children":[{"ownershipType":["createdBy"],"previewUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_1130314847650037761106/artifact/sftbr-04u.pdf","downloadUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_1130314847650037761106/prad-pdf-content-3_1590758142406_do_1130314847650037761106_1.0.ecar","channel":"in.ekstep","questions":[],"language":["English"],"variants":{"spine":{"ecarUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_1130314847650037761106/prad-pdf-content-3_1590758142803_do_1130314847650037761106_1.0_spine.ecar","size":859.0}},"mimeType":"application/pdf","usesContent":[],"artifactUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_1130314847650037761106/artifact/sftbr-04u.pdf","contentEncoding":"identity","contentType":"Resource","identifier":"do_1130314847650037761106","audience":["Learner"],"visibility":"Default","mediaType":"content","itemSets":[],"osId":"org.ekstep.quiz.app","lastPublishedBy":"System","version":2,"pragma":["external"],"prevState":"Draft","license":"CC BY 4.0","lastPublishedOn":"2020-05-29T13:15:41.207+0000","size":736473.0,"concepts":[],"name":"prad PDF Content-3","status":"Live","code":"test-Resourcce","prevStatus":"Processing","methods":[],"streamingUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_1130314847650037761106/artifact/sftbr-04u.pdf","idealScreenSize":"normal","createdOn":"2020-05-29T13:03:37.605+0000","contentDisposition":"inline","lastUpdatedOn":"2020-05-29T13:15:40.816+0000","SYS_INTERNAL_LAST_UPDATED_ON":"2020-05-29T13:15:43.114+0000","dialcodeRequired":"No","lastStatusChangedOn":"2020-05-29T13:15:43.110+0000","os":["All"],"cloudStorageKey":"content/do_1130314847650037761106/artifact/sftbr-04u.pdf","libraries":[],"pkgVersion":1.0,"versionKey":"1590758140816","idealScreenDensity":"hdpi","s3Key":"ecar_files/do_1130314847650037761106/prad-pdf-content-3_1590758142406_do_1130314847650037761106_1.0.ecar","framework":"NCF","compatibilityLevel":4,"index":1,"depth":6,"parent":"do_11305856007009075211332"},{"ownershipType":["createdBy"],"previewUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_1130314849898332161107/artifact/sftbr-04u.pdf","downloadUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_1130314849898332161107/prad-pdf-content-4_1590758137227_do_1130314849898332161107_1.0.ecar","channel":"in.ekstep","questions":[],"language":["English"],"variants":{"spine":{"ecarUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_1130314849898332161107/prad-pdf-content-4_1590758137643_do_1130314849898332161107_1.0_spine.ecar","size":860.0}},"mimeType":"application/pdf","usesContent":[],"artifactUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_1130314849898332161107/artifact/sftbr-04u.pdf","contentEncoding":"identity","contentType":"Resource","identifier":"do_1130314849898332161107","audience":["Learner"],"visibility":"Default","mediaType":"content","itemSets":[],"osId":"org.ekstep.quiz.app","lastPublishedBy":"System","version":2,"pragma":["external"],"prevState":"Draft","license":"CC BY 4.0","lastPublishedOn":"2020-05-29T13:15:32.747+0000","size":736472.0,"concepts":[],"name":"prad PDF Content-4","status":"Live","code":"test-Resourcce","prevStatus":"Processing","methods":[],"streamingUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_1130314849898332161107/artifact/sftbr-04u.pdf","idealScreenSize":"normal","createdOn":"2020-05-29T13:04:05.049+0000","contentDisposition":"inline","lastUpdatedOn":"2020-05-29T13:15:32.376+0000","SYS_INTERNAL_LAST_UPDATED_ON":"2020-05-29T13:15:37.968+0000","dialcodeRequired":"No","lastStatusChangedOn":"2020-05-29T13:15:37.963+0000","os":["All"],"cloudStorageKey":"content/do_1130314849898332161107/artifact/sftbr-04u.pdf","libraries":[],"pkgVersion":1.0,"versionKey":"1590758132376","idealScreenDensity":"hdpi","s3Key":"ecar_files/do_1130314849898332161107/prad-pdf-content-4_1590758137227_do_1130314849898332161107_1.0.ecar","framework":"NCF","compatibilityLevel":4,"index":2,"depth":6,"parent":"do_11305856007009075211332"}],"contentDisposition":"inline","lastUpdatedOn":"2020-07-06T19:10:42.581+0000","contentEncoding":"gzip","contentType":"CourseUnit","dialcodeRequired":"No","identifier":"do_11305856007009075211332","lastStatusChangedOn":"2020-07-06T19:08:28.556+0000","audience":["Learner"],"os":["All"],"visibility":"Parent","index":1,"mediaType":"content","osId":"org.ekstep.launcher","languageCode":["en"],"versionKey":"1594062508556","license":"CC BY 4.0","idealScreenDensity":"hdpi","framework":"NCFCOPY","depth":5,"compatibilityLevel":1,"name":"Unit - 2.1.1.1.1","status":"Live","lastPublishedOn":"2020-07-06T19:10:42.996+0000","pkgVersion":1.0,"leafNodesCount":2,"leafNodes":["do_1130314849898332161107","do_1130314847650037761106"],"downloadUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11305855864948326411234/course-hierarchy-test-1_1594062643205_do_11305855864948326411234_1.0_spine.ecar","variants":"{\"online\":{\"ecarUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11305855864948326411234/course-hierarchy-test-1_1594062643394_do_11305855864948326411234_1.0_online.ecar\",\"size\":7399.0},\"spine\":{\"ecarUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11305855864948326411234/course-hierarchy-test-1_1594062643205_do_11305855864948326411234_1.0_spine.ecar\",\"size\":40995.0}}"}],"contentDisposition":"inline","lastUpdatedOn":"2020-07-06T19:10:42.581+0000","contentEncoding":"gzip","contentType":"CourseUnit","dialcodeRequired":"No","identifier":"do_11305856007004160011324","lastStatusChangedOn":"2020-07-06T19:08:28.550+0000","audience":["Learner"],"os":["All"],"visibility":"Parent","index":1,"mediaType":"content","osId":"org.ekstep.launcher","languageCode":["en"],"versionKey":"1594062508550","license":"CC BY 4.0","idealScreenDensity":"hdpi","framework":"NCFCOPY","depth":4,"compatibilityLevel":1,"name":"Unit - 2.1.1.1","status":"Live","lastPublishedOn":"2020-07-06T19:10:42.996+0000","pkgVersion":1.0,"leafNodesCount":2,"leafNodes":["do_1130314849898332161107","do_1130314847650037761106"],"downloadUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11305855864948326411234/course-hierarchy-test-1_1594062643205_do_11305855864948326411234_1.0_spine.ecar","variants":"{\"online\":{\"ecarUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11305855864948326411234/course-hierarchy-test-1_1594062643394_do_11305855864948326411234_1.0_online.ecar\",\"size\":7399.0},\"spine\":{\"ecarUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11305855864948326411234/course-hierarchy-test-1_1594062643205_do_11305855864948326411234_1.0_spine.ecar\",\"size\":40995.0}}"}],"contentDisposition":"inline","lastUpdatedOn":"2020-07-06T19:10:42.581+0000","contentEncoding":"gzip","contentType":"CourseUnit","dialcodeRequired":"No","identifier":"do_11305856007007436811328","lastStatusChangedOn":"2020-07-06T19:08:28.554+0000","audience":["Learner"],"os":["All"],"visibility":"Parent","index":1,"mediaType":"content","osId":"org.ekstep.launcher","languageCode":["en"],"versionKey":"1594062508554","license":"CC BY 4.0","idealScreenDensity":"hdpi","framework":"NCFCOPY","depth":3,"compatibilityLevel":1,"name":"Unit - 2.1.1","status":"Live","lastPublishedOn":"2020-07-06T19:10:42.996+0000","pkgVersion":1.0,"leafNodesCount":2,"leafNodes":["do_1130314849898332161107","do_1130314847650037761106"],"downloadUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11305855864948326411234/course-hierarchy-test-1_1594062643205_do_11305855864948326411234_1.0_spine.ecar","variants":"{\"online\":{\"ecarUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11305855864948326411234/course-hierarchy-test-1_1594062643394_do_11305855864948326411234_1.0_online.ecar\",\"size\":7399.0},\"spine\":{\"ecarUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11305855864948326411234/course-hierarchy-test-1_1594062643205_do_11305855864948326411234_1.0_spine.ecar\",\"size\":40995.0}}"}],"contentDisposition":"inline","lastUpdatedOn":"2020-07-06T19:10:42.581+0000","contentEncoding":"gzip","contentType":"CourseUnit","dialcodeRequired":"No","identifier":"do_11305856007008256011330","lastStatusChangedOn":"2020-07-06T19:08:28.555+0000","audience":["Learner"],"os":["All"],"visibility":"Parent","index":1,"mediaType":"content","osId":"org.ekstep.launcher","languageCode":["en"],"versionKey":"1594062508555","license":"CC BY 4.0","idealScreenDensity":"hdpi","framework":"NCFCOPY","depth":2,"compatibilityLevel":1,"name":"Unit - 2.1","status":"Live","lastPublishedOn":"2020-07-06T19:10:42.996+0000","pkgVersion":1.0,"leafNodesCount":2,"leafNodes":["do_1130314849898332161107","do_1130314847650037761106"],"downloadUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11305855864948326411234/course-hierarchy-test-1_1594062643205_do_11305855864948326411234_1.0_spine.ecar","variants":"{\"online\":{\"ecarUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11305855864948326411234/course-hierarchy-test-1_1594062643394_do_11305855864948326411234_1.0_online.ecar\",\"size\":7399.0},\"spine\":{\"ecarUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11305855864948326411234/course-hierarchy-test-1_1594062643205_do_11305855864948326411234_1.0_spine.ecar\",\"size\":40995.0}}"}],"contentDisposition":"inline","lastUpdatedOn":"2020-07-06T19:10:42.581+0000","contentEncoding":"gzip","contentType":"CourseUnit","dialcodeRequired":"No","identifier":"do_11305856007005798411326","lastStatusChangedOn":"2020-07-06T19:08:28.552+0000","audience":["Learner"],"os":["All"],"visibility":"Parent","index":2,"mediaType":"content","osId":"org.ekstep.launcher","languageCode":["en"],"versionKey":"1594062508552","license":"CC BY 4.0","idealScreenDensity":"hdpi","framework":"NCFCOPY","depth":1,"compatibilityLevel":1,"name":"Unit - 2","status":"Live","lastPublishedOn":"2020-07-06T19:10:42.996+0000","pkgVersion":1.0,"leafNodesCount":2,"leafNodes":["do_1130314849898332161107","do_1130314847650037761106"],"downloadUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11305855864948326411234/course-hierarchy-test-1_1594062643205_do_11305855864948326411234_1.0_spine.ecar","variants":"{\"online\":{\"ecarUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11305855864948326411234/course-hierarchy-test-1_1594062643394_do_11305855864948326411234_1.0_online.ecar\",\"size\":7399.0},\"spine\":{\"ecarUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11305855864948326411234/course-hierarchy-test-1_1594062643205_do_11305855864948326411234_1.0_spine.ecar\",\"size\":40995.0}}"},{"ownershipType":["createdBy"],"parent":"do_11305855864948326411234","copyright":"Sunbird","code":"0cc610ce-b54e-469c-87b5-8aadbe55ead2","channel":"b00bc992ef25f1a9a8d63291e20efc8d","language":["English"],"mimeType":"application/vnd.ekstep.content-collection","idealScreenSize":"normal","createdOn":"2020-07-06T19:09:35.086+0000","objectType":"Content","children":[{"ownershipType":["createdBy"],"parent":"do_11305856061510451211340","copyright":"Sunbird","code":"73c4c338-5713-47c9-9d13-18b36952da43","channel":"b00bc992ef25f1a9a8d63291e20efc8d","language":["English"],"mimeType":"application/vnd.ekstep.content-collection","idealScreenSize":"normal","createdOn":"2020-07-06T19:09:35.098+0000","objectType":"Content","children":[{"ownershipType":["createdBy"],"parent":"do_11305856061520281611348","copyright":"Sunbird","code":"ac62c605-e41a-43a0-82e8-ca6930a17442","channel":"b00bc992ef25f1a9a8d63291e20efc8d","language":["English"],"mimeType":"application/vnd.ekstep.content-collection","idealScreenSize":"normal","createdOn":"2020-07-06T19:09:35.091+0000","objectType":"Content","children":[{"ownershipType":["createdBy"],"parent":"do_11305856061514547211344","copyright":"Sunbird","code":"beb2a366-c3ab-49aa-90e3-bac853837607","channel":"b00bc992ef25f1a9a8d63291e20efc8d","language":["English"],"mimeType":"application/vnd.ekstep.content-collection","idealScreenSize":"normal","createdOn":"2020-07-06T19:09:35.093+0000","objectType":"Content","children":[{"ownershipType":["createdBy"],"parent":"do_11305856061516185611346","copyright":"Sunbird","code":"dabe5332-cb83-45b5-9818-3b3b76b03299","channel":"b00bc992ef25f1a9a8d63291e20efc8d","language":["English"],"mimeType":"application/vnd.ekstep.content-collection","idealScreenSize":"normal","createdOn":"2020-07-06T19:09:35.090+0000","objectType":"Content","children":[{"ownershipType":["createdBy"],"previewUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_1130314841730334721104/artifact/sftbr-04u.pdf","downloadUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_1130314841730334721104/prad-pdf-content-1_1590758122228_do_1130314841730334721104_2.0.ecar","channel":"in.ekstep","questions":[],"language":["English"],"variants":{"spine":{"ecarUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_1130314841730334721104/prad-pdf-content-1_1590758122675_do_1130314841730334721104_2.0_spine.ecar","size":943.0}},"mimeType":"application/pdf","usesContent":[],"artifactUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_1130314841730334721104/artifact/sftbr-04u.pdf","contentEncoding":"identity","contentType":"Resource","identifier":"do_1130314841730334721104","audience":["Learner"],"visibility":"Default","mediaType":"content","itemSets":[],"osId":"org.ekstep.quiz.app","lastPublishedBy":"System","version":2,"pragma":["external"],"prevState":"Live","license":"CC BY 4.0","lastPublishedOn":"2020-05-29T13:15:18.492+0000","size":736557.0,"concepts":[],"name":"prad PDF Content-1","status":"Live","code":"test-Resourcce","prevStatus":"Processing","methods":[],"streamingUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_1130314841730334721104/artifact/sftbr-04u.pdf","idealScreenSize":"normal","createdOn":"2020-05-29T13:02:25.342+0000","contentDisposition":"inline","lastUpdatedOn":"2020-05-29T13:15:17.957+0000","SYS_INTERNAL_LAST_UPDATED_ON":"2020-05-29T13:15:23.067+0000","dialcodeRequired":"No","lastStatusChangedOn":"2020-05-29T13:15:23.061+0000","os":["All"],"cloudStorageKey":"content/do_1130314841730334721104/artifact/sftbr-04u.pdf","libraries":[],"pkgVersion":2.0,"versionKey":"1590758117957","idealScreenDensity":"hdpi","s3Key":"ecar_files/do_1130314841730334721104/prad-pdf-content-1_1590758122228_do_1130314841730334721104_2.0.ecar","framework":"NCF","compatibilityLevel":4,"index":1,"depth":6,"parent":"do_11305856061513728011342"},{"ownershipType":["createdBy"],"previewUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_1130314847650037761106/artifact/sftbr-04u.pdf","downloadUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_1130314847650037761106/prad-pdf-content-3_1590758142406_do_1130314847650037761106_1.0.ecar","channel":"in.ekstep","questions":[],"language":["English"],"variants":{"spine":{"ecarUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_1130314847650037761106/prad-pdf-content-3_1590758142803_do_1130314847650037761106_1.0_spine.ecar","size":859.0}},"mimeType":"application/pdf","usesContent":[],"artifactUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_1130314847650037761106/artifact/sftbr-04u.pdf","contentEncoding":"identity","contentType":"Resource","identifier":"do_1130314847650037761106","audience":["Learner"],"visibility":"Default","mediaType":"content","itemSets":[],"osId":"org.ekstep.quiz.app","lastPublishedBy":"System","version":2,"pragma":["external"],"prevState":"Draft","license":"CC BY 4.0","lastPublishedOn":"2020-05-29T13:15:41.207+0000","size":736473.0,"concepts":[],"name":"prad PDF Content-3","status":"Live","code":"test-Resourcce","prevStatus":"Processing","methods":[],"streamingUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_1130314847650037761106/artifact/sftbr-04u.pdf","idealScreenSize":"normal","createdOn":"2020-05-29T13:03:37.605+0000","contentDisposition":"inline","lastUpdatedOn":"2020-05-29T13:15:40.816+0000","SYS_INTERNAL_LAST_UPDATED_ON":"2020-05-29T13:15:43.114+0000","dialcodeRequired":"No","lastStatusChangedOn":"2020-05-29T13:15:43.110+0000","os":["All"],"cloudStorageKey":"content/do_1130314847650037761106/artifact/sftbr-04u.pdf","libraries":[],"pkgVersion":1.0,"versionKey":"1590758140816","idealScreenDensity":"hdpi","s3Key":"ecar_files/do_1130314847650037761106/prad-pdf-content-3_1590758142406_do_1130314847650037761106_1.0.ecar","framework":"NCF","compatibilityLevel":4,"index":2,"depth":6,"parent":"do_11305856061513728011342"},{"ownershipType":["createdBy"],"previewUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_1130314851303178241108/artifact/sftbr-04u.pdf","downloadUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_1130314851303178241108/prad-pdf-content-5_1590758139833_do_1130314851303178241108_1.0.ecar","channel":"in.ekstep","questions":[],"language":["English"],"variants":{"spine":{"ecarUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_1130314851303178241108/prad-pdf-content-5_1590758140392_do_1130314851303178241108_1.0_spine.ecar","size":857.0}},"mimeType":"application/pdf","usesContent":[],"artifactUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_1130314851303178241108/artifact/sftbr-04u.pdf","contentEncoding":"identity","contentType":"Resource","identifier":"do_1130314851303178241108","audience":["Learner"],"visibility":"Default","mediaType":"content","itemSets":[],"osId":"org.ekstep.quiz.app","lastPublishedBy":"System","version":2,"pragma":["external"],"prevState":"Draft","license":"CC BY 4.0","lastPublishedOn":"2020-05-29T13:15:39.820+0000","size":736469.0,"concepts":[],"name":"prad PDF Content-5","status":"Live","code":"test-Resourcce","prevStatus":"Processing","methods":[],"streamingUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_1130314851303178241108/artifact/sftbr-04u.pdf","idealScreenSize":"normal","createdOn":"2020-05-29T13:04:22.199+0000","contentDisposition":"inline","lastUpdatedOn":"2020-05-29T13:15:39.308+0000","SYS_INTERNAL_LAST_UPDATED_ON":"2020-05-29T13:15:40.726+0000","dialcodeRequired":"No","lastStatusChangedOn":"2020-05-29T13:15:40.722+0000","os":["All"],"cloudStorageKey":"content/do_1130314851303178241108/artifact/sftbr-04u.pdf","libraries":[],"pkgVersion":1.0,"versionKey":"1590758139308","idealScreenDensity":"hdpi","s3Key":"ecar_files/do_1130314851303178241108/prad-pdf-content-5_1590758139833_do_1130314851303178241108_1.0.ecar","framework":"NCF","compatibilityLevel":4,"index":3,"depth":6,"parent":"do_11305856061513728011342"},{"ownershipType":["createdBy"],"previewUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_1130314857102131201110/artifact/sftbr-04u.pdf","downloadUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_1130314857102131201110/prad-pdf-content-6_1590758138471_do_1130314857102131201110_1.0.ecar","channel":"in.ekstep","questions":[],"language":["English"],"variants":{"spine":{"ecarUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_1130314857102131201110/prad-pdf-content-6_1590758138908_do_1130314857102131201110_1.0_spine.ecar","size":857.0}},"mimeType":"application/pdf","usesContent":[],"artifactUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_1130314857102131201110/artifact/sftbr-04u.pdf","contentEncoding":"identity","contentType":"Resource","identifier":"do_1130314857102131201110","audience":["Learner"],"visibility":"Default","mediaType":"content","itemSets":[],"osId":"org.ekstep.quiz.app","lastPublishedBy":"System","version":2,"pragma":["external"],"prevState":"Draft","license":"CC BY 4.0","lastPublishedOn":"2020-05-29T13:15:38.461+0000","size":736468.0,"concepts":[],"name":"prad PDF Content-6","status":"Live","code":"test-Resourcce","prevStatus":"Processing","methods":[],"streamingUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_1130314857102131201110/artifact/sftbr-04u.pdf","idealScreenSize":"normal","createdOn":"2020-05-29T13:05:32.987+0000","contentDisposition":"inline","lastUpdatedOn":"2020-05-29T13:15:38.046+0000","SYS_INTERNAL_LAST_UPDATED_ON":"2020-05-29T13:15:39.231+0000","dialcodeRequired":"No","lastStatusChangedOn":"2020-05-29T13:15:39.226+0000","os":["All"],"cloudStorageKey":"content/do_1130314857102131201110/artifact/sftbr-04u.pdf","libraries":[],"pkgVersion":1.0,"versionKey":"1590758138046","idealScreenDensity":"hdpi","s3Key":"ecar_files/do_1130314857102131201110/prad-pdf-content-6_1590758138471_do_1130314857102131201110_1.0.ecar","framework":"NCF","compatibilityLevel":4,"index":4,"depth":6,"parent":"do_11305856061513728011342"}],"contentDisposition":"inline","lastUpdatedOn":"2020-07-06T19:10:42.581+0000","contentEncoding":"gzip","contentType":"CourseUnit","dialcodeRequired":"No","identifier":"do_11305856061513728011342","lastStatusChangedOn":"2020-07-06T19:09:35.090+0000","audience":["Learner"],"os":["All"],"visibility":"Parent","index":1,"mediaType":"content","osId":"org.ekstep.launcher","languageCode":["en"],"versionKey":"1594062575090","license":"CC BY 4.0","idealScreenDensity":"hdpi","framework":"NCFCOPY","depth":5,"compatibilityLevel":1,"name":"Unit - 3.1.1.1.1","status":"Live","lastPublishedOn":"2020-07-06T19:10:42.996+0000","pkgVersion":1.0,"leafNodesCount":4,"leafNodes":["do_1130314841730334721104","do_1130314857102131201110","do_1130314847650037761106","do_1130314851303178241108"],"downloadUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11305855864948326411234/course-hierarchy-test-1_1594062643205_do_11305855864948326411234_1.0_spine.ecar","variants":"{\"online\":{\"ecarUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11305855864948326411234/course-hierarchy-test-1_1594062643394_do_11305855864948326411234_1.0_online.ecar\",\"size\":7399.0},\"spine\":{\"ecarUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11305855864948326411234/course-hierarchy-test-1_1594062643205_do_11305855864948326411234_1.0_spine.ecar\",\"size\":40995.0}}"}],"contentDisposition":"inline","lastUpdatedOn":"2020-07-06T19:10:42.581+0000","contentEncoding":"gzip","contentType":"CourseUnit","dialcodeRequired":"No","identifier":"do_11305856061516185611346","lastStatusChangedOn":"2020-07-06T19:09:35.093+0000","audience":["Learner"],"os":["All"],"visibility":"Parent","index":1,"mediaType":"content","osId":"org.ekstep.launcher","languageCode":["en"],"versionKey":"1594062575093","license":"CC BY 4.0","idealScreenDensity":"hdpi","framework":"NCFCOPY","depth":4,"compatibilityLevel":1,"name":"Unit - 3.1.1.1","status":"Live","lastPublishedOn":"2020-07-06T19:10:42.996+0000","pkgVersion":1.0,"leafNodesCount":4,"leafNodes":["do_1130314841730334721104","do_1130314857102131201110","do_1130314847650037761106","do_1130314851303178241108"],"downloadUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11305855864948326411234/course-hierarchy-test-1_1594062643205_do_11305855864948326411234_1.0_spine.ecar","variants":"{\"online\":{\"ecarUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11305855864948326411234/course-hierarchy-test-1_1594062643394_do_11305855864948326411234_1.0_online.ecar\",\"size\":7399.0},\"spine\":{\"ecarUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11305855864948326411234/course-hierarchy-test-1_1594062643205_do_11305855864948326411234_1.0_spine.ecar\",\"size\":40995.0}}"}],"contentDisposition":"inline","lastUpdatedOn":"2020-07-06T19:10:42.581+0000","contentEncoding":"gzip","contentType":"CourseUnit","dialcodeRequired":"No","identifier":"do_11305856061514547211344","lastStatusChangedOn":"2020-07-06T19:09:35.091+0000","audience":["Learner"],"os":["All"],"visibility":"Parent","index":1,"mediaType":"content","osId":"org.ekstep.launcher","languageCode":["en"],"versionKey":"1594062575091","license":"CC BY 4.0","idealScreenDensity":"hdpi","framework":"NCFCOPY","depth":3,"compatibilityLevel":1,"name":"Unit - 3.1.1","status":"Live","lastPublishedOn":"2020-07-06T19:10:42.996+0000","pkgVersion":1.0,"leafNodesCount":4,"leafNodes":["do_1130314841730334721104","do_1130314857102131201110","do_1130314847650037761106","do_1130314851303178241108"],"downloadUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11305855864948326411234/course-hierarchy-test-1_1594062643205_do_11305855864948326411234_1.0_spine.ecar","variants":"{\"online\":{\"ecarUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11305855864948326411234/course-hierarchy-test-1_1594062643394_do_11305855864948326411234_1.0_online.ecar\",\"size\":7399.0},\"spine\":{\"ecarUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11305855864948326411234/course-hierarchy-test-1_1594062643205_do_11305855864948326411234_1.0_spine.ecar\",\"size\":40995.0}}"}],"contentDisposition":"inline","lastUpdatedOn":"2020-07-06T19:10:42.581+0000","contentEncoding":"gzip","contentType":"CourseUnit","dialcodeRequired":"No","identifier":"do_11305856061520281611348","lastStatusChangedOn":"2020-07-06T19:09:35.098+0000","audience":["Learner"],"os":["All"],"visibility":"Parent","index":1,"mediaType":"content","osId":"org.ekstep.launcher","languageCode":["en"],"versionKey":"1594062575098","license":"CC BY 4.0","idealScreenDensity":"hdpi","framework":"NCFCOPY","depth":2,"compatibilityLevel":1,"name":"Unit - 3.1","status":"Live","lastPublishedOn":"2020-07-06T19:10:42.996+0000","pkgVersion":1.0,"leafNodesCount":4,"leafNodes":["do_1130314841730334721104","do_1130314857102131201110","do_1130314847650037761106","do_1130314851303178241108"],"downloadUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11305855864948326411234/course-hierarchy-test-1_1594062643205_do_11305855864948326411234_1.0_spine.ecar","variants":"{\"online\":{\"ecarUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11305855864948326411234/course-hierarchy-test-1_1594062643394_do_11305855864948326411234_1.0_online.ecar\",\"size\":7399.0},\"spine\":{\"ecarUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11305855864948326411234/course-hierarchy-test-1_1594062643205_do_11305855864948326411234_1.0_spine.ecar\",\"size\":40995.0}}"}],"contentDisposition":"inline","lastUpdatedOn":"2020-07-06T19:10:42.581+0000","contentEncoding":"gzip","contentType":"CourseUnit","dialcodeRequired":"No","identifier":"do_11305856061510451211340","lastStatusChangedOn":"2020-07-06T19:09:35.086+0000","audience":["Learner"],"os":["All"],"visibility":"Parent","index":3,"mediaType":"content","osId":"org.ekstep.launcher","languageCode":["en"],"versionKey":"1594062575086","license":"CC BY 4.0","idealScreenDensity":"hdpi","framework":"NCFCOPY","depth":1,"compatibilityLevel":1,"name":"Unit - 3","status":"Live","lastPublishedOn":"2020-07-06T19:10:42.996+0000","pkgVersion":1.0,"leafNodesCount":4,"leafNodes":["do_1130314841730334721104","do_1130314857102131201110","do_1130314847650037761106","do_1130314851303178241108"],"downloadUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11305855864948326411234/course-hierarchy-test-1_1594062643205_do_11305855864948326411234_1.0_spine.ecar","variants":"{\"online\":{\"ecarUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11305855864948326411234/course-hierarchy-test-1_1594062643394_do_11305855864948326411234_1.0_online.ecar\",\"size\":7399.0},\"spine\":{\"ecarUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11305855864948326411234/course-hierarchy-test-1_1594062643205_do_11305855864948326411234_1.0_spine.ecar\",\"size\":40995.0}}"}],"contentEncoding":"gzip","lockKey":"98909e87-3361-4c6c-9b68-136fbead2bef","mimeTypesCount":"{\"application/pdf\":8,\"application/vnd.ekstep.content-collection\":15}","totalCompressedSize":5891940.0,"contentType":"Course","identifier":"do_11305855864948326411234","lastUpdatedBy":"95e4942d-cbe8-477d-aebd-ad8e6de4bfc8","audience":["Learner"],"toc_url":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11305855864948326411234/artifact/do_11305855864948326411234_toc.json","visibility":"Default","contentTypesCount":"{\"CourseUnit\":15,\"Resource\":8}","childNodes":["do_11305856007009075211332","do_11305856061513728011342","do_11305855931312537611237","do_1130314841730334721104","do_11305856061520281611348","do_11305856007005798411326","do_11305856007008256011330","do_1130314847650037761106","do_1130314851303178241108","do_11305855931317452811243","do_11305855931314995211239","do_11305856061514547211344","do_1130314849898332161107","do_11305855931315814411241","do_11305856007007436811328","do_1130314857102131201110","do_11305855931318272011245","do_11305856061510451211340","do_11305856061516185611346","do_1130314845426565121105","do_11305856007004160011324"],"consumerId":"273f3b18-5dda-4a27-984a-060c7cd398d3","mediaType":"content","osId":"org.ekstep.quiz.app","lastPublishedBy":"Ekstep","version":2,"prevState":"Draft","license":"CC BY 4.0","size":40995.0,"lastPublishedOn":"2020-07-06T19:10:42.996+0000","name":"Course Hierarchy Test - 1","status":"Live","code":"org.sunbird.pZaKxV","prevStatus":"Processing","description":"Enter description for Course","posterImage":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11299104587967692816/artifact/2a4b8abd789184932399d222d03d9b5c.jpg","idealScreenSize":"normal","createdOn":"2020-07-06T19:05:35.144+0000","copyrightYear":2020,"contentDisposition":"inline","lastUpdatedOn":"2020-07-06T19:10:42.581+0000","SYS_INTERNAL_LAST_UPDATED_ON":"2020-07-06T19:10:43.861+0000","dialcodeRequired":"No","creator":"Reviewer User","lastStatusChangedOn":"2020-07-06T19:10:43.856+0000","createdFor":["ORG_001"],"os":["All"],"pkgVersion":1.0,"versionKey":"1594062642581","idealScreenDensity":"hdpi","s3Key":"ecar_files/do_11305855864948326411234/course-hierarchy-test-1_1594062643205_do_11305855864948326411234_1.0_spine.ecar","depth":0,"framework":"NCFCOPY","createdBy":"95e4942d-cbe8-477d-aebd-ad8e6de4bfc8","leafNodesCount":6,"compatibilityLevel":4,"usedByContent":[],"board":"NCERT","resourceType":"Course","reservedDialcodes":{"K2D9J9":0},"c_sunbird_dev_private_batch_count":0,"c_sunbird_dev_open_batch_count":1,"batches":[{"createdFor":["ORG_001"],"endDate":null,"name":"Course Hierarchy Test - 1","batchId":"0130585642325770243","enrollmentType":"open","enrollmentEndDate":null,"startDate":"2020-07-06","status":0}]}'); -INSERT INTO hierarchy_store.content_hierarchy(identifier, hierarchy) VALUES ('KP_FT_1594149835504', '{"ownershipType":["createdBy"],"downloadUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/KP_FT_1594149835504/kp-integration-test-collection-content_1594149837215_kp_ft_1594149835504_1.0_spine.ecar","channel":"channel-01","language":["English"],"variants":{"online":{"ecarUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/KP_FT_1594149835504/kp-integration-test-collection-content_1594149837379_kp_ft_1594149835504_1.0_online.ecar","size":4124.0},"spine":{"ecarUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/KP_FT_1594149835504/kp-integration-test-collection-content_1594149837215_kp_ft_1594149835504_1.0_spine.ecar","size":4082.0}},"mimeType":"application/vnd.ekstep.content-collection","leafNodes":["KP_FT_1594149793834","KP_FT_1594149814674","KP_FT_1594149773180"],"objectType":"Content","collections":[],"children":[{"ownershipType":["createdBy"],"parent":"KP_FT_1594149835504","code":"b9a50833-eff6-4ef5-a2a4-2413f2d51f6c","channel":"channel-01","description":"Test_CourseUnit_desc_1","language":["English"],"mimeType":"application/vnd.ekstep.content-collection","idealScreenSize":"normal","createdOn":"2020-07-07T19:23:55.559+0000","objectType":"Content","children":[{"ownershipType":["createdBy"],"parent":"do_11305927545289932812008","code":"b9a50833-eff6-4ef5-a2a4-2413f2d51f6d","channel":"channel-01","description":"Test_CourseSubUnit_desc_1","language":["English"],"mimeType":"application/vnd.ekstep.content-collection","idealScreenSize":"normal","createdOn":"2020-07-07T19:23:55.557+0000","objectType":"Content","contentDisposition":"inline","lastUpdatedOn":"2020-07-07T19:23:56.928+0000","contentEncoding":"gzip","contentType":"TextBookUnit","dialcodeRequired":"No","identifier":"do_11305927545288294412006","lastStatusChangedOn":"2020-07-07T19:23:55.557+0000","audience":["Learner"],"os":["All"],"visibility":"Parent","index":1,"mediaType":"content","osId":"org.ekstep.launcher","languageCode":["en"],"versionKey":"1594149835557","license":"CC BY 4.0","idealScreenDensity":"hdpi","depth":2,"compatibilityLevel":1,"name":"Test_Course_SubUnit_name_1","status":"Live","lastPublishedOn":"2020-07-07T19:23:56.964+0000","pkgVersion":1.0,"leafNodesCount":0,"leafNodes":[],"downloadUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/KP_FT_1594149835504/kp-integration-test-collection-content_1594149837215_kp_ft_1594149835504_1.0_spine.ecar","variants":"{\"online\":{\"ecarUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/KP_FT_1594149835504/kp-integration-test-collection-content_1594149837379_kp_ft_1594149835504_1.0_online.ecar\",\"size\":4124.0},\"spine\":{\"ecarUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/KP_FT_1594149835504/kp-integration-test-collection-content_1594149837215_kp_ft_1594149835504_1.0_spine.ecar\",\"size\":4082.0}}"},{"ownershipType":["createdBy"],"parent":"do_11305927545289932812008","code":"b9a50833-eff6-4ef5-a2a4-2413f2d51f6e","channel":"channel-01","description":"Test_CourseSubUnit_desc_2","language":["English"],"mimeType":"application/vnd.ekstep.content-collection","idealScreenSize":"normal","createdOn":"2020-07-07T19:23:55.555+0000","objectType":"Content","contentDisposition":"inline","lastUpdatedOn":"2020-07-07T19:23:56.928+0000","contentEncoding":"gzip","contentType":"TextBookUnit","dialcodeRequired":"No","identifier":"do_11305927545286656012004","lastStatusChangedOn":"2020-07-07T19:23:55.555+0000","audience":["Learner"],"os":["All"],"visibility":"Parent","index":2,"mediaType":"content","osId":"org.ekstep.launcher","languageCode":["en"],"versionKey":"1594149835555","license":"CC BY 4.0","idealScreenDensity":"hdpi","depth":2,"compatibilityLevel":1,"name":"Test_Course_SubUnit_name_2","status":"Live","lastPublishedOn":"2020-07-07T19:23:56.964+0000","pkgVersion":1.0,"leafNodesCount":0,"leafNodes":[],"downloadUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/KP_FT_1594149835504/kp-integration-test-collection-content_1594149837215_kp_ft_1594149835504_1.0_spine.ecar","variants":"{\"online\":{\"ecarUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/KP_FT_1594149835504/kp-integration-test-collection-content_1594149837379_kp_ft_1594149835504_1.0_online.ecar\",\"size\":4124.0},\"spine\":{\"ecarUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/KP_FT_1594149835504/kp-integration-test-collection-content_1594149837215_kp_ft_1594149835504_1.0_spine.ecar\",\"size\":4082.0}}"},{"ownershipType":["createdBy"],"previewUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/kp_ft_1594149773180/artifact/sample.pdf","code":"kp.ft.resource.pdf","prevStatus":"Processing","downloadUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/KP_FT_1594149773180/kp-integration-test-content_1594149774171_kp_ft_1594149773180_1.0.ecar","channel":"channel-01","description":"KP Integration Test Content","language":["English"],"variants":{"spine":{"ecarUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/KP_FT_1594149773180/kp-integration-test-content_1594149774493_kp_ft_1594149773180_1.0_spine.ecar","size":899.0}},"streamingUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/kp_ft_1594149773180/artifact/sample.pdf","mimeType":"application/pdf","idealScreenSize":"normal","createdOn":"2020-07-07T19:22:53.182+0000","contentDisposition":"inline","artifactUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/kp_ft_1594149773180/artifact/sample.pdf","contentEncoding":"identity","lastUpdatedOn":"2020-07-07T19:22:53.852+0000","SYS_INTERNAL_LAST_UPDATED_ON":"2020-07-07T19:22:54.780+0000","contentType":"Resource","dialcodeRequired":"No","identifier":"KP_FT_1594149773180","audience":["Learner"],"lastStatusChangedOn":"2020-07-07T19:22:54.776+0000","visibility":"Default","os":["All"],"cloudStorageKey":"content/kp_ft_1594149773180/artifact/sample.pdf","consumerId":"f3acc49a-4c41-49a3-9442-aaf0db0f5b89","mediaType":"content","osId":"org.ekstep.quiz.app","lastPublishedBy":"KP_FT_PUBLISHER","version":2,"pkgVersion":1.0,"pragma":["external"],"versionKey":"1594149773852","prevState":"Draft","license":"CC BY 4.0","idealScreenDensity":"hdpi","s3Key":"ecar_files/KP_FT_1594149773180/kp-integration-test-content_1594149774171_kp_ft_1594149773180_1.0.ecar","framework":"NCF","lastPublishedOn":"2020-07-07T19:22:54.158+0000","size":412645.0,"compatibilityLevel":4,"name":"KP Integration Test Content","status":"Live","index":3,"depth":2,"parent":"do_11305927545289932812008"},{"ownershipType":["createdBy"],"previewUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/kp_ft_1594149793834/artifact/sample.pdf","code":"kp.ft.resource.pdf","prevStatus":"Processing","downloadUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/KP_FT_1594149793834/kp-integration-test-content_1594149795067_kp_ft_1594149793834_1.0.ecar","channel":"channel-01","description":"KP Integration Test Content","language":["English"],"variants":{"spine":{"ecarUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/KP_FT_1594149793834/kp-integration-test-content_1594149795411_kp_ft_1594149793834_1.0_spine.ecar","size":903.0}},"streamingUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/kp_ft_1594149793834/artifact/sample.pdf","mimeType":"application/pdf","idealScreenSize":"normal","createdOn":"2020-07-07T19:23:13.837+0000","contentDisposition":"inline","artifactUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/kp_ft_1594149793834/artifact/sample.pdf","contentEncoding":"identity","lastUpdatedOn":"2020-07-07T19:23:14.696+0000","SYS_INTERNAL_LAST_UPDATED_ON":"2020-07-07T19:23:15.691+0000","contentType":"Resource","dialcodeRequired":"No","identifier":"KP_FT_1594149793834","audience":["Learner"],"lastStatusChangedOn":"2020-07-07T19:23:15.687+0000","visibility":"Default","os":["All"],"cloudStorageKey":"content/kp_ft_1594149793834/artifact/sample.pdf","consumerId":"92d4dca0-590b-4960-a5cc-827b8dceb5fd","mediaType":"content","osId":"org.ekstep.quiz.app","lastPublishedBy":"KP_FT_PUBLISHER","version":2,"pkgVersion":1.0,"pragma":["external"],"versionKey":"1594149794696","prevState":"Draft","license":"CC BY 4.0","idealScreenDensity":"hdpi","s3Key":"ecar_files/KP_FT_1594149793834/kp-integration-test-content_1594149795067_kp_ft_1594149793834_1.0.ecar","framework":"NCF","lastPublishedOn":"2020-07-07T19:23:15.060+0000","size":412648.0,"compatibilityLevel":4,"name":"KP Integration Test Content","status":"Live","index":4,"depth":2,"parent":"do_11305927545289932812008"},{"ownershipType":["createdBy"],"previewUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/kp_ft_1594149814674/artifact/sample.pdf","code":"kp.ft.resource.pdf","prevStatus":"Processing","downloadUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/KP_FT_1594149814674/kp-integration-test-content_1594149815822_kp_ft_1594149814674_1.0.ecar","channel":"channel-01","description":"KP Integration Test Content","language":["English"],"variants":{"spine":{"ecarUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/KP_FT_1594149814674/kp-integration-test-content_1594149816213_kp_ft_1594149814674_1.0_spine.ecar","size":900.0}},"streamingUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/kp_ft_1594149814674/artifact/sample.pdf","mimeType":"application/pdf","idealScreenSize":"normal","createdOn":"2020-07-07T19:23:34.676+0000","contentDisposition":"inline","artifactUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/kp_ft_1594149814674/artifact/sample.pdf","contentEncoding":"identity","lastUpdatedOn":"2020-07-07T19:23:35.520+0000","SYS_INTERNAL_LAST_UPDATED_ON":"2020-07-07T19:23:36.530+0000","contentType":"Resource","dialcodeRequired":"No","identifier":"KP_FT_1594149814674","audience":["Learner"],"lastStatusChangedOn":"2020-07-07T19:23:36.526+0000","visibility":"Default","os":["All"],"cloudStorageKey":"content/kp_ft_1594149814674/artifact/sample.pdf","consumerId":"d3dec576-3257-4752-a33d-f0120b1bfbfd","mediaType":"content","osId":"org.ekstep.quiz.app","lastPublishedBy":"KP_FT_PUBLISHER","version":2,"pkgVersion":1.0,"pragma":["external"],"versionKey":"1594149815520","prevState":"Draft","license":"CC BY 4.0","idealScreenDensity":"hdpi","s3Key":"ecar_files/KP_FT_1594149814674/kp-integration-test-content_1594149815822_kp_ft_1594149814674_1.0.ecar","framework":"NCF","lastPublishedOn":"2020-07-07T19:23:35.813+0000","size":412648.0,"compatibilityLevel":4,"name":"KP Integration Test Content","status":"Live","index":5,"depth":2,"parent":"do_11305927545289932812008"}],"contentDisposition":"inline","lastUpdatedOn":"2020-07-07T19:23:56.928+0000","contentEncoding":"gzip","contentType":"TextBookUnit","dialcodeRequired":"No","identifier":"do_11305927545289932812008","lastStatusChangedOn":"2020-07-07T19:23:55.559+0000","audience":["Learner"],"os":["All"],"visibility":"Parent","index":1,"mediaType":"content","osId":"org.ekstep.launcher","languageCode":["en"],"versionKey":"1594149835559","license":"CC BY 4.0","idealScreenDensity":"hdpi","depth":1,"compatibilityLevel":1,"name":"Test_CourseUnit_1","status":"Live","lastPublishedOn":"2020-07-07T19:23:56.964+0000","pkgVersion":1.0,"leafNodesCount":3,"leafNodes":["KP_FT_1594149793834","KP_FT_1594149814674","KP_FT_1594149773180"],"downloadUrl":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/KP_FT_1594149835504/kp-integration-test-collection-content_1594149837215_kp_ft_1594149835504_1.0_spine.ecar","variants":"{\"online\":{\"ecarUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/KP_FT_1594149835504/kp-integration-test-collection-content_1594149837379_kp_ft_1594149835504_1.0_online.ecar\",\"size\":4124.0},\"spine\":{\"ecarUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/KP_FT_1594149835504/kp-integration-test-collection-content_1594149837215_kp_ft_1594149835504_1.0_spine.ecar\",\"size\":4082.0}}"}],"contentEncoding":"gzip","mimeTypesCount":"{\"application/pdf\":3,\"application/vnd.ekstep.content-collection\":3}","totalCompressedSize":1237941.0,"contentType":"TextBook","identifier":"KP_FT_1594149835504","audience":["Learner"],"toc_url":"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/kp_ft_1594149835504/artifact/kp_ft_1594149835504_toc.json","visibility":"Default","contentTypesCount":"{\"TextBookUnit\":3,\"Resource\":3}","childNodes":["KP_FT_1594149793834","KP_FT_1594149814674","KP_FT_1594149773180","do_11305927545288294412006","do_11305927545289932812008","do_11305927545286656012004"],"consumerId":"52025511-2192-49b7-926b-5b530ea5617e","mediaType":"content","osId":"org.ekstep.quiz.app","lastPublishedBy":"KP_FT_PUBLISHER","version":2,"prevState":"Draft","license":"CC BY 4.0","lastPublishedOn":"2020-07-07T19:23:56.964+0000","size":4082.0,"name":"KP Integration Test Collection Content","status":"Live","code":"kp.ft.collection.textbook","prevStatus":"Processing","description":"KP Integration Test Collection Content","idealScreenSize":"normal","createdOn":"2020-07-07T19:23:55.507+0000","contentDisposition":"inline","lastUpdatedOn":"2020-07-07T19:23:56.928+0000","SYS_INTERNAL_LAST_UPDATED_ON":"2020-07-07T19:23:57.725+0000","dialcodeRequired":"No","lastStatusChangedOn":"2020-07-07T19:23:57.721+0000","os":["All"],"pkgVersion":1.0,"versionKey":"1594149836928","idealScreenDensity":"hdpi","s3Key":"ecar_files/KP_FT_1594149835504/kp-integration-test-collection-content_1594149837215_kp_ft_1594149835504_1.0_spine.ecar","depth":0,"framework":"NCF","leafNodesCount":3,"compatibilityLevel":1,"usedByContent":[]}'); diff --git a/relation-cache-updater/src/test/scala/org/sunbird/job/fixture/EventFixture.scala b/relation-cache-updater/src/test/scala/org/sunbird/job/fixture/EventFixture.scala deleted file mode 100644 index d80f56a46..000000000 --- a/relation-cache-updater/src/test/scala/org/sunbird/job/fixture/EventFixture.scala +++ /dev/null @@ -1,13 +0,0 @@ -package org.sunbird.job.fixture - -object EventFixture { - - val EVENT_1: String = - """ - |{"actor":{"id":"Post Publish Processor", "type":"System"}, "eid":"BE_JOB_REQUEST", "edata":{"createdFor":["ORG_001"], "createdBy":"95e4942d-cbe8-477d-aebd-ad8e6de4bfc8", "name":"Untitled Course", "action":"post-publish-process", "iteration":1.0, "identifier":"do_11305855864948326411234", "mimeType":"application/vnd.ekstep.content-collection", "contentType":"Course", "pkgVersion":7.0, "status":"Live"}, "partition":0, "ets":"1.593769627322E12", "context":{"pdata":{"ver":1.0, "id":"org.ekstep.platform"}, "channel":"b00bc992ef25f1a9a8d63291e20efc8d", "env":"sunbirddev"}, "mid":"LP.1593769627322.459a018c-5ec3-4c11-96c1-cd84d3786b85", "object":{"ver":"1593769626118", "id":"do_11305855864948326411234"}} - |""".stripMargin - val EVENT_2: String = - """ - |{"actor":{"id":"Post Publish Processor", "type":"System"}, "eid":"BE_JOB_REQUEST", "edata":{"createdFor":["ORG_001"], "createdBy":"95e4942d-cbe8-477d-aebd-ad8e6de4bfc8", "name":"Untitled Course", "action":"post-publish-process", "iteration":1.0, "identifier":"KP_FT_1594149835504", "mimeType":"application/vnd.ekstep.content-collection", "contentType":"Course", "pkgVersion":7.0, "status":"Live"}, "partition":0, "ets":"1.593769627322E12", "context":{"pdata":{"ver":1.0, "id":"org.ekstep.platform"}, "channel":"b00bc992ef25f1a9a8d63291e20efc8d", "env":"sunbirddev"}, "mid":"LP.1593769627322.459a018c-5ec3-4c11-96c1-cd84d3786b85", "object":{"ver":"1593769626118", "id":"KP_FT_1594149835504"}} - |""".stripMargin -} \ No newline at end of file diff --git a/relation-cache-updater/src/test/scala/org/sunbird/job/spec/RelationCacheUpdaterTaskTestSpec.scala b/relation-cache-updater/src/test/scala/org/sunbird/job/spec/RelationCacheUpdaterTaskTestSpec.scala deleted file mode 100644 index f707fb27c..000000000 --- a/relation-cache-updater/src/test/scala/org/sunbird/job/spec/RelationCacheUpdaterTaskTestSpec.scala +++ /dev/null @@ -1,154 +0,0 @@ -package org.sunbird.job.spec - -import java.util -import com.google.gson.Gson -import com.typesafe.config.{Config, ConfigFactory} -import org.apache.flink.api.common.typeinfo.TypeInformation -import org.apache.flink.api.java.typeutils.TypeExtractor -import org.apache.flink.runtime.testutils.MiniClusterResourceConfiguration -import org.apache.flink.streaming.api.functions.source.SourceFunction -import org.apache.flink.streaming.api.functions.source.SourceFunction.SourceContext -import org.apache.flink.test.util.MiniClusterWithClientResource -import org.cassandraunit.CQLDataLoader -import org.cassandraunit.dataset.cql.FileCQLDataSet -import org.cassandraunit.utils.EmbeddedCassandraServerHelper -import org.mockito.Mockito -import org.mockito.Mockito._ -import org.sunbird.job.cache.RedisConnect -import org.sunbird.job.connector.FlinkKafkaConnector -import org.sunbird.job.fixture.EventFixture -import org.sunbird.job.relationcache.domain.Event -import org.sunbird.job.relationcache.task.{RelationCacheUpdaterConfig, RelationCacheUpdaterStreamTask} -import org.sunbird.job.util.{CassandraUtil, JSONUtil} -import org.sunbird.spec.{BaseMetricsReporter, BaseTestSpec} -import redis.clients.jedis.Jedis -import redis.embedded.RedisServer - -import scala.collection.JavaConverters._ - -class RelationCacheUpdaterTaskTestSpec extends BaseTestSpec { - - implicit val mapTypeInfo: TypeInformation[java.util.Map[String, AnyRef]] = TypeExtractor.getForClass(classOf[java.util.Map[String, AnyRef]]) - - val flinkCluster = new MiniClusterWithClientResource(new MiniClusterResourceConfiguration.Builder() - .setConfiguration(testConfiguration()) - .setNumberSlotsPerTaskManager(1) - .setNumberTaskManagers(1) - .build) - - var redisServer: RedisServer = _ - redisServer = new RedisServer(6340) - redisServer.start() - var relCacheDb: Jedis = _ - var contentCacheDb: Jedis = _ - val mockKafkaUtil: FlinkKafkaConnector = mock[FlinkKafkaConnector](Mockito.withSettings().serializable()) - val gson = new Gson() - val config: Config = ConfigFactory.load("test.conf") - val jobConfig: RelationCacheUpdaterConfig = new RelationCacheUpdaterConfig(config) - - - var cassandraUtil: CassandraUtil = _ - - override protected def beforeAll(): Unit = { - super.beforeAll() - val redisConnect = new RedisConnect(jobConfig) - relCacheDb = redisConnect.getConnection(jobConfig.relationCacheStore) - contentCacheDb = redisConnect.getConnection(jobConfig.collectionCacheStore) - EmbeddedCassandraServerHelper.startEmbeddedCassandra(80000L) - cassandraUtil = new CassandraUtil(jobConfig.dbHost, jobConfig.dbPort) - val session = cassandraUtil.session - - val dataLoader = new CQLDataLoader(session); - dataLoader.load(new FileCQLDataSet(getClass.getResource("/test.cql").getPath, true, true)); - // Clear the metrics - testCassandraUtil(cassandraUtil) - BaseMetricsReporter.gaugeMetrics.clear() - relCacheDb.flushDB() - contentCacheDb.flushDB() - flinkCluster.before() - } - - override protected def afterAll(): Unit = { - super.afterAll() - try { - EmbeddedCassandraServerHelper.cleanEmbeddedCassandra() - redisServer.stop() - } catch { - case ex: Exception => { - } - } - flinkCluster.after() - } - - - "RelationCacheUpdater " should "generate cache" in { - when(mockKafkaUtil.kafkaJobRequestSource[Event](jobConfig.kafkaInputTopic)).thenReturn(new RelationCacheUpdaterEventSource) - new RelationCacheUpdaterStreamTask(jobConfig, mockKafkaUtil).process() - BaseMetricsReporter.gaugeMetrics(s"${jobConfig.jobName}.${jobConfig.totalEventsCount}").getValue() should be(2) - BaseMetricsReporter.gaugeMetrics(s"${jobConfig.jobName}.${jobConfig.successEventCount}").getValue() should be(2) - BaseMetricsReporter.gaugeMetrics(s"${jobConfig.jobName}.${jobConfig.failedEventCount}").getValue() should be(0) - BaseMetricsReporter.gaugeMetrics(s"${jobConfig.jobName}.${jobConfig.skippedEventCount}").getValue() should be(0) - BaseMetricsReporter.gaugeMetrics(s"${jobConfig.jobName}.${jobConfig.dbReadCount}").getValue() should be(2) - BaseMetricsReporter.gaugeMetrics(s"${jobConfig.jobName}.${jobConfig.cacheWrite}").getValue() should be(43) - - // Assertion on total keys for leafnodes and ancestors. - getKeysLength("*:leafnodes", relCacheDb) should be (18) - getKeysLength("*:ancestors", relCacheDb) should be (9) - - // Checking assertion of leafNodes for some of the collections. - val leafNodes1 = getList("do_11305855864948326411234:do_11305855931314995211239:leafnodes", relCacheDb) - leafNodes1.size should be (2) - leafNodes1 should contain theSameElementsAs (List("do_1130314841730334721104", "do_1130314845426565121105")) - - val leafNodes2 = getList("do_11305855864948326411234:do_11305856061520281611348:leafnodes", relCacheDb) - leafNodes2.size should be (4) - leafNodes2 should contain theSameElementsAs (List("do_1130314847650037761106", "do_1130314857102131201110", "do_1130314841730334721104", "do_1130314851303178241108")) - - // Checking assertion of Ancestors for some of the resources. - val ancestors1 = getList("do_11305855864948326411234:do_1130314847650037761106:ancestors", relCacheDb) - ancestors1.size should be (11) - ancestors1 should contain theSameElementsAs (List("do_11305856007008256011330", "do_11305856061516185611346", "do_11305856061513728011342", - "do_11305856007004160011324", "do_11305856007007436811328", "do_11305856007009075211332", "do_11305856061514547211344", - "do_11305856061510451211340", "do_11305855864948326411234", "do_11305856007005798411326", "do_11305856061520281611348")) - - val ancestors2 = getList("do_11305855864948326411234:do_1130314857102131201110:ancestors", relCacheDb) - ancestors2.size should be (6) - ancestors2 should contain theSameElementsAs (List("do_11305856061516185611346", "do_11305856061513728011342", "do_11305856061514547211344", - "do_11305855864948326411234", "do_11305856061510451211340", "do_11305856061520281611348")) - - // Checking assertion of Collection metadata for some of the collection (with visibility parent) - val keys = getKeys("*", contentCacheDb) - keys should not contain ("do_11305855864948326411234") - keys.size should be (16) - contentCacheDb.get("do_11305927545289932812008") should not be empty - contentCacheDb.get("do_11305856007004160011324") should not be empty - contentCacheDb.get("do_11305855931314995211239") should not be empty - contentCacheDb.get("do_11305855931315814411241") should not be empty - } - - def testCassandraUtil(cassandraUtil: CassandraUtil): Unit = { - cassandraUtil.reconnect() - } - - def getKeys(pattern: String, redisDb: Jedis): List[String] = { - redisDb.keys(pattern).asScala.toList - } - - def getKeysLength(pattern: String, redisDb: Jedis): Int = { - redisDb.keys(pattern).size() - } - - def getList(key: String, redisDb: Jedis): List[String] = { - redisDb.smembers(key).asScala.toList - } - -} - -class RelationCacheUpdaterEventSource extends SourceFunction[Event] { - override def run(ctx: SourceContext[Event]): Unit = { - ctx.collect(new Event(JSONUtil.deserialize[util.Map[String, Any]](EventFixture.EVENT_1), 0, 10)) - ctx.collect(new Event(JSONUtil.deserialize[util.Map[String, Any]](EventFixture.EVENT_2), 0, 11)) - } - - override def cancel() = {} -} \ No newline at end of file diff --git a/search-indexer/src/main/resources/search-indexer.conf b/search-indexer/src/main/resources/search-indexer.conf index 7d28a11b4..904bd96ca 100644 --- a/search-indexer/src/main/resources/search-indexer.conf +++ b/search-indexer/src/main/resources/search-indexer.conf @@ -24,4 +24,10 @@ schema.definition_cache.expiry = 14400 restrict { metadata.objectTypes = [] objectTypes = ["EventSet", "Questionnaire", "Misconception", "FrameworkType", "EventSet", "Event"] -} \ No newline at end of file +} + +cloudstorage.metadata.replace_absolute_path=false +cloudstorage.relative_path_prefix= "CONTENT_STORAGE_BASE_PATH" +cloudstorage.read_base_path="https://sunbirddev.blob.core.windows.net" +cloudstorage.mecloudstorage.metadata.list=["appIcon","posterImage","artifactUrl","downloadUrl","variants","previewUrl","pdfUrl", "streamingUrl", "toc_url"] +cloud_storage_container="sunbird-content-dev" \ No newline at end of file diff --git a/search-indexer/src/main/scala/org/sunbird/job/searchindexer/functions/CompositeSearchIndexerFunction.scala b/search-indexer/src/main/scala/org/sunbird/job/searchindexer/functions/CompositeSearchIndexerFunction.scala index d0fb38ef5..998dce0f3 100644 --- a/search-indexer/src/main/scala/org/sunbird/job/searchindexer/functions/CompositeSearchIndexerFunction.scala +++ b/search-indexer/src/main/scala/org/sunbird/job/searchindexer/functions/CompositeSearchIndexerFunction.scala @@ -10,7 +10,7 @@ import org.sunbird.job.searchindexer.compositesearch.domain.Event import org.sunbird.job.searchindexer.compositesearch.helpers.CompositeSearchIndexerHelper import org.sunbird.job.searchindexer.models.CompositeIndexer import org.sunbird.job.searchindexer.task.SearchIndexerConfig -import org.sunbird.job.util.ElasticSearchUtil +import org.sunbird.job.util.{CSPMetaUtil, ElasticSearchUtil, ScalaJsonUtil} import org.sunbird.job.{BaseProcessFunction, Metrics} @@ -54,7 +54,13 @@ class CompositeSearchIndexerFunction(config: SearchIndexerConfig, val graphId = event.readOrDefault("graphId", "") val uniqueId = event.readOrDefault("nodeUniqueId", "") val messageId = event.readOrDefault("mid", "") - CompositeIndexer(graphId, objectType, uniqueId, messageId, event.getMap(), config) + + val updateEvent: java.util.Map[String, Any] = if(config.isrRelativePathEnabled) { + val json = CSPMetaUtil.updateAbsolutePath(event.getJson())(config) + ScalaJsonUtil.deserialize[java.util.Map[String, Any]](json) + } else event.getMap() + + CompositeIndexer(graphId, objectType, uniqueId, messageId, updateEvent, config) } override def metricsList(): List[String] = { diff --git a/search-indexer/src/main/scala/org/sunbird/job/searchindexer/task/SearchIndexerConfig.scala b/search-indexer/src/main/scala/org/sunbird/job/searchindexer/task/SearchIndexerConfig.scala index e5144ea81..9adad0e00 100644 --- a/search-indexer/src/main/scala/org/sunbird/job/searchindexer/task/SearchIndexerConfig.scala +++ b/search-indexer/src/main/scala/org/sunbird/job/searchindexer/task/SearchIndexerConfig.scala @@ -70,4 +70,6 @@ class SearchIndexerConfig(override val config: Config) extends BaseJobConfig(con val definitionCacheExpiry: Int = if (config.hasPath("schema.definition_cache.expiry")) config.getInt("schema.definition_cache.expiry") else 14400 val restrictObjectTypes: util.List[String] = if(config.hasPath("restrict.objectTypes")) config.getStringList("restrict.objectTypes") else new util.ArrayList[String] val ignoredFields: List[String] = if (config.hasPath("ignored.fields")) config.getStringList("ignored.fields").asScala.toList else List("responseDeclaration", "body") + + val isrRelativePathEnabled: Boolean = if (config.hasPath("cloudstorage.metadata.replace_absolute_path")) config.getBoolean("cloudstorage.metadata.replace_absolute_path") else false } diff --git a/video-stream-generator/pom.xml b/video-stream-generator/pom.xml index e9a511059..700c4c51f 100644 --- a/video-stream-generator/pom.xml +++ b/video-stream-generator/pom.xml @@ -29,6 +29,11 @@ ${flink.version} provided + + com.fasterxml.jackson.module + jackson-module-scala_${scala.version} + 2.14.2 + org.sunbird jobs-core @@ -84,12 +89,50 @@ 3.3.3 test + + com.oracle.oci.sdk + oci-java-sdk-common + 3.5.0 + + + com.oracle.oci.sdk + oci-java-sdk-mediaservices + 3.5.0 + + + com.oracle.oci.sdk + oci-java-sdk-common-httpclient-jersey + 3.5.0 + + + com.googlecode.json-simple + json-simple + 1.1 + src/main/scala src/test/scala + + org.codehaus.mojo + build-helper-maven-plugin + 3.2.0 + + + generate-sources + + add-source + + + + src/main/java + + + + + org.apache.maven.plugins maven-compiler-plugin diff --git a/video-stream-generator/src/main/java/org.sunbird.job.videostream.helpers/MediaServiceHelper.java b/video-stream-generator/src/main/java/org.sunbird.job.videostream.helpers/MediaServiceHelper.java new file mode 100644 index 000000000..e2050cd4e --- /dev/null +++ b/video-stream-generator/src/main/java/org.sunbird.job.videostream.helpers/MediaServiceHelper.java @@ -0,0 +1,107 @@ +package org.sunbird.job.videostream.helpers; + +import java.util.List; + +import com.oracle.bmc.ConfigFileReader; +import com.oracle.bmc.Region; +import com.oracle.bmc.auth.*; +import com.oracle.bmc.mediaservices.model.*; +import com.oracle.bmc.mediaservices.requests.*; +import com.oracle.bmc.mediaservices.responses.*; +import org.json.simple.JSONObject; + +import com.oracle.bmc.mediaservices.MediaServicesClient; +import org.json.simple.parser.JSONParser; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + + +public class MediaServiceHelper { + + public static Logger logger = LoggerFactory.getLogger(MediaServiceHelper.class); + + public JSONObject createJSONObject(String jsonString) { + JSONObject jsonObject = new JSONObject(); + JSONParser jsonParser = new JSONParser(); + if ((jsonString != null) && !(jsonString.isEmpty())) { + try { + jsonObject = (JSONObject) jsonParser.parse(jsonString); + } catch (org.json.simple.parser.ParseException e) { + e.printStackTrace(); + } + } + return jsonObject; + } + + public MediaWorkflowJob submitJob(String compartment_id, String mediaWorkFlowId, + JSONObject mediaFlowJobParameters) { + MediaServicesClient mediaClient = connectMediaService(); + logger.debug("mediaFlowJobParameters...." + mediaFlowJobParameters + " mediaWorkFlowId..." + mediaWorkFlowId + + " compartment_id..." + compartment_id); + CreateMediaWorkflowJobRequest request = CreateMediaWorkflowJobRequest.builder() + .createMediaWorkflowJobDetails(CreateMediaWorkflowJobByIdDetails.builder() + .displayName("media-flow-job-diksha").compartmentId(compartment_id) + .mediaWorkflowId(mediaWorkFlowId).parameters(mediaFlowJobParameters).build()) + .build(); + CreateMediaWorkflowJobResponse response = mediaClient.createMediaWorkflowJob(request); + logger.debug("JOB_ID::{}", response.getMediaWorkflowJob().getId()); + closeMediaClient(mediaClient); + return response.getMediaWorkflowJob(); + } + + public String getStreamingPaths(String mediaWorkflowId, String gatewayDomain) { + String streamingURL = null; + MediaWorkflowJob job = getWorkflowJob(mediaWorkflowId); + List outputList = job.getOutputs(); + String mediaAssetId = null; + logger.debug("getStreamingPaths() gatewayDomain::{}", mediaWorkflowId, gatewayDomain); + for (JobOutput output : outputList) { + if (output.getObjectName().contains("master.m3u8")) { + mediaAssetId = output.getId(); + break; + } + } + streamingURL = "https://" + gatewayDomain + "/" + mediaAssetId + "/master.m3u8"; + logger.debug("getStreamingPaths() streamingURL::{}", streamingURL); + return streamingURL; + } + + // Get Media Workflow Job + public MediaWorkflowJob getWorkflowJob(String mediaWorkflowJobId) { + logger.debug(" <<<< Entering getWorkflowJob() >>>> mediaWorkflowId::" + mediaWorkflowJobId); + GetMediaWorkflowJobRequest request = GetMediaWorkflowJobRequest.builder().mediaWorkflowJobId(mediaWorkflowJobId) + .build(); + MediaServicesClient mediaClient = connectMediaService(); + GetMediaWorkflowJobResponse response = mediaClient.getMediaWorkflowJob(request); + MediaWorkflowJob mediaWorkflowJob = response.getMediaWorkflowJob(); + logger.debug("mediaWorkflowJob::::::::::::::" + mediaWorkflowJob); + closeMediaClient(mediaClient); + logger.debug(" <<<< Exiting getWorkflowJob() >>>> mediaWorkflowId::" + mediaWorkflowJobId); + return mediaWorkflowJob; + } + + public MediaServicesClient connectMediaService() { + logger.debug("<<<< Entering connectMediaService() >>>>"); + MediaServicesClient mediaClient = null; + try { +// final ConfigFileReader.ConfigFile configFile = ConfigFileReader.parse("~/.oci/config", "SUNBIRD"); +// AuthenticationDetailsProvider authenticationDetailsProvider = new ConfigFileAuthenticationDetailsProvider(configFile); + + InstancePrincipalsAuthenticationDetailsProvider authenticationDetailsProvider = InstancePrincipalsAuthenticationDetailsProvider + .builder().build(); + + mediaClient = MediaServicesClient.builder().build(authenticationDetailsProvider); + logger.debug("mediaClient::::::::::::::" + mediaClient); + } catch (Exception myThrowableObject) { + myThrowableObject.printStackTrace(); + } + logger.debug("<<<< Exiting connectMediaService() >>>>"); + return mediaClient; + } + + // Close Media Service Client + private void closeMediaClient(MediaServicesClient mc) { + mc.close(); + } +} + diff --git a/video-stream-generator/src/main/resources/video-stream-generator.conf b/video-stream-generator/src/main/resources/video-stream-generator.conf index 0aac4f4c8..ae95f603c 100644 --- a/video-stream-generator/src/main/resources/video-stream-generator.conf +++ b/video-stream-generator/src/main/resources/video-stream-generator.conf @@ -67,7 +67,7 @@ azure_token_client_key="client key" azure_token_client_secret="client secret" # CSP Name. e.g: aws or azure -media_service_type="aws" +media_service_type="oci" #AWS Elemental Media Convert Config aws { @@ -91,4 +91,22 @@ aws { } } -media_service_job_success_status=["FINISHED", "COMPLETE"] +# OCI Elemental Media Convert Config +# values for unit test (if applicable) +oci { + region="your-oci-home-region" + compartment_id="your-oci-compartment-ocid" + namespace="your-oci-tenant-namespace" + bucket { + content_bucket_name="src_bucket" + processed_bucket_name="dest_bucket" + } + stream { + prefix_input="sunbird" + work_flow_id ="your-media-workflow-ocid" + distribution_channel_id="your-distribution-channel-ocid" + stream_package_config_id="your-media-package-config-ocid" + } +} + +media_service_job_success_status=["FINISHED", "COMPLETE", "SUCCEEDED"] diff --git a/video-stream-generator/src/main/scala/org/sunbird/job/videostream/helpers/JsonUtilility.scala b/video-stream-generator/src/main/scala/org/sunbird/job/videostream/helpers/JsonUtilility.scala new file mode 100644 index 000000000..c6a9fde22 --- /dev/null +++ b/video-stream-generator/src/main/scala/org/sunbird/job/videostream/helpers/JsonUtilility.scala @@ -0,0 +1,25 @@ +package org.sunbird.job.videostream.helpers + +import com.fasterxml.jackson.databind.{DeserializationFeature, ObjectMapper} +import com.fasterxml.jackson.module.scala.experimental.ScalaObjectMapper +import com.fasterxml.jackson.module.scala.DefaultScalaModule + +object JsonUtility { + val mapper = new ObjectMapper() with ScalaObjectMapper + mapper.registerModule(DefaultScalaModule) + mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + + def toJson(value: Map[Symbol, Any]): String = { + toJson(value map { case (k,v) => k.name -> v}) + } + + def toJson(value: Any): String = { + mapper.writeValueAsString(value) + } + + def toMap[V](json:String)(implicit m: Manifest[V]) = fromJson[Map[String,V]](json) + + def fromJson[T](json: String)(implicit m : Manifest[T]): T = { + mapper.readValue[T](json) + } +} diff --git a/video-stream-generator/src/main/scala/org/sunbird/job/videostream/helpers/OCIRequestBody.scala b/video-stream-generator/src/main/scala/org/sunbird/job/videostream/helpers/OCIRequestBody.scala new file mode 100644 index 000000000..6b2564ce5 --- /dev/null +++ b/video-stream-generator/src/main/scala/org/sunbird/job/videostream/helpers/OCIRequestBody.scala @@ -0,0 +1,14 @@ +package org.sunbird.job.videostream.helpers + +object OCIRequestBody { + val get_object_parameters = "{\"taskParameters\": [{\"storageType\": \"objectStorage\",\"target\": \"getFiles/${/input/objectName}\",\"namespaceName\": \"${/input/namespaceName}\",\"bucketName\": \"${/input/bucketName}\",\"objectName\": \"${/input/objectName}\"}]}" + val transcode_parameters = "{ \"transcodeType\": \"standardTranscode\", \"standardTranscode\": { \"input\": \"${/getFiles/taskParameters/0/target}\", \"outputPrefix\": \"${/transcode/outputPrefix}\", \"videoCodec\": \"h264\", \"audioCodec\": \"aac\", \"packaging\": { \"packageType\": \"hls\", \"segmentLength\": 6 }, \"ladder\": [ { \"size\": { \"height\": 1080, \"resizeMethod\": \"scale\" } }, { \"size\": { \"height\": 720, \"resizeMethod\": \"scale\" } }, { \"size\": { \"height\": 480, \"resizeMethod\": \"scale\" } }, { \"size\": { \"height\": 360, \"resizeMethod\": \"scale\" } } ] } }" + val thumbnail_parameters = "{ \"thumbnails\": { \"input\": \"${/getFiles/taskParameters/0/target}\", \"frameSelectors\": [ { \"namePrefix\": \"thumb\", \"format\": \"jpg\", \"sizes\": [ { \"width\": 390 }, { \"width\": 196 } ], \"clipImagePicker\": { \"percentList\": { \"pickList\": [ 10, 20, 30 ] } } } ] } }" + // val transcribe_parameters = "{ \"inputVideo\": \"${/getFiles/taskParameters/0/target}\", \"outputAudio\": \"${/output/objectNamePath}${/transcode/outputPrefix}.wav\", \"outputBucketName\": \"${/output/bucketName}\", \"outputNamespaceName\": \"${/output/namespaceName}\", \"outputTranscriptionPrefix\": \"${/output/objectNamePath}${/transcode/outputPrefix}\", \"transcriptionJobCompartment\": \"${/output/assetCompartmentId}\", \"waitForCompletion\" : true }" + val final_task_parameters = "{ \"taskParameters\": [ { \"namespaceName\": \"${/output/namespaceName}\", \"bucketName\": \"${/output/bucketName}\", \"source\": \"${/transcode/outputPrefix}*.m3u8\", \"objectName\": \"${/output/objectNamePath}${/transcode/outputPrefix}\", \"assetCompartmentId\": \"${/output/assetCompartmentId}\", \"registerMetadata\": true }, { \"namespaceName\": \"${/output/namespaceName}\", \"bucketName\": \"${/output/bucketName}\", \"source\": \"master.m3u8\", \"objectName\": \"${/output/objectNamePath}${/transcode/outputPrefix}-master.m3u8\", \"assetCompartmentId\": \"${/output/assetCompartmentId}\", \"registerMetadata\": true }, { \"namespaceName\": \"${/output/namespaceName}\", \"bucketName\": \"${/output/bucketName}\", \"source\": \"*.fmp4\", \"objectName\": \"${/output/objectNamePath}\", \"assetCompartmentId\": \"${/output/assetCompartmentId}\", \"registerMetadata\": true }, { \"namespaceName\": \"${/output/namespaceName}\", \"bucketName\": \"${/output/bucketName}\", \"source\": \"*.${/thumbnail/thumbnails/frameSelectors/0/format}\", \"objectName\": \"${/output/objectNamePath}${/transcode/outputPrefix}-\", \"assetCompartmentId\": \"${/output/assetCompartmentId}\", \"registerMetadata\": true } ] }" + val ingest_parameters = "{\"distributionChannelId\": \"distribution_channel_id\",\"masterPlaylistName\": \"master.m3u8\"}" + + val media_workflow_parameters = "{ \"input\": { \"objectName\": \"${/video/srcVideo}\", \"bucketName\": \"${/video/srcBucket}\", \"namespaceName\": \"${/video/namespace}\" }, \"output\": { \"bucketName\": \"${/video/dstBucket}\", \"namespaceName\": \"${/video/namespace}\", \"assetCompartmentId\": \"${/video/compartmentID}\", \"objectNamePath\": \"output/${/input/objectName}/\" }, \"transcode\": { \"outputPrefix\": \"${/video/outputPrefixName}\" } }" + val media_workflow_job_parameters = "{ \"video\": { \"srcBucket\": \"src_bucket\", \"dstBucket\": \"dst_bucket\", \"namespace\": \"namespace\", \"compartmentID\": \"compartment_id\", \"srcVideo\": \"src_video\", \"outputPrefixName\" : \"prefix_input\" } }" +} + diff --git a/video-stream-generator/src/main/scala/org/sunbird/job/videostream/helpers/OCIResult.scala b/video-stream-generator/src/main/scala/org/sunbird/job/videostream/helpers/OCIResult.scala new file mode 100644 index 000000000..871ba1338 --- /dev/null +++ b/video-stream-generator/src/main/scala/org/sunbird/job/videostream/helpers/OCIResult.scala @@ -0,0 +1,52 @@ +package org.sunbird.job.videostream.helpers + +import org.apache.commons.lang3.StringUtils + +import scala.collection.immutable.HashMap + +object OCIResult extends Result { + + override def getSubmitJobResult(response: MediaResponse): Map[String, AnyRef] = { + val result = response.result + HashMap[String, AnyRef]( + "job" -> HashMap[String, AnyRef]( + "id" -> result.getOrElse("id", "").toString, + "status" -> result.getOrElse("lifecycleState", "").toString.toUpperCase(), + "submittedOn" -> result.getOrElse("timeCreated", "").toString, + "lastModifiedOn" -> result.getOrElse("timeUpdated", "").toString + ) + ) + } + + override def getJobResult(response: MediaResponse): Map[String, AnyRef] = { + val result = response.result + + HashMap[String, AnyRef]( + "job" -> HashMap[String, AnyRef]( + "id" -> result.getOrElse("id", "").toString, + "status" -> result.getOrElse("lifecycleState", "").toString.toUpperCase(), + "submittedOn" -> result.getOrElse("timeCreated", "").toString, + "lastModifiedOn" -> result.getOrElse("timeUpdated", "").toString, + "error" -> { + if (StringUtils.equalsIgnoreCase(result.getOrElse("lifecycleState", "").toString.toUpperCase(),"FAILED")) { + Map[String, String]( + "errorCode" -> result.getOrElse("lifecycleState", "").toString, + "errorMessage" -> result.getOrElse("lifecycleDetails", "").toString + ) + } else { + null + } + } + ) + ) + } + + override def getCancelJobResult(response: MediaResponse): Map[String, AnyRef] = { + null + } + + override def getListJobResult(response: MediaResponse): Map[String, AnyRef] = { + null + } + +} diff --git a/video-stream-generator/src/main/scala/org/sunbird/job/videostream/service/VideoStreamService.scala b/video-stream-generator/src/main/scala/org/sunbird/job/videostream/service/VideoStreamService.scala index e83b17b0d..3dabfba85 100644 --- a/video-stream-generator/src/main/scala/org/sunbird/job/videostream/service/VideoStreamService.scala +++ b/video-stream-generator/src/main/scala/org/sunbird/job/videostream/service/VideoStreamService.scala @@ -18,7 +18,7 @@ class VideoStreamService(implicit config: VideoStreamGeneratorConfig, httpUtil: private lazy val mediaService = MediaServiceFactory.getMediaService(config) private lazy val dbKeyspace:String = config.dbKeyspace private lazy val dbTable:String = config.dbTable - lazy val cassandraUtil:CassandraUtil = new CassandraUtil(config.lmsDbHost, config.lmsDbPort) + lazy val cassandraUtil:CassandraUtil = new CassandraUtil(config.lmsDbHost, config.lmsDbPort, config) private lazy val clientKey:String = "SYSTEM_LP" private lazy val SUBMITTED:String = "SUBMITTED" private lazy val VIDEO_STREAMING:String = "VIDEO_STREAMING" @@ -49,22 +49,28 @@ class VideoStreamService(implicit config: VideoStreamGeneratorConfig, httpUtil: val streamStage = if (jobRequest.job_id != None) { val mediaResponse:MediaResponse = mediaService.getJob(jobRequest.job_id.get) logger.info("Get job details while saving: " + JSONUtil.serialize(mediaResponse.result)) + logger.info("updateProcessingRequest mediaResponse.responseCode: " + mediaResponse.responseCode) if(mediaResponse.responseCode.contentEquals("OK")) { val job = mediaResponse.result.getOrElse("job", Map()).asInstanceOf[Map[String, AnyRef]] val jobStatus = job.getOrElse("status","").asInstanceOf[String] - + val workFlowJobId = job.getOrElse("id","").asInstanceOf[String] + logger.info("updateProcessingRequest job: " + job) + logger.info("updateProcessingRequest workFlowJobId: " + workFlowJobId) if(config.jobStatus.contains(jobStatus)) { - val streamingUrl = mediaService.getStreamingPaths(jobRequest.job_id.get).result.getOrElse("streamUrl","").asInstanceOf[String] + val streamingUrl = mediaService.getStreamingPaths(workFlowJobId).result.getOrElse("streamUrl","").asInstanceOf[String] val requestData = JSONUtil.deserialize[Map[String, AnyRef]](jobRequest.request_data) val contentId = requestData.getOrElse("identifier", "").asInstanceOf[String] val channel = requestData.getOrElse("channel", "").asInstanceOf[String] - + logger.info("Before calling updatePreviewUrl : contentId ::" + contentId+" streamingUrl ::"+streamingUrl+" channel ::"+channel) if(updatePreviewUrl(contentId, streamingUrl, channel)) { + logger.info("updatePreviewUrl COMPLETE : contentId ::" + contentId) StreamingStage(jobRequest.request_id, jobRequest.client_key, jobRequest.job_id.get, stageName, jobStatus, "FINISHED", iteration + 1); } else { - null + // Set job status to FAILED + val errMessage = job.getOrElse("error", Map()).asInstanceOf[Map[String, AnyRef]].getOrElse("errorMessage", "Error updating streamingUrl").asInstanceOf[String] + StreamingStage(jobRequest.request_id, jobRequest.client_key, jobRequest.job_id.get, stageName, jobStatus, "FAILED", iteration + 1, errMessage) } - } else if(jobStatus.equalsIgnoreCase("ERROR")){ + } else if(jobStatus.equalsIgnoreCase("ERROR") || jobStatus.equalsIgnoreCase("FAILED")){ val errMessage = job.getOrElse("error", Map()).asInstanceOf[Map[String, AnyRef]].getOrElse("errorMessage", "No error message").asInstanceOf[String] StreamingStage(jobRequest.request_id, jobRequest.client_key, jobRequest.job_id.get, stageName, jobStatus, "FAILED", iteration + 1, errMessage) } else { @@ -121,13 +127,16 @@ class VideoStreamService(implicit config: VideoStreamGeneratorConfig, httpUtil: val requestBody = "{\"request\": {\"content\": {\"streamingUrl\":\""+ streamingUrl +"\"}}}" val url = config.lpURL + config.contentV4Update + contentId val headers = Map[String, String]("X-Channel-Id" -> channel, "Content-Type"->"application/json") + logger.info("Before updating streamingUrl : url ::" + url+" requestBody ::"+requestBody) val response:HTTPResponse = httpUtil.patch(url, requestBody, headers) - + logger.info("updatePreviewUrl() response.status ::" + response.status) if(response.status == 200){ + logger.info("StreamingUrl updated successfully. url ::" + url+" contentId ::"+contentId) true } else { logger.error("Error while updating previewUrl for content : " + contentId + " :: "+response.body) - throw new Exception("Error while updating previewUrl for content : " + contentId + " :: "+response.body) +// throw new Exception("Error while updating previewUrl for content : " + contentId + " :: "+response.body) + false } } else { false @@ -156,7 +165,7 @@ class VideoStreamService(implicit config: VideoStreamGeneratorConfig, httpUtil: }) selectWhere.and(QueryBuilder.eq("job_name", VIDEO_STREAMING)) - + logger.info("readFromDB : Query ::" + selectWhere.toString) val result = cassandraUtil.find(selectWhere.toString).asScala.toList.map { jr => JobRequest(jr.getString("client_key"), jr.getString("request_id"), Option(jr.getString("job_id")), jr.getString("status"), jr.getString("request_data"), jr.getInt("iteration"), stage=Option(jr.getString("stage")), stage_status=Option(jr.getString("stage_status")),job_name=Option(jr.getString("job_name"))) } diff --git a/video-stream-generator/src/main/scala/org/sunbird/job/videostream/service/impl/MediaServiceFactory.scala b/video-stream-generator/src/main/scala/org/sunbird/job/videostream/service/impl/MediaServiceFactory.scala index 16e841fdd..23a242b38 100644 --- a/video-stream-generator/src/main/scala/org/sunbird/job/videostream/service/impl/MediaServiceFactory.scala +++ b/video-stream-generator/src/main/scala/org/sunbird/job/videostream/service/impl/MediaServiceFactory.scala @@ -11,6 +11,7 @@ object MediaServiceFactory { serviceType match { case "azure" => AzureMediaServiceImpl case "aws" => AwsMediaServiceImpl + case "oci" => OCIMediaServiceImpl case _ => throw new MediaServiceException("ERR_INVALID_SERVICE_TYPE", "Please Provide Valid Media Service Name") } } diff --git a/video-stream-generator/src/main/scala/org/sunbird/job/videostream/service/impl/OCIMediaServiceImpl.scala b/video-stream-generator/src/main/scala/org/sunbird/job/videostream/service/impl/OCIMediaServiceImpl.scala new file mode 100644 index 000000000..e937a7b23 --- /dev/null +++ b/video-stream-generator/src/main/scala/org/sunbird/job/videostream/service/impl/OCIMediaServiceImpl.scala @@ -0,0 +1,118 @@ +package org.sunbird.job.videostream.service.impl + +import org.slf4j.LoggerFactory +import org.sunbird.job.util.HttpUtil +import org.sunbird.job.videostream.helpers.{MediaRequest, MediaResponse, MediaServiceHelper, OCIRequestBody, Response, ResponseCode} +import org.sunbird.job.videostream.service.IMediaService +import org.sunbird.job.videostream.task.VideoStreamGeneratorConfig +import scala.collection.immutable.HashMap +import com.google.gson.Gson +import java.util +import org.sunbird.job.util.JSONUtil + + +object OCIMediaServiceImpl extends IMediaService { + + private[this] val logger = LoggerFactory.getLogger("OCIMediaServiceImpl") + + override def submitJob(request: MediaRequest)(implicit config: VideoStreamGeneratorConfig, httpUtil: HttpUtil): MediaResponse = { + + val inputUrl = request.request.getOrElse("artifactUrl", "").toString + logger.info("inputUrl...{}",inputUrl) + val contentId = request.request.get("identifier").mkString + val compartment_id = config.getConfig("oci.compartment_id") + logger.info("compartment_id...{}",compartment_id) + val src_bucket = inputUrl.split("/")(3); + logger.info("src_bucket...{}",src_bucket) + val dst_bucket = config.getConfig("oci.bucket.processed_bucket_name") + val namespace = config.getConfig("oci.namespace") + val temp = inputUrl.splitAt(inputUrl.lastIndexOf("/") + 1) + val src_video = inputUrl.substring(inputUrl.indexOf(inputUrl.split("/")(3))+inputUrl.split("/")(3).length+1, inputUrl.length) + logger.info("src_video...{}",src_video) + val prefix_input = config.getConfig("oci.stream.prefix_input") + val media_flow_id = config.getConfig("oci.stream.work_flow_id") + logger.info("media_flow_id...{}",media_flow_id) + val mediaServiceHelper = new MediaServiceHelper() + val mediaflowjobParameters = "{ \"video\": { \"srcBucket\": \"" + src_bucket + "\", \"dstBucket\": \""+ dst_bucket + "\", \"namespace\": \"" + namespace + "\", \"compartmentID\": \"" + compartment_id + "\", \"srcVideo\": \"" + src_video + "\", \"outputPrefixName\" : \"" + prefix_input + "\" } }" + val mediaFlowResp = mediaServiceHelper.submitJob(compartment_id, media_flow_id, mediaServiceHelper.createJSONObject(mediaflowjobParameters)) + logger.info("mediaFlowResp.getId...{}",mediaFlowResp.getId) + if (mediaFlowResp.getLifecycleState != "FAILED") { + MediaResponse(mediaFlowResp.getId, System.currentTimeMillis().toString, new HashMap[String, AnyRef], + ResponseCode.OK.toString, HashMap[String, AnyRef]( + "job" -> HashMap[String, AnyRef]( + "id" -> mediaFlowResp.getId, + "status" -> mediaFlowResp.getLifecycleState.toString.toUpperCase, + "submittedOn" -> mediaFlowResp.getTimeCreated.toString, + "lastModifiedOn" -> mediaFlowResp.getTimeUpdated.toString + ) + )) + }else { + Response.getFailureResponse(HashMap[String, AnyRef]( + "job" -> HashMap[String, AnyRef]( + "id" -> mediaFlowResp.getId, + "status" -> mediaFlowResp.getLifecycleState.toString.toUpperCase, + "submittedOn" -> mediaFlowResp.getTimeCreated.toString, + "lastModifiedOn" -> mediaFlowResp.getTimeUpdated.toString + ) + ), "SERVER_ERROR", "Output Asset [ " + contentId + " ] Creation Failed for Job : " + mediaFlowResp.getId) + } + } + def jsonToMap(json: String): Map[String, AnyRef] = { + val gson = new Gson() + gson.fromJson(json, new util.LinkedHashMap[String, AnyRef]().getClass).asInstanceOf[Map[String, AnyRef]] + } + + override def getJob(jobId: String)(implicit config: VideoStreamGeneratorConfig, httpUtil: HttpUtil): MediaResponse = { + val mediaServiceHelper = new MediaServiceHelper() + try + { + val mediaFlowResp = mediaServiceHelper.getWorkflowJob(jobId); + logger.info("LifecycleState...{}",mediaFlowResp.getLifecycleState) + if (mediaFlowResp.getLifecycleState != "FAILED") { + MediaResponse(mediaFlowResp.getId, System.currentTimeMillis().toString, new HashMap[String, AnyRef], + ResponseCode.OK.toString, HashMap[String, AnyRef]( + "job" -> HashMap[String, AnyRef]( + "id" -> mediaFlowResp.getId, + "status" -> mediaFlowResp.getLifecycleState.toString.toUpperCase, + "submittedOn" -> mediaFlowResp.getTimeCreated.toString, + "lastModifiedOn" -> mediaFlowResp.getTimeUpdated.toString + ) + )) + } else { + Response.getFailureResponse(HashMap[String, AnyRef]( + "job" -> HashMap[String, AnyRef]( + "id" -> mediaFlowResp.getId, + "status" -> mediaFlowResp.getLifecycleState.toString.toUpperCase, + "submittedOn" -> mediaFlowResp.getTimeCreated.toString, + "lastModifiedOn" -> mediaFlowResp.getTimeUpdated.toString + ) + ), "SERVER_ERROR", "Get WorkFlowJob Failed for the Id : " + mediaFlowResp.getId) + } } catch { + case e: Exception => e.printStackTrace() + Response.getFailureResponse(new HashMap[String, AnyRef], "SERVER_ERROR", "Get WorkFlowJob Failed for Job : " + jobId) + } + } + + override def getStreamingPaths(jobId: String)(implicit config: VideoStreamGeneratorConfig, httpUtil: HttpUtil): MediaResponse = { + val mediaServiceHelper = new MediaServiceHelper() + val gatewayDomain = config.getConfig("oci.stream.gateway_domain") + val streamUrl = mediaServiceHelper.getStreamingPaths(jobId, gatewayDomain) + logger.info("streamUrl...{}",streamUrl) + if (streamUrl == null || streamUrl == None) { + Response.getFailureResponse(new HashMap[String, AnyRef], "SERVER_ERROR", "Streaming Locator Creation Failed for Job : " + jobId) + } + else + { + Response.getSuccessResponse(HashMap[String, AnyRef]("streamUrl" -> streamUrl)) + } + } + + override def listJobs(listJobsRequest: MediaRequest): MediaResponse = { + null + } + + override def cancelJob(cancelJobRequest: MediaRequest): MediaResponse = { + null + } + +} diff --git a/video-stream-generator/src/main/scala/org/sunbird/job/videostream/task/VideoStreamGeneratorConfig.scala b/video-stream-generator/src/main/scala/org/sunbird/job/videostream/task/VideoStreamGeneratorConfig.scala index de3fe50ac..196712dbc 100644 --- a/video-stream-generator/src/main/scala/org/sunbird/job/videostream/task/VideoStreamGeneratorConfig.scala +++ b/video-stream-generator/src/main/scala/org/sunbird/job/videostream/task/VideoStreamGeneratorConfig.scala @@ -43,7 +43,7 @@ class VideoStreamGeneratorConfig(override val config: Config) extends BaseJobCon val lpURL: String = config.getString("service.content.basePath") val contentV4Update = "/content/v4/system/update/" - val jobStatus:util.List[String] = if(config.hasPath("media_service_job_success_status")) config.getStringList("media_service_job_success_status") else util.Arrays.asList("FINISHED", "COMPLETE") + val jobStatus:util.List[String] = if(config.hasPath("media_service_job_success_status")) config.getStringList("media_service_job_success_status") else util.Arrays.asList("FINISHED", "COMPLETE", "SUCCEEDED") def getConfig(key: String): String = { if (config.hasPath(key)) diff --git a/video-stream-generator/src/test/scala/org/sunbird/job/spec/VideoStreamGeneratorTaskTestSpec.scala b/video-stream-generator/src/test/scala/org/sunbird/job/spec/VideoStreamGeneratorTaskTestSpec.scala index d33f0cf43..cff9e7e32 100644 --- a/video-stream-generator/src/test/scala/org/sunbird/job/spec/VideoStreamGeneratorTaskTestSpec.scala +++ b/video-stream-generator/src/test/scala/org/sunbird/job/spec/VideoStreamGeneratorTaskTestSpec.scala @@ -52,7 +52,7 @@ class VideoStreamGeneratorTaskTestSpec extends BaseTestSpec { override protected def beforeAll(): Unit = { EmbeddedCassandraServerHelper.startEmbeddedCassandra(80000L) - cassandraUtil = new CassandraUtil(jobConfig.lmsDbHost, jobConfig.lmsDbPort) + cassandraUtil = new CassandraUtil(jobConfig.lmsDbHost, jobConfig.lmsDbPort, jobConfig) val session = cassandraUtil.session val dataLoader = new CQLDataLoader(session); dataLoader.load(new FileCQLDataSet(getClass.getResource("/test.cql").getPath, true, true)); diff --git a/video-stream-generator/src/test/scala/org/sunbird/job/spec/service/VideoStreamServiceTestSpec.scala b/video-stream-generator/src/test/scala/org/sunbird/job/spec/service/VideoStreamServiceTestSpec.scala index c0b3fdcbe..e1d2998b9 100644 --- a/video-stream-generator/src/test/scala/org/sunbird/job/spec/service/VideoStreamServiceTestSpec.scala +++ b/video-stream-generator/src/test/scala/org/sunbird/job/spec/service/VideoStreamServiceTestSpec.scala @@ -37,7 +37,7 @@ class VideoStreamServiceTestSpec extends BaseTestSpec { override protected def beforeAll(): Unit = { DateTimeUtils.setCurrentMillisFixed(1605816926271L); EmbeddedCassandraServerHelper.startEmbeddedCassandra(80000L) - cassandraUtil = new CassandraUtil(jobConfig.lmsDbHost, jobConfig.lmsDbPort) + cassandraUtil = new CassandraUtil(jobConfig.lmsDbHost, jobConfig.lmsDbPort, jobConfig) val session = cassandraUtil.session val dataLoader = new CQLDataLoader(session); dataLoader.load(new FileCQLDataSet(getClass.getResource("/test.cql").getPath, true, true));