Skip to content

Commit 79818d2

Browse files
Merge branch 'develop' into slash-tc
2 parents afcd507 + dfc5e68 commit 79818d2

File tree

11 files changed

+138
-19
lines changed

11 files changed

+138
-19
lines changed

.circleci/config.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -356,7 +356,7 @@ workflows:
356356
filters:
357357
branches:
358358
only:
359-
- free
359+
- gig-cache-layer
360360
# This is beta env for production soft releases
361361
- "build-prod-beta":
362362
context : org-global

config/default.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -436,6 +436,7 @@ module.exports = {
436436
TC_EDU_SEARCH_BAR_MAX_RESULTS_EACH_GROUP: 3,
437437
POLICY_PAGES_PATH: '/policy',
438438
GIGS_PAGES_PATH: '/gigs',
439+
GIGS_LISTING_CACHE_TIME: 300, // in seconds
439440
START_PAGE_PATH: '/start',
440441
GUIKIT: {
441442
DEBOUNCE_ON_CHANGE_TIME: 150,

package-lock.json

Lines changed: 15 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@
9090
"multer": "^1.4.2",
9191
"navigation-component": "github:topcoder-platform/navigation-component#new-dashboard",
9292
"navigation-component-tco": "github:topcoder-platform/navigation-component-tco#new-dev",
93+
"node-cache": "^5.1.2",
9394
"node-forge": "^0.7.5",
9495
"nuka-carousel": "^4.5.3",
9596
"postcss": "^6.0.23",

src/server/routes/recruitCRM.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,11 @@ routes.use(cors());
2828
routes.options('*', cors());
2929

3030
routes.get('/jobs', (req, res, next) => new RecruitCRMService().getAllJobs(req, res, next));
31+
routes.get('/jobs/cache', (req, res, next) => new RecruitCRMService().getJobsCacheStats(req, res, next));
32+
routes.get('/jobs/cache/flush', (req, res, next) => authenticator(authenticatorOptions)(req, res, next), (req, res, next) => new RecruitCRMService().getJobsCacheFlush(req, res, next));
3133
routes.get('/jobs/search', (req, res, next) => new RecruitCRMService().getJobs(req, res, next));
3234
routes.get('/jobs/:id', (req, res, next) => new RecruitCRMService().getJob(req, res, next));
33-
routes.post('/jobs/:id/apply', upload.single('resume'), (req, res, next) => new RecruitCRMService().applyForJob(req, res, next));
35+
routes.post('/jobs/:id/apply', (req, res, next) => authenticator(authenticatorOptions)(req, res, next), upload.single('resume'), (req, res, next) => new RecruitCRMService().applyForJob(req, res, next));
3436
routes.get('/candidates/search', (req, res, next) => new RecruitCRMService().searchCandidates(req, res, next));
3537
// new router added
3638
routes.get('/profile', (req, res, next) => authenticator(authenticatorOptions)(req, res, next), (req, res, next) => new RecruitCRMService().getProfile(req, res, next));

src/server/services/recruitCRM.js

Lines changed: 95 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@ import { sendEmailDirect } from './sendGrid';
1515
const { api } = services;
1616

1717
const FormData = require('form-data');
18+
const NodeCache = require('node-cache');
19+
20+
// gigs list caching
21+
const CACHE_KEY = 'jobs';
22+
const gigsCache = new NodeCache({ stdTTL: config.GIGS_LISTING_CACHE_TIME, checkperiod: 10 });
1823

1924
const JOB_FIELDS_RESPONSE = [
2025
'id',
@@ -90,6 +95,17 @@ export default class RecruitCRMService {
9095
};
9196
}
9297

98+
// eslint-disable-next-line class-methods-use-this
99+
getJobsCacheStats(req, res) {
100+
return res.send(gigsCache.getStats());
101+
}
102+
103+
// eslint-disable-next-line class-methods-use-this
104+
getJobsCacheFlush(req, res) {
105+
gigsCache.flushAll();
106+
return res.send(gigsCache.getStats());
107+
}
108+
93109
/**
94110
* getJobsFromTaas endpoint.
95111
* @return {Promise}
@@ -180,12 +196,72 @@ export default class RecruitCRMService {
180196
}
181197
}
182198

199+
/**
200+
* Gets all jobs method.
201+
* @return {Promise}
202+
* @param {Object} query the request query.
203+
*/
204+
async getAll(query) {
205+
try {
206+
const response = await fetch(`${this.private.baseUrl}/v1/jobs/search?${qs.stringify(query)}`, {
207+
method: 'GET',
208+
headers: {
209+
'Content-Type': 'application/json',
210+
Authorization: this.private.authorization,
211+
},
212+
});
213+
if (response.status === 429) {
214+
await new Promise(resolve => setTimeout(resolve, 30000)); // wait 30sec
215+
return this.getAll(query);
216+
}
217+
if (response.status >= 400) {
218+
const error = {
219+
error: true,
220+
status: response.status,
221+
url: `${this.private.baseUrl}/v1/jobs/search?${qs.stringify(query)}`,
222+
errObj: await response.json(),
223+
};
224+
return error;
225+
}
226+
const data = await response.json();
227+
if (data.current_page < data.last_page) {
228+
const pages = _.range(2, data.last_page + 1);
229+
return Promise.all(
230+
pages.map(page => fetch(`${this.private.baseUrl}/v1/jobs/search?${qs.stringify(query)}&page=${page}`, {
231+
method: 'GET',
232+
headers: {
233+
'Content-Type': 'application/json',
234+
Authorization: this.private.authorization,
235+
},
236+
})),
237+
)
238+
.then(async (allPages) => {
239+
// eslint-disable-next-line no-restricted-syntax
240+
for (const pageDataRsp of allPages) {
241+
// eslint-disable-next-line no-await-in-loop
242+
const pageData = await pageDataRsp.json();
243+
data.data = _.flatten(data.data.concat(pageData.data));
244+
}
245+
const toSend = _.map(data.data, j => _.pick(j, JOB_FIELDS_RESPONSE));
246+
return toSend;
247+
});
248+
}
249+
const toSend = _.map(data.data, j => _.pick(j, JOB_FIELDS_RESPONSE));
250+
return toSend;
251+
} catch (err) {
252+
return err;
253+
}
254+
}
255+
183256
/**
184257
* Gets all jobs endpoint.
185258
* @return {Promise}
186259
* @param {Object} the request.
187260
*/
188261
async getAllJobs(req, res, next) {
262+
if (gigsCache.has(CACHE_KEY)) {
263+
return res.send(gigsCache.get(CACHE_KEY));
264+
}
189265
try {
190266
const response = await fetch(`${this.private.baseUrl}/v1/jobs/search?${qs.stringify(req.query)}`, {
191267
method: 'GET',
@@ -227,17 +303,17 @@ export default class RecruitCRMService {
227303
const pageData = await pageDataRsp.json();
228304
data.data = _.flatten(data.data.concat(pageData.data));
229305
}
230-
return res.send(
231-
_.map(data.data, j => _.pick(j, JOB_FIELDS_RESPONSE)),
232-
);
306+
const toSend = _.map(data.data, j => _.pick(j, JOB_FIELDS_RESPONSE));
307+
gigsCache.set(CACHE_KEY, toSend);
308+
return res.send(toSend);
233309
})
234310
.catch(e => res.send({
235311
error: e,
236312
}));
237313
}
238-
return res.send(
239-
_.map(data.data, j => _.pick(j, JOB_FIELDS_RESPONSE)),
240-
);
314+
const toSend = _.map(data.data, j => _.pick(j, JOB_FIELDS_RESPONSE));
315+
gigsCache.set(CACHE_KEY, toSend);
316+
return res.send(toSend);
241317
} catch (err) {
242318
return next(err);
243319
}
@@ -697,3 +773,16 @@ export default class RecruitCRMService {
697773
return data.data[0];
698774
}
699775
}
776+
777+
// Self update cache on expire to keep it fresh
778+
gigsCache.on('expired', async (key) => {
779+
if (key === CACHE_KEY) {
780+
const ss = new RecruitCRMService();
781+
const gigs = await ss.getAll({
782+
job_status: 1,
783+
});
784+
if (!gigs.error) {
785+
gigsCache.set(CACHE_KEY, gigs);
786+
}
787+
}
788+
});

src/shared/actions/recruitCRM.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,10 +114,10 @@ function normalizeRecruitPayload(job, payload) {
114114
/**
115115
* Apply for Job done
116116
*/
117-
async function applyForJobDone(job, payload) {
117+
async function applyForJobDone(job, payload, tokenV3) {
118118
const ss = new Service();
119119
try {
120-
const res = await ss.applyForJob(job.slug, normalizeRecruitPayload(job, payload));
120+
const res = await ss.applyForJob(job.slug, normalizeRecruitPayload(job, payload), tokenV3);
121121

122122
return {
123123
id: job.slug,

src/shared/components/Gigs/ReferralCode/index.jsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ function ReferralCode(props) {
4949
button: buttonThemes.tc['primary-borderless-sm'],
5050
}}
5151
>
52-
REFFER A FRIEND
52+
REFER A FRIEND
5353
</PrimaryButton>
5454
{
5555
loginModalOpen
@@ -98,7 +98,7 @@ function ReferralCode(props) {
9898
)
9999
}
100100
{
101-
growSurfState.error && <span>Ops, we couldn&apos;t load your profile. Please try again later or contact <a href="mailto:support@topcoder.com">support</a>.</span>
101+
growSurfState.error && <span>Oops, we couldn&apos;t load your profile. Please try again later or contact <a href="mailto:support@topcoder.com">support</a>.</span>
102102
}
103103
</React.Fragment>
104104
)

src/shared/containers/Gigs/RecruitCRMJobApply.jsx

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -123,12 +123,14 @@ class RecruitCRMJobApplyContainer extends React.Component {
123123
}
124124

125125
onApplyClick() {
126-
const { applyForJob, job, optimizely } = this.props;
126+
const {
127+
applyForJob, job, optimizely, auth,
128+
} = this.props;
127129
const { formData } = this.state;
128130
this.validateForm();
129131
this.setState((state) => {
130132
if (_.isEmpty(state.formErrors)) {
131-
applyForJob(job, formData);
133+
applyForJob(job, formData, auth.tokenV3);
132134
optimizely.track('Submit Application Form');
133135
const isFeatured = _.find(job.custom_fields, ['field_name', 'Featured']);
134136
const jobTags = _.find(job.custom_fields, ['field_name', 'Job Tag']);
@@ -279,6 +281,7 @@ RecruitCRMJobApplyContainer.defaultProps = {
279281
applying: false,
280282
application: null,
281283
recruitProfile: null,
284+
auth: {},
282285
};
283286

284287
RecruitCRMJobApplyContainer.propTypes = {
@@ -290,6 +293,7 @@ RecruitCRMJobApplyContainer.propTypes = {
290293
searchCandidates: PT.func.isRequired,
291294
recruitProfile: PT.shape(),
292295
optimizely: PT.shape().isRequired,
296+
auth: PT.object,
293297
};
294298

295299
function mapStateToProps(state, ownProps) {
@@ -313,15 +317,18 @@ function mapStateToProps(state, ownProps) {
313317
? state.recruitCRM[job.slug].application : null,
314318
recruitProfile: state.recruitCRM && profile && state.recruitCRM[profile.email]
315319
? state.recruitCRM[profile.email].profile : null,
320+
auth: {
321+
...state.auth,
322+
},
316323
};
317324
}
318325

319326
function mapDispatchToActions(dispatch) {
320327
const a = actions.recruit;
321328
return {
322-
applyForJob: (job, payload) => {
329+
applyForJob: (job, payload, tokenV3) => {
323330
dispatch(a.applyForJobInit(job, payload));
324-
dispatch(a.applyForJobDone(job, payload));
331+
dispatch(a.applyForJobDone(job, payload, tokenV3));
325332
},
326333
searchCandidates: (email) => {
327334
dispatch(a.searchCandidatesInit(email));

src/shared/containers/Gigs/RecruitCRMJobs.jsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -179,9 +179,9 @@ class RecruitCRMJobsContainer extends React.Component {
179179
// build current locations dropdown based on all data
180180
// and filter by selected location
181181
jobsToDisplay = _.filter(jobs, (job) => {
182-
const country = !job.country || job.country === 'Anywhere' || job.country === 'Any' ? 'All' : job.country;
182+
const country = _.trim(!job.country || job.country === 'Anywhere' || job.country === 'Any' ? 'All' : job.country);
183183
// build dropdown
184-
const found = _.findIndex(locations, { label: country });
184+
const found = _.findIndex(locations, l => l.label.toLowerCase() === country.toLowerCase());
185185
if (found === -1) {
186186
locations.push({
187187
label: country, selected: location.toLowerCase() === country.toLowerCase(),

src/shared/services/recruitCRM.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,15 +75,19 @@ export default class Service {
7575
* applyForJob for candidate
7676
* @param {string} id The job id to apply to
7777
* @param {object} payload The apply payload
78+
* @param {string} tokenV3 User token
7879
*/
79-
async applyForJob(id, payload) {
80+
async applyForJob(id, payload, tokenV3) {
8081
const { resume } = payload;
8182
const data = new FormData();
8283
data.append('resume', resume);
8384
data.append('form', JSON.stringify(_.omit(payload, 'resume')));
8485
const res = await fetch(`${this.baseUrl}/jobs/${id}/apply`, {
8586
method: 'POST',
8687
body: data,
88+
headers: new Headers({
89+
Authorization: `Bearer ${tokenV3}`,
90+
}),
8791
});
8892
if (!res.ok) {
8993
const error = new Error('Failed to apply for job');

0 commit comments

Comments
 (0)