diff --git a/src/common/constants.ts b/src/common/constants.ts index bc5ac0c..fed15eb 100644 --- a/src/common/constants.ts +++ b/src/common/constants.ts @@ -18,3 +18,4 @@ export const IMAGE_ADDRESS = 'https://raw.githubusercontent.com/cuappdev/assets/ export const IOS = 'IOS'; export const MAX_NUM_DAYS_OF_TRENDING_ARTICLES = 30; export const MAX_NUM_DAYS_OF_FEATURED_MAGAZINES = 30; +export const MAX_NUM_OF_TRENDING_FLYERS = 10; diff --git a/src/entities/Flyer.ts b/src/entities/Flyer.ts index 7d6801d..6fa9f66 100644 --- a/src/entities/Flyer.ts +++ b/src/entities/Flyer.ts @@ -9,7 +9,7 @@ export class Flyer { @Field() @Property() - date: Date; + endDate: Date; @Field() @Property({ nullable: true }) @@ -39,9 +39,13 @@ export class Flyer { @Property({ type: () => [String] }) organizationSlugs: [string]; + @Field() + @Property() + startDate: Date; + @Field() @Property({ default: 0 }) - shoutouts?: number; + timesClicked?: number; @Field() @Property() diff --git a/src/entities/Organization.ts b/src/entities/Organization.ts index 645594b..4cfa46f 100644 --- a/src/entities/Organization.ts +++ b/src/entities/Organization.ts @@ -7,11 +7,11 @@ export class Organization { id: string; @Field() - @Property() + @Property({ nullable: true }) backgroundImageURL: string; @Field() - @Property() + @Property({ nullable: true }) bio: string; @Field() @@ -23,7 +23,7 @@ export class Organization { name: string; @Field() - @Property() + @Property({ nullable: true }) profileImageURL: string; @Field() @@ -35,7 +35,7 @@ export class Organization { shoutouts?: number; @Field() - @Property() + @Property({ nullable: true }) websiteURL: string; } diff --git a/src/repos/FlyerRepo.ts b/src/repos/FlyerRepo.ts index 7555c34..8e1e801 100644 --- a/src/repos/FlyerRepo.ts +++ b/src/repos/FlyerRepo.ts @@ -8,6 +8,7 @@ import { DEFAULT_OFFSET, FILTERED_WORDS, MAX_NUM_DAYS_OF_TRENDING_ARTICLES, + MAX_NUM_OF_TRENDING_FLYERS, } from '../common/constants'; import { OrganizationModel } from '../entities/Organization'; @@ -23,7 +24,7 @@ function isFlyerFiltered(flyer: Flyer) { const getAllFlyers = async (offset = DEFAULT_OFFSET, limit = DEFAULT_LIMIT): Promise => { return FlyerModel.find({}) - .sort({ date: 'desc' }) + .sort({ startDate: 'desc' }) .skip(offset) .limit(limit) .then((flyers) => { @@ -31,6 +32,36 @@ const getAllFlyers = async (offset = DEFAULT_OFFSET, limit = DEFAULT_LIMIT): Pro }); }; +const getFlyersAfterDate = (since: string, limit = DEFAULT_LIMIT): Promise => { + return ( + FlyerModel.find({ + // Get all Flyers after or on the desired date + endDate: { $gte: new Date(since) }, + }) + // Sort dates in order of most recent to least + .sort({ endDate: 'desc' }) + .limit(limit) + .then((flyers) => { + return flyers.filter((flyer) => flyer !== null && !isFlyerFiltered(flyer)); + }) + ); +}; + +const getFlyersBeforeDate = (before: string, limit = DEFAULT_LIMIT): Promise => { + return ( + FlyerModel.find({ + // Get all Flyers before the desired date + endDate: { $lt: new Date(before) }, + }) + // Sort dates in order of most recent to least + .sort({ endDate: 'desc' }) + .limit(limit) + .then((flyers) => { + return flyers.filter((flyer) => flyer !== null && !isFlyerFiltered(flyer)); + }) + ); +}; + const getFlyerByID = async (id: string): Promise => { return FlyerModel.findById(new ObjectId(id)).then((flyer) => { if (!isFlyerFiltered(flyer)) { @@ -54,7 +85,7 @@ const getFlyersByOrganizationSlug = async ( offset: number = DEFAULT_OFFSET, ): Promise => { return FlyerModel.find({ organizationSlugs: slug }) - .sort({ date: 'desc' }) + .sort({ startDate: 'desc' }) .skip(offset) .limit(limit) .then((flyers) => { @@ -69,7 +100,7 @@ const getFlyersByOrganizationSlugs = async ( ): Promise => { const uniqueSlugs = [...new Set(slugs)]; return FlyerModel.find({ organizationSlugs: { $in: uniqueSlugs } }) - .sort({ date: 'desc' }) + .sort({ startDate: 'desc' }) .skip(offset) .limit(limit) .then((flyers) => { @@ -84,7 +115,7 @@ const getFlyersByOrganizationID = async ( ): Promise => { const organization = await (await OrganizationModel.findById(organizationID)).execPopulate(); return FlyerModel.find({ organizationSlugs: organization.slug }) - .sort({ date: 'desc' }) + .sort({ startDate: 'desc' }) .skip(offset) .limit(limit) .then((flyers) => { @@ -106,21 +137,6 @@ const getFlyersByOrganizationIDs = async ( ); }; -const getFlyersAfterDate = async (since: string, limit = DEFAULT_LIMIT): Promise => { - 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) => flyer !== null && !isFlyerFiltered(flyer)); - }) - ); -}; - /** * Performs fuzzy search on all Flyers to find Flyers with title/publisher matching the query. * @param query the term to search for @@ -163,17 +179,19 @@ const refreshTrendingFlyers = async (): Promise => { }); // 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), - ), - }, - }); + const flyers = await ( + await FlyerModel.aggregate() + // sort flyers by trendiness + .sort({ trendiness: 'desc' }) + // Only get flyers for events that start in the next few days + .match({ + startDate: { + $lte: new Date( + new Date().setDate(new Date().getDate() + MAX_NUM_DAYS_OF_TRENDING_ARTICLES), + ), + }, + }) + ).slice(0, MAX_NUM_OF_TRENDING_FLYERS); flyers.forEach(async (a) => { const flyer = await FlyerModel.findById(new ObjectId(a._id)); // eslint-disable-line @@ -185,14 +203,17 @@ const refreshTrendingFlyers = async (): Promise => { }; /** - * Increments number of shoutouts on an Flyer and publication by one. + * Increments number of times clicked on a flyer by one. * @function - * @param {string} id - string representing the unique Object Id of an Flyer. + * @param {string} id - string representing the unique Object Id of a flyer. */ -const incrementShoutouts = async (id: string): Promise => { +const incrementTimesClicked = async (id: string): Promise => { const flyer = await FlyerModel.findById(new ObjectId(id)); if (flyer) { - flyer.shoutouts += 1; + flyer.timesClicked += 1; + // update the trendiness of a flyer + flyer.trendiness = + (flyer.timesClicked / (flyer.startDate.getTime() - new Date().getTime())) * 10000000; return flyer.save(); } return flyer; @@ -213,13 +234,14 @@ export default { getAllFlyers, getFlyerByID, getFlyersAfterDate, + getFlyersBeforeDate, getFlyersByIDs, getFlyersByOrganizationID, getFlyersByOrganizationIDs, getFlyersByOrganizationSlug, getFlyersByOrganizationSlugs, getTrendingFlyers, - incrementShoutouts, + incrementTimesClicked, refreshTrendingFlyers, searchFlyers, }; diff --git a/src/repos/OrganizationRepo.ts b/src/repos/OrganizationRepo.ts index 6c3ce01..ca607c4 100644 --- a/src/repos/OrganizationRepo.ts +++ b/src/repos/OrganizationRepo.ts @@ -67,16 +67,16 @@ const getMostRecentFlyer = async (organization: Organization): Promise => // Organization['_doc'] must be used to access fields of a Organization object return FlyerModel.findOne({ organizationSlugs: organization['_doc'].slug, // eslint-disable-line - }).sort({ date: 'desc' }); + }).sort({ startDate: 'desc' }); }; /** - * Retrieves the number of shoutouts a Organization has by summing the shoutouts + * Retrieves the number of clicks an Organization has by summing the clicks * of all of its flyers. * @param {Organization} Organization * @returns {Number} */ -const getShoutouts = async (organization: Organization): Promise => { +const getClicks = async (organization: Organization): Promise => { // Due to the way Mongo interprets 'Organization' object, // Organization['_doc'] must be used to access fields of a Organization object const orgFlyers = await FlyerModel.find({ @@ -84,7 +84,7 @@ const getShoutouts = async (organization: Organization): Promise => { }); return orgFlyers.reduce((acc, flyer) => { - return acc + flyer.shoutouts; + return acc + flyer.timesClicked; }, 0); }; @@ -107,11 +107,11 @@ const getNumFlyers = async (organization: Organization): Promise => { export default { addOrganizationsToDB, getAllOrganizations, + getClicks, getMostRecentFlyer, getNumFlyers, getOrganizationsByCategory, getOrganizationByID, getOrganizationsByIDs, getOrganizationBySlug, - getShoutouts, }; diff --git a/src/resolvers/FlyerResolver.ts b/src/resolvers/FlyerResolver.ts index 42bfc52..f79d9cc 100644 --- a/src/resolvers/FlyerResolver.ts +++ b/src/resolvers/FlyerResolver.ts @@ -2,7 +2,6 @@ import { Resolver, Mutation, Arg, Query, FieldResolver, Root } from 'type-graphq import { Flyer } from '../entities/Flyer'; import FlyerRepo from '../repos/FlyerRepo'; import { DEFAULT_LIMIT, DEFAULT_OFFSET } from '../common/constants'; -import UserRepo from '../repos/UserRepo'; @Resolver((_of) => Flyer) class FlyerResolver { @@ -37,27 +36,27 @@ class FlyerResolver { @Query((_returns) => [Flyer], { nullable: false, description: - 'Returns a list of of size via the given . Results can offsetted by >= 0.', + 'Returns a list of of size via the given . Results can offsetted by >= 0.', }) async getFlyersByOrganizationID( - @Arg('publicationID') publicationID: string, + @Arg('organizationID') organizationID: string, @Arg('limit', { defaultValue: DEFAULT_LIMIT }) limit: number, @Arg('offset', { defaultValue: DEFAULT_OFFSET }) offset: number, ) { - return FlyerRepo.getFlyersByOrganizationID(publicationID, limit, offset); + return FlyerRepo.getFlyersByOrganizationID(organizationID, limit, offset); } @Query((_returns) => [Flyer], { nullable: false, description: - 'Returns a list of of size via the given list of . Results offsetted by >= 0.', + 'Returns a list of of size via the given list of . Results offsetted by >= 0.', }) async getFlyersByOrganizationIDs( - @Arg('publicationIDs', (type) => [String]) publicationIDs: string[], + @Arg('organizationIDs', (type) => [String]) organizationIDs: string[], @Arg('limit', { defaultValue: DEFAULT_LIMIT }) limit: number, @Arg('offset', { defaultValue: DEFAULT_OFFSET }) offset: number, ) { - return FlyerRepo.getFlyersByOrganizationIDs(publicationIDs, limit, offset); + return FlyerRepo.getFlyersByOrganizationIDs(organizationIDs, limit, offset); } @Query((_returns) => [Flyer], { @@ -98,6 +97,18 @@ class FlyerResolver { return FlyerRepo.getFlyersAfterDate(since, limit); } + @Query((_returns) => [Flyer], { + nullable: false, + description: `Returns a list of a given date, limited by . + is formatted as an compliant RFC 2822 timestamp. Valid examples include: "2019-01-31", "Aug 9, 1995", "Wed, 09 Aug 1995 00:00:00", etc. Default is ${DEFAULT_LIMIT}`, + }) + async getFlyersBeforeDate( + @Arg('before') before: string, + @Arg('limit', { defaultValue: DEFAULT_LIMIT }) limit: number, + ) { + return FlyerRepo.getFlyersBeforeDate(before, limit); + } + @Query((_returns) => [Flyer], { nullable: false, description: `Returns a list of trending of size . Default is ${DEFAULT_LIMIT}`, @@ -122,7 +133,7 @@ class FlyerResolver { const presentDate = new Date().getTime(); // Due to the way Mongo interprets 'Flyer' object, // Flyer['_doc'] must be used to access fields of a Flyer object - return flyer['_doc'].shoutouts / ((presentDate - flyer['_doc'].date.getTime())/1000); // eslint-disable-line + return flyer['_doc'].timesClicked / ((presentDate - flyer['_doc'].startDate.getTime()) / 1000); // eslint-disable-line } @FieldResolver((_returns) => Boolean, { @@ -134,12 +145,10 @@ class FlyerResolver { @Mutation((_returns) => Flyer, { nullable: true, - description: `Increments the shoutouts of a with the given . - Increments the numShoutouts given of the user with the given [uuid].`, + description: `Increments the times clicked of a with the given .`, }) - async incrementShoutouts(@Arg('uuid') uuid: string, @Arg('id') id: string) { - UserRepo.incrementShoutouts(uuid); - return FlyerRepo.incrementShoutouts(id); + async incrementTimesClicked(@Arg('id') id: string) { + return FlyerRepo.incrementTimesClicked(id); } } diff --git a/src/resolvers/OrganizationResolver.ts b/src/resolvers/OrganizationResolver.ts index 19a520c..c62da35 100644 --- a/src/resolvers/OrganizationResolver.ts +++ b/src/resolvers/OrganizationResolver.ts @@ -60,10 +60,10 @@ class OrganizationResolver { } @FieldResolver((_returns) => Number, { - description: "Returns the total shoutouts of an ", + description: "Returns the total times clicked of an ", }) - async shoutouts(@Root() organization: Organization): Promise { - return OrganizationRepo.getShoutouts(organization); + async clicks(@Root() organization: Organization): Promise { + return OrganizationRepo.getClicks(organization); } } diff --git a/src/tests/data/FactoryUtils.ts b/src/tests/data/FactoryUtils.ts index 6828a5b..1145098 100644 --- a/src/tests/data/FactoryUtils.ts +++ b/src/tests/data/FactoryUtils.ts @@ -16,14 +16,39 @@ class FactoryUtils { } /** - * Compares the time between two datess by most recent first + * Compares the time between two dates by most recent first * * @param a The first date to be compared * @param b The second date to be compared - * @returns 1 if b is more recent than a, -1 if a is more recent than b, 0 if a and b are at the same time + * @returns A positive integer if b is more recent than a, a negative integer + * if a is more recent than b, 0 if a and b are at the same time */ public static compareByDate(a, b) { return new Date(b.date).getTime() - new Date(a.date).getTime(); } + + /** + * Compares the time between two end dates by most recent first + * + * @param a The first date to be compared + * @param b The second date to be compared + * @returns A positive integer if b is more recent than a, a negative integer + * if a is more recent than b, 0 if a and b are at the same time + */ + public static compareByEndDate(a, b) { + return new Date(b.endDate).getTime() - new Date(a.endDate).getTime(); + } + + /** + * Compares the time between two start dates by most recent first + * + * @param a The first date to be compared + * @param b The second date to be compared + * @returns A positive integer if b is more recent than a, a negative integer + * if a is more recent than b, 0 if a and b are at the same time + */ + public static compareByStartDate(a, b) { + return new Date(b.startDate).getTime() - new Date(a.startDate).getTime(); + } } export default FactoryUtils; diff --git a/src/tests/data/FlyerFactory.ts b/src/tests/data/FlyerFactory.ts index 35f9780..1c7480f 100644 --- a/src/tests/data/FlyerFactory.ts +++ b/src/tests/data/FlyerFactory.ts @@ -47,7 +47,8 @@ class FlyerFactory { const fakeFlyer = new Flyer(); const exampleOrg = await OrganizationFactory.getRandomOrganization(); - fakeFlyer.date = faker.date.past(); + fakeFlyer.startDate = faker.date.past(); + fakeFlyer.endDate = faker.date.future(); fakeFlyer.imageURL = faker.image.cats(); fakeFlyer.flyerURL = faker.datatype.string(); fakeFlyer.location = faker.datatype.string(); @@ -56,7 +57,7 @@ class FlyerFactory { fakeFlyer.title = faker.commerce.productDescription(); fakeFlyer.isTrending = _.sample([true, false]); fakeFlyer.nsfw = _.sample([true, false]); - fakeFlyer.shoutouts = _.random(0, 50); + fakeFlyer.timesClicked = _.random(0, 50); fakeFlyer.trendiness = 0; return fakeFlyer; diff --git a/src/tests/flyer.test.ts b/src/tests/flyer.test.ts index 3cc4ed3..72d75e2 100644 --- a/src/tests/flyer.test.ts +++ b/src/tests/flyer.test.ts @@ -50,7 +50,7 @@ describe('getAllFlyer tests:', () => { test('getAllFlyers - Sort by date desc, offset 2, limit 2', async () => { const flyers = await FlyerFactory.create(5); - flyers.sort(FactoryUtils.compareByDate); + flyers.sort(FactoryUtils.compareByStartDate); await FlyerModel.insertMany(flyers); const flyerTitles = FactoryUtils.mapToValue(flyers.slice(2, 4), 'title'); // offset=2, limit=2 @@ -104,7 +104,7 @@ describe('getFlyersByOrganizationSlug(s) tests', () => { organizationSlugs: [org.slug], organizations: [org], }) - ).sort(FactoryUtils.compareByDate); + ).sort(FactoryUtils.compareByStartDate); await FlyerModel.insertMany(flyers); const getFlyersResponse = await FlyerRepo.getFlyersByOrganizationSlug(org.slug); @@ -121,7 +121,7 @@ describe('getFlyersByOrganizationSlug(s) tests', () => { await FlyerFactory.createSpecific(3, { organizationSlugs: [org1.slug, org2.slug], }) - ).sort(FactoryUtils.compareByDate); + ).sort(FactoryUtils.compareByEndDate); await FlyerModel.insertMany(flyers); const getFlyersResponse1 = await FlyerRepo.getFlyersByOrganizationSlugs([org1.slug]); @@ -137,11 +137,11 @@ describe('getFlyersAfterDate tests', () => { test('getFlyersAfterDate - filter 1 flyer', async () => { const today = new Date(); let flyers = await FlyerFactory.createSpecific(1, { - date: faker.date.recent(1), // 1 day ago + endDate: faker.date.recent(1), // 1 day ago }); flyers = flyers.concat( await FlyerFactory.createSpecific(1, { - date: faker.date.past(1, today.getDate() - 2), // 1 year ago + endDate: faker.date.past(1, today.getDate() - 2), // 1 year ago }), ); await FlyerModel.insertMany(flyers); @@ -153,6 +153,18 @@ describe('getFlyersAfterDate tests', () => { }); }); +describe('getFlyersBeforeDate tests', () => { + test('getFlyersBeforeDate - three flyers', async () => { + const flyers = await FlyerFactory.createSpecific(3, { + endDate: faker.date.past(1), // 1 year ago + }); + await FlyerModel.insertMany(flyers); + + const getFlyersResponse = await FlyerRepo.getFlyersBeforeDate(faker.date.future(2).toString()); + expect(getFlyersResponse).toHaveLength(3); + }); +}); + describe('searchFlyer tests', () => { test('searchFlyer - 1 flyer', async () => { const flyers = await FlyerFactory.create(1); @@ -184,13 +196,13 @@ describe('searchFlyer tests', () => { describe('incrementShoutouts tests', () => { test('incrementShoutouts - Shoutout 1 flyer', async () => { const flyers = await FlyerFactory.create(1); - const oldShoutouts = flyers[0].shoutouts; + const oldClicks = flyers[0].timesClicked; const insertOutput = await FlyerModel.insertMany(flyers); - await FlyerRepo.incrementShoutouts(insertOutput[0]._id); + await FlyerRepo.incrementTimesClicked(insertOutput[0]._id); const getFlyersResponse = await FlyerRepo.getFlyerByID(insertOutput[0]._id); - expect(getFlyersResponse.shoutouts).toEqual(oldShoutouts + 1); + expect(getFlyersResponse.timesClicked).toEqual(oldClicks + 1); }); }); diff --git a/src/tests/organization.test.ts b/src/tests/organization.test.ts index 02d47e5..55b14d9 100644 --- a/src/tests/organization.test.ts +++ b/src/tests/organization.test.ts @@ -48,9 +48,9 @@ describe('getMostRecentFlyer tests:', () => { await FlyerModel.insertMany(flyers); const getOrganizationsResponse = await OrganizationRepo.getMostRecentFlyer(org); - const respDate = new Date(getOrganizationsResponse.date); + const respDate = new Date(getOrganizationsResponse.startDate); - const flyerDates = FactoryUtils.mapToValue(flyers, 'date'); + const flyerDates = FactoryUtils.mapToValue(flyers, 'startDate'); const isMin = flyerDates.every((d) => { return respDate.getTime() >= new Date(d).getTime(); @@ -113,21 +113,21 @@ describe('getOrganizationBySlug tests:', () => { }); }); -describe('getShoutouts tests:', () => { - test('getShoutouts - Random number of flyers with 2 shoutouts, 1 org', async () => { +describe('getClicks tests:', () => { + test('getClicks - Random number of flyers with 2 clicks, 1 org', async () => { const org = await OrganizationRepo.getOrganizationBySlug( (await OrganizationFactory.getRandomOrganization()).slug, ); const numFlyers = _.random(1, 20); - const numShoutouts = numFlyers * 2; + const numClicks = numFlyers * 2; const flyers = await FlyerFactory.createSpecific(numFlyers, { organizationSlugs: [org.slug], organizations: [org], - shoutouts: 2, + timesClicked: 2, }); await FlyerModel.insertMany(flyers); - const getOrganizationsResponse = await OrganizationRepo.getShoutouts(org); + const getOrganizationsResponse = await OrganizationRepo.getClicks(org); - expect(getOrganizationsResponse).toEqual(numShoutouts); + expect(getOrganizationsResponse).toEqual(numClicks); }); });