From c7c2a19c1e22e04f0a14717b732f18acc7770e55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Minelli?= <git@minelli.swiss> Date: Tue, 25 Mar 2025 14:28:56 +0100 Subject: [PATCH] ExerciseSearchCommand => Refactor and fix the command --- .../subcommands/ExerciseListCommand.ts | 235 ------------- .../subcommands/ExerciseSearchCommand.ts | 312 ++++++++++++++++++ NodeApp/src/sharedByClients | 2 +- 3 files changed, 313 insertions(+), 236 deletions(-) delete mode 100644 NodeApp/src/commander/exercise/subcommands/ExerciseListCommand.ts create mode 100644 NodeApp/src/commander/exercise/subcommands/ExerciseSearchCommand.ts diff --git a/NodeApp/src/commander/exercise/subcommands/ExerciseListCommand.ts b/NodeApp/src/commander/exercise/subcommands/ExerciseListCommand.ts deleted file mode 100644 index 319e3bc..0000000 --- a/NodeApp/src/commander/exercise/subcommands/ExerciseListCommand.ts +++ /dev/null @@ -1,235 +0,0 @@ -// ExerciseListCommand.ts -import CommanderCommand from '../../CommanderCommand'; -import chalk from 'chalk'; -import ora from 'ora'; -import DojoBackendManager from '../../../managers/DojoBackendManager'; -import AccessesHelper from '../../../helpers/AccessesHelper'; -import Exercise from '../../../sharedByClients/models/Exercise'; -import inquirer from 'inquirer'; -import Table from 'cli-table3'; - -import Fuse from 'fuse.js'; -import User from '../../../sharedByClients/models/User'; - -class ExerciseListCommand extends CommanderCommand { - protected commandName: string = 'list'; - - protected defineCommand(): void { - this.command - .description('list your exercises') - .action(this.commandAction.bind(this)); - } - - protected async commandAction(): Promise<void> { - console.log(chalk.cyan('Please wait while we retrieve your exercises...')); - - // Check access - if (!await AccessesHelper.checkStudent()) { - return; - } - - // Fetch user's exercises - const userExercises: Exercise[] | undefined = await DojoBackendManager.getUserExercises(); - - if (!userExercises || userExercises.length === 0) { - ora().info('You have no exercises yet.'); - return; - } - - // Display the list of exercises - this.displayExerciseList(userExercises); - - // Ask the user for further actions - await this.askUserForActions(userExercises); - } - - private async askUserForActions(exercises: Exercise[]): Promise<void> { - const { action } = await inquirer.prompt([ - { - type: 'list', - name: 'action', - message: 'Que souhaitez-vous faire ?', - choices: [ - { name: 'Voir les détails d\'exercice', value: 'details'}, - { name: 'Filter les exercises', value: 'filter' }, - { name: 'Exit', value: 'exit' }, - ], - }, - ]); - - if (action === 'details') { - await this.selectExerciseForDetails(exercises); - } else if (action === 'filter') { - await this.filterExercises(exercises); - } else { - ora().info('No further actions selected.'); - } - } - - private async selectExerciseForDetails(exercises: Exercise[]): Promise<void> { - const { selectedExercise } = await inquirer.prompt([{ - type: 'list', - name: 'selectedExercise', - message: 'Selectionner un exercice :', - choices: [ - ...exercises.map(exercise => ({ - name: exercise.name, - value: exercise.id, - })), - { name: 'Exit', value: 'exit' }, - ], - }]); - - if (selectedExercise === 'exit') { - ora().info('Pas de détails requis: détails dispo avec la commande `dojo exercise info <id>`.'); - return; - } - - const selected = exercises.find(ex => ex.id === selectedExercise); - if (selected) { - await this.displayExerciseDetails(selected); - } else { - ora().info('Invalid selection. No exercise details to show.'); - } - } - - private async filterExercises(exercises: Exercise[]): Promise<void> { - const { filterType } = await inquirer.prompt([ - { - type: 'list', - name: 'filterType', - message: 'Comment souhaitez-vous filtrer les exercices ?', - choices: [ - { name: 'Par saisie texte', value: 'fuzzy' }, - //{ name: 'Par professeurs', value: 'professor' }, - { name: 'Exit', value: 'exit' }, - ], - }, - ]); - - if (filterType === 'fuzzy') { - await this.fuzzySearchExercises(exercises); - } else if (filterType === 'professor') { - await this.filterByProfessor(exercises); - } else { - ora().info('No filtering selected.'); - } - } - - private async fuzzySearchExercises(exercises: Exercise[]): Promise<void> { - const { searchQuery } = await inquirer.prompt([ - { - type: 'input', - name: 'searchQuery', - message: 'Entrez le nom de l\'exercice (laisser vide pour la liste complète) :', - }, - ]); - - if (!searchQuery) { - this.displayExerciseList(exercises); - return; - } - - const fuse = new Fuse(exercises, { - keys: ['name'], - threshold: 0.5, - distance: 150, - }); - - const searchResults = fuse.search(searchQuery).map(result => result.item); - - if (searchResults.length === 0) { - ora().info('Aucun exercice trouvé correspondant à votre recherche.'); - return; - } - - if (searchResults.length === 1) { - // Display details and members for the single matching exercise - const singleExercise = searchResults[0]; - this.displayExerciseDetails(singleExercise); - } else { - // Display only exercise names and info about viewing details - ora().info(' Plusieurs exercices trouvés correspondant à votre recherche :'); - const exerciseNames = searchResults.map(exercise => exercise.name); - console.log(' ', exerciseNames.join('\n ')); - - ora().info('Les détails sont disponibles avec la commande : `dojo exercise info <id>`.'); - } - } - - private async filterByProfessor(exercises: Exercise[]): Promise<void> { - - const professors: User[] | undefined = await DojoBackendManager.getProfessors(); - - if (!professors || professors.length === 0) { - ora().info('No professors found.'); - return; - } - - const professorChoices = professors.map(professor => ({ - name: `${professor.gitlabUsername}`, - value: professor // Use the professor object as the value - })); - - const { selectedProfessor } = await inquirer.prompt([ - { - type: 'list', - name: 'selectedProfessor', - message: 'Selectionnez un professeur:', - choices: professorChoices - } - ]); - - console.log(`Selected professor: ${selectedProfessor.gitlabUsername}`); - ora().info('Filter by professor is not yet implemented.'); - } - - private displayExerciseList(exercises: Exercise[]): void { - const headers = ['Exercise Name', 'GitLab Link']; - - // Calculate the maximum width for each column - const maxWidths = headers.map(header => header.length); - - exercises.forEach(exercise => { - maxWidths[0] = Math.max(maxWidths[0], exercise.name.length); - maxWidths[1] = Math.max(maxWidths[1], exercise.gitlabLink.length); - }); - - const table = new Table({ - head: headers, - }); - - exercises.forEach((exercise) => { - table.push([ - exercise.name, - exercise.gitlabLink, - ]); - }); - - ora().info('Your exercises:'); - console.log(table.toString()); - } - - private async displayExerciseDetails(exercise: Exercise): Promise<void> { - ora().info(`Detail of Exercise with ID: ${exercise.id}`); - console.log(chalk.magenta(' - Exercise Name:'), exercise.name); - console.log(chalk.magenta(' - Assignment Name:'), exercise.assignmentName); - console.log(chalk.magenta(' - GitLab ID:'), exercise.gitlabId); - console.log(chalk.magenta(' - GitLab Link:'), chalk.blue.underline(exercise.gitlabLink)); - console.log(chalk.magenta(' - GitLab Last Info Date:'), exercise.gitlabLastInfoDate); - - // Fetch exercise members - const exerciseMembers = await DojoBackendManager.getExerciseMembers(exercise.id); - - if (exerciseMembers && exerciseMembers.length > 0) { - ora().info('Exercise Members:'); - exerciseMembers.forEach(member => { - console.log(chalk.magenta(` - ${member.id} ${member.name}`)); - }); - } else { - ora().info('No members found for this exercise.'); - } - } -} - -export default new ExerciseListCommand(); \ No newline at end of file diff --git a/NodeApp/src/commander/exercise/subcommands/ExerciseSearchCommand.ts b/NodeApp/src/commander/exercise/subcommands/ExerciseSearchCommand.ts new file mode 100644 index 0000000..39c3cb7 --- /dev/null +++ b/NodeApp/src/commander/exercise/subcommands/ExerciseSearchCommand.ts @@ -0,0 +1,312 @@ +// ExerciseListCommand.ts +import CommanderCommand from '../../CommanderCommand'; +import ora from 'ora'; +import DojoBackendManager from '../../../managers/DojoBackendManager'; +import AccessesHelper from '../../../helpers/AccessesHelper'; +import Exercise from '../../../sharedByClients/models/Exercise'; +import inquirer from 'inquirer'; +import Table from 'cli-table3'; +import Fuse from 'fuse.js'; +import User from '../../../sharedByClients/models/User'; +import TextStyle from '../../../types/TextStyle'; +import ExerciseHelper from '../../../helpers/Dojo/ExerciseHelper'; +import { Option } from 'commander'; +import Config from '../../../config/Config'; + + +type CommandOptions = { all: boolean, name: string, teacher: string }; + + +class ExerciseListCommand extends CommanderCommand { + protected commandName: string = 'list'; + + protected teachers: User[] = []; + + protected allExercises: Exercise[] = []; + protected filteredExercises: Exercise[] = []; + + protected currentSearchFilter = ''; + + protected defineCommand(): void { + this.command + .description('list your exercises') + .addOption(new Option('-a, --all', 'list all exercises').conflicts([ 'name', 'teacher' ])) + .addOption(new Option('-n, --name <pattern_to_search>', 'search exercises by name').conflicts([ 'all', 'teacher' ])) + .addOption(new Option('-t, --teacher <pattern_to_search>', 'search exercises by teacher').conflicts([ 'all', 'name' ])) + .action(this.commandAction.bind(this)); + } + + private async dataRetrieval(getTeachers: boolean) { + console.log(TextStyle.BLOCK('Please wait while we verify and retrieve data...')); + + // Check access + await AccessesHelper.checkStudent(); + + // Fetch teachers + if ( getTeachers ) { + const teachersGetSpinner: ora.Ora = ora(`Fetching teachers`).start(); + try { + const teachers: Array<User> | undefined = await DojoBackendManager.getTeachers(); + if ( teachers ) { + this.teachers = teachers; + } else { + throw new Error(); + } + } catch ( error ) { + teachersGetSpinner.fail(`Error while fetching teachers.`); + throw new Error(); + } + teachersGetSpinner.succeed(`Teachers fetched successfully.`); + } + + // Fetch user's exercises + const exercisesGetSpinner: ora.Ora = ora(`Checking user's exercises`).start(); + + this.allExercises = await DojoBackendManager.getUserExercises() ?? []; + this.filteredExercises = this.allExercises; + + if ( this.allExercises.length === 0 ) { + exercisesGetSpinner.fail(`You do not have any exercises yet.`); + throw new Error(); + } + + exercisesGetSpinner.succeed(`User's exercises fetched successfully.`); + } + + private clear(): void { + this.currentSearchFilter = ''; + this.filteredExercises = this.allExercises; + } + + private async displayMenu(): Promise<void> { + // eslint-disable-next-line no-constant-condition + while ( true ) { + console.log(''); + ora(`${ '='.repeat(25) } Current filter: ${ this.currentSearchFilter == '' ? 'no filter' : this.currentSearchFilter } ${ '='.repeat(25) }`).info(); + + const action: string = (await inquirer.prompt([ { + type : 'list', + name : 'action', + message: 'What do you want ?', + choices: [ { + name : 'Display current filtered exercises list', + value: 'list' + }, new inquirer.Separator(), { + name : 'Get details of an exercise', + value: 'details' + }, new inquirer.Separator(), { + name : 'Filter by name', + value: 'fuzzy' + }, { + name : 'Filter by teacher', + value: 'teacher' + }, new inquirer.Separator(), { + name : 'Clear filters', + value: 'clear' + }, new inquirer.Separator(), { + name : 'Exit', + value: 'exit' + } ] + } ])).action; + + switch ( action ) { + case 'list': + await this.displayExerciseList(); + break; + case 'details': + await this.selectExerciseForDetails(); + return; + case 'fuzzy': + await this.filterByExerciseName(); + break; + case 'teacher': + await this.filterByTeacherInteractive(); + break; + case 'clear': + this.clear(); + break; + case 'exit': + throw new Error(); + default: + ora().info('Invalid filter type.'); + return; + } + } + } + + private async selectExerciseForDetails(): Promise<void> { + const { selectedExercise } = await inquirer.prompt([ { + type : 'list', + name : 'selectedExercise', + message: 'Please select an exercise :', + choices: [ ...this.filteredExercises.map(exercise => ({ + name : exercise.name, + value: exercise.id + })), new inquirer.Separator(), { + name : 'Cancel', + value: 'cancel' + } ] + } ]); + + if ( selectedExercise === 'cancel' ) { + return; + } + + const selected = this.filteredExercises.find(ex => ex.id === selectedExercise); + if ( selected ) { + return ExerciseHelper.displayDetails(selected, true); + } else { + ora().info('Invalid selection. No exercise details to show.'); + } + } + + private async filterByExerciseName(searchQuery: string | undefined = undefined): Promise<void> { + if ( searchQuery === undefined ) { + searchQuery = (await inquirer.prompt([ { + type : 'input', + name : 'searchQuery', + message: 'Please enter the searched string (leave blank if you want all exercises list):' + } ])).searchQuery; + } + + this.currentSearchFilter = `[Name] ${ searchQuery }`; + + if ( !searchQuery ) { + this.filteredExercises = this.allExercises; + } else { + const fuse = new Fuse(this.allExercises, { + keys : [ 'name' ], + threshold: 0.5, + distance : 150 + }); + + this.filteredExercises = fuse.search(searchQuery).map(result => result.item); + } + + await this.displayExerciseList(); + } + + private async filterByTeacher(searchQuery: string): Promise<void> { + if ( this.teachers.length === 0 ) { + ora().info('No teachers found.'); + return; + } + + this.currentSearchFilter = `[Teacher] ${ searchQuery }`; + + const exercises: Array<Exercise & { teachers: string }> = this.allExercises.map(exercise => ({ + ...exercise, + teachers: (exercise.assignment?.staff ?? []).map(staff => staff.gitlabUsername).join(' ') + })) as Array<Exercise & { teachers: string }>; + + const fuse = new Fuse(exercises, { + keys : [ 'teachers' ], + threshold: 0.5, + distance : 150 + }); + + this.filteredExercises = fuse.search(searchQuery).map(result => result.item); + + await this.displayExerciseList(); + } + + private async filterByTeacherInteractive(): Promise<void> { + if ( this.teachers.length === 0 ) { + ora().info('No teachers found.'); + return; + } + + const teacherChoices = this.teachers.map(teacher => ({ + name : `${ teacher.gitlabUsername }`, + value: teacher + })); + + teacherChoices.sort((a, b) => a.name.split('.')[1].localeCompare(b.name.split('.')[1])); + + const selectedTeacher: User = (await inquirer.prompt([ { + type : 'list', + name : 'selectedTeacher', + message: 'Please select a teacher:', + choices: teacherChoices + } ])).selectedTeacher; + + this.currentSearchFilter = `[Teacher] ${ selectedTeacher.gitlabUsername }`; + + this.filteredExercises = this.allExercises.filter(exercise => (exercise.assignment?.staff ?? []).find(staff => staff.id === selectedTeacher.id) !== undefined); + + await this.displayExerciseList(); + } + + private async displayExerciseList(): Promise<void> { + ora(`Search results for filter: ${ this.currentSearchFilter == '' ? 'no filter' : this.currentSearchFilter }`).info(); + + if ( this.filteredExercises.length === 0 ) { + ora().info('No exercises found.'); + return; + } + + this.filteredExercises.forEach(exercise => { + console.log(TextStyle.LIST_ITEM_NAME(`➡ ${ exercise.name }`)); + console.log(` ${ TextStyle.LIST_SUBITEM_NAME('- Id:') } ${ exercise.id }`); + console.log(` ${ TextStyle.LIST_SUBITEM_NAME('- Gitlab URL:') } ${ exercise.gitlabCreationInfo.web_url }`); + }); + } + + private async displayExerciseTable(): Promise<void> { + ora(`Search results for filter: ${ this.currentSearchFilter == '' ? 'no filter' : this.currentSearchFilter }`).info(); + + if ( this.filteredExercises.length === 0 ) { + ora().info('No exercises found.'); + return; + } + + const headers = [ 'Exercise Name', 'GitLab Link' ]; + + // Calculate the maximum width for each column + const maxWidths = headers.map(header => header.length); + + this.filteredExercises.forEach(exercise => { + maxWidths[0] = Math.max(maxWidths[0], exercise.name.length); + maxWidths[1] = Math.max(maxWidths[1], exercise.gitlabLink.length); + }); + + const table = new Table({ + head: headers + }); + + this.filteredExercises.forEach((exercise) => { + table.push([ exercise.name, exercise.gitlabLink ]); + }); + + console.log(table.toString()); + } + + protected async commandAction(options: CommandOptions): Promise<void> { + try { + if ( !options.all && !options.name && !options.teacher && !Config.interactiveMode ) { + ora().fail('At least one filter or interactive mode is required.'); + this.command.help(); + return; + } + + await this.dataRetrieval(!(options.all || options.name)); + + if ( Config.interactiveMode ) { + await this.displayMenu(); + } else { + if ( options.all ) { + await this.displayExerciseList(); + } else if ( options.name ) { + await this.filterByExerciseName(options.name); + } else if ( options.teacher ) { + await this.filterByTeacher(options.teacher); + } + + ora().info(`${ TextStyle.TIPS('[Tips]') } If you want to see more details about an exercise, use the command ${ TextStyle.CODE('dojo exercise info <id or url>') }.`); + } + } catch ( e ) { /* Do nothing */ } + } +} + + +export default new ExerciseListCommand(); \ No newline at end of file diff --git a/NodeApp/src/sharedByClients b/NodeApp/src/sharedByClients index 530fe74..81c1c69 160000 --- a/NodeApp/src/sharedByClients +++ b/NodeApp/src/sharedByClients @@ -1 +1 @@ -Subproject commit 530fe7459023f7fa11b14a9edf7a99629304cf8b +Subproject commit 81c1c69cdc9ed0b381c60fe4fd2e4668abe00625 -- GitLab