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

Target

Select target project
  • Dojo_Project_Nguyen/ui/dojocli
  • dojo_project/projects/ui/dojocli
  • tom.andrivet/dojocli
  • orestis.malaspin/dojocli
4 results
Select Git revision
Show changes
Commits on Source (14)
Showing
with 623 additions and 130 deletions
...@@ -18,6 +18,12 @@ ...@@ -18,6 +18,12 @@
--> -->
## 3.5.0 (???)
### ✨ Feature
- Link a commit of an exercise as a corrige of an assignment
## 3.4.2 (2024-01-23) ## 3.4.2 (2024-01-23)
### 🐛 Bugfix ### 🐛 Bugfix
......
This diff is collapsed.
{ {
"name" : "dojo_cli", "name" : "dojo_cli",
"description" : "CLI of the Dojo project", "description" : "CLI of the Dojo project",
"version" : "3.4.2", "version" : "3.5.0",
"license" : "AGPLv3", "license" : "AGPLv3",
"author" : "Michaël Minelli <dojo@minelli.me>", "author" : "Michaël Minelli <dojo@minelli.me>",
"main" : "dist/app.js", "main" : "dist/app.js",
...@@ -33,6 +33,7 @@ ...@@ -33,6 +33,7 @@
"test" : "echo \"Error: no test specified\" && exit 1" "test" : "echo \"Error: no test specified\" && exit 1"
}, },
"dependencies" : { "dependencies" : {
"@gitbeaker/rest" : "^39.34.3",
"appdata-path" : "^1.0.0", "appdata-path" : "^1.0.0",
"axios" : "^1.6.5", "axios" : "^1.6.5",
"boxen" : "^5.1.2", "boxen" : "^5.1.2",
......
...@@ -26,6 +26,7 @@ class CommanderApp { ...@@ -26,6 +26,7 @@ class CommanderApp {
sortSubcommands : true sortSubcommands : true
}) })
.option('-H, --host <string>', 'override the Dojo API endpoint', ClientsSharedConfig.apiURL) .option('-H, --host <string>', 'override the Dojo API endpoint', ClientsSharedConfig.apiURL)
.option('-I, --interactive', 'show interactive interface when available', Config.interactiveMode)
.addOption(new Option('--debug').hideHelp()) .addOption(new Option('--debug').hideHelp())
.hook('preAction', () => { .hook('preAction', () => {
this.warnDevelopmentVersion(); this.warnDevelopmentVersion();
...@@ -37,6 +38,10 @@ class CommanderApp { ...@@ -37,6 +38,10 @@ class CommanderApp {
ClientsSharedConfig.apiURL = this.program.opts().host; ClientsSharedConfig.apiURL = this.program.opts().host;
}); });
this.program.on('option:interactive', () => {
Config.interactiveMode = this.program.opts().interactive;
});
this.program.on('option:debug', () => { this.program.on('option:debug', () => {
SharedConfig.debug = true; SharedConfig.debug = true;
}); });
......
...@@ -4,6 +4,7 @@ import AssignmentPublishCommand from './subcommands/AssignmentPublishCommand'; ...@@ -4,6 +4,7 @@ import AssignmentPublishCommand from './subcommands/AssignmentPublishCommand';
import AssignmentUnpublishCommand from './subcommands/AssignmentUnpublishCommand'; import AssignmentUnpublishCommand from './subcommands/AssignmentUnpublishCommand';
import AssignmentCheckCommand from './subcommands/AssignmentCheckCommand'; import AssignmentCheckCommand from './subcommands/AssignmentCheckCommand';
import AssignmentRunCommand from './subcommands/AssignmentRunCommand'; import AssignmentRunCommand from './subcommands/AssignmentRunCommand';
import AssignmentCorrectionCommand from './subcommands/correction/AssignmentCorrectionCommand';
class AssignmentCommand extends CommanderCommand { class AssignmentCommand extends CommanderCommand {
...@@ -20,6 +21,7 @@ class AssignmentCommand extends CommanderCommand { ...@@ -20,6 +21,7 @@ class AssignmentCommand extends CommanderCommand {
AssignmentRunCommand.registerOnCommand(this.command); AssignmentRunCommand.registerOnCommand(this.command);
AssignmentPublishCommand.registerOnCommand(this.command); AssignmentPublishCommand.registerOnCommand(this.command);
AssignmentUnpublishCommand.registerOnCommand(this.command); AssignmentUnpublishCommand.registerOnCommand(this.command);
AssignmentCorrectionCommand.registerOnCommand(this.command);
} }
protected async commandAction(): Promise<void> { } protected async commandAction(): Promise<void> { }
......
import CommanderCommand from '../../../CommanderCommand';
import AssignmentCorrectionLinkCommand from './subcommands/AssignmentCorrectionLinkCommand';
import AssignmentCorrectionUpdateCommand from './subcommands/AssignmentCorrectionUpdateCommand';
class AssignmentCorrectionCommand extends CommanderCommand {
protected commandName: string = 'correction';
protected defineCommand() {
this.command
.description('manage corrections of an assignment');
}
protected defineSubCommands() {
AssignmentCorrectionLinkCommand.registerOnCommand(this.command);
AssignmentCorrectionUpdateCommand.registerOnCommand(this.command);
}
protected async commandAction(): Promise<void> { }
}
export default new AssignmentCorrectionCommand();
\ No newline at end of file
import AssignmentCorrectionLinkUpdateCommand from './AssignmentCorrectionLinkUpdateCommand';
class AssignmentCorrectionLinkCommand extends AssignmentCorrectionLinkUpdateCommand {
protected commandName: string = 'link';
protected isUpdate: boolean = false;
}
export default new AssignmentCorrectionLinkCommand();
\ No newline at end of file
import CommanderCommand from '../../../../CommanderCommand';
import chalk from 'chalk';
import ora from 'ora';
import DojoBackendManager from '../../../../../managers/DojoBackendManager';
import SessionManager from '../../../../../managers/SessionManager';
import Assignment from '../../../../../sharedByClients/models/Assignment';
abstract class AssignmentCorrectionLinkUpdateCommand extends CommanderCommand {
protected abstract isUpdate: boolean;
protected defineCommand() {
this.command
.description(this.isUpdate ? 'update a correction of an assignment' : 'link an exercise repo as a correction for an assignment')
.argument('<string>', 'id or url of the exercise that is the correction')
.requiredOption('-a, --assignment <string>', 'id or url of the assignment of the correction')
.action(this.commandAction.bind(this));
}
protected async commandAction(exerciseIdOrUrl: string, options: { assignment: string }): Promise<void> {
let assignment!: Assignment | undefined;
// Check access
{
console.log(chalk.cyan('Please wait while we check access...'));
const assignmentGetSpinner: ora.Ora = ora('Checking if assignment exists').start();
assignment = await DojoBackendManager.getAssignment(options.assignment);
if ( !assignment ) {
assignmentGetSpinner.fail(`The assignment doesn't exists`);
return;
}
assignmentGetSpinner.succeed(`The assignment exists`);
const assignmentAccessSpinner: ora.Ora = ora('Checking assignment access').start();
if ( assignment.staff.find(staff => staff.id === SessionManager.profile?.id) === undefined ) {
assignmentAccessSpinner.fail(`You are not in the staff of the assignment`);
return;
}
assignmentAccessSpinner.succeed(`You are in the staff of the assignment`);
const assignmentPublishedSpinner: ora.Ora = ora('Checking assignment').start();
if ( !assignment.published ) {
assignmentPublishedSpinner.fail(`Assignment is not published`);
return;
}
assignmentPublishedSpinner.succeed(`Assignment is published`);
}
// Link the exercise
{
console.log(chalk.cyan('Please wait while we link the exercise...'));
await DojoBackendManager.linkUpdateCorrection(exerciseIdOrUrl, assignment, this.isUpdate);
}
}
}
export default AssignmentCorrectionLinkUpdateCommand;
\ No newline at end of file
import AssignmentCorrectionLinkUpdateCommand from './AssignmentCorrectionLinkUpdateCommand';
class AssignmentCorrectionUpdateCommand extends AssignmentCorrectionLinkUpdateCommand {
protected commandName: string = 'update';
protected isUpdate: boolean = true;
}
export default new AssignmentCorrectionUpdateCommand();
\ No newline at end of file
import CommanderCommand from '../CommanderCommand'; import CommanderCommand from '../CommanderCommand';
import ExerciseCreateCommand from './subcommands/ExerciseCreateCommand'; import ExerciseCreateCommand from './subcommands/ExerciseCreateCommand';
import ExerciseRunCommand from './subcommands/ExerciseRunCommand'; import ExerciseRunCommand from './subcommands/ExerciseRunCommand';
import ExerciseCorrectionCommand from './subcommands/ExerciseCorrectionCommand';
class ExerciseCommand extends CommanderCommand { class ExerciseCommand extends CommanderCommand {
...@@ -14,6 +15,7 @@ class ExerciseCommand extends CommanderCommand { ...@@ -14,6 +15,7 @@ class ExerciseCommand extends CommanderCommand {
protected defineSubCommands() { protected defineSubCommands() {
ExerciseCreateCommand.registerOnCommand(this.command); ExerciseCreateCommand.registerOnCommand(this.command);
ExerciseRunCommand.registerOnCommand(this.command); ExerciseRunCommand.registerOnCommand(this.command);
ExerciseCorrectionCommand.registerOnCommand(this.command);
} }
protected async commandAction(): Promise<void> { } protected async commandAction(): Promise<void> { }
......
import CommanderCommand from '../../CommanderCommand';
import ora from 'ora';
import DojoBackendManager from '../../../managers/DojoBackendManager';
import Config from '../../../config/Config';
import Assignment from '../../../sharedByClients/models/Assignment';
import inquirer from 'inquirer';
import open from 'open';
import chalk from 'chalk';
type CorrectionResume = { name: string, value: string }
class ExerciseCorrectionCommand extends CommanderCommand {
protected commandName: string = 'correction';
protected defineCommand() {
this.command
.description('link an exercise repo as a correction for an assignment')
.requiredOption('-a, --assignment <string>', 'id or url of the assignment of the correction')
.action(this.commandAction.bind(this));
}
protected async commandAction(options: { assignment: string }): Promise<void> {
const assignmentGetSpinner: ora.Ora = ora('Fetching assignment data').start();
const assignment = await DojoBackendManager.getAssignment(options.assignment);
if ( !assignment ) {
assignmentGetSpinner.fail(`The assignment doesn't exists`);
return;
}
if ( assignment.corrections && assignment.corrections.length > 0 ) {
Config.interactiveMode ? await this.showCorrectionsInteractive(assignment, assignmentGetSpinner) : this.showCorrections(assignment, assignmentGetSpinner);
} else {
assignmentGetSpinner.fail(`The assignment doesn't have any corrections yet`);
return;
}
}
private getCorrections(assignment: Assignment): Array<CorrectionResume> {
return assignment.corrections.map(correction => {
return {
name : correction.name.replace(correction.assignmentName, '').split('-')[2].trim(),
value: correction.correctionCommit!.web_url?.replace('/commit/', '/tree/') ?? ''
};
});
}
private showCorrections(assignment: Assignment, spinner: ora.Ora) {
spinner.succeed(`Here are corrections of the assignment '${ assignment.name }':`);
this.getCorrections(assignment).forEach(correction => {
console.log(chalk.green(`- ${ correction.name }`));
console.log(` ${ correction.value }`);
});
}
private async showCorrectionsInteractive(assignment: Assignment, spinner: ora.Ora) {
spinner.stop();
const correctionUrl: string = (await inquirer.prompt({
name : 'correctionUrl',
message: 'Which correction do you want to consult? (use arrow keys then enter)',
type : 'list',
choices: this.getCorrections(assignment),
default: false
})).correctionUrl;
console.log(chalk.green(correctionUrl));
open(correctionUrl).then();
}
}
export default new ExerciseCorrectionCommand();
\ No newline at end of file
...@@ -30,6 +30,8 @@ class Config { ...@@ -30,6 +30,8 @@ class Config {
neededFiles: Array<string> neededFiles: Array<string>
}; };
public interactiveMode: boolean;
constructor() { constructor() {
this.localConfig = { this.localConfig = {
folder : getAppDataPath('DojoCLI'), folder : getAppDataPath('DojoCLI'),
...@@ -62,6 +64,8 @@ class Config { ...@@ -62,6 +64,8 @@ class Config {
this.exercise = { this.exercise = {
neededFiles: JSON.parse(process.env.EXERCISE_NEEDED_FILES || '[]') neededFiles: JSON.parse(process.env.EXERCISE_NEEDED_FILES || '[]')
}; };
this.interactiveMode = process.env.INTERACTIVE_MODE === 'true';
} }
} }
......
...@@ -3,25 +3,34 @@ import GitlabManager from '../managers/GitlabManager'; ...@@ -3,25 +3,34 @@ import GitlabManager from '../managers/GitlabManager';
class AccessesHelper { class AccessesHelper {
async checkStudent(): Promise<boolean> { async checkStudent(testGitlab: boolean = false): Promise<boolean> {
const sessionResult = await SessionManager.testSession(true, [ 'student' ]); const sessionResult = await SessionManager.testSession(true, [ 'student' ]);
if ( !sessionResult ) { if ( !sessionResult ) {
return false; return false;
} }
if ( testGitlab ) {
return (await GitlabManager.testToken(true)).every(result => result); return (await GitlabManager.testToken(true)).every(result => result);
} else {
return true;
}
} }
async checkTeachingStaff(): Promise<boolean> { async checkTeachingStaff(testGitlab: boolean = false): Promise<boolean> {
const sessionResult = await SessionManager.testSession(true, [ 'teachingStaff' ]); const sessionResult = await SessionManager.testSession(true, [ 'teachingStaff' ]);
if ( !sessionResult || !sessionResult.teachingStaff ) { if ( !sessionResult || !sessionResult.teachingStaff ) {
return false; return false;
} }
if ( testGitlab ) {
return (await GitlabManager.testToken(true)).every(result => result); return (await GitlabManager.testToken(true)).every(result => result);
} else {
return true;
}
} }
} }
export default new AccessesHelper(); export default new AccessesHelper();
\ No newline at end of file
...@@ -192,6 +192,49 @@ class DojoBackendManager { ...@@ -192,6 +192,49 @@ class DojoBackendManager {
throw error; throw error;
} }
} }
public async linkUpdateCorrection(exerciseIdOrUrl: string, assignment: Assignment, 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 : axios.post;
const route = isUpdate ? ApiRoute.ASSIGNMENT_CORRECTION_UPDATE : ApiRoute.ASSIGNMENT_CORRECTION_LINK;
await axiosFunction(this.getApiUrl(route).replace('{{assignmentNameOrUrl}}', encodeURIComponent(assignment.name)).replace('{{exerciseIdOrUrl}}', encodeURIComponent(exerciseIdOrUrl)), {
exerciseIdOrUrl: exerciseIdOrUrl
});
if ( verbose ) {
spinner.succeed(`Correction ${ isUpdate ? 'updated' : 'linked' }`);
}
return true;
} catch ( error ) {
if ( verbose ) {
if ( error instanceof AxiosError ) {
if ( error.response?.data ) {
if ( error.response.data.code === DojoStatusCode.ASSIGNMENT_EXERCISE_NOT_RELATED ) {
spinner.fail(`The exercise does not belong to the assignment.`);
} else if ( error.response.data.code === 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.`);
} else if ( error.response.data.code === DojoStatusCode.EXERCISE_CORRECTION_NOT_EXIST ) {
spinner.fail(`The exercise is not labelled as a correction so it's not possible to update it.`);
}
} else {
spinner.fail(`Correction ${ isUpdate ? 'update' : 'link' } error: ${ error.response?.statusText }`);
}
} else {
spinner.fail(`Correction ${ isUpdate ? 'update' : 'link' } error: ${ error }`);
}
}
return false;
}
}
} }
......
...@@ -38,11 +38,11 @@ class LoginServer { ...@@ -38,11 +38,11 @@ class LoginServer {
if ( req.url?.match(Config.login.server.route) ) { if ( req.url?.match(Config.login.server.route) ) {
const urlParts = req.url.split('='); const urlParts = req.url.split('=');
if ( urlParts.length > 0 ) { if ( urlParts.length > 0 ) {
this.events.emit('code', urlParts[1]);
res.writeHead(HttpStatusCode.Ok, { 'Content-Type': 'text/html' }); res.writeHead(HttpStatusCode.Ok, { 'Content-Type': 'text/html' });
res.write(`<html lang="en"><body><h1 style="color: green">DojoCLI login successful</h1><h3>You can close this window.</h3></body></html>`); res.write(`<html lang="en"><body><h1 style="color: green">DojoCLI login successful</h1><h3>You can close this window.</h3></body></html>`);
res.end(); res.end();
this.events.emit('code', urlParts[1]);
return; return;
} }
...@@ -66,6 +66,7 @@ class LoginServer { ...@@ -66,6 +66,7 @@ class LoginServer {
stop() { stop() {
try { try {
this.server.close(); this.server.close();
this.server.closeAllConnections();
this.events.emit('stopped'); this.events.emit('stopped');
} catch ( error ) { } catch ( error ) {
this.events.emit('error', error); this.events.emit('error', error);
......
Subproject commit 75f67b647da34337f3b220cacf78b2115d6022bc Subproject commit 9e3f29d2f313ef96944a199da0db39f1827c496a
Subproject commit 098c6d20f6ed84240c086b979b56afd598fdfea4 Subproject commit 4efff1c5127c6f84104016d7041d0cf281d981f8