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

Implement community board [1/7] #81

Merged
merged 3 commits into from
Apr 21, 2023
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
10 changes: 7 additions & 3 deletions src/entities/Flyer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,19 @@ export class Flyer {

@Field()
@Property()
flyerURL: string;
date: Date;

@Field()
@Property()
date: Date;
imageURL: string;

@Field()
@Property({ nullable: true })
description: string;

@Field()
@Property()
imageURL: string;
location: string;

@Field((type) => Organization)
@Property({ type: () => Organization })
Expand Down
229 changes: 229 additions & 0 deletions src/repos/FlyerRepo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
import Filter from 'bad-words';
import { ObjectId } from 'mongodb';
import Fuse from 'fuse.js';
import { Flyer, FlyerModel } from '../entities/Flyer';
import {
DEFAULT_LIMIT,
MAX_NUM_DAYS_OF_TRENDING_ARTICLES,
FILTERED_WORDS,
DEFAULT_OFFSET,
Copy link
Contributor

Choose a reason for hiding this comment

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

alphabetize!

Choose a reason for hiding this comment

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

trueee

Copy link
Contributor Author

Choose a reason for hiding this comment

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

prod

} from '../common/constants';
import { OrganizationModel } from '../entities/Organization';

const { IS_FILTER_ACTIVE } = process.env;

function isFlyerFiltered(flyer: Flyer) {
if (IS_FILTER_ACTIVE === 'true') {
if (flyer.isFiltered) {
// If the body has been checked already in microservice
return true;
}
const filter = new Filter({ list: FILTERED_WORDS });
return filter.isProfane(flyer.title);
Copy link
Contributor

Choose a reason for hiding this comment

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

i agree with @katesliang 's comments from ur last PR

why do you check if a flyer is profane twice? does microservice only check flyer description or smth?

Choose a reason for hiding this comment

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

thanks shungo

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yeah i think the microservice for articles only checks the body, so i was planning on doing the same thing for flyers, but i realized that flyers actually don't have a body (it's just an image) so I'll get rid of this part

Choose a reason for hiding this comment

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

nice

}
return false;
}

const getFlyerByID = async (id: string): Promise<Flyer> => {
return FlyerModel.findById(new ObjectId(id)).then((flyer) => {
if (!isFlyerFiltered(flyer)) {
return flyer;
}
return null;
});
};

const getFlyersByIDs = async (ids: string[]): Promise<Flyer[]> => {
return Promise.all(ids.map((id) => FlyerModel.findById(new ObjectId(id)))).then((flyers) => {
// Filter out all null values that were returned by ObjectIds not associated
// with Flyers in database
return flyers.filter((flyer) => flyer !== null && !isFlyerFiltered(flyer));
});
};

const getAllFlyers = async (offset = DEFAULT_OFFSET, limit = DEFAULT_LIMIT): Promise<Flyer[]> => {
return FlyerModel.find({})
.sort({ date: 'desc' })
.skip(offset)
.limit(limit)
.then((flyers) => {
return flyers.filter((flyer) => !isFlyerFiltered(flyer));

Choose a reason for hiding this comment

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

why do you check if it's not null here but in the above and below functions?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

i think getAllFlyers shouldn't return extra nulls because it's using find({}) and getFlyerByID is only looking for one article so it'll only return null if there aren't any flyers to be found

});
};

const getFlyersByOrganizationSlug = async (
slug: string,
limit: number = DEFAULT_LIMIT,
offset: number = DEFAULT_OFFSET,
): Promise<Flyer[]> => {
return FlyerModel.find({ 'organization.slug': slug })
.sort({ date: 'desc' })
.skip(offset)
.limit(limit)
.then((flyers) => {
return flyers.filter((flyer) => flyer !== null && !isFlyerFiltered(flyer));
});
};

const getFlyersByOrganizationSlugs = async (
slugs: string[],
limit: number = DEFAULT_LIMIT,
offset: number = DEFAULT_OFFSET,
): Promise<Flyer[]> => {
const uniqueSlugs = [...new Set(slugs)];
return FlyerModel.find({ 'organization.slug': { $in: uniqueSlugs } })
.sort({ date: 'desc' })
.skip(offset)
.limit(limit)
.then((flyers) => {
return flyers.filter((flyer) => flyer !== null && !isFlyerFiltered(flyer));
});
};

const getFlyersByOrganizationID = async (
organizationID: string,
limit: number = DEFAULT_LIMIT,
offset: number = DEFAULT_OFFSET,
): Promise<Flyer[]> => {
const organization = await (await OrganizationModel.findById(organizationID)).execPopulate();
return FlyerModel.find({ 'organization.slug': organization.slug })
.sort({ date: 'desc' })
.skip(offset)
.limit(limit)
.then((flyers) => {
return flyers.filter((flyer) => !isFlyerFiltered(flyer));

Choose a reason for hiding this comment

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

same comment as above

Copy link
Contributor Author

Choose a reason for hiding this comment

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

you're right i'll check for null in getFlyersByOrganizationID

});
};

const getFlyersByOrganizationIDs = async (
organizationIDs: string[],
limit: number = DEFAULT_LIMIT,
offset: number = DEFAULT_OFFSET,
): Promise<Flyer[]> => {
const uniqueOrgIDs = [...new Set(organizationIDs)].map((id) => new ObjectId(id));
console.log(uniqueOrgIDs);

Choose a reason for hiding this comment

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

delete?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

true

const orgSlugs = await OrganizationModel.find({ _id: { $in: uniqueOrgIDs } }).select('slug');
return getFlyersByOrganizationSlugs(
orgSlugs.map((org) => org.slug),
limit,
offset,
);
};

const getFlyersAfterDate = async (since: string, limit = DEFAULT_LIMIT): Promise<Flyer[]> => {
return (
FlyerModel.find({
// Get all Flyers after or on the desired date
date: { $gte: new Date(new Date(since).setHours(0, 0, 0)) },
})
// Sort dates in order of most recent to least
.sort({ date: 'desc' })
.limit(limit)
.then((flyers) => {
return flyers.filter((flyer) => !isFlyerFiltered(flyer));

Choose a reason for hiding this comment

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

same

Copy link
Contributor Author

Choose a reason for hiding this comment

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

truee

})
);
};

/**
* Performs fuzzy search on all Flyers to find Flyers with title/publisher matching the query.
* @param query the term to search for
* @param limit the number of results to return
* @returns at most limit Flyers with titles or publishers matching the query
*/
const searchFlyers = async (query: string, limit = DEFAULT_LIMIT) => {
const allFlyers = await FlyerModel.find({});
const searcher = new Fuse(allFlyers, {
keys: ['title', 'organization.name'],
});

return searcher
.search(query)
.map((searchRes) => searchRes.item)
.slice(0, limit);
};

/**
* Computes and returns the trending Flyers in the database.
*
* @function
* @param {number} limit - number of Flyers to retrieve.
*/
const getTrendingFlyers = async (limit = DEFAULT_LIMIT): Promise<Flyer[]> => {
const flyers = await FlyerModel.find({ isTrending: true }).exec();
return flyers.filter((flyer) => !isFlyerFiltered(flyer)).slice(0, limit);
};

/**
* Refreshes trending Flyers.
*/
const refreshTrendingFlyers = async (): Promise<Flyer[]> => {
// Set previous trending Flyers to not trending
const oldTrendingFlyers = await FlyerModel.find({ isTrending: true }).exec();
oldTrendingFlyers.forEach(async (a) => {
const flyer = await FlyerModel.findById(new ObjectId(a._id)); // eslint-disable-line
flyer.isTrending = false;
await flyer.save();
});

// Get new trending Flyers
const flyers = await FlyerModel.aggregate()
// Get a sample of random Flyers
.sample(100)
// Get Flyers after 30 days ago
.match({
date: {
$gte: new Date(
new Date().setDate(new Date().getDate() - MAX_NUM_DAYS_OF_TRENDING_ARTICLES),
),
},
Comment on lines +168 to +175
Copy link
Contributor

Choose a reason for hiding this comment

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

and this
from @katesliang:

how exactly are we determining if something is trending?

from what i see we just get a random sample of 100 flyers and then see if they are from within the last 30 days?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

rn we're just randomly sampling because we don't have any flyers T_T but I can fix it later after we get the app launched

Choose a reason for hiding this comment

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

okie you should leave a comment or create an issue or smth to document it somewhere

Copy link
Contributor

Choose a reason for hiding this comment

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

let's create an issue! @isaachan100

Copy link
Contributor Author

Choose a reason for hiding this comment

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

prod

});

flyers.forEach(async (a) => {
const flyer = await FlyerModel.findById(new ObjectId(a._id)); // eslint-disable-line
flyer.isTrending = true;
await flyer.save();
});

return flyers;
};

/**
* Increments number of shoutouts on an Flyer and publication by one.
* @function
* @param {string} id - string representing the unique Object Id of an Flyer.
*/
const incrementShoutouts = async (id: string): Promise<Flyer> => {
const flyer = await FlyerModel.findById(new ObjectId(id));
if (flyer) {
flyer.shoutouts += 1;
return flyer.save();
}
return flyer;
};

/**
* Checks if an Flyer's title contains profanity.
* @function
* @param {string} title - Flyer title.
*/
const checkProfanity = async (title: string): Promise<boolean> => {
const filter = new Filter();
return filter.isProfane(title);
};

export default {
checkProfanity,
getAllFlyers,
getFlyerByID,
getFlyersAfterDate,
getFlyersByIDs,
getFlyersByOrganizationID,
getFlyersByOrganizationIDs,
getFlyersByOrganizationSlug,
getFlyersByOrganizationSlugs,
searchFlyers,
getTrendingFlyers,
incrementShoutouts,
refreshTrendingFlyers,
Copy link
Contributor

Choose a reason for hiding this comment

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

alphabetize

Copy link
Contributor Author

Choose a reason for hiding this comment

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

appdev teaching me the alphabet im so thankful

};