diff --git a/NodeApp/package-lock.json b/NodeApp/package-lock.json index 6bb6f37914629570747b79e26795d2aa055cc242..91b8632d612a211522a582126948057f656d9928 100644 --- a/NodeApp/package-lock.json +++ b/NodeApp/package-lock.json @@ -18,6 +18,7 @@ "axios": "^1.7.2", "boxen": "^5.1.2", "chalk": "^4.1.2", + "cli-table": "^0.3.11", "commander": "^12.1.0", "form-data": "^4.0.0", "fs-extra": "^11.2.0", @@ -2349,6 +2350,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/cli-table": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/cli-table/-/cli-table-0.3.11.tgz", + "integrity": "sha512-IqLQi4lO0nIB4tcdTpN4LCB9FI3uqrJZK7RC515EnhZ6qBaglkIgICb1wjeAqpdoOabm1+SuQtkXIPdYC93jhQ==", + "dependencies": { + "colors": "1.0.3" + }, + "engines": { + "node": ">= 0.2.0" + } + }, "node_modules/cli-width": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", @@ -2440,6 +2452,14 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" }, + "node_modules/colors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz", + "integrity": "sha512-pFGrxThWcWQ2MsAz6RtgeWe4NK2kUE1WfsrvvlctdII745EW9I0yflqhe7++M5LEc7bV2c/9/5zc8sFcpL0Drw==", + "engines": { + "node": ">=0.1.90" + } + }, "node_modules/colorspace": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz", diff --git a/NodeApp/package.json b/NodeApp/package.json index b6155d914200399fedf4c430cd13d65757a80cb9..c091bd791cb2b3f4cc5f6f3f25fbeb9ac1a434e3 100644 --- a/NodeApp/package.json +++ b/NodeApp/package.json @@ -1,16 +1,16 @@ { - "name" : "dojo_cli", - "description" : "CLI of the Dojo project", - "version" : "4.1.1", - "license" : "AGPLv3", - "author" : "Michaël Minelli <dojo@minelli.me>", - "main" : "dist/app.js", - "bin" : { + "name": "dojo_cli", + "description": "CLI of the Dojo project", + "version": "4.1.1", + "license": "AGPLv3", + "author": "Michaël Minelli <dojo@minelli.me>", + "main": "dist/app.js", + "bin": { "dojo": "./dist/app.js" }, - "pkg" : { + "pkg": { "scripts": [], - "assets" : [ + "assets": [ "node_modules/axios/dist/node/axios.cjs", ".env", "assets/**/*" @@ -24,57 +24,58 @@ "node18-win-x86" ] }, - "scripts" : { + "scripts": { "dotenv:build": "npx dotenvx encrypt", - "lint" : "npx eslint .", - "genversion" : "npx genversion -s -e src/config/Version.ts", - "build" : "npm run genversion; npx tsc", - "start:dev" : "npm run genversion; npm run lint; tsc --noEmit && npx tsx src/app.ts", - "test" : "echo \"Error: no test specified\" && exit 1" + "lint": "npx eslint .", + "genversion": "npx genversion -s -e src/config/Version.ts", + "build": "npm run genversion; npx tsc", + "start:dev": "npm run genversion; npm run lint; tsc --noEmit && npx tsx src/app.ts", + "test": "echo \"Error: no test specified\" && exit 1" }, - "dependencies" : { - "@dotenvx/dotenvx" : "^0.44.1", - "@eslint/js" : "^9.3.0", - "@gitbeaker/core" : "^40.0.3", + "dependencies": { + "@dotenvx/dotenvx": "^0.44.1", + "@eslint/js": "^9.3.0", + "@gitbeaker/core": "^40.0.3", "@gitbeaker/requester-utils": "^40.0.3", - "@gitbeaker/rest" : "^40.0.3", - "appdata-path" : "^1.0.0", - "axios" : "^1.7.2", - "boxen" : "^5.1.2", - "chalk" : "^4.1.2", - "commander" : "^12.1.0", - "form-data" : "^4.0.0", - "fs-extra" : "^11.2.0", - "http-status-codes" : "^2.3.0", - "inquirer" : "^8.2.6", - "json5" : "^2.2.3", - "jsonwebtoken" : "^8.5.1", - "open" : "^8.4.2", - "ora" : "^5.4.1", - "semver" : "^7.6.2", - "tar-stream" : "^3.1.7", - "winston" : "^3.13.0", - "winston-transport" : "^4.7.0", - "yaml" : "^2.4.2", - "zod" : "^3.23.8", - "zod-validation-error" : "^3.3.0" + "@gitbeaker/rest": "^40.0.3", + "appdata-path": "^1.0.0", + "axios": "^1.7.2", + "boxen": "^5.1.2", + "chalk": "^4.1.2", + "cli-table": "^0.3.11", + "commander": "^12.1.0", + "form-data": "^4.0.0", + "fs-extra": "^11.2.0", + "http-status-codes": "^2.3.0", + "inquirer": "^8.2.6", + "json5": "^2.2.3", + "jsonwebtoken": "^8.5.1", + "open": "^8.4.2", + "ora": "^5.4.1", + "semver": "^7.6.2", + "tar-stream": "^3.1.7", + "winston": "^3.13.0", + "winston-transport": "^4.7.0", + "yaml": "^2.4.2", + "zod": "^3.23.8", + "zod-validation-error": "^3.3.0" }, "devDependencies": { - "@types/fs-extra" : "^11.0.4", - "@types/inquirer" : "^8.2.10", - "@types/jsonwebtoken" : "^8.5.9", - "@types/node" : "^18.19.33", - "@types/semver" : "^7.5.8", - "@types/tar-stream" : "^3.1.3", + "@types/fs-extra": "^11.0.4", + "@types/inquirer": "^8.2.10", + "@types/jsonwebtoken": "^8.5.9", + "@types/node": "^18.19.33", + "@types/semver": "^7.5.8", + "@types/tar-stream": "^3.1.3", "@typescript-eslint/eslint-plugin": "^7.11.0", - "@typescript-eslint/parser" : "^7.11.0", - "dotenv-vault" : "^1.26.1", - "eslint" : "^8.57.0", - "genversion" : "^3.2.0", - "pkg" : "^5.8.1", - "tiny-typed-emitter" : "^2.1.0", - "tsx" : "^4.11.0", - "typescript" : "^5.4.5", - "typescript-eslint" : "^7.11.0" + "@typescript-eslint/parser": "^7.11.0", + "dotenv-vault": "^1.26.1", + "eslint": "^8.57.0", + "genversion": "^3.2.0", + "pkg": "^5.8.1", + "tiny-typed-emitter": "^2.1.0", + "tsx": "^4.11.0", + "typescript": "^5.4.5", + "typescript-eslint": "^7.11.0" } } diff --git a/NodeApp/src/commander/exercise/ExerciseCommand.ts b/NodeApp/src/commander/exercise/ExerciseCommand.ts index 1138f67cb5da89411d20ba7facc165672cc63fbf..690e6a28f43d635cad839ba32cc6c717bc27319c 100644 --- a/NodeApp/src/commander/exercise/ExerciseCommand.ts +++ b/NodeApp/src/commander/exercise/ExerciseCommand.ts @@ -2,7 +2,9 @@ import CommanderCommand from '../CommanderCommand.js'; import ExerciseCreateCommand from './subcommands/ExerciseCreateCommand.js'; import ExerciseRunCommand from './subcommands/ExerciseRunCommand.js'; import ExerciseCorrectionCommand from './subcommands/ExerciseCorrectionCommand.js'; -import ExerciseListCommand from "./subcommands/ExerciseListCommand"; +import ExerciseListCommand from "./subcommands/ExerciseListCommand"; +import ExerciseResultCommand from "./subcommands/ExerciseResultCommand"; +import ExerciseSummaryCommand from "./subcommands/ExerciseSummaryCommand"; class ExerciseCommand extends CommanderCommand { @@ -18,6 +20,9 @@ class ExerciseCommand extends CommanderCommand { ExerciseRunCommand.registerOnCommand(this.command); ExerciseCorrectionCommand.registerOnCommand(this.command); ExerciseListCommand.registerOnCommand(this.command); + ExerciseResultCommand.registerOnCommand(this.command); + ExerciseSummaryCommand.registerOnCommand(this.command); + } protected async commandAction(): Promise<void> { diff --git a/NodeApp/src/commander/exercise/subcommands/ExerciseListCommand.ts b/NodeApp/src/commander/exercise/subcommands/ExerciseListCommand.ts index 14ec7d36e75fde800708092f469a6b5578fa36c4..319e3bc3350282e6dce6af7496f8fe49e89cb999 100644 --- a/NodeApp/src/commander/exercise/subcommands/ExerciseListCommand.ts +++ b/NodeApp/src/commander/exercise/subcommands/ExerciseListCommand.ts @@ -101,7 +101,7 @@ class ExerciseListCommand extends CommanderCommand { message: 'Comment souhaitez-vous filtrer les exercices ?', choices: [ { name: 'Par saisie texte', value: 'fuzzy' }, - { name: 'Par professeurs', value: 'professor' }, + //{ name: 'Par professeurs', value: 'professor' }, { name: 'Exit', value: 'exit' }, ], }, diff --git a/NodeApp/src/commander/exercise/subcommands/ExerciseResultCommand.ts b/NodeApp/src/commander/exercise/subcommands/ExerciseResultCommand.ts index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..e46b7fd2652ffb9376118931a140657278a50235 100644 --- a/NodeApp/src/commander/exercise/subcommands/ExerciseResultCommand.ts +++ b/NodeApp/src/commander/exercise/subcommands/ExerciseResultCommand.ts @@ -0,0 +1,122 @@ +import CommanderCommand from '../../CommanderCommand'; +import chalk from 'chalk'; +import ora from 'ora'; +import DojoBackendManager from '../../../managers/DojoBackendManager'; +import Result from '../../../sharedByClients/models/Result'; +import inquirer from 'inquirer'; + + +class ExerciseResultCommand extends CommanderCommand { + protected commandName: string = 'result'; + + protected defineCommand(): void { + this.command + .description('results of an exercise') + .argument('<idOrLink>', 'display results of a specific exercise by Id or Gitlab Link') + .action(this.commandAction.bind(this)); + } + + protected async commandAction(idOrLink: string): Promise<void> { + const spinner = ora('Fetching exercise results...').start(); + + try { + const exerciseId = this.extractExerciseId(idOrLink); + // console.log('Exercise ID:', exerciseId); + spinner.info(`Fetching results for exercise with ID: ${exerciseId}`); + const results = await DojoBackendManager.getExerciseResults(exerciseId); + + if (!results) { + spinner.info('No results found for this exercise.'); + spinner.succeed('Exercise results fetched successfully.'); + return; + } + + if (results.length === 0) { + spinner.info('No results found for this exercise.'); + } else { + const answer = await inquirer.prompt([ + { + type: 'list', + name: 'testType', + message: 'Choisissez le type de tests à afficher:', + choices: ['Tests réussis', 'Tests échoués', 'Les deux'], + } + ]); + + const { testType } = answer; + + this.displayResults(results, testType); + spinner.succeed('Exercise results fetched successfully.'); + } + } catch (error) { + spinner.fail('Error fetching exercise results.'); + console.error(error); + } + } + + private extractExerciseId(idOrLink: string): string { + if (idOrLink.length <= 36) { + return idOrLink; + } else { + const lastUnderscoreIndex = idOrLink.lastIndexOf('_'); + // console.log('Last underscore index:', lastUnderscoreIndex); + if (lastUnderscoreIndex !== -1) { // -1 = pas de underscore trouvé + return idOrLink.substring(lastUnderscoreIndex + 1); // Extrait la sous-chaîne après le dernier underscore dans idOrLink + } else { + return ''; + } + } + } + + private displayResults(results: Result[], testType: string): void { + if (!results || results.length === 0) { + console.log('No results to display.'); + return; + } + + // Filtrer les résultats en fonction du type de test choisi + const filteredResults = results.filter(result => { + if (testType === 'Tests réussis') { + return result.success; + } else if (testType === 'Tests échoués') { + return !result.success; + } + return true; // 'Les deux' ou autre + }); + + if (filteredResults.length === 0) { + console.log('No results to display for the selected test type.'); + return; + } + + filteredResults.forEach(result => { + console.log(chalk.magenta(`Résultats de l\`exercice : ${result.exerciseId}`)); + console.log(` - Date et heure : ${result.dateTime}`); + console.log(` - Succès : ${result.success ? chalk.green('Oui') : chalk.red('Non')}`); + console.log(' - Détails des résultats :'); + console.log(` - Tests réussis : ${result.results.successfulTests}`); + console.log(` - Tests échoués : ${result.results.failedTests}`); + + if (testType === 'Tests réussis' || testType === 'Les deux') { + console.log(' - Liste des tests réussis :'); + if (Array.isArray(result.results.successfulTestsList)) { + result.results.successfulTestsList.forEach((test: string) => { + console.log(` - ${test} ${chalk.green('\u2713')}`); + }); + } + } + + if (testType === 'Tests échoués' || testType === 'Les deux') { + console.log(' - Liste des tests échoués :'); + if (Array.isArray(result.results.failedTestsList)) { + result.results.failedTestsList.forEach((test: string) => { + console.log(` - ${test} ${chalk.red('\u2717')}`); + }); + } + } + + console.log('-----------------------------------'); + }); + } +} +export default new ExerciseResultCommand(); diff --git a/NodeApp/src/commander/exercise/subcommands/ExerciseSummaryCommand.ts b/NodeApp/src/commander/exercise/subcommands/ExerciseSummaryCommand.ts index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..2e316fbe767c7332de795aeb53f273302b9c87d8 100644 --- a/NodeApp/src/commander/exercise/subcommands/ExerciseSummaryCommand.ts +++ b/NodeApp/src/commander/exercise/subcommands/ExerciseSummaryCommand.ts @@ -0,0 +1,103 @@ +import CommanderCommand from '../../CommanderCommand'; +import ora from 'ora'; +import DojoBackendManager from '../../../managers/DojoBackendManager'; +import Exercise from "../../../sharedByClients/models/Exercise"; +import Result from '../../../sharedByClients/models/Result'; +import Table from 'cli-table3'; + +class ExerciseSummaryCommand extends CommanderCommand { + protected commandName: string = 'summary'; + + protected defineCommand(): void { + this.command + .description('Display top exercises based on successful tests') + .action(this.commandAction.bind(this)); + } + + protected async commandAction(): Promise<void> { + const spinner = ora('Classsement... \n').start(); + + try { + const exercises = await DojoBackendManager.getUserExercises(); + const exerciseResults = await this.fetchExerciseResults(exercises); + + if (exerciseResults.length === 0) { + spinner.info('No exercise results found.'); + spinner.succeed('Exercise summary fetched successfully.'); + return; + } + + const sortedExercises = this.sortExercisesBySuccessfulTests(exerciseResults); + + this.displayExerciseSummary(sortedExercises); + + spinner.succeed('Exercise summary fetched successfully.'); + } catch (error) { + spinner.fail('Error fetching exercise summary.'); + console.error(error); + } + } + + private async fetchExerciseResults(exercises: Exercise[] | undefined): Promise<{ exercise: Exercise; successfulTests: number; dateTime: string }[]> { + const results: { exercise: Exercise, successfulTests: number, dateTime: string }[] = []; + + // @ts-ignore + for (const exercise of exercises) { + try { + const exerciseId = exercise.id; + const exerciseResults = await DojoBackendManager.getExerciseResults(exerciseId); + + if (exerciseResults) { + const successfulTests = this.countSuccessfulTests(exerciseResults); + results.push({ exercise, successfulTests, dateTime: exerciseResults[0]?.dateTime || '' }); + } + } catch (error) { + console.error(`Error fetching results for exercise ${exercise.id}:`, error); + } + } + + return results; + } + + private countSuccessfulTests(results: Result[]): number { + return results.reduce((count, result) => count + (result.success ? result.results.successfulTestsList.length : 0), 0); + } + + private sortExercisesBySuccessfulTests(exerciseResults: { exercise: Exercise, successfulTests: number, dateTime: string }[]): { exercise: Exercise, successfulTests: number, dateTime: string }[] { + return exerciseResults.sort((a, b) => b.successfulTests - a.successfulTests); + } + + private displayExerciseSummary(sortedExercises: { exercise: Exercise, successfulTests: number, dateTime: string }[]): void { + // Calculate the maximum width for each column + const headers = ['#', 'Exercise', 'Nb de tests réussis', 'Date']; + const maxWidths = headers.map(header => header.length); + + sortedExercises.forEach((exercise, index) => { + maxWidths[0] = Math.max(maxWidths[0], (index + 1).toString().length); + maxWidths[1] = Math.max(maxWidths[1], exercise.exercise.name?.length); + maxWidths[2] = Math.max(maxWidths[2], exercise.successfulTests.toString().length); + maxWidths[3] = Math.max(maxWidths[3], exercise.dateTime.length); + }); + + // Define colWidths based on maxWidths + const colWidths = maxWidths.map(width => ({ width })); + + // Create the table + const table = new Table({ + head: headers, + }); + + // Populate the table with data + sortedExercises.forEach((exercise, index) => { + table.push([ + index + 1, + exercise.exercise.name, + exercise.successfulTests, + exercise.dateTime + ]); + }); + + console.log(table.toString(),'\n'); + } +} +export default new ExerciseSummaryCommand();