Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions apps/backend/db_patches/db_seeds/0007_CoProposersInvite.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
DO
$DO$
DECLARE
invite_id_var1 int;
invite_id_var2 int;
invite_id_var3 int;
invite_id_var4 int;
BEGIN

INSERT INTO invites (code, email, created_by, created_at, claimed_by, claimed_at, is_email_sent, expires_at, template_id)
VALUES ('DAVE001', '[email protected]', 1, NOW(), NULL, NULL, false, NULL, 'user-office-registration-invitation-co-proposer');

SELECT invite_id
INTO invite_id_var1
FROM invites
WHERE email = '[email protected]' AND code = 'DAVE001';

INSERT INTO invites (code, email, created_by, created_at, claimed_by, claimed_at, is_email_sent, expires_at, template_id)
VALUES ('BEN001', '[email protected]', 1, NOW(), NULL, NULL, false, NULL, 'user-office-registration-invitation-co-proposer');

SELECT invite_id
INTO invite_id_var2
FROM invites
WHERE email = '[email protected]' AND code = 'BEN001';

INSERT INTO co_proposer_claims (invite_id, proposal_pk) VALUES (invite_id_var1, 1);
INSERT INTO co_proposer_claims (invite_id, proposal_pk) VALUES (invite_id_var2, 1);

INSERT INTO invites (code, email, created_by, created_at, claimed_by, claimed_at, is_email_sent, expires_at, template_id)
VALUES ('DAVE002', '[email protected]', 1, NOW(), NULL, NULL, false, NULL, '');

SELECT invite_id
INTO invite_id_var3
FROM invites
WHERE email = '[email protected]' AND code = 'DAVE002';

INSERT INTO invites (code, email, created_by, created_at, claimed_by, claimed_at, is_email_sent, expires_at, template_id)
VALUES ('BEN002', '[email protected]', 1, NOW(), NULL, NULL, false, NULL, '');

SELECT invite_id
INTO invite_id_var4
FROM invites
WHERE email = '[email protected]' AND code = 'BEN002';

INSERT INTO co_proposer_claims (invite_id, proposal_pk) VALUES (invite_id_var3, 2);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder why we actually need this table. Is it possible to create a single invitation for multiple proposals? If not, proposal_pk could be part of invites table.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is not yet likely that an invite could be used for more than one proposal. However, the whole design is it make sure that the invite system works not just for entity like Proposal, but also for other entities like Reviewers, Instrument Scientist, Visits etc.,

INSERT INTO co_proposer_claims (invite_id, proposal_pk) VALUES (invite_id_var4, 2);

END;
$DO$
LANGUAGE plpgsql;
6 changes: 6 additions & 0 deletions apps/backend/src/datasources/InviteDataSource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ export interface GetInvitesFilter {
createdAfter?: Date;
isClaimed?: boolean;
isExpired?: boolean;
email?: string;
}

export interface GetCoProposerInvitesFilter extends GetInvitesFilter {
proposalPk?: number;
}

export interface InviteDataSource {
Expand All @@ -27,6 +32,7 @@ export interface InviteDataSource {
isClaimed?: boolean
): Promise<Invite[]>;
getInvites(filter: GetInvitesFilter): Promise<Invite[]>;
getCoProposerInvites(filter: GetCoProposerInvitesFilter): Promise<Invite[]>;

update(args: {
id: number;
Expand Down
3 changes: 2 additions & 1 deletion apps/backend/src/datasources/ProposalDataSource.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Event } from '../events/event.enum';
import { Call } from '../models/Call';
import { Proposal, Proposals } from '../models/Proposal';
import { InvitedProposal, Proposal, Proposals } from '../models/Proposal';
import { ProposalView } from '../models/ProposalView';
import { TechnicalReview } from '../models/TechnicalReview';
import { UserWithRole } from '../models/User';
Expand Down Expand Up @@ -92,4 +92,5 @@ export interface ProposalDataSource {
sortDirection?: string,
searchText?: string
): Promise<{ totalCount: number; proposals: ProposalView[] }>;
getInvitedProposal(inviteId: number): Promise<InvitedProposal | null>;
}
62 changes: 61 additions & 1 deletion apps/backend/src/datasources/mockups/InviteDataSource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,19 @@ import { inject, injectable } from 'tsyringe';

import { Tokens } from '../../config/Tokens';
import { EmailTemplateId } from '../../eventHandlers/email/essEmailHandler';
import { CoProposerClaim } from '../../models/CoProposerClaim';
import { Invite } from '../../models/Invite';
import { CoProposerClaimDataSource } from '../CoProposerClaimDataSource';
import { GetInvitesFilter, InviteDataSource } from '../InviteDataSource';
import {
GetCoProposerInvitesFilter,
GetInvitesFilter,
InviteDataSource,
} from '../InviteDataSource';

@injectable()
export class InviteDataSourceMock implements InviteDataSource {
private invites: Invite[];
private coProposerClaims: CoProposerClaim[];

constructor(
@inject(Tokens.CoProposerClaimDataSource)
Expand Down Expand Up @@ -86,6 +92,8 @@ export class InviteDataSourceMock implements InviteDataSource {
EmailTemplateId.USER_OFFICE_REGISTRATION_INVITATION_REVIEWER
),
];

this.coProposerClaims = [new CoProposerClaim(2, 1)];
}

async findByCode(code: string): Promise<Invite | null> {
Expand Down Expand Up @@ -177,4 +185,56 @@ export class InviteDataSourceMock implements InviteDataSource {

return invite;
}
getCoProposerInvites(filter: GetCoProposerInvitesFilter): Promise<Invite[]> {
return new Promise((resolve) => {
const filteredInvites = this.invites.filter((invite) => {
if (filter.createdBefore) {
if (invite.createdAt >= filter.createdBefore) {
return false;
}
}

if (filter.createdAfter) {
if (invite.createdAt <= filter.createdAfter) {
return false;
}
}

if (filter.isClaimed !== undefined) {
if (invite.claimedAt === null && filter.isClaimed) {
return false;
}
if (invite.claimedAt !== null && !filter.isClaimed) {
return false;
}
}

if (filter.isExpired) {
if (invite.expiresAt && invite.expiresAt < new Date()) {
return false;
}
}

if (filter.email) {
if (invite.email !== filter.email) {
return false;
}
}

if (filter.proposalPk) {
const inviteIds = this.coProposerClaims
.filter((claim) => claim.proposalPk === filter.proposalPk)
.map((claim) => claim.inviteId);

if (!inviteIds.includes(invite.id)) {
return false;
}
}

return true;
});

resolve(filteredInvites);
});
}
}
21 changes: 19 additions & 2 deletions apps/backend/src/datasources/mockups/ProposalDataSource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@ import 'reflect-metadata';
import { Event } from '../../events/event.enum';
import { AllocationTimeUnits, Call } from '../../models/Call';
import { FapMeetingDecision } from '../../models/FapMeetingDecision';
import { Proposal, ProposalEndStatus, Proposals } from '../../models/Proposal';
import {
InvitedProposal,
Proposal,
ProposalEndStatus,
Proposals,
} from '../../models/Proposal';
import { ProposalView } from '../../models/ProposalView';
import {
TechnicalReview,
Expand All @@ -19,6 +24,7 @@ export let dummyProposal: Proposal;
export let dummyProposalView: ProposalView;
export let dummyProposalSubmitted: Proposal;
export let dummyProposalWithNotActiveCall: Proposal;
export let dummyProposalWithoutInvitation: Proposal;

let allProposals: Proposal[];

Expand Down Expand Up @@ -160,6 +166,12 @@ export class ProposalDataSourceMock implements ProposalDataSource {
callId: 2,
});

dummyProposalWithoutInvitation = dummyProposalFactory({
primaryKey: 4,
title: 'Proposal without invitation',
proposalId: 'no-invite',
});

dummyProposalView = new ProposalView(
1,
'',
Expand Down Expand Up @@ -207,6 +219,7 @@ export class ProposalDataSourceMock implements ProposalDataSource {
dummyProposal,
dummyProposalSubmitted,
dummyProposalWithNotActiveCall,
dummyProposalWithoutInvitation,
];

this.proposalsUpdated = [];
Expand Down Expand Up @@ -404,7 +417,7 @@ export class ProposalDataSourceMock implements ProposalDataSource {
}

async getProposalById(proposalId: string): Promise<Proposal | null> {
return dummyProposal.proposalId === proposalId ? dummyProposal : null;
return allProposals.find((p) => p.proposalId === proposalId) || null;
}

async doesProposalNeedTechReview(proposalPk: number): Promise<boolean> {
Expand All @@ -422,4 +435,8 @@ export class ProposalDataSourceMock implements ProposalDataSource {
) {
return { totalCount: 1, proposals: [dummyProposalView] };
}

getInvitedProposal(inviteId: number): Promise<InvitedProposal | null> {
throw new Error('Method not implemented.');
}
}
52 changes: 50 additions & 2 deletions apps/backend/src/datasources/postgres/InviteDataSource.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
/* eslint-disable @typescript-eslint/naming-convention */

import { Invite } from '../../models/Invite';
import { GetInvitesFilter, InviteDataSource } from '../InviteDataSource';
import {
GetCoProposerInvitesFilter,
GetInvitesFilter,
InviteDataSource,
} from '../InviteDataSource';
import database from './database';
import { createInviteObject, InviteRecord } from './records';

export default class PostgresInviteDataSource implements InviteDataSource {
findCoProposerInvites(
proposalPk: number,
Expand Down Expand Up @@ -100,6 +103,51 @@ export default class PostgresInviteDataSource implements InviteDataSource {
if (filter.isExpired) {
query.where('expires_at', '<', new Date());
}

if (filter.email) {
query.whereRaw('lower(email) = ?', filter.email.toLowerCase());
}
})
.then((invites: InviteRecord[]) => invites.map(createInviteObject));
}

getCoProposerInvites(filter: GetCoProposerInvitesFilter): Promise<Invite[]> {
return database
.select('*')
.from('invites')
.join(
'co_proposer_claims',
'invites.invite_id',
'co_proposer_claims.invite_id'
)
.modify((query) => {
if (filter.createdBefore) {
query.where('created_at', '<', filter.createdBefore);
}

if (filter.createdAfter) {
query.where('created_at', '>', filter.createdAfter);
}

if (filter.isClaimed !== undefined) {
if (filter.isClaimed) {
query.whereNotNull('claimed_at');
} else {
query.whereNull('claimed_at');
}
}

if (filter.isExpired) {
query.where('expires_at', '<', new Date());
}

if (filter.email) {
query.whereRaw('lower(email) = ?', filter.email.toLowerCase());
}

if (filter.proposalPk) {
query.where('co_proposer_claims.proposal_pk', filter.proposalPk);
}
})
.then((invites: InviteRecord[]) => invites.map(createInviteObject));
}
Expand Down
24 changes: 23 additions & 1 deletion apps/backend/src/datasources/postgres/ProposalDataSource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { inject, injectable } from 'tsyringe';
import { Tokens } from '../../config/Tokens';
import { Event } from '../../events/event.enum';
import { Call } from '../../models/Call';
import { Proposal, Proposals } from '../../models/Proposal';
import { InvitedProposal, Proposal, Proposals } from '../../models/Proposal';
import { ProposalView } from '../../models/ProposalView';
import { getQuestionDefinition } from '../../models/questionTypes/QuestionRegistry';
import { ReviewerFilter } from '../../models/Review';
Expand Down Expand Up @@ -40,6 +40,8 @@ import {
TechnicalReviewRecord,
TechniqueRecord,
createProposalViewObjectWithTechniques,
InvitedProposalRecord,
createInvitedProposalObject,
} from './records';

const fieldMap: { [key: string]: string } = {
Expand Down Expand Up @@ -1309,4 +1311,24 @@ export default class PostgresProposalDataSource implements ProposalDataSource {

return proposal ? createProposalObject(proposal[0]) : null;
}

async getInvitedProposal(inviteId: number): Promise<InvitedProposal | null> {
const proposals: InvitedProposalRecord[] | undefined = await database
.select(
'proposals.proposal_id',
'proposer.firstname as proposer_name',
'proposals.abstract',
'proposals.title'
)
.from('co_proposer_claims')
.join('proposals', {
'co_proposer_claims.proposal_pk': 'proposals.proposal_pk',
})
.join('users as proposer', {
'proposals.proposer_id': 'proposer.user_id',
})
.where('invite_id', inviteId);

return proposals ? createInvitedProposalObject(proposals[0]) : null;
}
}
22 changes: 21 additions & 1 deletion apps/backend/src/datasources/postgres/records.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,11 @@ import { Institution } from '../../models/Institution';
import { Instrument } from '../../models/Instrument';
import { Invite } from '../../models/Invite';
import { PredefinedMessage } from '../../models/PredefinedMessage';
import { Proposal, ProposalEndStatus } from '../../models/Proposal';
import {
InvitedProposal,
Proposal,
ProposalEndStatus,
} from '../../models/Proposal';
import { ProposalInternalComment } from '../../models/ProposalInternalComment';
import { ProposalPdfTemplate } from '../../models/ProposalPdfTemplate';
import { ProposalView } from '../../models/ProposalView';
Expand Down Expand Up @@ -173,6 +177,13 @@ export interface ProposalViewRecord {
readonly techniques: ProposalViewTechnique[];
}

export interface InvitedProposalRecord {
readonly proposal_id: string;
readonly proposer_name: string;
readonly title: string;
readonly abstract: string;
}

export interface TopicRecord {
readonly topic_id: number;
readonly topic_title: string;
Expand Down Expand Up @@ -901,6 +912,15 @@ export const createProposalViewObject = (proposal: ProposalViewRecord) => {
);
};

export const createInvitedProposalObject = (record: InvitedProposalRecord) => {
return new InvitedProposal(
record.proposal_id,
record.proposer_name,
record.title,
record.abstract
);
};

export const createFieldDependencyObject = (
fieldDependency: FieldDependencyRecord & { natural_key: string }
) => {
Expand Down
Loading
Loading