Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found
Select Git revision
  • Jw_sonar_backup
  • add_route_assignments
  • add_route_user
  • assignment_filter
  • bedran_exercise-list
  • exercise_list_filter
  • interactive-mode-preference
  • jw_sonar
  • main
  • move-to-esm-only
  • v6.0.0
  • 2.0.0
  • 2.1.0
  • 2.2.0
  • 3.0.0
  • 3.0.1
  • 3.1.0
  • 3.1.1
  • 3.1.2
  • 3.2.0
  • 3.2.2
  • 3.2.3
  • 3.3.0
  • 3.4.1
  • 3.4.2
  • 3.5.0
  • 4.0.0
  • 4.0.1
  • 4.1.0
  • 4.1.1
  • 4.2.0
  • 5.0.0
  • 6.0.0-dev
  • Latest
  • Pre-alpha
  • v1.0.1
36 results

Target

Select target project
No results found
Select Git revision
  • Jw_sonar_backup
  • add_route_assignments
  • add_route_user
  • assignment_filter
  • bedran_exercise-list
  • exercise_list_filter
  • interactive-mode-preference
  • jw_sonar
  • main
  • move-to-esm-only
  • v6.0.0
  • 2.0.0
  • 2.1.0
  • 2.2.0
  • 3.0.0
  • 3.0.1
  • 3.1.0
  • 3.1.1
  • 3.1.2
  • 3.2.0
  • 3.2.2
  • 3.2.3
  • 3.3.0
  • 3.4.1
  • 3.4.2
  • 3.5.0
  • 4.0.0
  • 4.0.1
  • 4.1.0
  • 4.1.1
  • 4.2.0
  • 5.0.0
  • 6.0.0-dev
  • Latest
  • Pre-alpha
  • v1.0.1
36 results
Show changes

Commits on Source 9

17 files
+ 617
368
Compare changes
  • Side-by-side
  • Inline

Files

Original line number Diff line number Diff line
@@ -2,6 +2,7 @@
<project version="4">
  <component name="VcsDirectoryMappings">
    <mapping directory="$PROJECT_DIR$/.." vcs="Git" />
    <mapping directory="$PROJECT_DIR$/DojoExercise_c_hello_world" vcs="Git" />
    <mapping directory="$PROJECT_DIR$/src/shared" vcs="Git" />
    <mapping directory="$PROJECT_DIR$/src/sharedByClients" vcs="Git" />
  </component>
Original line number Diff line number Diff line
@@ -22,6 +22,7 @@
                "commander": "^13.1.0",
                "form-data": "^4.0.2",
                "fs-extra": "^11.3.0",
                "fuse.js": "^7.1.0",
                "http-status-codes": "^2.3.0",
                "inquirer": "^8.2.6",
                "json5": "^2.2.3",
@@ -2725,14 +2726,6 @@
            "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
            "license": "MIT"
        },
        "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",
@@ -4008,6 +4001,15 @@
                "url": "https://github.com/sponsors/ljharb"
            }
        },
        "node_modules/fuse.js": {
            "version": "7.1.0",
            "resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-7.1.0.tgz",
            "integrity": "sha512-trLf4SzuuUxfusZADLINj+dE8clK1frKdmqiJNb1Es75fmI5oY6X2mxLVUciLLjxqw/xr72Dhy+lER6dGd02FQ==",
            "license": "Apache-2.0",
            "engines": {
                "node": ">=10"
            }
        },
        "node_modules/genversion": {
            "version": "3.2.0",
            "resolved": "https://registry.npmjs.org/genversion/-/genversion-3.2.0.tgz",
Original line number Diff line number Diff line
@@ -46,6 +46,7 @@
        "commander"                 : "^13.1.0",
        "form-data"                 : "^4.0.2",
        "fs-extra"                  : "^11.3.0",
        "fuse.js"                   : "^7.1.0",
        "http-status-codes"         : "^2.3.0",
        "inquirer"                  : "^8.2.6",
        "json5"                     : "^2.2.3",
Original line number Diff line number Diff line
@@ -3,9 +3,8 @@ import ExerciseCreateCommand from './subcommands/ExerciseCreateCommand.js';
import ExerciseRunCommand        from './subcommands/ExerciseRunCommand.js';
import ExerciseCorrectionCommand from './subcommands/ExerciseCorrectionCommand.js';
import ExerciseDeleteCommand     from './subcommands/ExerciseDeleteCommand';
import ExerciseListCommand       from "./subcommands/ExerciseListCommand";
import ExerciseResultCommand     from "./subcommands/ExerciseResultCommand";
import ExerciseSummaryCommand   from "./subcommands/ExerciseSummaryCommand";
import ExerciseSearchCommand     from './subcommands/ExerciseSearchCommand';
import ExerciseInfoCommand       from './subcommands/ExerciseInfoCommand';


class ExerciseCommand extends CommanderCommand {
@@ -21,10 +20,10 @@ class ExerciseCommand extends CommanderCommand {
        ExerciseRunCommand.registerOnCommand(this.command);
        ExerciseDeleteCommand.registerOnCommand(this.command);
        ExerciseCorrectionCommand.registerOnCommand(this.command);
        ExerciseListCommand.registerOnCommand(this.command);
        ExerciseResultCommand.registerOnCommand(this.command);
        ExerciseSummaryCommand.registerOnCommand(this.command);

        ExerciseSearchCommand.registerOnCommand(this.command);
        ExerciseInfoCommand.registerOnCommand(this.command);
        // ExerciseResultCommand.registerOnCommand(this.command);
        // ExerciseSummaryCommand.registerOnCommand(this.command);
    }

    protected async commandAction(): Promise<void> {
Original line number Diff line number Diff line
@@ -10,6 +10,7 @@ import inquirer from 'inquirer';
import Config              from '../../../config/Config';
import ClientsSharedConfig from '../../../sharedByClients/config/ClientsSharedConfig';
import { Option }          from 'commander';
import ExerciseHelper      from '../../../helpers/Dojo/ExerciseHelper';


type CommandOptions = { assignment: string, members_id?: Array<number>, members_username?: Array<string>, clone?: string | boolean, force?: boolean };
@@ -161,26 +162,7 @@ class ExerciseCreateCommand extends CommanderCommand {

        this.exercise = await DojoBackendManager.createExercise(this.assignment!.name, this.members!);

        const oraInfo = (message: string) => {
            ora({
                    text  : message,
                    indent: 4
                }).start().info();
        };

        oraInfo(`${ TextStyle.LIST_ITEM_NAME('Id:') } ${ this.exercise.id }`);
        oraInfo(`${ TextStyle.LIST_ITEM_NAME('Name:') } ${ this.exercise.name }`);
        oraInfo(`${ TextStyle.LIST_ITEM_NAME('Web URL:') } ${ this.exercise.gitlabCreationInfo.web_url }`);
        oraInfo(`${ TextStyle.LIST_ITEM_NAME('HTTP Repo:') } ${ this.exercise.gitlabCreationInfo.http_url_to_repo }`);
        oraInfo(`${ TextStyle.LIST_ITEM_NAME('SSH Repo:') } ${ this.exercise.gitlabCreationInfo.ssh_url_to_repo }`);
    }

    private async cloneRepository(options: CommandOptions) {
        if ( options.clone ) {
            console.log(TextStyle.BLOCK('Please wait while we are cloning the repository...'));

            await Config.gitlabManager.cloneRepository(options.clone, this.exercise.gitlabCreationInfo.ssh_url_to_repo, `DojoExercise_${ this.exercise.assignmentName }`, true, 0);
        }
        await ExerciseHelper.displayDetails(this.exercise);
    }


@@ -188,7 +170,7 @@ class ExerciseCreateCommand extends CommanderCommand {
        try {
            await this.dataRetrieval(options);
            await this.createExercise();
            await this.cloneRepository(options);
            await ExerciseHelper.clone(this.exercise, options.clone ?? false);
        } catch ( e ) { /* Do nothing */ }
    }
}
Original line number Diff line number Diff line
import CommanderCommand from '../../CommanderCommand';
import DojoBackendManager from '../../../managers/DojoBackendManager';
import AccessesHelper   from '../../../helpers/AccessesHelper';
import TextStyle        from '../../../types/TextStyle';
import ExerciseHelper   from '../../../helpers/Dojo/ExerciseHelper';


class ExerciseDeleteCommand extends CommanderCommand {
@@ -20,16 +20,10 @@ class ExerciseDeleteCommand extends CommanderCommand {
        await AccessesHelper.checkStudent();
    }

    private async deleteExercise(exerciseIdOrUrl: string) {
        console.log(TextStyle.BLOCK('Please wait while we are deleting the exercise...'));

        await DojoBackendManager.deleteExercise(exerciseIdOrUrl);
    }

    protected async commandAction(exerciseIdOrUrl: string): Promise<void> {
        try {
            await this.dataRetrieval();
            await this.deleteExercise(exerciseIdOrUrl);
            await ExerciseHelper.delete(exerciseIdOrUrl);
        } catch ( e ) { /* Do nothing */ }
    }
}
Original line number Diff line number Diff line
import CommanderCommand   from '../../CommanderCommand';
import AccessesHelper     from '../../../helpers/AccessesHelper';
import TextStyle          from '../../../types/TextStyle';
import ExerciseHelper     from '../../../helpers/Dojo/ExerciseHelper';
import Exercise           from '../../../sharedByClients/models/Exercise';
import DojoBackendManager from '../../../managers/DojoBackendManager';
import ora                from 'ora';
import Config             from '../../../config/Config';


class ExerciseInfoCommand extends CommanderCommand {
    protected commandName: string = 'info';

    protected defineCommand(): void {
        this.command
            .description('delete an exercise')
            .argument('id or url', 'id or url of the exercise')
            .action(this.commandAction.bind(this));
    }

    private async dataRetrieval(exerciseIdOrUrl: string): Promise<Exercise> {
        console.log(TextStyle.BLOCK('Please wait while we verify and retrieve data...'));

        await AccessesHelper.checkStudent();


        // Fetch exercise
        const exercisesGetSpinner: ora.Ora = ora({
                                                     text  : `Checking exercise`,
                                                     indent: 4
                                                 }).start();

        const exercise = await DojoBackendManager.getExercise(exerciseIdOrUrl);

        if ( !exercise ) {
            exercisesGetSpinner.fail(`Exercise not found`);
            throw new Error();
        }

        exercisesGetSpinner.succeed(`Exercise fetched successfully`);

        return exercise;
    }

    protected async commandAction(exerciseIdOrUrl: string): Promise<void> {
        try {
            const exercise = await this.dataRetrieval(exerciseIdOrUrl);
            return ExerciseHelper.displayDetails(exercise, Config.interactiveMode);
        } catch ( e ) { /* Do nothing */ }
    }
}


export default new ExerciseInfoCommand();
Original line number Diff line number Diff line
// 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
Original line number Diff line number Diff line
@@ -6,6 +6,8 @@ import Result from '../../../sharedByClients/models/Result';
import inquirer           from 'inquirer';


// THIS COMMAND IS NOT WORKING YET - NEEDS TO BE REWRITTEN

class ExerciseResultCommand extends CommanderCommand {
    protected commandName: string = 'result';

@@ -34,18 +36,16 @@ class ExerciseResultCommand extends CommanderCommand {
            if ( results.length === 0 ) {
                spinner.info('No results found for this exercise.');
            } else {
                const answer = await inquirer.prompt([
                    {
                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'],
                    }
                ]);
                    choices: [ 'Tests réussis', 'Tests échoués', 'Les deux' ]
                } ]);

                const { testType } = answer;

                this.displayResults(results, testType);
                this.displayResults(results, testType as string);
                spinner.succeed('Exercise results fetched successfully.');
            }
        } catch ( error ) {
@@ -119,4 +119,6 @@ class ExerciseResultCommand extends CommanderCommand {
        });
    }
}


export default new ExerciseResultCommand();
Original line number Diff line number Diff line
@@ -7,7 +7,7 @@ class ExerciseRunCommand extends CommanderCommand {
    protected commandName: string = 'run';

    protected defineCommand() {
        GlobalHelper.runCommandDefinition(this.command)
        GlobalHelper.runCommandDefinition(this.command, false)
            .description('locally run an exercise')
            .action(this.commandAction.bind(this));
    }
Original line number Diff line number Diff line
// 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 ExerciseSearchCommand extends CommanderCommand {
    protected commandName: string = 'search';
    protected aliasNames: 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'
                }, new inquirer.Separator() ]
            } ])).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 ExerciseSearchCommand();
 No newline at end of file
Original line number Diff line number Diff line
import CommanderCommand   from '../../CommanderCommand';
import ora                from 'ora';
import DojoBackendManager from '../../../managers/DojoBackendManager';
import Exercise from "../../../sharedByClients/models/Exercise";
import Exercise           from '../../../sharedByClients/models/Exercise';
import Result             from '../../../sharedByClients/models/Result';
import Table              from 'cli-table3';


// THIS COMMAND IS NOT WORKING - NEEDS TO BE REWRITTEN AND THINK OF HIS INTEREST

class ExerciseSummaryCommand extends CommanderCommand {
    protected commandName: string = 'summary';

@@ -41,15 +44,18 @@ class ExerciseSummaryCommand extends CommanderCommand {
    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) {
        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 || '' });
                    results.push({
                                     exercise,
                                     successfulTests,
                                     dateTime: exerciseResults[0]?.dateTime || ''
                                 });
                }
            } catch ( error ) {
                console.error(`Error fetching results for exercise ${ exercise.id }:`, error);
@@ -60,7 +66,7 @@ class ExerciseSummaryCommand extends CommanderCommand {
    }

    private countSuccessfulTests(results: Result[]): number {
        return results.reduce((count, result) => count + (result.success ? result.results.successfulTestsList.length : 0), 0);
        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 }[] {
@@ -80,24 +86,21 @@ class ExerciseSummaryCommand extends CommanderCommand {
        });

        // Define colWidths based on maxWidths
        const colWidths = maxWidths.map(width => ({ width }));
        // const colWidths = maxWidths.map(width => ({ width }));

        // Create the table
        const table = new Table({
            head: headers,
                                    head: headers
                                });

        // Populate the table with data
        sortedExercises.forEach((exercise, index) => {
            table.push([
                index + 1,
                exercise.exercise.name,
                exercise.successfulTests,
                exercise.dateTime
            ]);
            table.push([ index + 1, exercise.exercise.name, exercise.successfulTests, exercise.dateTime ]);
        });

        console.log(table.toString(), '\n');
    }
}


export default new ExerciseSummaryCommand();
Original line number Diff line number Diff line
import Exercise           from '../../sharedByClients/models/Exercise';
import ora                from 'ora';
import TextStyle          from '../../types/TextStyle';
import inquirer           from 'inquirer';
import Config             from '../../config/Config';
import DojoBackendManager from '../../managers/DojoBackendManager';


class ExerciseHelper {
    /**
     * Clone the exercise repository
     * @param exercise
     * @param providedPath If a string is provided, the repository will be cloned in the specified directory. If true, the repository will be cloned in the current directory. If false, the repository will not be cloned, and if undefined, the user will be prompted for the path.
     */
    async clone(exercise: Exercise, providedPath: string | boolean | undefined) {
        if ( providedPath === false ) {
            return;
        }

        let path: string | boolean = './';
        if ( providedPath === undefined ) {
            path = (await inquirer.prompt([ {
                type   : 'input',
                name   : 'path',
                message: `Please enter the path (blank, '.' or './' for current directory):`
            } ])).path;
        } else {
            path = providedPath;
        }

        console.log(TextStyle.BLOCK('Please wait while we are cloning the repository...'));

        await Config.gitlabManager.cloneRepository(path === '' ? true : path, exercise.gitlabCreationInfo.ssh_url_to_repo, `DojoExercise_${ exercise.assignmentName }`, true, 0);
    }

    async delete(exerciseIdOrUrl: string) {
        console.log(TextStyle.BLOCK('Please wait while we are deleting the exercise...'));

        await DojoBackendManager.deleteExercise(exerciseIdOrUrl);
    }

    async actionMenu(exercise: Exercise): Promise<void> {
        // eslint-disable-next-line no-constant-condition
        while ( true ) {
            const action: string = (await inquirer.prompt([ {
                type   : 'list',
                name   : 'action',
                message: 'What action do you want to do on the exercise ?',
                choices: [ {
                    name : 'Display details of the exercise',
                    value: 'info'
                }, new inquirer.Separator(), {
                    name : 'Clone (SSH required) in current directory (will create a subdirectory)',
                    value: 'cloneInCurrentDirectory'
                }, {
                    name : 'Clone (SSH required) in the specified directory (will create a subdirectory)',
                    value: 'clone'
                }, new inquirer.Separator(), {
                    name : 'Delete the exercise',
                    value: 'delete'
                }, new inquirer.Separator(), {
                    name : 'Exit',
                    value: 'exit'
                }, new inquirer.Separator() ]
            } ])).action;

            switch ( action ) {
                case 'info':
                    await this.displayDetails(exercise, false);
                    break;
                case 'cloneInCurrentDirectory':
                    await this.clone(exercise, true);
                    break;
                case 'clone':
                    await this.clone(exercise, undefined);
                    break;
                case 'delete':
                    await this.delete(exercise.id);
                    return;
                case 'exit':
                    return;
                default:
                    ora().fail('Invalid option.');
                    return;
            }
        }
    }

    async displayDetails(exercise: Exercise, showActionMenu: boolean = false): Promise<void> {
        ora().info(`Details of the exercise:`);

        const oraInfo = (message: string, indent: number = 4) => {
            ora({
                    text  : message,
                    indent: indent
                }).start().info();
        };

        oraInfo(`${ TextStyle.LIST_ITEM_NAME('Id:') } ${ exercise.id }`);
        oraInfo(`${ TextStyle.LIST_ITEM_NAME('Name:') } ${ exercise.name }`);
        oraInfo(`${ TextStyle.LIST_ITEM_NAME('Assignment:') } ${ exercise.assignmentName }`);

        // Display exercise teachers
        if ( exercise.assignment?.staff && exercise.assignment?.staff.length > 0 ) {
            oraInfo(`${ TextStyle.LIST_ITEM_NAME('Teachers:') }`);
            exercise.assignment?.staff.forEach(staff => {
                console.log(`        - ${ staff.gitlabUsername }`);
            });
        } else {
            ora({
                    text  : `${ TextStyle.LIST_ITEM_NAME('Teachers:') } No teachers found for this exercise.`,
                    indent: 4
                }).start().warn();
        }

        // Display exercise members
        if ( exercise.members && exercise.members.length > 0 ) {
            oraInfo(`${ TextStyle.LIST_ITEM_NAME('Members:') }`);
            exercise.members.forEach(member => {
                console.log(`        - ${ member.gitlabUsername }`);
            });
        } else {
            ora({
                    text  : `${ TextStyle.LIST_ITEM_NAME('Members:') } No members found for this exercise.`,
                    indent: 4
                }).start().warn();
        }

        oraInfo(`${ TextStyle.LIST_ITEM_NAME('Gitlab URL:') } ${ exercise.gitlabCreationInfo.web_url }`);
        oraInfo(`${ TextStyle.LIST_ITEM_NAME('HTTP Repo:') } ${ exercise.gitlabCreationInfo.http_url_to_repo }`);
        oraInfo(`${ TextStyle.LIST_ITEM_NAME('SSH Repo:') } ${ exercise.gitlabCreationInfo.ssh_url_to_repo }`);

        if ( showActionMenu ) {
            await this.actionMenu(exercise);
        }
    }
}


export default new ExerciseHelper();
 No newline at end of file
Original line number Diff line number Diff line
@@ -8,9 +8,9 @@ import Config from '../config/Config';


class GlobalHelper {
    public runCommandDefinition(command: Command) {
    public runCommandDefinition(command: Command, isAssignment: boolean = true): Command {
        command
            .option('-p, --path <value>', 'assignment path', Config.folders.defaultLocalExercise)
            .option('-p, --path <value>', `${ isAssignment ? 'assignment' : 'exercise' } path`, Config.folders.defaultLocalExercise)
            .option('-v, --verbose', 'verbose mode - display principal container output in live')
            .addOption(new Option('-w, --super-verbose', 'verbose mode - display all docker compose logs (build included) in live').conflicts('verbose'))
            .addOption(new Option('--verbose-ssj2').hideHelp().implies({ superVerbose: true }));
Original line number Diff line number Diff line
@@ -17,6 +17,8 @@ import ClientsSharedConfig from '../sharedByClients/config/ClientsSharedConfig
import inquirer              from 'inquirer';
import SharedConfig          from '../shared/config/SharedConfig';
import ConfigFiles           from '../config/ConfigFiles';
import SessionManager        from './SessionManager';
import UserRole              from '../sharedByClients/models/UserRole';


class DojoBackendManager {
@@ -478,7 +480,7 @@ class DojoBackendManager {

    public async getUserExercises(): Promise<Array<Exercise> | undefined> {
        try {
            const response = await axios.get<DojoBackendResponse<Array<Exercise>>>(DojoBackendHelper.getApiUrl(ApiRoute.EXERCISE_LIST));
            const response = await axios.get<DojoBackendResponse<Array<Exercise>>>(DojoBackendHelper.getApiUrl(ApiRoute.USER_EXERCISES_LIST, { userId: SessionManager.profile?.id }));
            return response.data.data;
        } catch ( error ) {
            console.error('Error fetching user exercises:', error);
@@ -486,13 +488,13 @@ class DojoBackendManager {
        }
    }

    public async getExerciseDetails(exerciseIdOrUrl: string): Promise<Exercise | undefined> {
    public async getExercise(exerciseIdOrUrl: string): Promise<Exercise | undefined> {
        try {

            const response = await axios.get<Exercise>(DojoBackendHelper.getApiUrl(ApiRoute.EXERCISE_DETAILS_GET, {
            const response = await axios.get<DojoBackendResponse<Exercise>>(DojoBackendHelper.getApiUrl(ApiRoute.EXERCISE_GET_DELETE, {
                exerciseIdOrUrl: exerciseIdOrUrl
            }));
            return response.data;
            return response.data.data;
        } catch ( error ) {
            console.error('Error fetching exercise details:', error);
            return undefined;
@@ -540,30 +542,19 @@ class DojoBackendManager {
        }
    }

    public async getUsers(roleFilter?: string): Promise<Array<User> | undefined> {
    public async getUsers(role?: string): Promise<Array<User> | undefined> {
        try {
            const response = await axios.get<DojoBackendResponse<Array<User>>>(DojoBackendHelper.getApiUrl(ApiRoute.USER_LIST), { params: roleFilter ? { roleFilter: roleFilter } : {} });
            const response = await axios.get<DojoBackendResponse<Array<User>>>(DojoBackendHelper.getApiUrl(ApiRoute.USER_LIST), { params: role ? { role: role } : {} });

            return response.data.data;
        } catch ( error ) {
            console.error('Error fetching professors:', error);
            console.error('Error fetching users:', error);
            return undefined;
        }
    }

    public async getTeachers(): Promise<Array<User> | undefined> {
        return this.getUsers('teacher');
    }

    public async getExerciseDetail(exerciseId: string): Promise<Exercise | undefined> {
        try {

            const response = await axios.get<Exercise>(DojoBackendHelper.getApiUrl(ApiRoute.EXERCISE_DETAIL).replace('{{exerciseId}}', String(exerciseId)));
            return response.data;
        } catch ( error ) {
            console.error('Error fetching exercise details:', error);
            return undefined;
        }
        return this.getUsers(UserRole.TEACHING_STAFF);
    }
}

Original line number Diff line number Diff line
@@ -5,7 +5,9 @@ class TextStyle {
    public readonly BLOCK = chalk.cyan;
    public readonly CODE = chalk.bgHex('F7F7F7').grey.italic;
    public readonly LIST_ITEM_NAME = chalk.magenta;
    public readonly LIST_SUBITEM_NAME = chalk.green;
    public readonly QUESTION = chalk.greenBright;
    public readonly TIPS = chalk.blueBright;
    public readonly URL = chalk.blue.underline;
    public readonly WARNING = chalk.red;
}