Skip to content

Commit

Permalink
Add Other types of campaigns to projectBySlug webservice
Browse files Browse the repository at this point in the history
  • Loading branch information
mohammadranjbarz committed Sep 27, 2023
1 parent 6adbc81 commit db91549
Show file tree
Hide file tree
Showing 8 changed files with 272 additions and 28 deletions.
3 changes: 3 additions & 0 deletions config/example.env
Original file line number Diff line number Diff line change
Expand Up @@ -202,3 +202,6 @@ POWER_BALANCE_AGGREGATOR_ADAPTER=powerBalanceAggregator
NUMBER_OF_BALANCE_AGGREGATOR_BATCH=20

QF_ROUND_ESTIMATED_MATCHING_CACHE_DURATION=60000

# OPTIONAL - default: Every 10 minutes
PROJECT_CAMPAIGNS_CACHE_DURATION=600000
2 changes: 2 additions & 0 deletions config/test.env
Original file line number Diff line number Diff line change
Expand Up @@ -175,3 +175,5 @@ NUMBER_OF_BALANCE_AGGREGATOR_BATCH=7

# ! millisecond cache, if we increase cache in test ENV we might get some errors in tests
QF_ROUND_ESTIMATED_MATCHING_CACHE_DURATION=1
# ! millisecond cache, if we increase cache in test ENV we might get some errors in tests
PROJECT_CAMPAIGNS_CACHE_DURATION=1
17 changes: 17 additions & 0 deletions src/entities/campaign.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,26 @@ export enum CampaignFilterField {
}

export enum CampaignType {
// https://github.com/Giveth/impact-graph/blob/staging/docs/campaignsInstruction.md

// In these type of projects we pick some projects to show them in campaign,
// for instance for Turkey earthquake we pick some projects.
// so we just need to add slug of those projects in Related Projects Slugs and in
// what order we add them they will be shown in frontend
ManuallySelected = 'ManuallySelected',

// Sometimes in a campaign we just want to show projects in an specified order,
// for instance we can create a campaign like ** Check projects that received most likes** so for
// this campaign you set SortField as campaign type and then you can use one of below sorting fields
SortField = 'SortField',

// Sometimes we need to filter some projects in a campaign,
// for instance Let's verified projects that accept funds on Gnosis chain,
// for this we can Add verified and acceptFundOnGnosis filters
FilterFields = 'FilterFields',

// Some campaigns don't include any project in them and they are just some banner
// like Feeling $nice? campaign in below image
WithoutProjects = 'WithoutProjects',
}

Expand Down
47 changes: 36 additions & 11 deletions src/repositories/projectRepository.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
findProjectById,
findProjectBySlug,
findProjectBySlugWithoutAnyJoin,
findProjectByWalletAddress,
findProjectsByIdArray,
findProjectsBySlugArray,
Expand Down Expand Up @@ -56,6 +57,20 @@ describe(
updateDescriptionSummaryTestCases,
);

describe('verifyProject test cases', verifyProjectTestCases);
describe('verifyMultipleProjects test cases', verifyMultipleProjectsTestCases);
describe('findProjectById test cases', findProjectByIdTestCases);
describe('findProjectsByIdArray test cases', findProjectsByIdArrayTestCases);
describe('findProjectBySlug test cases', findProjectBySlugTestCases);
describe(
'findProjectBySlugWithoutAnyJoin test cases',
findProjectBySlugWithoutAnyJoinTestCases,
);
describe(
'findProjectsBySlugArray test cases',
findProjectsBySlugArrayTestCases,
);

function projectsWithoutUpdateAfterTimeFrameTestCases() {
it('should return projects created a long time ago', async () => {
const superExpiredProject = await saveProjectDirectlyToDb({
Expand Down Expand Up @@ -96,22 +111,13 @@ function projectsWithoutUpdateAfterTimeFrameTestCases() {
});
}

describe('verifyProject test cases', verifyProjectTestCases);
describe('verifyMultipleProjects test cases', verifyMultipleProjectsTestCases);
describe('findProjectById test cases', findProjectByIdTestCases);
describe('findProjectsByIdArray test cases', findProjectsByIdArrayTestCases);
describe('findProjectBySlug test cases', findProjectBySlugTestCases);
describe(
'findProjectsBySlugArray test cases',
findProjectsBySlugArrayTestCases,
);

function findProjectBySlugTestCases() {
it('Should find project by id', async () => {
it('Should find project by slug', async () => {
const project = await saveProjectDirectlyToDb(createProjectData());
const foundProject = await findProjectBySlug(project.slug as string);
assert.isOk(foundProject);
assert.equal(foundProject?.id, project.id);
assert.isOk(foundProject?.adminUser);
});

it('should not find project when project doesnt exists', async () => {
Expand All @@ -120,6 +126,25 @@ function findProjectBySlugTestCases() {
});
}

function findProjectBySlugWithoutAnyJoinTestCases() {
it('Should find project by slug', async () => {
const project = await saveProjectDirectlyToDb(createProjectData());
const foundProject = await findProjectBySlugWithoutAnyJoin(
project.slug as string,
);
assert.isOk(foundProject);
assert.equal(foundProject?.id, project.id);
assert.isNotOk(foundProject?.adminUser);
});

it('should not find project when project doesnt exists', async () => {
const foundProject = await findProjectBySlugWithoutAnyJoin(
new Date().toString(),
);
assert.isNull(foundProject);
});
}

function findProjectsBySlugArrayTestCases() {
it('Should find project multi projects by slug', async () => {
const project1 = await saveProjectDirectlyToDb(createProjectData());
Expand Down
11 changes: 11 additions & 0 deletions src/repositories/projectRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,17 @@ export const findProjectBySlug = (slug: string): Promise<Project | null> => {
);
};

export const findProjectBySlugWithoutAnyJoin = (
slug: string,
): Promise<Project | null> => {
// check current slug and previous slugs
return Project.createQueryBuilder('project')
.where(`:slug = ANY(project."slugHistory") or project.slug = :slug`, {
slug,
})
.getOne();
};

export const verifyMultipleProjects = async (params: {
verified: boolean;
projectsIds: string[] | number[];
Expand Down
136 changes: 134 additions & 2 deletions src/resolvers/projectResolver.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,12 @@ import { refreshUserProjectPowerView } from '../repositories/userProjectPowerVie
import { AppDataSource } from '../orm';
// We are using cache so redis needs to be cleared for tests with same filters
import { redis } from '../redis';
import { Campaign, CampaignType } from '../entities/campaign';
import {
Campaign,
CampaignFilterField,
CampaignSortingField,
CampaignType,
} from '../entities/campaign';
import { generateRandomString, getHtmlTextSummary } from '../utils/utils';
import { FeaturedUpdate } from '../entities/featuredUpdate';
import {
Expand Down Expand Up @@ -5456,7 +5461,7 @@ function projectBySlugTestCases() {
assert.isTrue(project.projectPower.totalPower > 0);
});

it('should return projects including active campaigns', async () => {
it('should return projects including active ManuallySelected campaigns', async () => {
const projectWithCampaign = await saveProjectDirectlyToDb({
...createProjectData(),
title: String(new Date().getTime()),
Expand Down Expand Up @@ -5505,6 +5510,133 @@ function projectBySlugTestCases() {

await campaign.remove();
});
it('should return projects including active SortField campaigns', async () => {
const projectWithCampaign = await saveProjectDirectlyToDb({
...createProjectData(),
title: String(new Date().getTime()),
slug: String(new Date().getTime()),
});
const campaign = await Campaign.create({
isActive: true,
type: CampaignType.SortField,
sortingField: CampaignSortingField.Newest,
slug: generateRandomString(),
title: 'title1',
description: 'description1',
photo: 'https://google.com',
order: 1,
}).save();
const result = await axios.post(graphqlUrl, {
query: fetchProjectsBySlugQuery,
variables: {
slug: projectWithCampaign.slug,
},
});

const project = result.data.data.projectBySlug;
assert.equal(Number(project.id), projectWithCampaign.id);

assert.exists(project.campaigns);
assert.isNotEmpty(project.campaigns);
assert.equal(project.campaigns[0].id, campaign.id);

const projectWithoutCampaignResult = await axios.post(graphqlUrl, {
query: fetchProjectsBySlugQuery,
variables: {
// and old project that I'm sure it would not be in the Newest campaign
slug: SEED_DATA.FIRST_PROJECT.slug,
},
});

const project2 = projectWithoutCampaignResult.data.data.projectBySlug;
assert.equal(Number(project2.id), SEED_DATA.FIRST_PROJECT.id);

assert.isEmpty(project2.campaigns);

await campaign.remove();
});

it('should return projects including active FilterField campaigns (acceptOnGnosis)', async () => {
// In this filter the default sorting for projects is givPower so I need to create a project with power
// to be sure that it will be in the campaign
await PowerBoosting.clear();
await InstantPowerBalance.clear();

const user1 = await saveUserDirectlyToDb(generateRandomEtheriumAddress());

const projectWithCampaign = await saveProjectDirectlyToDb({
...createProjectData(),
title: String(new Date().getTime()),
slug: String(new Date().getTime()),
networkId: NETWORK_IDS.XDAI,
});

const projectWithoutCampaign = await saveProjectDirectlyToDb({
...createProjectData(),
title: String(new Date().getTime()),
slug: String(new Date().getTime()),
networkId: NETWORK_IDS.POLYGON,
});

await Promise.all(
[[user1, projectWithCampaign, 10]].map(item => {
const [user, project, percentage] = item as [User, Project, number];
return insertSinglePowerBoosting({
user,
project,
percentage,
});
}),
);

await saveOrUpdateInstantPowerBalances([
{
userId: user1.id,
balance: 10000,
balanceAggregatorUpdatedAt: new Date(1_000_000),
},
]);

await updateInstantBoosting();

const campaign = await Campaign.create({
isActive: true,
type: CampaignType.FilterFields,
filterFields: [CampaignFilterField.acceptFundOnGnosis],
slug: generateRandomString(),
title: 'title1',
description: 'description1',
photo: 'https://google.com',
order: 1,
}).save();
const result = await axios.post(graphqlUrl, {
query: fetchProjectsBySlugQuery,
variables: {
slug: projectWithCampaign.slug,
},
});

const fetchedProject = result.data.data.projectBySlug;
assert.equal(Number(fetchedProject.id), projectWithCampaign.id);

assert.exists(fetchedProject.campaigns);
assert.isNotEmpty(fetchedProject.campaigns);
assert.equal(fetchedProject.campaigns[0].id, campaign.id);

const projectWithoutCampaignResult = await axios.post(graphqlUrl, {
query: fetchProjectsBySlugQuery,
variables: {
slug: projectWithoutCampaign.slug,
},
});

const project2 = projectWithoutCampaignResult.data.data.projectBySlug;
assert.equal(Number(project2.id), projectWithoutCampaign.id);

assert.isEmpty(project2.campaigns);

await campaign.remove();
});

it('should return projects including active campaigns, even when sent slug is in the slugHistory of project', async () => {
const projectWithCampaign = await saveProjectDirectlyToDb({
Expand Down
25 changes: 17 additions & 8 deletions src/resolvers/projectResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ import {
FilterProjectQueryInputParams,
filterProjectsQuery,
findProjectById,
findProjectBySlugWithoutAnyJoin,
totalProjectsPerDate,
totalProjectsPerDateByMonthAndYear,
userIsOwnerOfProject,
Expand Down Expand Up @@ -108,6 +109,7 @@ import { FeaturedUpdate } from '../entities/featuredUpdate';
import { PROJECT_UPDATE_CONTENT_MAX_LENGTH } from '../constants/validators';
import { calculateGivbackFactor } from '../services/givbackService';
import { ProjectBySlugResponse } from './types/projectResolver';
import { getAllProjectsRelatedToActiveCampaigns } from '../services/campaignService';

@ObjectType()
class AllProjects {
Expand Down Expand Up @@ -898,11 +900,18 @@ export class ProjectResolver {
isOwnerOfProject = await userIsOwnerOfProject(viewerUserId, slug);
}

const minimalProject = await findProjectBySlugWithoutAnyJoin(slug);
if (!minimalProject) {
throw new Error(i18n.__(translationErrorMessagesKeys.PROJECT_NOT_FOUND));
}
const campaignSlugs = (await getAllProjectsRelatedToActiveCampaigns())[
minimalProject.id
];

let query = this.projectRepository
.createQueryBuilder('project')
// check current slug and previous slugs
.where(`:slug = ANY(project."slugHistory") or project.slug = :slug`, {
slug,
.where(`project.id = :id`, {
id: minimalProject.id,
})
.leftJoinAndSelect('project.status', 'status')
.leftJoinAndSelect(
Expand All @@ -922,9 +931,10 @@ export class ProjectResolver {
'project.campaigns',
Campaign,
'campaigns',
'(campaigns."relatedProjectsSlugs" && ARRAY[:slug]::text[] OR campaigns."relatedProjectsSlugs" && project."slugHistory") AND campaigns."isActive" = TRUE',
'((campaigns."relatedProjectsSlugs" && ARRAY[:slug]::text[] OR campaigns."relatedProjectsSlugs" && project."slugHistory") AND campaigns."isActive" = TRUE) OR (campaigns.slug = ANY(:campaignSlugs))',
{
slug,
campaignSlugs,
},
)
.leftJoin('project.adminUser', 'user')
Expand All @@ -946,17 +956,16 @@ export class ProjectResolver {
});

const project = await query.getOne();
if (!project) {
throw new Error(i18n.__(translationErrorMessagesKeys.PROJECT_NOT_FOUND));
}
canUserVisitProject(project, String(user?.userId));
const verificationForm =
project?.projectVerificationForm ||
(await getVerificationFormByProjectId(project?.id as number));
if (verificationForm) {
(project as Project).verificationFormStatus = verificationForm?.status;
}
const { givbackFactor } = await calculateGivbackFactor(project.id);

// We know that we have the project because if we reach this line means minimalProject is not null
const { givbackFactor } = await calculateGivbackFactor(project!.id);

return { ...project, givbackFactor };
}
Expand Down
Loading

0 comments on commit db91549

Please sign in to comment.