Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Other types of campaigns to projectBySlug webservice #1136

Merged
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
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,
mohammadranjbarz marked this conversation as resolved.
Show resolved Hide resolved
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
Loading