Skip to content

Commit

Permalink
Merge pull request #1137 from Giveth/feature_new_qf_filters_table
Browse files Browse the repository at this point in the history
Add qfFilter with estimatedMatchingView
  • Loading branch information
CarlosQ96 authored Oct 3, 2023
2 parents 05eeae0 + 4ca466b commit 97d1a2f
Show file tree
Hide file tree
Showing 7 changed files with 142 additions and 11 deletions.
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

34 changes: 30 additions & 4 deletions src/entities/ProjectEstimatedMatchingView.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,19 @@
import { Entity, Column, Index, PrimaryColumn } from 'typeorm';
import { Field, ObjectType } from 'type-graphql';
import {
Entity,
Column,
Index,
PrimaryColumn,
BaseEntity,
ViewEntity,
ManyToOne,
RelationId,
ViewColumn,
JoinColumn,
} from 'typeorm';
import { Project } from './project';

@Entity({ name: 'project_estimated_matching_view' })
@ViewEntity('project_estimated_matching_view', { synchronize: false })
@Index('project_estimated_matching_view_project_id_qfround_id', [
'projectId',
'qfRoundId',
Expand All @@ -13,28 +26,41 @@ import { Entity, Column, Index, PrimaryColumn } from 'typeorm';
@Index('project_estimated_matching_view_unique_donation_count', [
'uniqueDonationCount',
])
export class ProjectEstimatedMatchingView {
// Project ID associated with the donations
@ObjectType()
export class ProjectEstimatedMatchingView extends BaseEntity {
@Field(type => Project)
@ManyToOne(type => Project, project => project.projectEstimatedMatchingView)
@JoinColumn({ referencedColumnName: 'id' })
project: Project;

@Field()
@ViewColumn()
@PrimaryColumn()
projectId: number;

// QF Round ID associated with the donations
@ViewColumn()
@Field()
@PrimaryColumn()
qfRoundId: number;

// Sum of the square root of the value in USD of the donations
@ViewColumn()
@Column('double precision')
sqrtRootSum: number;

// Count of unique donations per user per project per QF round
@ViewColumn()
@Column('int')
uniqueDonationCount: number;

// Sum of the value in USD of the donations for active QF rounds where the donation status is verified
@ViewColumn()
@Column('double precision')
sumValueUsd: number;

// Count of unique donors who have verified donations for each project
@ViewColumn()
@Column('int')
uniqueDonorsCount: number;
}
2 changes: 2 additions & 0 deletions src/entities/entities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import { ProjectInstantPowerView } from '../views/projectInstantPowerView';
import { QfRound } from './qfRound';
import { ReferredEvent } from './referredEvent';
import { QfRoundHistory } from './qfRoundHistory';
import { ProjectEstimatedMatchingView } from './ProjectEstimatedMatchingView';

export const getEntities = (): DataSourceOptions['entities'] => {
return [
Expand Down Expand Up @@ -81,6 +82,7 @@ export const getEntities = (): DataSourceOptions['entities'] => {
LastSnapshotProjectPowerView,
ProjectInstantPowerView,
ProjectUserInstantPowerView,
ProjectEstimatedMatchingView,

// historic snapshots
PowerSnapshotHistory,
Expand Down
9 changes: 9 additions & 0 deletions src/entities/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import {
} from '../repositories/qfRoundRepository';
import { EstimatedMatching } from '../types/qfTypes';
import { Campaign } from './campaign';
import { ProjectEstimatedMatchingView } from './ProjectEstimatedMatchingView';
// tslint:disable-next-line:no-var-requires
const moment = require('moment');

Expand All @@ -76,6 +77,7 @@ export enum SortingField {
QualityScore = 'QualityScore',
GIVPower = 'GIVPower',
InstantBoosting = 'InstantBoosting',
ActiveQfRoundRaisedFunds = 'ActiveQfRoundRaisedFunds',
}

export enum FilterField {
Expand Down Expand Up @@ -341,6 +343,13 @@ export class Project extends BaseEntity {
@OneToMany(type => SocialProfile, socialProfile => socialProfile.project)
socialProfiles?: SocialProfile[];

@Field(type => [ProjectEstimatedMatchingView], { nullable: true })
@OneToMany(
type => ProjectEstimatedMatchingView,
projectEstimatedMatchingView => projectEstimatedMatchingView.project,
)
projectEstimatedMatchingView?: ProjectEstimatedMatchingView[];

@Field(type => Float)
@Column({ type: 'real' })
totalDonations: number;
Expand Down
30 changes: 24 additions & 6 deletions src/repositories/projectRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,10 @@ import {
} from '../entities/project';
import { ProjectVerificationForm } from '../entities/projectVerificationForm';
import { ProjectAddress } from '../entities/projectAddress';
import {
errorMessages,
i18n,
translationErrorMessagesKeys,
} from '../utils/errorMessages';
import { i18n, translationErrorMessagesKeys } from '../utils/errorMessages';
import { User, publicSelectionFields } from '../entities/user';
import { ResourcesTotalPerMonthAndYear } from '../resolvers/donationResolver';
import { OrderDirection, ProjectResolver } from '../resolvers/projectResolver';

export const findProjectById = (projectId: number): Promise<Project | null> => {
// return Project.findOne({ id: projectId });

Expand Down Expand Up @@ -59,6 +54,7 @@ export type FilterProjectQueryInputParams = {
slugArray?: string[];
sortingBy?: SortingField;
qfRoundId?: number;
activeQfRoundId?: number;
};
export const filterProjectsQuery = (params: FilterProjectQueryInputParams) => {
const {
Expand All @@ -71,6 +67,7 @@ export const filterProjectsQuery = (params: FilterProjectQueryInputParams) => {
sortingBy,
slugArray,
qfRoundId,
activeQfRoundId,
} = params;

let query = Project.createQueryBuilder('project')
Expand Down Expand Up @@ -168,6 +165,27 @@ export const filterProjectsQuery = (params: FilterProjectQueryInputParams) => {
'NULLS LAST',
);
break;
case SortingField.ActiveQfRoundRaisedFunds:
if (activeQfRoundId) {
query
.innerJoin(
'project.projectEstimatedMatchingView',
'projectEstimatedMatchingView',
)
.addSelect([
'projectEstimatedMatchingView.sumValueUsd',
'projectEstimatedMatchingView.qfRoundId',
])
.andWhere('projectEstimatedMatchingView.qfRoundId = :qfRoundId', {
qfRoundId: activeQfRoundId,
})
.orderBy(
'projectEstimatedMatchingView.sumValueUsd',
OrderDirection.DESC,
)
.addOrderBy(`project.verified`, OrderDirection.DESC);
}
break;
default:
query
.orderBy('projectInstantPower.totalPower', OrderDirection.DESC)
Expand Down
68 changes: 68 additions & 0 deletions src/resolvers/projectResolver.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -603,6 +603,74 @@ function allProjectsTestCases() {
);
});

it('should return projects, sort by project raised funds in the active QF round DESC', async () => {
const donor = await saveUserDirectlyToDb(generateRandomEtheriumAddress());
const project1 = await saveProjectDirectlyToDb({
...createProjectData(),
title: String(new Date().getTime()),
slug: String(new Date().getTime()),
});
const project2 = await saveProjectDirectlyToDb({
...createProjectData(),
title: String(new Date().getTime()),
slug: String(new Date().getTime()),
});

const qfRound = await QfRound.create({
isActive: true,
name: 'test filter by qfRoundId',
minimumPassportScore: 10,
allocatedFund: 100,
beginDate: new Date(),
endDate: moment().add(1, 'day').toDate(),
}).save();
project1.qfRounds = [qfRound];
await project1.save();
project2.qfRounds = [qfRound];
await project2.save();

const donation1 = await saveDonationDirectlyToDb(
{
...createDonationData(),
status: 'verified',
qfRoundId: qfRound.id,
valueUsd: 2,
},
donor.id,
project1.id,
);

const donation2 = await saveDonationDirectlyToDb(
{
...createDonationData(),
status: 'verified',
qfRoundId: qfRound.id,
valueUsd: 20,
},
donor.id,
project2.id,
);

await refreshProjectEstimatedMatchingView();
await refreshProjectDonationSummaryView();

const result = await axios.post(graphqlUrl, {
query: fetchMultiFilterAllProjectsQuery,
variables: {
sortingBy: SortingField.ActiveQfRoundRaisedFunds,
limit: 10,
},
});

assert.equal(result.data.data.allProjects.projects.length, 2);
assert.equal(result.data.data.allProjects.projects[0].id, project2.id);
result.data.data.allProjects.projects.forEach(project => {
assert.equal(project.qfRounds[0].id, qfRound.id);
});
qfRound.isActive = false;
await qfRound.save();
});

it('should return projects, sort by project instant power DESC', async () => {
await PowerBoosting.clear();
await InstantPowerBalance.clear();
Expand Down
8 changes: 8 additions & 0 deletions src/resolvers/projectResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,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 { findActiveQfRound } from '../repositories/qfRoundRepository';
import { getAllProjectsRelatedToActiveCampaigns } from '../services/campaignService';

@ObjectType()
Expand Down Expand Up @@ -761,6 +762,12 @@ export class ProjectResolver {
): Promise<AllProjects> {
let projects: Project[];
let totalCount: number;
let activeQfRoundId: number | undefined;

if (sortingBy === SortingField.ActiveQfRoundRaisedFunds) {
activeQfRoundId = (await findActiveQfRound())?.id;
}

const filterQueryParams: FilterProjectQueryInputParams = {
limit,
skip,
Expand All @@ -770,6 +777,7 @@ export class ProjectResolver {
filters,
sortingBy,
qfRoundId,
activeQfRoundId,
};
let campaign;
if (campaignSlug) {
Expand Down

0 comments on commit 97d1a2f

Please sign in to comment.