From aa8012232724574e82c2675526a9abdd8564dbc6 Mon Sep 17 00:00:00 2001 From: eisbilir Date: Thu, 23 Nov 2023 15:08:48 +0300 Subject: [PATCH 01/11] feat: add api to fetch srm schedule --- .circleci/config.yml | 1 + config/default.js | 2 + package.json | 1 + src/common/srm-helper.js | 111 +++++++++++++++++++++++++ src/controllers/ChallengeController.js | 11 +++ src/routes.js | 6 ++ src/services/ChallengeService.js | 26 ++++++ yarn.lock | 44 ++++++++++ 8 files changed, 202 insertions(+) create mode 100644 src/common/srm-helper.js diff --git a/.circleci/config.yml b/.circleci/config.yml index cc1a3e94..b5bbd3f0 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -91,6 +91,7 @@ workflows: only: - dev - hotfix/budget-update + - CORE-140 - "build-qa": context: org-global diff --git a/config/default.js b/config/default.js index 3c4f0045..39f05b88 100644 --- a/config/default.js +++ b/config/default.js @@ -128,4 +128,6 @@ module.exports = { INTERNAL_CACHE_TTL: process.env.INTERNAL_CACHE_TTL || 1800, GRPC_CHALLENGE_SERVER_HOST: process.env.GRPC_DOMAIN_CHALLENGE_SERVER_HOST || "localhost", GRPC_CHALLENGE_SERVER_PORT: process.env.GRPC_DOMAIN_CHALLENGE_SERVER_PORT || 8888, + GRPC_ACL_SERVER_HOST: process.env.GRPC_ACL_SERVER_HOST || "localhost", + GRPC_ACL_SERVER_PORT: process.env.GRPC_ACL_SERVER_HOST || 8889, }; diff --git a/package.json b/package.json index 2226eb2f..c79e4272 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "@grpc/grpc-js": "^1.8.12", "@opensearch-project/opensearch": "^2.2.0", "@topcoder-framework/domain-challenge": "^0.24.0", + "@topcoder-framework/domain-acl": "^0.24.0", "@topcoder-framework/lib-common": "^0.24.0", "aws-sdk": "^2.1145.0", "axios": "^0.19.0", diff --git a/src/common/srm-helper.js b/src/common/srm-helper.js new file mode 100644 index 00000000..8c722e2c --- /dev/null +++ b/src/common/srm-helper.js @@ -0,0 +1,111 @@ +const _ = require("lodash"); +const moment = require("moment"); + +const SRMScheduleKeyMappings = _.reduce( + [ + "roundId", + "name", + "shortName", + "contestName", + "roundType", + "status", + "registrationStartTime", + "registrationEndTime", + "codingStartTime", + "codingEndTime", + "intermissionStartTime", + "intermissionEndTime", + "challengeStartTime", + "challengeEndTime", + "systestStartTime", + "systestEndTime", + ], + (acc, field) => ({ ...acc, [_.toLower(field)]: field }), + {} +); + +/** + * Get schedule query + * @param {Object} filter the query filter + * @param {Array} filter.statuses the statues + * @param {Date} filter.registrationStartTimeAfter the start of the registration time + * @param {Date} filter.registrationStartTimeBefore the end of the registration time + */ +function getSRMScheduleQuery(filter) { + const statuses = _.join( + _.map(filter.statuses, (s) => `'${_.toUpper(s)}'`), + "," + ); + const registrationTimeFilter = `reg.start_time >= ${moment( + filter.registrationStartTimeAfter + ).format("yyyy-MM-DD HH:mm:ss")}${ + filter.registrationStartTimeBefore + ? ` AND reg.start_time <= ${moment(filter.registrationStartTimeBefore).format( + "yyyy-MM-DD HH:mm:ss" + )}` + : "" + }`; + + const query = `SELECT + FIRST 50 + r.round_id AS roundId + , r.name AS name + , r.short_name AS shortName + , c.name AS contestName + , rt.round_type_desc AS roundType + , r.status AS status + , reg.start_time AS registrationStartTime + , reg.end_time AS registrationEndTime + , coding.start_time AS codingStartTime + , coding.end_time AS codingEndTime + , intermission.start_time AS intermissionStartTime + , intermission.end_time AS intermissionEndTime + , challenge.start_time AS challengeStartTime + , challenge.end_time AS challengeEndTime + , systest.start_time AS systestStartTime + , systest.end_time AS systestEndTime + FROM + informixoltp:contest AS c + INNER JOIN informixoltp:round AS r ON r.contest_id = c.contest_id + INNER JOIN informixoltp:round_type_lu AS rt ON rt.round_type_id = r.round_type_id + LEFT JOIN informixoltp:round_segment AS reg ON reg.round_id = r.round_id AND reg.segment_id = 1 + LEFT JOIN informixoltp:round_segment AS coding ON coding.round_id = r.round_id AND coding.segment_id = 2 + LEFT JOIN informixoltp:round_segment AS intermission ON intermission.round_id = r.round_id AND intermission.segment_id = 3 + LEFT JOIN informixoltp:round_segment AS challenge ON challenge.round_id = r.round_id AND challenge.segment_id = 4 + LEFT JOIN informixoltp:round_segment AS systest ON systest.round_id = r.round_id AND systest.segment_id = 5 + WHERE + r.round_type_id in (1,2,10) AND + UPPER(r.status) in (${statuses}) AND + ${registrationTimeFilter} + ORDER BY + reg.start_time DESC`; + return query; +} + +function convertSRMScheduleQueryOutput(queryOutput) { + return transformDatabaseResponse(queryOutput, SRMScheduleKeyMappings); +} + +function transformDatabaseResponse(databaseResponse, keyMappings) { + const transformedData = []; + + if (databaseResponse && databaseResponse.rows && Array.isArray(databaseResponse.rows)) { + databaseResponse.rows.forEach((row) => { + const record = {}; + if (row.fields && Array.isArray(row.fields)) { + row.fields.forEach((field) => { + const lowercaseKey = field.key.toLowerCase(); + const mappedKey = keyMappings[lowercaseKey] || lowercaseKey; + record[mappedKey] = field.value; + }); + } + transformedData.push(record); + }); + } + return transformedData; +} + +module.exports = { + getSRMScheduleQuery, + convertSRMScheduleQueryOutput, +}; diff --git a/src/controllers/ChallengeController.js b/src/controllers/ChallengeController.js index 18c61937..d279aab5 100644 --- a/src/controllers/ChallengeController.js +++ b/src/controllers/ChallengeController.js @@ -126,6 +126,16 @@ async function advancePhase(req, res) { res.send(await service.advancePhase(req, req.authUser, req.params.challengeId, req.body)); } +/** + * Get SRM Schedule + * @param {Object} req the request + * @param {Object} res the response + */ +async function getSRMSchedule(req, res) { + const result = await service.getSRMSchedule(req, req.query); + res.send(result); +} + module.exports = { searchChallenges, createChallenge, @@ -135,4 +145,5 @@ module.exports = { getChallengeStatistics, sendNotifications, advancePhase, + getSRMSchedule, }; diff --git a/src/routes.js b/src/routes.js index 74e571c8..eb121a6d 100644 --- a/src/routes.js +++ b/src/routes.js @@ -40,6 +40,12 @@ module.exports = { method: "createRequest", }, }, + "/challenges/srms/schedule": { + get: { + controller: "ChallengeController", + method: "getSRMSchedule", + }, + }, "/challenges/health": { get: { controller: "HealthController", diff --git a/src/services/ChallengeService.js b/src/services/ChallengeService.js index f0f7b110..c25a9ae1 100644 --- a/src/services/ChallengeService.js +++ b/src/services/ChallengeService.js @@ -33,6 +33,7 @@ const esClient = helper.getESClient(); const PhaseAdvancer = require("../phase-management/PhaseAdvancer"); const { ChallengeDomain } = require("@topcoder-framework/domain-challenge"); +const { QueryDomain } = require("@topcoder-framework/domain-acl"); const { hasAdminRole } = require("../common/role-helper"); const { @@ -44,6 +45,7 @@ const { } = require("../common/challenge-helper"); const deepEqual = require("deep-equal"); const { getM2MToken } = require("../common/m2m-helper"); +const { getSRMScheduleQuery, convertSRMScheduleQueryOutput } = require("../common/srm-helper"); const challengeDomain = new ChallengeDomain( GRPC_CHALLENGE_SERVER_HOST, @@ -65,6 +67,9 @@ const challengeDomain = new ChallengeDomain( }), } ); + +const aclQueryDomain = new QueryDomain(GRPC_ACL_SERVER_HOST, GRPC_ACL_SERVER_PORT); + const phaseAdvancer = new PhaseAdvancer(challengeDomain); /** @@ -2454,6 +2459,27 @@ async function indexChallengeAndPostToKafka(updatedChallenge, track, type) { }); } +/** + * Get SRM Schedule + * @param {Object} criteria the criteria + */ +async function getSRMSchedule(criteria = {}) { + if (!criteria.statuses) { + criteria.statuses = ["A", "F", "P"]; + } + const sql = getSRMScheduleQuery(criteria); + const result = await aclQueryDomain.rawQuery({ sql }); + return convertSRMScheduleQueryOutput(result); +} + +getSRMSchedule.schema = { + criteria: Joi.object().keys({ + registrationStartTimeAfter: Joi.date().default(new Date()), + registrationStartTimeBefore: Joi.date(), + statuses: Joi.array().items(Joi.string().valid(["A", "F", "P"])), + }), +}; + module.exports = { searchChallenges, createChallenge, diff --git a/yarn.lock b/yarn.lock index e8d35dd2..3bccd284 100644 --- a/yarn.lock +++ b/yarn.lock @@ -134,6 +134,14 @@ "@grpc/proto-loader" "^0.7.8" "@types/node" ">=12.12.47" +"@grpc/grpc-js@^1.8.7": + version "1.9.11" + resolved "https://registry.yarnpkg.com/@grpc/grpc-js/-/grpc-js-1.9.11.tgz#7b21195c910a49c0bb5d0df21d28a30c4e174851" + integrity sha512-QDhMfbTROOXUhLHMroow8f3EHiCKUOh6UwxMP5S3EuXMnWMNSVIhatGZRwkpg9OUTYdZPsDUVH3cOAkWhGFUJw== + dependencies: + "@grpc/proto-loader" "^0.7.8" + "@types/node" ">=12.12.47" + "@grpc/proto-loader@^0.7.8": version "0.7.10" resolved "https://registry.yarnpkg.com/@grpc/proto-loader/-/proto-loader-0.7.10.tgz#6bf26742b1b54d0a473067743da5d3189d06d720" @@ -260,6 +268,27 @@ topcoder-proto-registry "0.1.0" tslib "^2.4.1" +"@topcoder-framework/client-relational@^0.24.1": + version "0.24.1" + resolved "https://topcoder-409275337247.d.codeartifact.us-east-1.amazonaws.com/npm/topcoder-framework/@topcoder-framework/client-relational/-/client-relational-0.24.1.tgz#a01c4bb5a6117c38d5b61e1a79ded58abeb5b7c5" + integrity sha512-yZJS2N6l1YT/wadWRgMhMzrtFNfPgCBMVLUPO2ORHq182xXSIBkpXLRvX99z2XVPRGpjCVkfumh+jANQKX8AXw== + dependencies: + "@grpc/grpc-js" "^1.8.0" + "@topcoder-framework/lib-common" "^0.24.1" + topcoder-proto-registry "0.2.0" + tslib "^2.4.1" + +"@topcoder-framework/domain-acl@^0.24.0": + version "0.24.1" + resolved "https://topcoder-409275337247.d.codeartifact.us-east-1.amazonaws.com/npm/topcoder-framework/@topcoder-framework/domain-acl/-/domain-acl-0.24.1.tgz#61435da8d28756a319bc31bfd4f5b4458614f64f" + integrity sha512-7+uvCxkfURuXsBIwQBvn9aBobBheZCKlKQWzH4Dk5sCBxyhbjQ0B+TkFnfn2+65NzblwuvYEAEnJdLyAYu4RPA== + dependencies: + "@grpc/grpc-js" "^1.8.7" + "@topcoder-framework/client-relational" "^0.24.1" + "@topcoder-framework/lib-common" "^0.24.1" + topcoder-proto-registry "0.2.0" + tslib "^2.4.1" + "@topcoder-framework/domain-challenge@^0.24.0": version "0.24.0" resolved "https://topcoder-409275337247.d.codeartifact.us-east-1.amazonaws.com/npm/topcoder-framework/@topcoder-framework/domain-challenge/-/domain-challenge-0.24.0.tgz#023e57b95cc5213650eebffb860939b61d9b8068" @@ -281,6 +310,16 @@ topcoder-proto-registry "0.1.0" tslib "^2.4.1" +"@topcoder-framework/lib-common@^0.24.1": + version "0.24.1" + resolved "https://topcoder-409275337247.d.codeartifact.us-east-1.amazonaws.com/npm/topcoder-framework/@topcoder-framework/lib-common/-/lib-common-0.24.1.tgz#fc69af0f3deb263d347bfb8ac014065c5a7ceeec" + integrity sha512-Av/v5YybzyrJlhxANFxy+uJR938OWzd4vkcBZvAWmY4wX9D8UOiBA1nF2EMZ5+9xhY+PD3O/yuqnfqUs/4qT+g== + dependencies: + "@grpc/grpc-js" "^1.8.0" + rimraf "^3.0.2" + topcoder-proto-registry "0.2.0" + tslib "^2.4.1" + "@types/body-parser@*": version "1.19.4" resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.4.tgz#78ad68f1f79eb851aa3634db0c7f57f6f601b462" @@ -4012,6 +4051,11 @@ topcoder-proto-registry@0.1.0: resolved "https://registry.yarnpkg.com/topcoder-proto-registry/-/topcoder-proto-registry-0.1.0.tgz#7bdcb7df7c8bbf9d54beba1c69a6210d0f4ca097" integrity sha512-2RYGdDfCaX02pNcJu7ofb26O0SPe4MA6yfvpzXx6DjiuGtZu5QSZHkeaxqAlzRc9/F5zfWmGJwin4TOppo2xrA== +topcoder-proto-registry@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/topcoder-proto-registry/-/topcoder-proto-registry-0.2.0.tgz#703d636d2581b7b3903fe299f6c3d572c5f728c0" + integrity sha512-qmoAY0jb25A4S4bunUagj+wP++d1Db0iZqMc0SaMFjzW33dXjay7TpJDBbNZuVk4He7kUhYXrn2CDikbPM3TFw== + topo@3.x.x: version "3.0.3" resolved "https://registry.yarnpkg.com/topo/-/topo-3.0.3.tgz#d5a67fb2e69307ebeeb08402ec2a2a6f5f7ad95c" From 7e95bcce2303720e971fe93427a5e2d3dc2a62d6 Mon Sep 17 00:00:00 2001 From: eisbilir Date: Thu, 23 Nov 2023 15:16:35 +0300 Subject: [PATCH 02/11] fix: config --- src/services/ChallengeService.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/ChallengeService.js b/src/services/ChallengeService.js index c25a9ae1..682258f2 100644 --- a/src/services/ChallengeService.js +++ b/src/services/ChallengeService.js @@ -68,7 +68,7 @@ const challengeDomain = new ChallengeDomain( } ); -const aclQueryDomain = new QueryDomain(GRPC_ACL_SERVER_HOST, GRPC_ACL_SERVER_PORT); +const aclQueryDomain = new QueryDomain(config.GRPC_ACL_SERVER_HOST, config.GRPC_ACL_SERVER_PORT); const phaseAdvancer = new PhaseAdvancer(challengeDomain); From 4cd454d2a734aa6927d4ee995100c888563fd5f8 Mon Sep 17 00:00:00 2001 From: eisbilir Date: Thu, 23 Nov 2023 15:21:56 +0300 Subject: [PATCH 03/11] fix: export --- src/services/ChallengeService.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/services/ChallengeService.js b/src/services/ChallengeService.js index 682258f2..0b13b18f 100644 --- a/src/services/ChallengeService.js +++ b/src/services/ChallengeService.js @@ -2489,6 +2489,7 @@ module.exports = { getChallengeStatistics, sendNotifications, advancePhase, + getSRMSchedule, }; logger.buildService(module.exports); From 2b71a5493f74294f11fbd874a063dbf2fd8eb654 Mon Sep 17 00:00:00 2001 From: eisbilir Date: Thu, 23 Nov 2023 15:41:09 +0300 Subject: [PATCH 04/11] fix: config --- config/default.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/default.js b/config/default.js index 39f05b88..c0a71032 100644 --- a/config/default.js +++ b/config/default.js @@ -129,5 +129,5 @@ module.exports = { GRPC_CHALLENGE_SERVER_HOST: process.env.GRPC_DOMAIN_CHALLENGE_SERVER_HOST || "localhost", GRPC_CHALLENGE_SERVER_PORT: process.env.GRPC_DOMAIN_CHALLENGE_SERVER_PORT || 8888, GRPC_ACL_SERVER_HOST: process.env.GRPC_ACL_SERVER_HOST || "localhost", - GRPC_ACL_SERVER_PORT: process.env.GRPC_ACL_SERVER_HOST || 8889, + GRPC_ACL_SERVER_PORT: process.env.GRPC_ACL_SERVER_PORT || 8889, }; From 0bd5aaac43289c48f239be6c3cd4e5bfac6dad80 Mon Sep 17 00:00:00 2001 From: eisbilir Date: Thu, 23 Nov 2023 16:03:58 +0300 Subject: [PATCH 05/11] fix: query --- src/common/srm-helper.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/common/srm-helper.js b/src/common/srm-helper.js index 8c722e2c..4abea316 100644 --- a/src/common/srm-helper.js +++ b/src/common/srm-helper.js @@ -36,13 +36,13 @@ function getSRMScheduleQuery(filter) { _.map(filter.statuses, (s) => `'${_.toUpper(s)}'`), "," ); - const registrationTimeFilter = `reg.start_time >= ${moment( + const registrationTimeFilter = `reg.start_time >= '${moment( filter.registrationStartTimeAfter - ).format("yyyy-MM-DD HH:mm:ss")}${ + ).format("yyyy-MM-DD HH:mm:ss")}'${ filter.registrationStartTimeBefore - ? ` AND reg.start_time <= ${moment(filter.registrationStartTimeBefore).format( + ? ` AND reg.start_time <= '${moment(filter.registrationStartTimeBefore).format( "yyyy-MM-DD HH:mm:ss" - )}` + )}'` : "" }`; From c8dcd1437547028b659ba3598f9bbcbba03d05b8 Mon Sep 17 00:00:00 2001 From: eisbilir Date: Fri, 24 Nov 2023 14:16:09 +0300 Subject: [PATCH 06/11] add api to fetch srm practice problems --- .circleci/config.yml | 1 - src/common/srm-helper.js | 124 ++++++++++++++++++++++++- src/controllers/ChallengeController.js | 11 +++ src/routes.js | 7 ++ src/services/ChallengeService.js | 44 ++++++++- 5 files changed, 180 insertions(+), 7 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index b5bbd3f0..cc1a3e94 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -91,7 +91,6 @@ workflows: only: - dev - hotfix/budget-update - - CORE-140 - "build-qa": context: org-global diff --git a/src/common/srm-helper.js b/src/common/srm-helper.js index 4abea316..4c14ce44 100644 --- a/src/common/srm-helper.js +++ b/src/common/srm-helper.js @@ -24,12 +24,30 @@ const SRMScheduleKeyMappings = _.reduce( {} ); +const PracticeProblemsKeyMappings = _.reduce( + [ + "problemId", + "componentId", + "roomId", + "roundId", + "divisionId", + "problemName", + "problemType", + "difficulty", + "status", + "points", + "myPoints", + ], + (acc, field) => ({ ...acc, [_.toLower(field)]: field }), + {} +); + /** * Get schedule query * @param {Object} filter the query filter * @param {Array} filter.statuses the statues * @param {Date} filter.registrationStartTimeAfter the start of the registration time - * @param {Date} filter.registrationStartTimeBefore the end of the registration time + * @param {Date=} filter.registrationStartTimeBefore the end of the registration time */ function getSRMScheduleQuery(filter) { const statuses = _.join( @@ -82,10 +100,112 @@ function getSRMScheduleQuery(filter) { return query; } +/** + * Get schedule query + * @param {Object} criteria the query criteria + * @param {String} criteria.userId the user id + * @param {String} criteria.sortBy the sort field + * @param {String} criteria.sortOrder the sort order + * @param {Number} criteria.page the sort order + * @param {Number} criteria.perPage the sort order + * @param {String=} criteria.difficulty the sort order + * @param {String=} criteria.status the sort order + * @param {Number=} criteria.pointsLowerBound the sort order + * @param {Number=} criteria.pointsUpperBound the statues + * @param {String=} criteria.problemName the start of the registration time + */ +function getPracticeProblemsQuery(criteria) { + const offset = (criteria.page - 1) * criteria.perPage; + let sortBy = criteria.sortBy; + if (criteria.sortBy === "problemId") { + sortBy = "p.problem_id"; + } else if (criteria.sortBy === "problemName") { + sortBy = "p.name"; + } else if (criteria.sortBy === "problemType") { + sortBy = "ptl.problem_type_desc"; + } else if (criteria.sortBy === "points") { + sortBy = "rc.points"; + } else if (criteria.sortBy === "difficulty") { + sortBy = "p.proposed_difficulty_id"; + } else if (criteria.sortBy === "status") { + sortBy = "pcs.status_id"; + } else if (criteria.sortBy === "myPoints") { + sortBy = "pcs.point"; + } + const filters = []; + if (criteria.difficulty) { + if (criteria.difficulty === "easy") { + filters.push(`p.proposed_difficulty_id=1`); + } else if (criteria.difficulty === "medium") { + filters.push(`p.proposed_difficulty_id=2`); + } else if (criteria.difficulty === "hard") { + filters.push(`p.proposed_difficulty_id=3`); + } + } + if (criteria.status) { + if (criteria.status === "new") { + filters.push("NVL(pcs.status_id, 0) < 120"); + } else if (criteria.status === "viewed") { + filters.push("pcs.status_id >= 120 AND pcs.status_id != 150"); + } else if (criteria.status === "solved") { + filters.push("pcs.status_id = 150"); + } + } + if (criteria.pointsLowerBound) { + filters.push(`rc.points >= ${criteria.pointsLowerBound}`); + } + if (criteria.pointsUpperBound) { + filters.push(`rc.points <= ${criteria.pointsUpperBound}`); + } + if (criteria.problemName) { + filters.push( + `lower(p.name) like '%${_.toLower(_.replace(criteria.problemName, /[^a-z0-9]/gi, ""))}%'` + ); + } + + const query = `SELECT + SKIP ${offset} + FIRST ${criteria.perPage} + p.problem_id AS problemId + , c.component_id AS componentId + , ro.room_id AS roomId + , rc.round_id AS roundId + , rc.division_id AS divisionId + , p.name AS problemName + , ptl.problem_type_desc AS problemType + , CASE WHEN (p.problem_type_id = 1 AND p.proposed_difficulty_id = 1) THEN 'Easy'::nvarchar(50) + WHEN (p.problem_type_id = 1 AND p.proposed_difficulty_id = 2) THEN 'Medium'::nvarchar(50) + WHEN (p.problem_type_id = 1 AND p.proposed_difficulty_id = 3) THEN 'Hard'::nvarchar(50) + END AS difficulty + , rc.points AS points + , CASE WHEN NVL(pcs.status_id, 0) < 120 THEN 'New'::nvarchar(50) + WHEN pcs.status_id = 150 THEN 'Solved'::nvarchar(50) + WHEN pcs.status_id >= 120 AND pcs.status_id != 150 THEN 'Viewed'::nvarchar(50) + END AS status + , NVL(pcs.points, 0) AS myPoints + FROM informixoltp:problem p + INNER JOIN informixoltp:problem_type_lu ptl ON ptl.problem_type_id = p.problem_type_id + INNER JOIN informixoltp:component c ON c.problem_id = p.problem_id + INNER JOIN informixoltp:round_component rc ON rc.component_id = c.component_id + INNER JOIN informixoltp:round r ON r.round_id = rc.round_id AND r.status = 'A' AND r.round_type_id = 3 + INNER JOIN informixoltp:room ro ON ro.round_id = rc.round_id AND ro.room_type_id = 3 + LEFT JOIN informixoltp:component_state pcs ON pcs.round_id = rc.round_id AND pcs.component_id = c.component_id AND pcs.coder_id = ${ + criteria.userId + } + WHERE ${_.join(filters, " AND ")} + ORDER BY ${sortBy} ${criteria.sortOrder} + `; + return query; +} + function convertSRMScheduleQueryOutput(queryOutput) { return transformDatabaseResponse(queryOutput, SRMScheduleKeyMappings); } +function convertPracticeProblemsQueryOutput(queryOutput) { + return transformDatabaseResponse(queryOutput, PracticeProblemsKeyMappings); +} + function transformDatabaseResponse(databaseResponse, keyMappings) { const transformedData = []; @@ -108,4 +228,6 @@ function transformDatabaseResponse(databaseResponse, keyMappings) { module.exports = { getSRMScheduleQuery, convertSRMScheduleQueryOutput, + getPracticeProblemsQuery, + convertPracticeProblemsQueryOutput, }; diff --git a/src/controllers/ChallengeController.js b/src/controllers/ChallengeController.js index d279aab5..42db68f6 100644 --- a/src/controllers/ChallengeController.js +++ b/src/controllers/ChallengeController.js @@ -136,6 +136,16 @@ async function getSRMSchedule(req, res) { res.send(result); } +/** + * Get Practice Problems + * @param {Object} req the request + * @param {Object} res the response + */ +async function getPracticeProblems(req, res) { + const result = await service.getPracticeProblems(req, req.authUser, req.query); + res.send(result); +} + module.exports = { searchChallenges, createChallenge, @@ -146,4 +156,5 @@ module.exports = { sendNotifications, advancePhase, getSRMSchedule, + getPracticeProblems, }; diff --git a/src/routes.js b/src/routes.js index eb121a6d..8d46e69d 100644 --- a/src/routes.js +++ b/src/routes.js @@ -46,6 +46,13 @@ module.exports = { method: "getSRMSchedule", }, }, + "/challenges/srms/practice/problems": { + get: { + controller: "ChallengeController", + method: "getPracticeProblems", + auth: "jwt", + }, + }, "/challenges/health": { get: { controller: "HealthController", diff --git a/src/services/ChallengeService.js b/src/services/ChallengeService.js index 0b13b18f..8ad032e0 100644 --- a/src/services/ChallengeService.js +++ b/src/services/ChallengeService.js @@ -45,7 +45,12 @@ const { } = require("../common/challenge-helper"); const deepEqual = require("deep-equal"); const { getM2MToken } = require("../common/m2m-helper"); -const { getSRMScheduleQuery, convertSRMScheduleQueryOutput } = require("../common/srm-helper"); +const { + getSRMScheduleQuery, + getPracticeProblemsQuery, + convertSRMScheduleQueryOutput, + convertPracticeProblemsQueryOutput, +} = require("../common/srm-helper"); const challengeDomain = new ChallengeDomain( GRPC_CHALLENGE_SERVER_HOST, @@ -2464,9 +2469,6 @@ async function indexChallengeAndPostToKafka(updatedChallenge, track, type) { * @param {Object} criteria the criteria */ async function getSRMSchedule(criteria = {}) { - if (!criteria.statuses) { - criteria.statuses = ["A", "F", "P"]; - } const sql = getSRMScheduleQuery(criteria); const result = await aclQueryDomain.rawQuery({ sql }); return convertSRMScheduleQueryOutput(result); @@ -2476,7 +2478,38 @@ getSRMSchedule.schema = { criteria: Joi.object().keys({ registrationStartTimeAfter: Joi.date().default(new Date()), registrationStartTimeBefore: Joi.date(), - statuses: Joi.array().items(Joi.string().valid(["A", "F", "P"])), + statuses: Joi.array() + .items(Joi.string().valid(["A", "F", "P"])) + .default(["A", "F", "P"]), + }), +}; + +/** + * Get SRM Schedule + * @param {Object} currentUser the user who perform operation + * @param {Object} criteria the criteria + */ +async function getPracticeProblems(currentUser, criteria = {}) { + criteria.userId = currentUser.userId; + const sql = getPracticeProblemsQuery(criteria); + const result = await aclQueryDomain.rawQuery({ sql }); + return convertPracticeProblemsQueryOutput(result); +} + +getPracticeProblems.schema = { + currentUser: Joi.any(), + criteria: Joi.object().keys({ + sortBy: Joi.string() + .valid(["problemName", "problemType", "points", "difficulty", "status", "myPoints"]) + .default("problemId"), + sortOrder: Joi.string().valid(["asc", "desc"]).default("desc"), + page: Joi.page(), + perPage: Joi.perPage(), + difficulty: Joi.string().valid(["easy", "medium", "hard"]), + status: Joi.string().valid(["new", "viewed", "solved"]), + pointsLowerBound: Joi.number().integer(), + pointsUpperBound: Joi.number().integer(), + problemName: Joi.string(), }), }; @@ -2490,6 +2523,7 @@ module.exports = { sendNotifications, advancePhase, getSRMSchedule, + getPracticeProblems, }; logger.buildService(module.exports); From 97e600724cc28181e0f7a60e0cde1e0f718d96f8 Mon Sep 17 00:00:00 2001 From: eisbilir Date: Fri, 24 Nov 2023 16:21:35 +0300 Subject: [PATCH 07/11] feat: add pagination to practice problems api --- src/common/srm-helper.js | 17 +++++++++++------ src/controllers/ChallengeController.js | 3 ++- src/services/ChallengeService.js | 9 ++++++--- 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/src/common/srm-helper.js b/src/common/srm-helper.js index 4c14ce44..21d0f09b 100644 --- a/src/common/srm-helper.js +++ b/src/common/srm-helper.js @@ -163,7 +163,9 @@ function getPracticeProblemsQuery(criteria) { ); } - const query = `SELECT + const queryCount = `SELECT count(*) AS count`; + + const querySelect = `SELECT SKIP ${offset} FIRST ${criteria.perPage} p.problem_id AS problemId @@ -182,8 +184,9 @@ function getPracticeProblemsQuery(criteria) { WHEN pcs.status_id = 150 THEN 'Solved'::nvarchar(50) WHEN pcs.status_id >= 120 AND pcs.status_id != 150 THEN 'Viewed'::nvarchar(50) END AS status - , NVL(pcs.points, 0) AS myPoints - FROM informixoltp:problem p + , NVL(pcs.points, 0) AS myPoints`; + + const queryFrom = `FROM informixoltp:problem p INNER JOIN informixoltp:problem_type_lu ptl ON ptl.problem_type_id = p.problem_type_id INNER JOIN informixoltp:component c ON c.problem_id = p.problem_id INNER JOIN informixoltp:round_component rc ON rc.component_id = c.component_id @@ -193,9 +196,11 @@ function getPracticeProblemsQuery(criteria) { criteria.userId } WHERE ${_.join(filters, " AND ")} - ORDER BY ${sortBy} ${criteria.sortOrder} - `; - return query; + ORDER BY ${sortBy} ${criteria.sortOrder}`; + + const query = `${querySelect} ${queryFrom}`; + const countQuery = `${queryCount} ${queryFrom}`; + return { query, countQuery }; } function convertSRMScheduleQueryOutput(queryOutput) { diff --git a/src/controllers/ChallengeController.js b/src/controllers/ChallengeController.js index 42db68f6..38768dc1 100644 --- a/src/controllers/ChallengeController.js +++ b/src/controllers/ChallengeController.js @@ -143,7 +143,8 @@ async function getSRMSchedule(req, res) { */ async function getPracticeProblems(req, res) { const result = await service.getPracticeProblems(req, req.authUser, req.query); - res.send(result); + helper.setResHeaders(req, res, result); + res.send(result.result); } module.exports = { diff --git a/src/services/ChallengeService.js b/src/services/ChallengeService.js index 8ad032e0..ca12770f 100644 --- a/src/services/ChallengeService.js +++ b/src/services/ChallengeService.js @@ -2491,9 +2491,12 @@ getSRMSchedule.schema = { */ async function getPracticeProblems(currentUser, criteria = {}) { criteria.userId = currentUser.userId; - const sql = getPracticeProblemsQuery(criteria); - const result = await aclQueryDomain.rawQuery({ sql }); - return convertPracticeProblemsQueryOutput(result); + const { query, countQuery } = getPracticeProblemsQuery(criteria); + const resultOutput = await aclQueryDomain.rawQuery({ sql: query }); + const countOutput = await aclQueryDomain.rawQuery({ sql: countQuery }); + const result = convertPracticeProblemsQueryOutput(resultQueryOutput); + const total = countOutput.rows[0].fields[0].value; + return { total, page: criteria.page, perPage: criteria.perPage, result }; } getPracticeProblems.schema = { From 04bda304269154cf7334cb26a46bc01547216087 Mon Sep 17 00:00:00 2001 From: eisbilir Date: Fri, 24 Nov 2023 16:47:56 +0300 Subject: [PATCH 08/11] ci: deploy to dev --- .circleci/config.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index cc1a3e94..b5bbd3f0 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -91,6 +91,7 @@ workflows: only: - dev - hotfix/budget-update + - CORE-140 - "build-qa": context: org-global From c9695b59c6cc80628e01bb71a13bfdcab0bc4a66 Mon Sep 17 00:00:00 2001 From: eisbilir Date: Fri, 24 Nov 2023 17:01:02 +0300 Subject: [PATCH 09/11] fix: omit order by clause from count queryt --- src/common/srm-helper.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/common/srm-helper.js b/src/common/srm-helper.js index 21d0f09b..54a28b06 100644 --- a/src/common/srm-helper.js +++ b/src/common/srm-helper.js @@ -192,13 +192,13 @@ function getPracticeProblemsQuery(criteria) { INNER JOIN informixoltp:round_component rc ON rc.component_id = c.component_id INNER JOIN informixoltp:round r ON r.round_id = rc.round_id AND r.status = 'A' AND r.round_type_id = 3 INNER JOIN informixoltp:room ro ON ro.round_id = rc.round_id AND ro.room_type_id = 3 - LEFT JOIN informixoltp:component_state pcs ON pcs.round_id = rc.round_id AND pcs.component_id = c.component_id AND pcs.coder_id = ${ - criteria.userId - } - WHERE ${_.join(filters, " AND ")} - ORDER BY ${sortBy} ${criteria.sortOrder}`; + LEFT JOIN informixoltp:component_state pcs ON pcs.round_id = rc.round_id AND pcs.component_id = c.component_id AND pcs.coder_id = ${criteria.userId}`; + + const queryWhere = filters.length ? `WHERE ${_.join(filters, " AND ")}` : ""; + + const queryOrder = `ORDER BY ${sortBy} ${criteria.sortOrder}`; - const query = `${querySelect} ${queryFrom}`; + const query = `${querySelect} ${queryFrom} ${queryWhere} ${queryOrder}`; const countQuery = `${queryCount} ${queryFrom}`; return { query, countQuery }; } From 0b40d0ea8d8f5cea1c4543b19604e931edd349af Mon Sep 17 00:00:00 2001 From: eisbilir Date: Fri, 24 Nov 2023 17:19:38 +0300 Subject: [PATCH 10/11] feat: add pagination to srm schedule --- src/common/srm-helper.js | 21 +++++++++++++++++---- src/services/ChallengeService.js | 8 +++++++- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/src/common/srm-helper.js b/src/common/srm-helper.js index 54a28b06..afe5ed4a 100644 --- a/src/common/srm-helper.js +++ b/src/common/srm-helper.js @@ -48,8 +48,21 @@ const PracticeProblemsKeyMappings = _.reduce( * @param {Array} filter.statuses the statues * @param {Date} filter.registrationStartTimeAfter the start of the registration time * @param {Date=} filter.registrationStartTimeBefore the end of the registration time + * @param {String} filter.sortBy the sort field + * @param {String} filter.sortOrder the sort order + * @param {Number} filter.page the sort order + * @param {Number} filter.perPage the sort order */ function getSRMScheduleQuery(filter) { + const offset = (filter.page - 1) * filter.perPage; + let sortBy = filter.sortBy; + if (criteria.sortBy === "registrationStartTime") { + sortBy = "reg.start_time"; + } else if (criteria.sortBy === "codingStartTime") { + sortBy = "coding.start_time"; + } else if (criteria.sortBy === "challengeStartTime") { + sortBy = "challenge.start_time"; + } const statuses = _.join( _.map(filter.statuses, (s) => `'${_.toUpper(s)}'`), "," @@ -64,8 +77,9 @@ function getSRMScheduleQuery(filter) { : "" }`; - const query = `SELECT - FIRST 50 + const query = `SELECT + SKIP ${offset} + FIRST ${filter.perPage} r.round_id AS roundId , r.name AS name , r.short_name AS shortName @@ -95,8 +109,7 @@ function getSRMScheduleQuery(filter) { r.round_type_id in (1,2,10) AND UPPER(r.status) in (${statuses}) AND ${registrationTimeFilter} - ORDER BY - reg.start_time DESC`; + ORDER BY ${sortBy} ${filter.sortOrder}`; return query; } diff --git a/src/services/ChallengeService.js b/src/services/ChallengeService.js index ca12770f..c0c28d12 100644 --- a/src/services/ChallengeService.js +++ b/src/services/ChallengeService.js @@ -2481,6 +2481,12 @@ getSRMSchedule.schema = { statuses: Joi.array() .items(Joi.string().valid(["A", "F", "P"])) .default(["A", "F", "P"]), + sortBy: Joi.string() + .valid(["registrationStartTime", "codingStartTime", "challengeStartTime"]) + .default("registrationStartTime"), + sortOrder: Joi.string().valid(["asc", "desc"]).default("asc"), + page: Joi.page(), + perPage: Joi.perPage(), }), }; @@ -2494,7 +2500,7 @@ async function getPracticeProblems(currentUser, criteria = {}) { const { query, countQuery } = getPracticeProblemsQuery(criteria); const resultOutput = await aclQueryDomain.rawQuery({ sql: query }); const countOutput = await aclQueryDomain.rawQuery({ sql: countQuery }); - const result = convertPracticeProblemsQueryOutput(resultQueryOutput); + const result = convertPracticeProblemsQueryOutput(resultOutput); const total = countOutput.rows[0].fields[0].value; return { total, page: criteria.page, perPage: criteria.perPage, result }; } From b6bd76b5853522ae6f40fed6d05f340f329946f3 Mon Sep 17 00:00:00 2001 From: eisbilir Date: Fri, 24 Nov 2023 17:38:46 +0300 Subject: [PATCH 11/11] update count query --- src/common/srm-helper.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/common/srm-helper.js b/src/common/srm-helper.js index afe5ed4a..a87791e0 100644 --- a/src/common/srm-helper.js +++ b/src/common/srm-helper.js @@ -56,11 +56,11 @@ const PracticeProblemsKeyMappings = _.reduce( function getSRMScheduleQuery(filter) { const offset = (filter.page - 1) * filter.perPage; let sortBy = filter.sortBy; - if (criteria.sortBy === "registrationStartTime") { + if (filter.sortBy === "registrationStartTime") { sortBy = "reg.start_time"; - } else if (criteria.sortBy === "codingStartTime") { + } else if (filter.sortBy === "codingStartTime") { sortBy = "coding.start_time"; - } else if (criteria.sortBy === "challengeStartTime") { + } else if (filter.sortBy === "challengeStartTime") { sortBy = "challenge.start_time"; } const statuses = _.join( @@ -143,7 +143,7 @@ function getPracticeProblemsQuery(criteria) { } else if (criteria.sortBy === "status") { sortBy = "pcs.status_id"; } else if (criteria.sortBy === "myPoints") { - sortBy = "pcs.point"; + sortBy = "NVL(pcs.points, 0)"; } const filters = []; if (criteria.difficulty) { @@ -212,7 +212,7 @@ function getPracticeProblemsQuery(criteria) { const queryOrder = `ORDER BY ${sortBy} ${criteria.sortOrder}`; const query = `${querySelect} ${queryFrom} ${queryWhere} ${queryOrder}`; - const countQuery = `${queryCount} ${queryFrom}`; + const countQuery = `${queryCount} ${queryFrom} ${queryWhere}`; return { query, countQuery }; }