From 7e741e2d9c8deed39d2e9a4f5d897b65ce260261 Mon Sep 17 00:00:00 2001 From: bwhm Date: Fri, 29 Oct 2021 17:20:57 +0200 Subject: [PATCH] sudo commands for hiring Leads --- cli/src/base/ApiCommandBase.ts | 20 ++ cli/src/base/WorkingGroupsCommandBase.ts | 14 ++ .../commands/working-groups/createOpening.ts | 45 +++- .../commands/working-groups/fillOpening.ts | 79 +++++- .../working-groups/startReviewPeriod.ts | 28 ++- .../working-groups/sudoCreateOpening.ts | 233 ++++++++++++++++++ .../working-groups/sudoFillOpening.ts | 83 +++++++ .../working-groups/sudoStartReviewPeriod.ts | 34 +++ 8 files changed, 506 insertions(+), 30 deletions(-) create mode 100644 cli/src/commands/working-groups/sudoCreateOpening.ts create mode 100644 cli/src/commands/working-groups/sudoFillOpening.ts create mode 100644 cli/src/commands/working-groups/sudoStartReviewPeriod.ts diff --git a/cli/src/base/ApiCommandBase.ts b/cli/src/base/ApiCommandBase.ts index 337f9c1723..518bf18ed4 100644 --- a/cli/src/base/ApiCommandBase.ts +++ b/cli/src/base/ApiCommandBase.ts @@ -58,6 +58,26 @@ export default abstract class ApiCommandBase extends StateAwareCommandBase { return this.getApi().getUnaugmentedApi() } + async sendAndFollowNamedSudoTx< + Module extends keyof AugmentedSubmittables<'promise'>, + Method extends keyof AugmentedSubmittables<'promise'>[Module] & string, + Submittable extends AugmentedSubmittables<'promise'>[Module][Method] +>( + sudoAccount: KeyringPair, + module: Module, + method: Method, + params: Submittable extends (...args: any[]) => any ? Parameters : [] +): Promise { + this.log( + chalk.magentaBright( + `\nSending ${module}.${method} extrinsic from ${sudoAccount.meta.name ? sudoAccount.meta.name : sudoAccount.address}...` + ) + ) + const tx = await this.getUnaugmentedApi().tx[module][method](...params) + const sudoTx = await this.getUnaugmentedApi().tx.sudo.sudo(tx) + return await this.sendAndFollowTx(sudoAccount, sudoTx) //, warnOnly) +} + getTypesRegistry(): Registry { return this.getOriginalApi().registry } diff --git a/cli/src/base/WorkingGroupsCommandBase.ts b/cli/src/base/WorkingGroupsCommandBase.ts index 4127c0e2db..0cdfc86f22 100644 --- a/cli/src/base/WorkingGroupsCommandBase.ts +++ b/cli/src/base/WorkingGroupsCommandBase.ts @@ -127,6 +127,20 @@ export default abstract class WorkingGroupsCommandBase extends RolesCommandBase return acceptedApplications } + async getOpening(id: number, requiredStatus?: OpeningStatus): Promise { + const opening = await this.getApi().groupOpening(this.group, id) + + if (requiredStatus && opening.stage.status !== requiredStatus) { + this.error( + `The opening needs to be in "${_.startCase(requiredStatus)}" stage! ` + + `This one is: "${_.startCase(opening.stage.status)}"`, + { exit: ExitCodes.InvalidInput } + ) + } + + return opening + } + async getOpeningForLeadAction(id: number, requiredStatus?: OpeningStatus): Promise { const opening = await this.getApi().groupOpening(this.group, id) diff --git a/cli/src/commands/working-groups/createOpening.ts b/cli/src/commands/working-groups/createOpening.ts index 3658bd522c..ed97c97957 100644 --- a/cli/src/commands/working-groups/createOpening.ts +++ b/cli/src/commands/working-groups/createOpening.ts @@ -42,6 +42,13 @@ export default class WorkingGroupsCreateOpening extends WorkingGroupsCommandBase '(can be used to generate a "draft" which can be provided as input later)', dependsOn: ['output'], }), + sudo: flags.boolean({ + char: 's', + required: false, + hidden: true, + description: + 'Wrappes the command in sudo', + }), } getHRTDefaults(memberHandle: string): HRTJson { @@ -111,7 +118,7 @@ export default class WorkingGroupsCreateOpening extends WorkingGroupsCommandBase } async promptForData( - lead: GroupMember, + lead: GroupMember | string, rememberedInput?: [WGOpeningJson, HRTJson] ): Promise<[WGOpeningJson, HRTJson]> { const openingDefaults = rememberedInput?.[0] @@ -120,8 +127,12 @@ export default class WorkingGroupsCreateOpening extends WorkingGroupsCommandBase openingDefaults ) const wgOpeningJson = await openingPrompt.promptAll() + let profile = "Sudo" + if (typeof lead !== "string") { + profile = lead.profile.handle.toString() + } - const hrtDefaults = rememberedInput?.[1] || this.getHRTDefaults(lead.profile.handle.toString()) + const hrtDefaults = rememberedInput?.[1] || this.getHRTDefaults(profile) this.log(`Values for ${chalk.greenBright('human_readable_text')} json:`) const hrtPropmpt = new JsonSchemaPrompter((HRTSchema as unknown) as JSONSchema, hrtDefaults) // Prompt only for 'headline', 'job', 'application', 'reward' and 'process', leave the rest default @@ -163,12 +174,11 @@ export default class WorkingGroupsCreateOpening extends WorkingGroupsCommandBase async run() { const account = await this.getRequiredSelectedAccount() - // lead-only gate - const lead = await this.getRequiredLead() + await this.requestAccountDecoding(account) // Prompt for password const { - flags: { input, output, edit, dryRun }, + flags: { input, output, edit, dryRun, sudo }, } = this.parse(WorkingGroupsCreateOpening) ensureOutputFileIsWriteable(output) @@ -179,6 +189,11 @@ export default class WorkingGroupsCreateOpening extends WorkingGroupsCommandBase if (edit) { rememberedInput = await this.getInputFromFile(input as string) } + let lead: string | GroupMember = "Sudo" + if (!sudo) { + // lead-only gate + lead = await this.getRequiredLead() + } // Either prompt for the data or get it from input file const [openingJson, hrtJson] = !input || edit || tryAgain @@ -190,6 +205,9 @@ export default class WorkingGroupsCreateOpening extends WorkingGroupsCommandBase // Generate and ask to confirm tx params const txParams = this.createTxParams(openingJson, hrtJson) + if (sudo) { + txParams[3] = createTypeFromConstructor(OpeningType, 'Leader') + } this.jsonPrettyPrint(JSON.stringify(txParams)) const confirmed = await this.simplePrompt({ type: 'confirm', @@ -217,10 +235,19 @@ export default class WorkingGroupsCreateOpening extends WorkingGroupsCommandBase // Send the tx this.log(chalk.magentaBright('Sending the extrinsic...')) try { - await this.sendAndFollowTx( - account, - this.getOriginalApi().tx[apiModuleByGroup[this.group]].addOpening(...txParams) - ) + if (!sudo) { + await this.sendAndFollowTx( + account, + this.getOriginalApi().tx[apiModuleByGroup[this.group]].addOpening(...txParams) + ) + } else { + await this.sendAndFollowNamedSudoTx( + account, + apiModuleByGroup[this.group], + 'addOpening', + txParams, + ) + } this.log(chalk.green('Opening successfully created!')) tryAgain = false } catch (e) { diff --git a/cli/src/commands/working-groups/fillOpening.ts b/cli/src/commands/working-groups/fillOpening.ts index cd5826ccee..9261cb026b 100644 --- a/cli/src/commands/working-groups/fillOpening.ts +++ b/cli/src/commands/working-groups/fillOpening.ts @@ -1,9 +1,13 @@ import WorkingGroupsCommandBase from '../../base/WorkingGroupsCommandBase' -import { OpeningStatus } from '../../Types' +import { GroupOpening, OpeningStatus } from '../../Types' import { apiModuleByGroup } from '../../Api' import chalk from 'chalk' +import { flags } from '@oclif/command' import { createParamOptions } from '../../helpers/promptOptions' import { createType } from '@joystream/types' +import { IRewardPolicy } from '@joystream/types/working-group' +import { Codec } from '@polkadot/types/types' + export default class WorkingGroupsFillOpening extends WorkingGroupsCommandBase { static description = "Allows filling working group opening that's currently in review. Requires lead access." static args = [ @@ -16,28 +20,77 @@ export default class WorkingGroupsFillOpening extends WorkingGroupsCommandBase { static flags = { ...WorkingGroupsCommandBase.flags, + applicantIds: flags.integer({ + char: 'a', + required: false, + multiple: true, + description: 'List of applicants to hire, eg. 1 2 3', + }), + rewardPolicy: flags.integer({ + char: 'r', + multiple: true, + required: false, + description: 'Set the Recurring Reward Policy, eg. [amount] [nextpayment] ', + }), + sudo: flags.boolean({ + char: 's', + required: false, + hidden: true, + description: + 'Wrappes the command in sudo', + }), } async run() { const { args } = this.parse(WorkingGroupsFillOpening) - const account = await this.getRequiredSelectedAccount() - // Lead-only gate - await this.getRequiredLead() + const { + flags: { applicantIds, rewardPolicy, sudo }, + } = this.parse(WorkingGroupsFillOpening) + const account = await this.getRequiredSelectedAccount() const openingId = parseInt(args.wgOpeningId) - const opening = await this.getOpeningForLeadAction(openingId, OpeningStatus.InReview) + let opening: GroupOpening + + if (!sudo) { + // Lead-only gate + await this.getRequiredLead() + opening = await this.getOpeningForLeadAction(openingId, OpeningStatus.InReview) + } else { + opening = await this.getOpening(openingId, OpeningStatus.InReview) + } - const applicationIds = await this.promptForApplicationsToAccept(opening) - const rewardPolicyOpt = await this.promptForParam(`Option`, createParamOptions('RewardPolicy')) + let applicationIds: number[] = [] + if (!applicantIds) { + applicationIds = await this.promptForApplicationsToAccept(opening) + } else { + applicationIds = applicantIds + } + let rewardPolicyOpt: IRewardPolicy | Codec + if (rewardPolicy.length >= 2) { + rewardPolicyOpt = { + amount_per_payout: createType('u128', rewardPolicy[0]), + next_payment_at_block: createType('u32', rewardPolicy[1]), + payout_interval: createType('Option',rewardPolicy[2]), + } + } else { + rewardPolicyOpt = await this.promptForParam(`Option`, createParamOptions('RewardPolicy')) + } await this.requestAccountDecoding(account) - - await this.sendAndFollowNamedTx(account, apiModuleByGroup[this.group], 'fillOpening', [ - openingId, - createType('BTreeSet', applicationIds), - rewardPolicyOpt, - ]) + if (!sudo) { + await this.sendAndFollowNamedTx(account, apiModuleByGroup[this.group], 'fillOpening', [ + openingId, + createType('BTreeSet', applicationIds), + rewardPolicyOpt, + ]) + } else { + await this.sendAndFollowNamedSudoTx(account, apiModuleByGroup[this.group], 'fillOpening', [ + openingId, + createType('BTreeSet', applicationIds), + rewardPolicyOpt, + ]) + } this.log(chalk.green(`Opening ${chalk.magentaBright(openingId)} successfully filled!`)) this.log( diff --git a/cli/src/commands/working-groups/startReviewPeriod.ts b/cli/src/commands/working-groups/startReviewPeriod.ts index 11937c31fc..4fc7c774e7 100644 --- a/cli/src/commands/working-groups/startReviewPeriod.ts +++ b/cli/src/commands/working-groups/startReviewPeriod.ts @@ -2,6 +2,7 @@ import WorkingGroupsCommandBase from '../../base/WorkingGroupsCommandBase' import { OpeningStatus } from '../../Types' import { apiModuleByGroup } from '../../Api' import chalk from 'chalk' +import { flags } from '@oclif/command' export default class WorkingGroupsStartReviewPeriod extends WorkingGroupsCommandBase { static description = 'Changes the status of active opening to "In review". Requires lead access.' @@ -15,21 +16,32 @@ export default class WorkingGroupsStartReviewPeriod extends WorkingGroupsCommand static flags = { ...WorkingGroupsCommandBase.flags, + sudo: flags.boolean({ + char: 's', + required: false, + hidden: true, + description: + 'Wrappes the command in sudo', + }), } async run() { const { args } = this.parse(WorkingGroupsStartReviewPeriod) + const { flags: { sudo } } = this.parse(WorkingGroupsStartReviewPeriod) const account = await this.getRequiredSelectedAccount() - // Lead-only gate - await this.getRequiredLead() - + const openingId = parseInt(args.wgOpeningId) - await this.validateOpeningForLeadAction(openingId, OpeningStatus.AcceptingApplications) - - await this.requestAccountDecoding(account) - - await this.sendAndFollowNamedTx(account, apiModuleByGroup[this.group], 'beginApplicantReview', [openingId]) + if (!sudo) { + // Lead-only gate + await this.getRequiredLead() + await this.requestAccountDecoding(account) + await this.validateOpeningForLeadAction(openingId, OpeningStatus.AcceptingApplications) + await this.sendAndFollowNamedTx(account, apiModuleByGroup[this.group], 'beginApplicantReview', [openingId]) + } else { + await this.requestAccountDecoding(account) + await this.sendAndFollowNamedSudoTx(account, apiModuleByGroup[this.group], 'beginApplicantReview', [openingId]) + } this.log( chalk.green(`Opening ${chalk.magentaBright(openingId)} status changed to: ${chalk.magentaBright('In Review')}`) diff --git a/cli/src/commands/working-groups/sudoCreateOpening.ts b/cli/src/commands/working-groups/sudoCreateOpening.ts new file mode 100644 index 0000000000..4fd29736d9 --- /dev/null +++ b/cli/src/commands/working-groups/sudoCreateOpening.ts @@ -0,0 +1,233 @@ +import WorkingGroupsCommandBase from '../../base/WorkingGroupsCommandBase' +import chalk from 'chalk' +import { apiModuleByGroup } from '../../Api' +import HRTSchema from '@joystream/types/hiring/schemas/role.schema.json' +import { GenericJoyStreamRoleSchema as HRTJson } from '@joystream/types/hiring/schemas/role.schema.typings' +import { JsonSchemaPrompter } from '../../helpers/JsonSchemaPrompt' +import { JSONSchema } from '@apidevtools/json-schema-ref-parser' +import WGOpeningSchema from '../../schemas/json/WorkingGroupOpening.schema.json' +import { WorkingGroupOpening as WGOpeningJson } from '../../schemas/typings/WorkingGroupOpening.schema' +import _ from 'lodash' +import { IOFlags, getInputJson, ensureOutputFileIsWriteable, saveOutputJsonToFile } from '../../helpers/InputOutput' +import Ajv from 'ajv' +import ExitCodes from '../../ExitCodes' +import { flags } from '@oclif/command' +import { CLIError } from '@oclif/errors' +import { createTypeFromConstructor } from '@joystream/types' +import { OpeningPolicyCommitment, OpeningType } from '@joystream/types/working-group' +import { ActivateOpeningAt } from '@joystream/types/hiring' + +export default class WorkingGroupsSudoCreateOpening extends WorkingGroupsCommandBase { + static description = 'Create working group opening (requires lead access)' + static flags = { + ...WorkingGroupsCommandBase.flags, + input: IOFlags.input, + output: flags.string({ + char: 'o', + required: false, + description: 'Path to the file where the output JSON should be saved (this output can be then reused as input)', + }), + edit: flags.boolean({ + char: 'e', + required: false, + description: + 'If provided along with --input - launches in edit mode allowing to modify the input before sending the exstinsic', + dependsOn: ['input'], + }), + dryRun: flags.boolean({ + required: false, + description: + 'If provided along with --output - skips sending the actual extrinsic' + + '(can be used to generate a "draft" which can be provided as input later)', + dependsOn: ['output'], + }), + } + + getHRTDefaults(memberHandle: string): HRTJson { + const groupName = _.startCase(this.group) + return { + version: 1, + headline: `Looking for ${groupName}!`, + job: { + title: groupName, + description: `Become part of the ${groupName} Group! This is a great opportunity to support Joystream!`, + }, + application: { + sections: [ + { + title: 'About you', + questions: [ + { + title: 'Your name', + type: 'text', + }, + { + title: 'What makes you a good fit for the job?', + type: 'text area', + }, + ], + }, + ], + }, + reward: '10k JOY per 3600 blocks', + creator: { + membership: { + handle: memberHandle, + }, + }, + } + } + + createTxParams( + wgOpeningJson: WGOpeningJson, + hrtJson: HRTJson + ): [ActivateOpeningAt, OpeningPolicyCommitment, string, OpeningType] { + return [ + createTypeFromConstructor(ActivateOpeningAt, wgOpeningJson.activateAt), + createTypeFromConstructor(OpeningPolicyCommitment, { + max_review_period_length: wgOpeningJson.maxReviewPeriodLength, + application_rationing_policy: wgOpeningJson.maxActiveApplicants + ? { max_active_applicants: wgOpeningJson.maxActiveApplicants } + : null, + application_staking_policy: wgOpeningJson.applicationStake + ? { + amount: wgOpeningJson.applicationStake.value, + amount_mode: wgOpeningJson.applicationStake.mode, + } + : null, + role_staking_policy: wgOpeningJson.roleStake + ? { + amount: wgOpeningJson.roleStake.value, + amount_mode: wgOpeningJson.roleStake.mode, + } + : null, + terminate_role_stake_unstaking_period: wgOpeningJson.terminateRoleUnstakingPeriod, + exit_role_stake_unstaking_period: wgOpeningJson.leaveRoleUnstakingPeriod, + }), + JSON.stringify(hrtJson), + createTypeFromConstructor(OpeningType, 'Leader'), + ] + } + + async promptForData( + rememberedInput?: [WGOpeningJson, HRTJson] + ): Promise<[WGOpeningJson, HRTJson]> { + const openingDefaults = rememberedInput?.[0] + const openingPrompt = new JsonSchemaPrompter( + (WGOpeningSchema as unknown) as JSONSchema, + openingDefaults + ) + const wgOpeningJson = await openingPrompt.promptAll() + + const hrtDefaults = rememberedInput?.[1] || this.getHRTDefaults('Sudo') + this.log(`Values for ${chalk.greenBright('human_readable_text')} json:`) + const hrtPropmpt = new JsonSchemaPrompter((HRTSchema as unknown) as JSONSchema, hrtDefaults) + // Prompt only for 'headline', 'job', 'application', 'reward' and 'process', leave the rest default + const headline = await hrtPropmpt.promptSingleProp('headline') + this.log('General information about the job:') + const job = await hrtPropmpt.promptSingleProp('job') + this.log('Application form sections and questions:') + const application = await hrtPropmpt.promptSingleProp('application') + this.log('Reward displayed in the opening box:') + const reward = await hrtPropmpt.promptSingleProp('reward') + this.log('Hiring process details (additional information)') + const process = await hrtPropmpt.promptSingleProp('process') + + const hrtJson = { ...hrtDefaults, job, headline, application, reward, process } + + return [wgOpeningJson, hrtJson] + } + + async getInputFromFile(filePath: string): Promise<[WGOpeningJson, HRTJson]> { + const ajv = new Ajv({ allErrors: true }) + const inputParams = await getInputJson<[WGOpeningJson, HRTJson]>(filePath) + if (!Array.isArray(inputParams) || inputParams.length !== 2) { + this.error('Invalid input file', { exit: ExitCodes.InvalidInput }) + } + const [openingJson, hrtJson] = inputParams + if (!ajv.validate(WGOpeningSchema, openingJson)) { + this.error(`Invalid input file:\n${ajv.errorsText(undefined, { dataVar: 'openingJson', separator: '\n' })}`, { + exit: ExitCodes.InvalidInput, + }) + } + if (!ajv.validate(HRTSchema, hrtJson)) { + this.error(`Invalid input file:\n${ajv.errorsText(undefined, { dataVar: 'hrtJson', separator: '\n' })}`, { + exit: ExitCodes.InvalidInput, + }) + } + + return [openingJson, hrtJson] + } + + async run() { + const account = await this.getRequiredSelectedAccount() + const { + flags: { input, output, edit, dryRun }, + } = this.parse(WorkingGroupsSudoCreateOpening) + + await this.requestAccountDecoding(account) // Prompt for password + + ensureOutputFileIsWriteable(output) + + let tryAgain = false + let rememberedInput: [WGOpeningJson, HRTJson] | undefined + do { + if (edit) { + rememberedInput = await this.getInputFromFile(input as string) + } + // Either prompt for the data or get it from input file + const [openingJson, hrtJson] = + !input || edit || tryAgain + ? await this.promptForData(rememberedInput) + : await this.getInputFromFile(input) + + // Remember the provided/fetched data in a variable + rememberedInput = [openingJson, hrtJson] + + // Generate and ask to confirm tx params + const txParams = this.createTxParams(openingJson, hrtJson) + + this.jsonPrettyPrint(JSON.stringify(txParams)) + const confirmed = await this.simplePrompt({ + type: 'confirm', + message: 'Do you confirm these extrinsic parameters?', + }) + if (!confirmed) { + tryAgain = await this.simplePrompt({ type: 'confirm', message: 'Try again with remembered input?' }) + continue + } + + // Save output to file + if (output) { + try { + saveOutputJsonToFile(output, rememberedInput) + this.log(chalk.green(`Output successfully saved in: ${chalk.magentaBright(output)}!`)) + } catch (e) { + this.warn(`Could not save output to ${output}!`) + } + } + + if (dryRun) { + this.exit(ExitCodes.OK) + } + + // Send the tx + this.log(chalk.magentaBright('Sending the extrinsic...')) + try { + await await this.sendAndFollowNamedSudoTx( + account, + apiModuleByGroup[this.group], + 'addOpening', + txParams, + ) + this.log(chalk.green('Opening successfully created!')) + tryAgain = false + } catch (e) { + if (e instanceof CLIError) { + this.warn(e.message) + } + tryAgain = await this.simplePrompt({ type: 'confirm', message: 'Try again with remembered input?' }) + } + } while (tryAgain) + } +} diff --git a/cli/src/commands/working-groups/sudoFillOpening.ts b/cli/src/commands/working-groups/sudoFillOpening.ts new file mode 100644 index 0000000000..aa2f03c265 --- /dev/null +++ b/cli/src/commands/working-groups/sudoFillOpening.ts @@ -0,0 +1,83 @@ +import WorkingGroupsCommandBase from '../../base/WorkingGroupsCommandBase' +import { OpeningStatus } from '../../Types' +import { apiModuleByGroup } from '../../Api' +import chalk from 'chalk' +import { flags } from '@oclif/command' +import { createParamOptions } from '../../helpers/promptOptions' +import { createType } from '@joystream/types' +import { IRewardPolicy } from '@joystream/types/working-group' +import { Codec } from '@polkadot/types/types' + +export default class WorkingGroupsSudoFillOpening extends WorkingGroupsCommandBase { + static description = "Allows filling working group opening that's currently in review. Requires lead access." + static args = [ + { + name: 'wgOpeningId', + required: true, + description: 'Working Group Opening ID', + }, + ] + + static flags = { + ...WorkingGroupsCommandBase.flags, + applicantIds: flags.integer({ + char: 'a', + required: false, + multiple: true, + description: 'List of applicants to hire, eg. 1 2 3', + }), + rewardPolicy: flags.integer({ + char: 'r', + hidden: false, + multiple: true, + required: false, + description: 'Set the Recurring Reward Policy, eg. [amount] [nextpayment] ', + }), + } + + async run() { + const { args } = this.parse(WorkingGroupsSudoFillOpening) + const { + flags: { applicantIds, rewardPolicy }, + } = this.parse(WorkingGroupsSudoFillOpening) + + const account = await this.getRequiredSelectedAccount() + + const openingId = parseInt(args.wgOpeningId) + + const opening = await this.getOpening(openingId, OpeningStatus.InReview) + + let applicationIds: number[] = [] + if (!applicantIds) { + applicationIds = await this.promptForApplicationsToAccept(opening) + } else { + applicationIds = applicantIds + } + + + + let rewardPolicyOpt: IRewardPolicy | Codec + if (rewardPolicy.length >= 2) { + rewardPolicyOpt = { + amount_per_payout: createType('u128', rewardPolicy[0]), + next_payment_at_block: createType('u32', rewardPolicy[1]), + payout_interval: createType('Option',rewardPolicy[2]), + } + } else { + rewardPolicyOpt = await this.promptForParam(`Option`, createParamOptions('RewardPolicy')) + } + + await this.requestAccountDecoding(account) + await this.sendAndFollowNamedSudoTx(account, apiModuleByGroup[this.group], 'fillOpening', [ + openingId, + createType('BTreeSet', applicationIds), + rewardPolicyOpt, + ]) + + this.log(chalk.green(`Opening ${chalk.magentaBright(openingId)} successfully filled!`)) + this.log( + chalk.green('Accepted working group application IDs: ') + + chalk.magentaBright(applicationIds.length ? applicationIds.join(chalk.green(', ')) : 'NONE') + ) + } +} diff --git a/cli/src/commands/working-groups/sudoStartReviewPeriod.ts b/cli/src/commands/working-groups/sudoStartReviewPeriod.ts new file mode 100644 index 0000000000..4741bc1fdb --- /dev/null +++ b/cli/src/commands/working-groups/sudoStartReviewPeriod.ts @@ -0,0 +1,34 @@ +import WorkingGroupsCommandBase from '../../base/WorkingGroupsCommandBase' +import { apiModuleByGroup } from '../../Api' +import chalk from 'chalk' + +export default class WorkingGroupsSudoStartReviewPeriod extends WorkingGroupsCommandBase { + static description = 'Changes the status of active opening to "In review". Requires lead access.' + static args = [ + { + name: 'wgOpeningId', + required: true, + description: 'Working Group Opening ID', + }, + ] + + static flags = { + ...WorkingGroupsCommandBase.flags, + } + + async run() { + const { args } = this.parse(WorkingGroupsSudoStartReviewPeriod) + + const account = await this.getRequiredSelectedAccount() + + const openingId = parseInt(args.wgOpeningId) + + await this.requestAccountDecoding(account) + + await this.sendAndFollowNamedSudoTx(account, apiModuleByGroup[this.group], 'beginApplicantReview', [openingId]) + + this.log( + chalk.green(`Opening ${chalk.magentaBright(openingId)} status changed to: ${chalk.magentaBright('In Review')}`) + ) + } +}