Skip to content

Commit e577f16

Browse files
cbishopveltiht2
authored andcommitted
feat: LL-482 Feature expiration (LearningLocker#1105)
* Start of expiration * Only site_admin can change the expiration. * Show expired organisation after login. * Added expiration email. * White space. * Fixed tests. * Fixed console errors. * Changing the expiration date will reset the email status. * Test old expiration, only if it was set. * Added worker log. * Removed re throwing error. * Added schedular to pm2. * Fixed typo. * Fixed build error. * test: Remove only * Fixed worker not building. * test: Add organisation to test dashboard token * Tweak email wording * Tweak email wording
1 parent 4a881f2 commit e577f16

File tree

24 files changed

+554
-30
lines changed

24 files changed

+554
-30
lines changed

api/src/routes/HttpRoutes.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,18 @@ router.get(
198198
* REST APIS
199199
*/
200200
restify.defaults(RESTIFY_DEFAULTS);
201-
restify.serve(router, Organisation);
201+
restify.serve(router, Organisation, {
202+
preUpdate: (req, res, next) => {
203+
const authInfo = getAuthFromRequest(req);
204+
const scopes = getScopesFromRequest(authInfo);
205+
if (
206+
findIndex(scopes, item => item === SITE_ADMIN) < 0
207+
) {
208+
req.body = omit(req.body, 'expiration');
209+
}
210+
next();
211+
}
212+
});
202213
restify.serve(router, Stream);
203214
restify.serve(router, Export);
204215
restify.serve(router, Download);

api/src/routes/tests/utils/models/createDashboard.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,6 @@ export default (opts = { _id: testId }) =>
66
Dashboard.create({
77
name: 'Test dashboard',
88
owner: ownerId,
9+
organisation: testId,
910
...opts
1011
});

cli/build.js

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import 'dotenv/config';
22
import path from 'path';
3+
import { all } from 'bluebird';
34
import run from '../lib/tools/run';
45
import bundle from '../lib/tools/bundle';
56
import getWebpackConfig from '../lib/tools/getWebpackConfig';
@@ -25,13 +26,28 @@ const webpackConfig = getWebpackConfig({
2526
}
2627
});
2728

29+
const schedulerWebpackConfig = getWebpackConfig({
30+
isDebug,
31+
isVerbose,
32+
isClient: false,
33+
sourceDir,
34+
outputDir: path.resolve(__dirname, 'dist', 'scheduler'),
35+
stats,
36+
publicPath: '/',
37+
entry: {
38+
scheduler: 'scheduler.js'
39+
}
40+
});
2841

2942
/**
3043
* Uses webpack to compile the API server
3144
* into a single file executable by node
3245
*/
3346
async function build() {
34-
await run(bundle, { webpackConfig, watch });
47+
await all([
48+
run(bundle, { webpackConfig, watch }),
49+
run(bundle, { webpackConfig: schedulerWebpackConfig, watch })
50+
]);
3551
}
3652

3753
export default build();
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import Organisation, { EMAIL_PROCESSING, EMAIL_NOOP } from 'lib/models/organisation';
2+
import moment from 'moment';
3+
import { expect } from 'chai';
4+
import {
5+
WEEK_BEFORE_NOTIFICATION,
6+
EXPIRATION_NOTIFICATION
7+
} from 'lib/constants/expirationNotifications';
8+
import expirationNotificationEmails from './expirationNotificationEmails';
9+
10+
describe('expirationNotificationEmails', () => {
11+
beforeEach(async () => {
12+
await Organisation.remove({});
13+
});
14+
15+
it('should send weekBefore email', async () => {
16+
const organisation = await Organisation.create({
17+
expiration: moment().add(1, 'day').toDate()
18+
});
19+
20+
let publishCalled = false;
21+
await expirationNotificationEmails({
22+
publish: ({
23+
payload
24+
}) => {
25+
publishCalled = true;
26+
expect(payload.organisationId).to.equal(organisation._id.toString());
27+
expect(payload.emailType).to.equal(WEEK_BEFORE_NOTIFICATION);
28+
},
29+
dontExit: true
30+
});
31+
32+
expect(publishCalled).to.equal(true);
33+
34+
const newOrg = await Organisation.findById(organisation._id);
35+
expect(newOrg.expirationNotifications.weekBeforeNotificationSent).to.equal(EMAIL_PROCESSING);
36+
expect(newOrg.expirationNotifications.expirationNotificationSent).to.equal(EMAIL_NOOP);
37+
});
38+
39+
it('should not send any email', async () => {
40+
const organisation = await Organisation.create({
41+
expiration: moment().add(9, 'day').toDate()
42+
});
43+
44+
let publishCalled = false;
45+
await expirationNotificationEmails({
46+
publish: () => {
47+
publishCalled = true;
48+
},
49+
dontExit: true
50+
});
51+
52+
expect(publishCalled).to.equal(false);
53+
54+
const newOrg = await Organisation.findById(organisation._id);
55+
expect(newOrg.expirationNotifications.weekBeforeNotificationSent).to.equal(EMAIL_NOOP);
56+
expect(newOrg.expirationNotifications.expirationNotificationSent).to.equal(EMAIL_NOOP);
57+
});
58+
59+
it('should send expiration email', async () => {
60+
const organisation = await Organisation.create({
61+
expiration: moment().subtract(1, 'day').toDate()
62+
});
63+
64+
let publishCalled = false;
65+
await expirationNotificationEmails({
66+
publish: ({
67+
payload
68+
}) => {
69+
publishCalled = true;
70+
expect(payload.organisationId).to.equal(organisation._id.toString());
71+
expect(payload.emailType).to.equal(EXPIRATION_NOTIFICATION);
72+
},
73+
dontExit: true
74+
});
75+
76+
expect(publishCalled).to.equal(true);
77+
78+
const newOrg = await Organisation.findById(organisation._id);
79+
expect(newOrg.expirationNotifications.weekBeforeNotificationSent).to.equal(EMAIL_NOOP);
80+
expect(newOrg.expirationNotifications.expirationNotificationSent).to.equal(EMAIL_PROCESSING);
81+
});
82+
});
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import Organisation, { EMAIL_NOOP, EMAIL_PROCESSING } from 'lib/models/organisation';
2+
import moment from 'moment';
3+
import { map } from 'bluebird';
4+
import { publish as publishToQueue } from 'lib/services/queue';
5+
import {
6+
EXPIRATION_NOTIFICATION,
7+
WEEK_BEFORE_NOTIFICATION,
8+
EMAIL_EXPIRATION_NOTIFICATION_QUEUE
9+
} from 'lib/constants/expirationNotifications';
10+
11+
export default async function ({
12+
weekBefore, // for testing, iso string
13+
publish = publishToQueue, // for testing
14+
dontExit = false
15+
}) {
16+
let weekBeforeMoment;
17+
if (weekBefore) {
18+
weekBeforeMoment = moment(weekBefore);
19+
} else {
20+
weekBeforeMoment = moment().add(1, 'week');
21+
}
22+
23+
const todayMoment = moment();
24+
25+
const weekBeforeDate = weekBeforeMoment.toDate();
26+
const todayDate = todayMoment.toDate();
27+
28+
const toSendEmail = await Organisation.find({
29+
$or: [
30+
{
31+
'expirationNotifications.weekBeforeNotificationSent': EMAIL_NOOP,
32+
expiration: {
33+
$lt: weekBeforeDate
34+
}
35+
},
36+
{
37+
'expirationNotifications.expirationNotificationSent': EMAIL_NOOP,
38+
expiration: {
39+
$lt: todayDate
40+
}
41+
}
42+
]
43+
}).exec();
44+
45+
await map(toSendEmail, async (organisation) => {
46+
if (
47+
weekBeforeMoment.isAfter(moment(organisation.expiration)) &&
48+
todayMoment.isAfter(moment(organisation.expiration))
49+
) {
50+
if (organisation.expirationNotifications.expirationNotificationSent === EMAIL_NOOP) {
51+
// send expiration email
52+
await publish({
53+
queueName: EMAIL_EXPIRATION_NOTIFICATION_QUEUE,
54+
payload: {
55+
organisationId: organisation._id.toString(),
56+
emailType: EXPIRATION_NOTIFICATION
57+
}
58+
});
59+
60+
organisation.expirationNotifications.expirationNotificationSent = EMAIL_PROCESSING;
61+
await organisation.save();
62+
}
63+
} else if (weekBeforeMoment.isAfter(moment(organisation.expiration))) {
64+
if (organisation.expirationNotifications.weekBeforeNotificationSent === EMAIL_NOOP) {
65+
// send week before email
66+
await publish({
67+
queueName: EMAIL_EXPIRATION_NOTIFICATION_QUEUE,
68+
payload: {
69+
organisationId: organisation._id.toString(),
70+
emailType: WEEK_BEFORE_NOTIFICATION
71+
}
72+
});
73+
74+
organisation.expirationNotifications.weekBeforeNotificationSent = EMAIL_PROCESSING;
75+
await organisation.save();
76+
}
77+
}
78+
});
79+
if (!dontExit) {
80+
process.exit();
81+
}
82+
}

cli/src/commands/v2-migrations/20171127214500_remove_unused_persona_props.js

Lines changed: 0 additions & 17 deletions
This file was deleted.
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { getConnection } from 'lib/connections/mongoose';
2+
import { EMAIL_NOOP } from 'lib/models/organisation';
3+
4+
const up = async () => {
5+
const connection = getConnection();
6+
7+
await connection.collection('organisations').update({
8+
expirationNotifications: { $exists: false }
9+
}, {
10+
$set: {
11+
expirationNotifications: {
12+
expirationNotificationSent: EMAIL_NOOP,
13+
weekBeforeNotificationSent: EMAIL_NOOP
14+
}
15+
}
16+
}, {
17+
upsert: false,
18+
multi: true
19+
});
20+
};
21+
22+
const down = async () => {
23+
const connection = getConnection();
24+
25+
await connection.collection('organisation').update({}, {
26+
$unset: {
27+
expirationNotifications: ''
28+
}
29+
}, {
30+
upsert: false,
31+
multi: true
32+
});
33+
};
34+
35+
export default { up, down };
Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { OrderedMap } from 'immutable';
22
import commonIndexesMigration from './20171122100800_common_indexes';
33
import updateRefs from './20171121153300_update_refs';
4-
// import removeUnusedPersonaProps from './20171127214500_remove_unused_persona_props';
4+
import expiration from './20180212_expiration';
55
import migrateIdentifiers from './20171127214900_migrate_identifiers';
66
import personasIndexes from './20171008104700_personas_indexes';
77
// import removeOldIdents from './20171128144900_remove_old_idents';
@@ -12,6 +12,5 @@ export default new OrderedMap()
1212
.set('20171121153300_update_refs', updateRefs)
1313
.set('20171008104700_personas_indexes', personasIndexes)
1414
.set('20171127214900_migrate_identifiers', migrateIdentifiers)
15-
// .set('20171127214500_remove_unused_persona_props', removeUnusedPersonaProps)
16-
// .set('20171128144900_remove_old_idents', removeOldIdents)
15+
.set('20180212_expiration', expiration)
1716
.set('20180320093000_shareable_dashboards', shareableDashboards);

cli/src/scheduler.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import expirationNotificationEmails from 'cli/commands/expirationNotificationEmails';
2+
import logger from 'lib/logger';
3+
4+
const timeout = 15 * 60 * 1000;
5+
6+
const run = async () => {
7+
logger.info('processing expiration');
8+
const startTime = Date.now();
9+
10+
await expirationNotificationEmails({
11+
dontExit: true
12+
});
13+
14+
setTimeout(run, (startTime - Date.now()) + timeout);
15+
};
16+
17+
run();

cli/src/server.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import testQueryBuilderCache from 'cli/commands/testQueryBuilderCache';
2727
import migrateMongo, { MIGRATIONS_PATH } from 'cli/migrateMongo';
2828

2929
import seed from 'cli/seed';
30+
import expirationNotificationEmails from 'cli/commands/expirationNotificationEmails';
3031

3132
program.version('0.0.1');
3233

@@ -140,4 +141,10 @@ program
140141
.option('-i, --info [info]', "Display the state of the migrations, optional ['v'|'verbose']");
141142
// node cli/dist/server migrateMongo
142143

144+
program
145+
.command('expirationNotificationEmails')
146+
.action(expirationNotificationEmails)
147+
.option('--weekBefore [weekBefore]', 'The date of when to send the week before email');
148+
// node cli/dist/server expirationNotificationEmails
149+
143150
program.parse(process.argv);

0 commit comments

Comments
 (0)