Browse Source

More updates following hireable lead upgrade

Leszek Wiesner 4 years ago
parent
commit
e04f96ab63

+ 4 - 1
cli/src/Api.ts

@@ -360,6 +360,7 @@ export default class Api {
         return {
             wgApplicationId,
             applicationId: appId.toNumber(),
+            wgOpeningId: wgApplication.opening_id.toNumber(),
             member: await this.memberProfileById(wgApplication.member_id),
             roleAccout: wgApplication.role_account_id,
             stakes: {
@@ -407,6 +408,7 @@ export default class Api {
         const opening = await this.hiringOpeningById(openingId);
         const applications = await this.groupOpeningApplications(group, wgOpeningId);
         const stage = await this.parseOpeningStage(opening.stage);
+        const type = groupOpening.opening_type;
         const stakes = {
             application: opening.application_staking_policy.unwrapOr(undefined),
             role: opening.role_staking_policy.unwrapOr(undefined)
@@ -418,7 +420,8 @@ export default class Api {
             opening,
             stage,
             stakes,
-            applications
+            applications,
+            type
         });
     }
 

+ 3 - 1
cli/src/Types.ts

@@ -7,7 +7,7 @@ import { u32 } from '@polkadot/types/primitive';
 import { BlockNumber, Balance, AccountId } from '@polkadot/types/interfaces';
 import { DerivedBalances } from '@polkadot/api-derive/types';
 import { KeyringPair } from '@polkadot/keyring/types';
-import { WorkerId } from '@joystream/types/working-group';
+import { WorkerId, OpeningType } from '@joystream/types/working-group';
 import { Profile, MemberId } from '@joystream/types/members';
 import {
     GenericJoyStreamRoleSchema,
@@ -113,6 +113,7 @@ export type GroupMember = {
 export type GroupApplication = {
     wgApplicationId: number;
     applicationId: number;
+    wgOpeningId: number;
     member: Profile | null;
     roleAccout: AccountId;
     stakes: {
@@ -150,6 +151,7 @@ export type GroupOpening = {
     opening: Opening;
     stakes: GroupOpeningStakes;
     applications: GroupApplication[];
+    type: OpeningType;
 }
 
 // Some helper structs for generating human_readable_text in working group opening extrinsic

+ 59 - 1
cli/src/base/WorkingGroupsCommandBase.ts

@@ -1,7 +1,7 @@
 import ExitCodes from '../ExitCodes';
 import AccountsCommandBase from './AccountsCommandBase';
 import { flags } from '@oclif/command';
-import { WorkingGroups, AvailableGroups, NamedKeyringPair, GroupMember, GroupOpening, ApiMethodArg, ApiMethodNamedArgs } from '../Types';
+import { WorkingGroups, AvailableGroups, NamedKeyringPair, GroupMember, GroupOpening, ApiMethodArg, ApiMethodNamedArgs, OpeningStatus, GroupApplication } from '../Types';
 import { apiModuleByGroup } from '../Api';
 import { CLIError } from '@oclif/errors';
 import fs from 'fs';
@@ -153,6 +153,64 @@ export default abstract class WorkingGroupsCommandBase extends AccountsCommandBa
         return selectedDraftName;
     }
 
+    async getOpeningForLeadAction(id: number, requiredStatus?: OpeningStatus): Promise<GroupOpening> {
+        const opening = await this.getApi().groupOpening(this.group, id);
+
+        if (!opening.type.isOfType('Worker')) {
+            this.error('A lead can only manage Worker openings!',  { exit: ExitCodes.AccessDenied });
+        }
+
+        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 getApplicationForLeadAction(id: number, requiredStatus?: ApplicationStageKeys): Promise<GroupApplication> {
+        const application = await this.getApi().groupApplication(this.group, id);
+        const opening = await this.getApi().groupOpening(this.group, application.wgOpeningId);
+
+        if (!opening.type.isOfType('Worker')) {
+            this.error('A lead can only manage Worker opening applications!',  { exit: ExitCodes.AccessDenied });
+        }
+
+        if (requiredStatus && application.stage !== requiredStatus) {
+            this.error(
+                `The application needs to have "${_.startCase(requiredStatus)}" status! ` +
+                `This one has: "${_.startCase(application.stage)}"`,
+                { exit: ExitCodes.InvalidInput }
+            );
+        }
+
+        return application;
+    }
+
+    async getWorkerForLeadAction(id: number, requireStakeProfile: boolean = false) {
+        const groupMember = await this.getApi().groupMember(this.group, id);
+        const groupLead = await this.getApi().groupLead(this.group);
+
+        if (groupLead?.workerId.eq(groupMember.workerId)) {
+            this.error('A lead cannot manage his own role this way!', { exit: ExitCodes.AccessDenied });
+        }
+
+        if (requireStakeProfile && !groupMember.stake) {
+            this.error('This worker has no associated role stake profile!', { exit: ExitCodes.InvalidInput });
+        }
+
+        return groupMember;
+    }
+
+    // Helper for better TS handling.
+    // We could also use some magic with conditional types instead, but those don't seem be very well supported yet.
+    async getWorkerWithStakeForLeadAction(id: number) {
+        return (await this.getWorkerForLeadAction(id, true)) as (GroupMember & Required<Pick<GroupMember, 'stake'>>);
+    }
+
     loadOpeningDraftParams(draftName: string): ApiMethodNamedArgs {
         const draftFilePath = this.getOpeningDraftPath(draftName);
         const params = this.extrinsicArgsFromDraft(

+ 1 - 6
cli/src/commands/working-groups/decreaseWorkerStake.ts

@@ -32,12 +32,7 @@ export default class WorkingGroupsDecreaseWorkerStake extends WorkingGroupsComma
         await this.getRequiredLead();
 
         const workerId = parseInt(args.workerId);
-        // This will also make sure the worker is valid
-        const groupMember = await this.getApi().groupMember(this.group, workerId);
-
-        if (!groupMember.stake) {
-            this.error('This worker has no associated role stake profile!', { exit: ExitCodes.InvalidInput });
-        }
+        const groupMember = await this.getWorkerWithStakeForLeadAction(workerId);
 
         this.log(chalk.white('Current worker stake: ', formatBalance(groupMember.stake)));
         const balanceValidator = minMaxInt(1, groupMember.stake.toNumber());

+ 3 - 2
cli/src/commands/working-groups/evictWorker.ts

@@ -29,9 +29,10 @@ export default class WorkingGroupsEvictWorker extends WorkingGroupsCommandBase {
 
         const workerId = parseInt(args.workerId);
         // This will also make sure the worker is valid
-        const groupMember = await this.getApi().groupMember(this.group, workerId);
+        const groupMember = await this.getWorkerForLeadAction(workerId);
 
-        const rationale = await this.promptForParam('Bytes', createParamOptions('rationale'));  // TODO: Terminate worker text limits? (minMaxStr)
+        // TODO: Terminate worker text limits? (minMaxStr)
+        const rationale = await this.promptForParam('Bytes', createParamOptions('rationale'));
         const shouldSlash = groupMember.stake
             ?
                 await this.simplePrompt({

+ 4 - 8
cli/src/commands/working-groups/fillOpening.ts

@@ -1,7 +1,6 @@
 import WorkingGroupsCommandBase from '../../base/WorkingGroupsCommandBase';
 import _ from 'lodash';
 import { OpeningStatus } from '../../Types';
-import ExitCodes from '../../ExitCodes';
 import { apiModuleByGroup } from '../../Api';
 import { OpeningId } from '@joystream/types/hiring';
 import { ApplicationIdSet } from '@joystream/types/working-group';
@@ -29,11 +28,8 @@ export default class WorkingGroupsFillOpening extends WorkingGroupsCommandBase {
         // Lead-only gate
         await this.getRequiredLead();
 
-        const opening = await this.getApi().groupOpening(this.group, parseInt(args.wgOpeningId));
-
-        if (opening.stage.status !== OpeningStatus.InReview) {
-            this.error('This opening is not in the Review stage!', { exit: ExitCodes.InvalidInput });
-        }
+        const openingId = parseInt(args.wgOpeningId);
+        const opening = await this.getOpeningForLeadAction(openingId, OpeningStatus.InReview);
 
         const applicationIds = await this.promptForApplicationsToAccept(opening);
         const rewardPolicyOpt = await this.promptForParam(`Option<${RewardPolicy.name}>`, createParamOptions('RewardPolicy'));
@@ -45,13 +41,13 @@ export default class WorkingGroupsFillOpening extends WorkingGroupsCommandBase {
             apiModuleByGroup[this.group],
             'fillOpening',
             [
-                new OpeningId(opening.wgOpeningId),
+                new OpeningId(openingId),
                 new ApplicationIdSet(applicationIds),
                 rewardPolicyOpt
             ]
         );
 
-        this.log(chalk.green(`Opening ${chalk.white(opening.wgOpeningId)} succesfully filled!`));
+        this.log(chalk.green(`Opening ${chalk.white(openingId)} succesfully filled!`));
         this.log(
             chalk.green('Accepted working group application IDs: ') +
             chalk.white(applicationIds.length ? applicationIds.join(chalk.green(', ')) : 'NONE')

+ 1 - 1
cli/src/commands/working-groups/increaseStake.ts

@@ -10,7 +10,7 @@ import { createParamOptions } from '../../helpers/promptOptions';
 
 export default class WorkingGroupsIncreaseStake extends WorkingGroupsCommandBase {
     static description =
-        'Increases role (lead/worker) stake. Requires active role account to be selected.';
+        'Increases current role (lead/worker) stake. Requires active role account to be selected.';
     static flags = {
         ...WorkingGroupsCommandBase.flags,
     };

+ 1 - 1
cli/src/commands/working-groups/leaveRole.ts

@@ -6,7 +6,7 @@ import chalk from 'chalk';
 import { createParamOptions } from '../../helpers/promptOptions';
 
 export default class WorkingGroupsLeaveRole extends WorkingGroupsCommandBase {
-    static description = 'Leave the role associated with currently selected account.';
+    static description = 'Leave the worker or lead role associated with currently selected account.';
     static flags = {
         ...WorkingGroupsCommandBase.flags,
     };

+ 1 - 0
cli/src/commands/working-groups/opening.ts

@@ -58,6 +58,7 @@ export default class WorkingGroupsOpening extends WorkingGroupsCommandBase {
         const openingRow = {
             'WG Opening ID': opening.wgOpeningId,
             'Opening ID': opening.openingId,
+            'Type': opening.type.type,
             ...this.stageColumns(opening.stage),
             ...this.stakeColumns(opening.stakes)
         };

+ 1 - 0
cli/src/commands/working-groups/openings.ts

@@ -14,6 +14,7 @@ export default class WorkingGroupsOpenings extends WorkingGroupsCommandBase {
         const openingsRows = openings.map(o => ({
             'WG Opening ID': o.wgOpeningId,
             'Opening ID': o.openingId,
+            'Type': o.type.type,
             'Stage': `${_.startCase(o.stage.status)}${o.stage.block ? ` (#${o.stage.block})` : ''}`,
             'Applications': o.applications.length
         }));

+ 1 - 0
cli/src/commands/working-groups/overview.ts

@@ -28,6 +28,7 @@ export default class WorkingGroupsOverview extends WorkingGroupsCommandBase {
 
         displayHeader('Members');
         const membersRows = members.map(m => ({
+            '': lead?.workerId.eq(m.workerId) ? "\u{2B50}" : '', // A nice star for the lead
             'Worker id': m.workerId.toString(),
             'Member id': m.memberId.toString(),
             'Member handle': m.profile.handle.toString(),

+ 1 - 6
cli/src/commands/working-groups/slashWorker.ts

@@ -30,12 +30,7 @@ export default class WorkingGroupsSlashWorker extends WorkingGroupsCommandBase {
         await this.getRequiredLead();
 
         const workerId = parseInt(args.workerId);
-        // This will also make sure the worker is valid
-        const groupMember = await this.getApi().groupMember(this.group, workerId);
-
-        if (!groupMember.stake) {
-            this.error('This worker has no associated role stake profile!', { exit: ExitCodes.InvalidInput });
-        }
+        const groupMember = await this.getWorkerWithStakeForLeadAction(workerId);
 
         this.log(chalk.white('Current worker stake: ', formatBalance(groupMember.stake)));
         const balanceValidator = minMaxInt(1, groupMember.stake.toNumber());

+ 5 - 8
cli/src/commands/working-groups/startAcceptingApplications.ts

@@ -1,7 +1,6 @@
 import WorkingGroupsCommandBase from '../../base/WorkingGroupsCommandBase';
 import _ from 'lodash';
 import { OpeningStatus } from '../../Types';
-import ExitCodes from '../../ExitCodes';
 import { apiModuleByGroup } from '../../Api';
 import { OpeningId } from '@joystream/types/hiring';
 import chalk from 'chalk';
@@ -26,11 +25,9 @@ export default class WorkingGroupsStartAcceptingApplications extends WorkingGrou
         // Lead-only gate
         await this.getRequiredLead();
 
-        const opening = await this.getApi().groupOpening(this.group, parseInt(args.wgOpeningId));
-
-        if (opening.stage.status !== OpeningStatus.WaitingToBegin) {
-            this.error('This opening is not in "Waiting To Begin" stage!', { exit: ExitCodes.InvalidInput });
-        }
+        const openingId = parseInt(args.wgOpeningId);
+        // We don't need the actual opening here, so this is just for validation purposes
+        await this.getOpeningForLeadAction(openingId, OpeningStatus.WaitingToBegin);
 
         await this.requestAccountDecoding(account);
 
@@ -38,9 +35,9 @@ export default class WorkingGroupsStartAcceptingApplications extends WorkingGrou
             account,
             apiModuleByGroup[this.group],
             'acceptApplications',
-            [ new OpeningId(opening.wgOpeningId) ]
+            [ new OpeningId(openingId) ]
         );
 
-        this.log(chalk.green(`Opening ${chalk.white(opening.wgOpeningId)} status changed to: ${ chalk.white('Accepting Applications') }`));
+        this.log(chalk.green(`Opening ${chalk.white(openingId)} status changed to: ${ chalk.white('Accepting Applications') }`));
     }
 }

+ 5 - 8
cli/src/commands/working-groups/startReviewPeriod.ts

@@ -1,7 +1,6 @@
 import WorkingGroupsCommandBase from '../../base/WorkingGroupsCommandBase';
 import _ from 'lodash';
 import { OpeningStatus } from '../../Types';
-import ExitCodes from '../../ExitCodes';
 import { apiModuleByGroup } from '../../Api';
 import { OpeningId } from '@joystream/types/hiring';
 import chalk from 'chalk';
@@ -26,11 +25,9 @@ export default class WorkingGroupsStartReviewPeriod extends WorkingGroupsCommand
         // Lead-only gate
         await this.getRequiredLead();
 
-        const opening = await this.getApi().groupOpening(this.group, parseInt(args.wgOpeningId));
-
-        if (opening.stage.status !== OpeningStatus.AcceptingApplications) {
-            this.error('This opening is not in "Accepting Applications" stage!', { exit: ExitCodes.InvalidInput });
-        }
+        const openingId = parseInt(args.wgOpeningId);
+        // We don't need the actual opening here, so this is just for validation purposes
+        await this.getOpeningForLeadAction(openingId, OpeningStatus.AcceptingApplications);
 
         await this.requestAccountDecoding(account);
 
@@ -38,9 +35,9 @@ export default class WorkingGroupsStartReviewPeriod extends WorkingGroupsCommand
             account,
             apiModuleByGroup[this.group],
             'beginApplicantReview',
-            [ new OpeningId(opening.wgOpeningId) ]
+            [ new OpeningId(openingId) ]
         );
 
-        this.log(chalk.green(`Opening ${chalk.white(opening.wgOpeningId)} status changed to: ${ chalk.white('In Review') }`));
+        this.log(chalk.green(`Opening ${chalk.white(openingId)} status changed to: ${ chalk.white('In Review') }`));
     }
 }

+ 5 - 7
cli/src/commands/working-groups/terminateApplication.ts

@@ -25,11 +25,9 @@ export default class WorkingGroupsTerminateApplication extends WorkingGroupsComm
         // Lead-only gate
         await this.getRequiredLead();
 
-        const application = await this.getApi().groupApplication(this.group, parseInt(args.wgApplicationId));
-
-        if (application.stage !== ApplicationStageKeys.Active) {
-            this.error('This application is not active!', { exit: ExitCodes.InvalidInput });
-        }
+        const applicationId = parseInt(args.wgApplicationId);
+        // We don't really need the application itself here, so this one is just for validation purposes
+        await this.getApplicationForLeadAction(applicationId, ApplicationStageKeys.Active);
 
         await this.requestAccountDecoding(account);
 
@@ -37,9 +35,9 @@ export default class WorkingGroupsTerminateApplication extends WorkingGroupsComm
             account,
             apiModuleByGroup[this.group],
             'terminateApplication',
-            [new ApplicationId(application.wgApplicationId)]
+            [new ApplicationId(applicationId)]
         );
 
-        this.log(chalk.green(`Application ${chalk.white(application.wgApplicationId)} has been succesfully terminated!`));
+        this.log(chalk.green(`Application ${chalk.white(applicationId)} has been succesfully terminated!`));
     }
 }

+ 7 - 1
cli/src/commands/working-groups/updateWorkerReward.ts

@@ -7,6 +7,7 @@ import chalk from 'chalk';
 import { Reward } from '../../Types';
 import { positiveInt } from '../../validators/common';
 import { createParamOptions } from '../../helpers/promptOptions';
+import ExitCodes from '../../ExitCodes';
 
 export default class WorkingGroupsUpdateWorkerReward extends WorkingGroupsCommandBase {
     static description = 'Change given worker\'s reward (amount only). Requires lead access.';
@@ -40,9 +41,14 @@ export default class WorkingGroupsUpdateWorkerReward extends WorkingGroupsComman
 
         const workerId = parseInt(args.workerId);
         // This will also make sure the worker is valid
-        const groupMember = await this.getApi().groupMember(this.group, workerId);
+        const groupMember = await this.getWorkerForLeadAction(workerId);
 
         const { reward } = groupMember;
+
+        if (!reward) {
+            this.error('There is no reward relationship associated with this worker!', { exit: ExitCodes.InvalidInput });
+        }
+
         console.log(chalk.white(`Current worker reward: ${this.formatReward(reward)}`));
 
         const newRewardValue = await this.promptForParam('BalanceOfMint', createParamOptions('new_amount', undefined, positiveInt()));

+ 2 - 2
cli/src/promptOptions/addWorkerOpening.ts

@@ -1,5 +1,5 @@
 import { ApiParamsOptions, ApiParamOptions, HRTStruct } from '../Types';
-import { OpeningType, OpeningTypeKeys, SlashingTerms, UnslashableTerms } from '@joystream/types/working-group';
+import { OpeningType, SlashingTerms, UnslashableTerms, OpeningType_Worker } from '@joystream/types/working-group';
 import { Bytes } from '@polkadot/types';
 import { schemaValidator } from '@joystream/types/hiring';
 import { WorkingGroupOpeningPolicyCommitment } from '@joystream/types/working-group';
@@ -19,7 +19,7 @@ class AddWrokerOpeningOptions implements ApiParamsOptions {
     // Lock value for opening_type
     public opening_type: ApiParamOptions<OpeningType> = {
         value: {
-            default: new OpeningType(OpeningTypeKeys.Worker), // TODO: Use JoyEnum
+            default: OpeningType.create('Worker', new OpeningType_Worker()),
             locked: true
         }
     };

+ 7 - 16
types/src/working-group/index.ts

@@ -1,4 +1,4 @@
-import { getTypeRegistry, Bytes, BTreeMap, Option, Enum } from '@polkadot/types';
+import { getTypeRegistry, Bytes, BTreeMap, Option} from '@polkadot/types';
 import { u16, Null } from '@polkadot/types/primitive';
 import { AccountId, BlockNumber } from '@polkadot/types/interfaces';
 import { BTreeSet, JoyStruct } from '../common';
@@ -240,22 +240,13 @@ export class WorkingGroupOpeningPolicyCommitment extends JoyStruct<IWorkingGroup
   }
 };
 
-export enum OpeningTypeKeys {
-  Leader = 'Leader',
-  Worker = 'Worker'
-};
 
-export class OpeningType extends Enum {
-  constructor (value?: any, index?: number) {
-    super(
-      {
-        Leader: Null,
-        Worker: Null
-      },
-      value, index
-    );
-  }
-};
+export class OpeningType_Leader extends Null { };
+export class OpeningType_Worker extends Null { };
+export class OpeningType extends JoyEnum({
+  Leader: OpeningType_Leader,
+  Worker: OpeningType_Worker
+} as const) { };
 
 export type IOpening = {
   hiring_opening_id: OpeningId,