Skip to content
Snippets Groups Projects
Select Git revision
  • 8872f91f280e60287c4dba46de58f3f412e0a462
  • main default protected
  • jw_sonar
  • v6.0
  • ask-user-to-delete-exercises-on-duplicates
  • v5.0
  • open_tool_for_self_hosting
  • jw_sonar_backup
  • move-to-esm-only
  • v4.2
  • v4.1
11 results

ExerciceResultsValidation.ts

Blame
  • DojoBackendManager.ts 24.09 KiB
    import axios, { AxiosError } from 'axios';
    import ora                   from 'ora';
    import ApiRoute              from '../sharedByClients/types/Dojo/ApiRoute.js';
    import Assignment            from '../sharedByClients/models/Assignment.js';
    import DojoBackendResponse   from '../shared/types/Dojo/DojoBackendResponse.js';
    import Exercise              from '../sharedByClients/models/Exercise.js';
    import GitlabToken           from '../shared/types/Gitlab/GitlabToken.js';
    import User                  from '../sharedByClients/models/User.js';
    import DojoStatusCode        from '../shared/types/Dojo/DojoStatusCode.js';
    import * as Gitlab           from '@gitbeaker/rest';
    import DojoBackendHelper     from '../sharedByClients/helpers/Dojo/DojoBackendHelper.js';
    import GitlabPipelineStatus  from '../shared/types/Gitlab/GitlabPipelineStatus.js';
    import Tag                   from '../sharedByClients/models/Tag';
    import TagProposal           from '../sharedByClients/models/TagProposal';
    import Result                from '../sharedByClients/models/Result';
    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 {
        readonly API_URL_CONFIG_KEY = 'apiUrl';
    
        public async getApiUrl(): Promise<string> {
            //Read the config file to get the api url. If there is no api url, then ask the user to provide one.
            return ConfigFiles.stateConfigFile.getParam(this.API_URL_CONFIG_KEY) as string | null || await this.askApiUrl();
        }
    
        public async cleanApiUrl(): Promise<void> {
            const cleanApiSpinner: ora.Ora = ora('Cleaning Api URL...').start();
            ConfigFiles.stateConfigFile.setParam(this.API_URL_CONFIG_KEY, null);
            cleanApiSpinner.succeed('API URL successfully cleaned');
        }
    
        /**
         * Check if the given api url is valid and save it in the config file if it is.
         * @param apiUrl
         * @returns {Promise<boolean>} true if the api url is valid, false otherwise
         */
        public async setApiUrl(apiUrl: string): Promise<boolean> {
            let isApiUrlValid: boolean = false;
    
            const testApiSpinner: ora.Ora = ora('Testing Api URL...').start();
            try {
                isApiUrlValid = (await axios.get<DojoBackendResponse<unknown>>(`${ apiUrl }${ ApiRoute.CLIENTS_CONFIG }`)).status === 200;
            } catch ( e ) {
                isApiUrlValid = false;
            }
    
            if ( isApiUrlValid ) {
                // End step: Store the preference in the config file
                ConfigFiles.stateConfigFile.setParam(this.API_URL_CONFIG_KEY, apiUrl);
            }
    
            isApiUrlValid ? testApiSpinner.succeed('API URL successfully saved') : testApiSpinner.fail('The API URL is invalid');
    
            return isApiUrlValid;
        }
    
        public async askApiUrl(showClean: boolean = false): Promise<string> {
            const presets: Array<{ name: string, value: string } | inquirer.Separator> = [ {
                name : 'HEPIA',
                value: 'https://rdps.hesge.ch/dojo/api'
            }, {
                name : 'Other',
                value: 'other'
            } ];
    
            if ( !SharedConfig.production ) {
                presets.unshift({
                                    name : 'DEV',
                                    value: 'http://localhost:30993'
                                }, {
                                    name : 'TEST',
                                    value: 'https://dojo-test.edu.hesge.ch/dojo/api'
                                }, new inquirer.Separator());
            }
    
            if ( showClean ) {
                presets.push(new inquirer.Separator(), {
                    name : 'Clean API settings',
                    value: 'clean'
                });
            }
    
            presets.push(new inquirer.Separator(), {
                name : 'Quit',
                value: 'quit'
            });
    
            let apiUrl: string = '';
            let isApiUrlValid: boolean = false;
    
            do {
                // First step: Propose some presets with inquirer
                apiUrl = (await inquirer.prompt({
                                                    name    : 'apiUrl',
                                                    message : 'Which API do you want to use?',
                                                    type    : 'list',
                                                    pageSize: 1000,
                                                    choices : presets,
                                                    default : SharedConfig.production ? 'other' : 'http://localhost:30993'
                                                })).apiUrl;
    
    
                // Second step: If the user chooses other, then ask for the url of the api
                switch ( apiUrl ) {
                    case 'other':
                        apiUrl = (await inquirer.prompt({
                                                            name   : 'apiUrl',
                                                            message: 'Please provide the URL of the API',
                                                            type   : 'input'
                                                        })).apiUrl;
                        break;
                    case 'clean':
                        await this.cleanApiUrl();
                        return '';
                    case 'quit':
                        process.exit(0);
                }
    
                // Third step: Test the api url
                isApiUrlValid = await this.setApiUrl(apiUrl);
            } while ( !isApiUrlValid );
    
            return apiUrl;
        }
    
        private async handleApiError(error: unknown, spinner: ora.Ora, verbose: boolean, defaultErrorMessage?: string, otherErrorHandler?: (error: AxiosError, spinner: ora.Ora, verbose: boolean) => void) {
            const unknownErrorMessage: string = 'unknown error';
    
            if ( verbose ) {
                if ( error instanceof AxiosError ) {
                    switch ( error.response?.data?.code ) {
                        case DojoStatusCode.ASSIGNMENT_EXERCISE_NOT_RELATED:
                            spinner.fail(`The exercise does not belong to the assignment.`);
                            break;
                        case DojoStatusCode.ASSIGNMENT_PUBLISH_NO_PIPELINE:
                            spinner.fail(`No pipeline found for this assignment.`);
                            break;
                        case DojoStatusCode.ASSIGNMENT_PUBLISH_PIPELINE_FAILED:
                            spinner.fail((error.response?.data?.message as string | undefined) ?? `Last pipeline status is not "${ GitlabPipelineStatus.SUCCESS }".`);
                            break;
                        case DojoStatusCode.EXERCISE_CORRECTION_ALREADY_EXIST:
                            spinner.fail(`This exercise is already labelled as a correction. If you want to update it, please use the update command.`);
                            break;
                        case DojoStatusCode.EXERCISE_CORRECTION_NOT_EXIST:
                            spinner.fail(`The exercise is not labelled as a correction so it's not possible to update it.`);
                            break;
                        case DojoStatusCode.ASSIGNMENT_NAME_CONFLICT:
                            spinner.fail(`Assignment creation error: The assignment name already exists. Please choose another name.`);
                            break;
                        case DojoStatusCode.ASSIGNMENT_CREATION_GITLAB_ERROR:
                            spinner.fail(`Assignment creation error: An unknown error occurred while creating the assignment on GitLab (internal error message: ${ error.response?.data?.description ?? unknownErrorMessage }). Please try again later or contact an administrator.`);
                            break;
                        case DojoStatusCode.ASSIGNMENT_CREATION_INTERNAL_ERROR:
                            spinner.fail(`Assignment creation error: An unknown error occurred while creating the assignment (internal error message: ${ error.response?.data?.description ?? unknownErrorMessage }). Please try again later or contact an administrator.`);
                            break;
                        case DojoStatusCode.MAX_EXERCISE_PER_ASSIGNMENT_REACHED:
                            spinner.fail(`The following users have reached the maximum number of exercise of this assignment : ${ ((error.response.data as DojoBackendResponse<Array<Gitlab.UserSchema>>).data).map(user => user.name).join(', ') }.`);
                            break;
                        case DojoStatusCode.EXERCISE_CREATION_INTERNAL_ERROR:
                            spinner.fail(`Exercise creation error: An unknown error occurred while creating the exercise (internal error message: ${ error.response?.data?.description ?? unknownErrorMessage }). Please try again later or contact an administrator.`);
                            break;
                        case DojoStatusCode.EXERCISE_CREATION_GITLAB_ERROR:
                            spinner.fail(`Exercise creation error: An unknown error occurred while creating the exercise on GitLab (internal error message: ${ error.response?.data?.description ?? unknownErrorMessage }). Please try again later or contact an administrator.`);
                            break;
                        case DojoStatusCode.GITLAB_TEMPLATE_NOT_FOUND:
                            spinner.fail(`Template not found or access denied. Please check the template ID or url. Also, please check that the template have public/internal visibility or that your and Dojo account (${ ClientsSharedConfig.gitlab.dojoAccount.username }) have at least reporter role to the template (if private).`);
                            break;
                        case DojoStatusCode.GITLAB_TEMPLATE_ACCESS_UNAUTHORIZED:
                            spinner.fail(`Please check that the template have public/internal visibility or that your and Dojo account (${ ClientsSharedConfig.gitlab.dojoAccount.username }) have at least reporter role to the template (if private).`);
                            break;
                        case DojoStatusCode.TAG_ONLY_ADMIN_CREATION:
                            spinner.fail(`Only admins can create non UserDefined tags.`);
                            break;
                        case DojoStatusCode.TAG_WITH_ACTIVE_LINK_DELETION:
                            spinner.fail(`This tag is used in resources (e.g. assignments). Please remove this tag from these resources before deleting it.`);
                            break;
                        case DojoStatusCode.TAG_PROPOSAL_ANSWER_NOT_PENDING:
                            spinner.fail(`This tag proposal have already been answered.`);
                            break;
                        default:
                            if ( otherErrorHandler ) {
                                otherErrorHandler(error, spinner, verbose);
                            } else {
                                spinner.fail(defaultErrorMessage ?? 'Unknown error');
                            }
                            break;
                    }
                } else {
                    spinner.fail(defaultErrorMessage ?? 'Unknown error');
                }
            }
        }
    
        public async login(gitlabTokens: GitlabToken): Promise<User | undefined> {
            try {
                return (await axios.post<DojoBackendResponse<User>>(DojoBackendHelper.getApiUrl(ApiRoute.LOGIN), {
                    accessToken : gitlabTokens.access_token,
                    refreshToken: gitlabTokens.refresh_token
                })).data.data;
            } catch ( error ) {
                return undefined;
            }
        }
    
    
        public async refreshTokens(refreshToken: string): Promise<GitlabToken> {
            return (await axios.post<DojoBackendResponse<GitlabToken>>(DojoBackendHelper.getApiUrl(ApiRoute.REFRESH_TOKENS), {
                refreshToken: refreshToken
            })).data.data;
        }
    
    
        public async getAssignment(nameOrUrl: string, getMyExercises: boolean = false): Promise<Assignment | undefined> {
            try {
                return (await axios.get<DojoBackendResponse<Assignment>>(DojoBackendHelper.getApiUrl(ApiRoute.ASSIGNMENT_GET, { assignmentNameOrUrl: nameOrUrl }), getMyExercises ? { params: { getMyExercises } } : {})).data.data;
            } catch ( error ) {
                return undefined;
            }
        }
    
    
        public async checkTemplateAccess(idOrNamespace: string, verbose: boolean = true): Promise<boolean> {
            const spinner: ora.Ora = ora('Checking template access');
    
            if ( verbose ) {
                spinner.start();
            }
    
            try {
                await axios.get(DojoBackendHelper.getApiUrl(ApiRoute.GITLAB_CHECK_TEMPLATE_ACCESS, { gitlabProjectId: idOrNamespace }));
    
                if ( verbose ) {
                    spinner.succeed('Template access granted');
                }
    
                return true;
            } catch ( error ) {
                await this.handleApiError(error, spinner, verbose, `Template error: ${ error }`);
    
                return false;
            }
        }
    
        public async createAssignment(name: string, members: Array<Gitlab.UserSchema>, templateIdOrNamespace: string | null, verbose: boolean = true): Promise<Assignment> {
            const spinner: ora.Ora = ora('Creating assignment...');
    
            if ( verbose ) {
                spinner.start();
            }
    
            try {
                const response = await axios.post<DojoBackendResponse<Assignment>>(DojoBackendHelper.getApiUrl(ApiRoute.ASSIGNMENT_CREATE), Object.assign({
                                                                                                                                                              name   : name,
                                                                                                                                                              members: JSON.stringify(members)
                                                                                                                                                          }, templateIdOrNamespace ? { template: templateIdOrNamespace } : {}));
    
                if ( verbose ) {
                    spinner.succeed(`Assignment successfully created`);
                }
    
                return response.data.data;
            } catch ( error ) {
                await this.handleApiError(error, spinner, verbose, `Assignment creation error: unknown error`);
    
                throw error;
            }
        }
    
        public async createExercise(assignmentName: string, members: Array<Gitlab.UserSchema>, verbose: boolean = true): Promise<Exercise> {
            const spinner: ora.Ora = ora('Creating exercise...');
    
            if ( verbose ) {
                spinner.start();
            }
    
            try {
                const response = await axios.post<DojoBackendResponse<Exercise>>(DojoBackendHelper.getApiUrl(ApiRoute.EXERCISE_CREATE, { assignmentNameOrUrl: assignmentName }), { members: JSON.stringify(members) });
    
                if ( verbose ) {
                    spinner.succeed(`Exercise successfully created`);
                }
    
                return response.data.data;
            } catch ( error ) {
                await this.handleApiError(error, spinner, verbose, `Exercise creation error: unknown error`);
    
                throw error;
            }
        }
    
        public async changeAssignmentPublishedStatus(assignment: Assignment, publish: boolean, verbose: boolean = true) {
            const spinner: ora.Ora = ora('Changing published status...');
    
            if ( verbose ) {
                spinner.start();
            }
    
            try {
                await axios.patch<DojoBackendResponse<null>>(DojoBackendHelper.getApiUrl(publish ? ApiRoute.ASSIGNMENT_PUBLISH : ApiRoute.ASSIGNMENT_UNPUBLISH, { assignmentNameOrUrl: assignment.name }), {});
    
                if ( verbose ) {
                    spinner.succeed(`Assignment ${ assignment.name } successfully ${ publish ? 'published' : 'unpublished' }`);
                }
    
                return;
            } catch ( error ) {
                await this.handleApiError(error, spinner, verbose, `Assignment visibility change error: ${ error }`);
    
                throw error;
            }
        }
    
        public async linkUpdateCorrection(exerciseIdOrUrl: string, assignment: Assignment, commit: string | undefined, description: string | undefined, isUpdate: boolean, verbose: boolean = true): Promise<boolean> {
            const spinner: ora.Ora = ora(`${ isUpdate ? 'Updating' : 'Linking' } correction`);
    
            if ( verbose ) {
                spinner.start();
            }
    
            try {
                const axiosFunction = isUpdate ? axios.patch.bind(axios) : axios.post.bind(axios);
                const route = isUpdate ? ApiRoute.ASSIGNMENT_CORRECTION_UPDATE_DELETE : ApiRoute.ASSIGNMENT_CORRECTION_LINK;
    
                await axiosFunction(DojoBackendHelper.getApiUrl(route, {
                    assignmentNameOrUrl: assignment.name,
                    exerciseIdOrUrl    : exerciseIdOrUrl
                }), {
                                        exerciseIdOrUrl: exerciseIdOrUrl,
                                        commit         : commit,
                                        description    : description
                                    });
    
                if ( verbose ) {
                    spinner.succeed(`Correction ${ isUpdate ? 'updated' : 'linked' }`);
                }
    
                return true;
            } catch ( error ) {
                await this.handleApiError(error, spinner, verbose, `Correction ${ isUpdate ? 'update' : 'link' } error: ${ error }`);
    
                return false;
            }
        }
    
        public async unlinkCorrection(exerciseIdOrUrl: string, assignment: Assignment, verbose: boolean = true): Promise<boolean> {
            const spinner: ora.Ora = ora(`Unlinking correction`);
    
            if ( verbose ) {
                spinner.start();
            }
    
            try {
                await axios.delete(DojoBackendHelper.getApiUrl(ApiRoute.ASSIGNMENT_CORRECTION_UPDATE_DELETE, {
                    assignmentNameOrUrl: assignment.name,
                    exerciseIdOrUrl    : exerciseIdOrUrl
                }));
    
                if ( verbose ) {
                    spinner.succeed(`Correction unlinked`);
                }
    
                return true;
            } catch ( error ) {
                await this.handleApiError(error, spinner, verbose, `Correction unlink error: ${ error }`);
    
                return false;
            }
        }
    
        public async createTag(name: string, type: string, verbose: boolean = true): Promise<Tag | undefined> {
            const spinner: ora.Ora = ora('Creating tag...');
    
            if ( verbose ) {
                spinner.start();
            }
    
            try {
                const response = await axios.post<DojoBackendResponse<Tag>>(DojoBackendHelper.getApiUrl(ApiRoute.TAG_CREATE), {
                    name: name,
                    type: type
                });
    
                if ( verbose ) {
                    spinner.succeed(`Tag successfully created`);
                }
    
                return response.data.data;
            } catch ( error ) {
                await this.handleApiError(error, spinner, verbose, `Tag creation error: ${ error }`);
    
                return undefined;
            }
        }
    
        public async deleteTag(name: string, verbose: boolean = true): Promise<boolean> {
            const spinner: ora.Ora = ora('Deleting tag...');
    
            if ( verbose ) {
                spinner.start();
            }
    
            try {
                await axios.delete<DojoBackendResponse<Tag>>(DojoBackendHelper.getApiUrl(ApiRoute.TAG_DELETE, { tagName: name }));
    
                if ( verbose ) {
                    spinner.succeed(`Tag successfully deleted`);
                }
    
                return true;
            } catch ( error ) {
                await this.handleApiError(error, spinner, verbose, `Tag deletion error: ${ error }`);
    
                return false;
            }
        }
    
        public async getTagProposals(state: string | undefined): Promise<Array<TagProposal> | undefined> {
            try {
                return (await axios.get<DojoBackendResponse<Array<TagProposal>>>(DojoBackendHelper.getApiUrl(ApiRoute.TAG_PROPOSAL_GET_CREATE), { params: { stateFilter: state } })).data.data;
            } catch ( error ) {
                return undefined;
            }
        }
    
        public async createTagProposal(name: string, type: string, verbose: boolean = true): Promise<TagProposal | undefined> {
            const spinner: ora.Ora = ora('Creating tag...');
    
            if ( verbose ) {
                spinner.start();
            }
    
            try {
                const response = await axios.post<DojoBackendResponse<TagProposal>>(DojoBackendHelper.getApiUrl(ApiRoute.TAG_PROPOSAL_GET_CREATE), {
                    name: name,
                    type: type
                });
    
                if ( verbose ) {
                    spinner.succeed(`Tag proposal successfully created`);
                }
    
                return response.data.data;
            } catch ( error ) {
                await this.handleApiError(error, spinner, verbose, `Tag proposal creation error: ${ error }`);
    
                return undefined;
            }
        }
    
        public async answerTagProposal(tagProposalName: string, state: 'Approved' | 'Declined', details: string, verbose: boolean = true): Promise<boolean> {
            const spinner: ora.Ora = ora('Answering tag proposal...');
    
            if ( verbose ) {
                spinner.start();
            }
    
            try {
                await axios.patch<DojoBackendResponse<TagProposal>>(DojoBackendHelper.getApiUrl(ApiRoute.TAG_PROPOSAL_UPDATE, { tagName: tagProposalName }), {
                    state  : state,
                    details: details
                });
    
                if ( verbose ) {
                    spinner.succeed(`Tag proposal ${ state.toLowerCase() } with success`);
                }
    
                return true;
            } catch ( error ) {
                await this.handleApiError(error, spinner, verbose, `Tag proposal answer error: ${ error }`);
    
                return false;
            }
        }
    
        public async getUserExercises(): Promise<Array<Exercise> | undefined> {
            try {
                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);
                return undefined;
            }
        }
    
        public async getExercise(exerciseIdOrUrl: string): Promise<Exercise | undefined> {
            try {
    
                const response = await axios.get<DojoBackendResponse<Exercise>>(DojoBackendHelper.getApiUrl(ApiRoute.EXERCISE_GET_DELETE, {
                    exerciseIdOrUrl: exerciseIdOrUrl
                }));
                return response.data.data;
            } catch ( error ) {
                console.error('Error fetching exercise details:', error);
                return undefined;
            }
        }
    
        public async deleteExercise(exerciseIdOrUrl: string, verbose: boolean = true): Promise<void> {
            const spinner: ora.Ora = ora('Deleting exercise...');
    
            if ( verbose ) {
                spinner.start();
            }
    
            try {
                await axios.delete<DojoBackendResponse<Exercise>>(DojoBackendHelper.getApiUrl(ApiRoute.EXERCISE_GET_DELETE, {
                    exerciseIdOrUrl: exerciseIdOrUrl
                }));
    
                if ( verbose ) {
                    spinner.succeed(`Exercise deleted with success`);
                }
            } catch ( error ) {
                await this.handleApiError(error, spinner, verbose, `Exercise deleting error: ${ error }`);
    
                throw error;
            }
        }
    
        public async getExerciseMembers(exerciseIdOrUrl: string): Promise<Array<User>> {
            return (await axios.get<DojoBackendResponse<Array<User>>>(DojoBackendHelper.getApiUrl(ApiRoute.EXERCISE_MEMBERS_GET, {
                exerciseIdOrUrl: exerciseIdOrUrl
            }))).data.data;
        }
    
        public async getExerciseResults(exerciseIdOrUrl: string): Promise<Array<Result>> {
            try {
                const response = await axios.get(DojoBackendHelper.getApiUrl(ApiRoute.EXERCISE_RESULTS, {
                    exerciseIdOrUrl: exerciseIdOrUrl
                }));
    
                return response.data as Array<Result>;
            } catch ( error ) {
                console.error('Error fetching exercise results:', error);
                return [];
            }
        }
    
        public async getUsers(role?: string): Promise<Array<User> | undefined> {
            try {
                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 users:', error);
                return undefined;
            }
        }
    
        public async getTeachers(): Promise<Array<User> | undefined> {
            return this.getUsers(UserRole.TEACHING_STAFF);
        }
    }
    
    
    export default new DojoBackendManager();