2015-10-26 16:30:00 +00:00
|
|
|
/**
|
2015-11-19 15:39:14 +00:00
|
|
|
Definition of the JRSResume class.
|
2015-12-31 08:34:41 +00:00
|
|
|
@license MIT. See LICENSE.md for details.
|
2016-01-20 01:09:59 +00:00
|
|
|
@module core/jrs-resume
|
2015-10-26 16:30:00 +00:00
|
|
|
*/
|
|
|
|
|
2016-01-06 04:59:41 +00:00
|
|
|
|
|
|
|
|
2015-10-26 16:30:00 +00:00
|
|
|
(function() {
|
|
|
|
|
2016-01-06 04:59:41 +00:00
|
|
|
|
|
|
|
|
2015-10-26 16:30:00 +00:00
|
|
|
var FS = require('fs')
|
2016-01-18 22:31:08 +00:00
|
|
|
, extend = require('extend')
|
2015-10-26 16:30:00 +00:00
|
|
|
, validator = require('is-my-json-valid')
|
|
|
|
, _ = require('underscore')
|
|
|
|
, PATH = require('path')
|
2015-12-29 15:01:45 +00:00
|
|
|
, MD = require('marked')
|
2016-01-16 17:40:16 +00:00
|
|
|
, CONVERTER = require('fresh-jrs-converter')
|
2015-10-26 16:30:00 +00:00
|
|
|
, moment = require('moment');
|
|
|
|
|
2016-01-06 04:59:41 +00:00
|
|
|
|
|
|
|
|
2015-10-26 16:30:00 +00:00
|
|
|
/**
|
2016-01-06 04:59:41 +00:00
|
|
|
A JRS resume or CV. JRS resumes are backed by JSON, and each JRSResume object
|
|
|
|
is an instantiation of that JSON decorated with utility methods.
|
2015-11-19 15:39:14 +00:00
|
|
|
@class JRSResume
|
2015-10-26 16:30:00 +00:00
|
|
|
*/
|
2015-11-19 15:39:14 +00:00
|
|
|
function JRSResume() {
|
2015-10-26 16:30:00 +00:00
|
|
|
|
|
|
|
}
|
|
|
|
|
2016-01-06 04:59:41 +00:00
|
|
|
|
|
|
|
|
2015-10-26 16:30:00 +00:00
|
|
|
/**
|
2016-01-06 04:59:41 +00:00
|
|
|
Initialize the JSResume from file.
|
2015-10-26 16:30:00 +00:00
|
|
|
*/
|
2015-11-19 15:39:14 +00:00
|
|
|
JRSResume.prototype.open = function( file, title ) {
|
2016-01-06 04:59:41 +00:00
|
|
|
//this.imp = { fileName: file }; <-- schema violation, tuck it into .basics
|
2015-12-12 15:48:26 +00:00
|
|
|
this.basics = {
|
|
|
|
imp: {
|
2016-01-18 05:34:57 +00:00
|
|
|
file: file,
|
2015-12-12 15:48:26 +00:00
|
|
|
raw: FS.readFileSync( file, 'utf8' )
|
|
|
|
}
|
|
|
|
};
|
|
|
|
return this.parse( this.basics.imp.raw, title );
|
2015-10-26 16:30:00 +00:00
|
|
|
};
|
|
|
|
|
2016-01-06 04:59:41 +00:00
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
Initialize the the JSResume from string.
|
|
|
|
*/
|
|
|
|
JRSResume.prototype.parse = function( stringData, opts ) {
|
|
|
|
opts = opts || { };
|
|
|
|
var rep = JSON.parse( stringData );
|
|
|
|
return this.parseJSON( rep, opts );
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2016-01-20 01:09:59 +00:00
|
|
|
Initialize the JRSResume object from JSON.
|
|
|
|
Open and parse the specified JRS resume. Merge the JSON object model onto
|
|
|
|
this Sheet instance with extend() and convert sheet dates to a safe &
|
|
|
|
consistent format. Then sort each section by startDate descending.
|
|
|
|
@param rep {Object} The raw JSON representation.
|
|
|
|
@param opts {Object} Resume loading and parsing options.
|
|
|
|
{
|
|
|
|
date: Perform safe date conversion.
|
|
|
|
sort: Sort resume items by date.
|
|
|
|
compute: Prepare computed resume totals.
|
|
|
|
}
|
2016-01-06 04:59:41 +00:00
|
|
|
*/
|
|
|
|
JRSResume.prototype.parseJSON = function( rep, opts ) {
|
|
|
|
opts = opts || { };
|
2016-01-20 01:09:59 +00:00
|
|
|
|
|
|
|
// Ignore any element with the 'ignore: true' designator.
|
|
|
|
var that = this, traverse = require('traverse'), ignoreList = [];
|
|
|
|
var scrubbed = traverse( rep ).map( function( x ) {
|
|
|
|
if( !this.isLeaf && this.node.ignore ) {
|
|
|
|
if ( this.node.ignore === true || this.node.ignore === 'true' ) {
|
|
|
|
ignoreList.push( this.node );
|
|
|
|
this.remove();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
// Extend resume properties onto ourself.
|
|
|
|
extend( true, this, scrubbed );
|
|
|
|
|
2016-01-06 04:59:41 +00:00
|
|
|
// Set up metadata
|
|
|
|
if( opts.imp === undefined || opts.imp ) {
|
|
|
|
this.basics.imp = this.basics.imp || { };
|
|
|
|
this.basics.imp.title =
|
|
|
|
(opts.title || this.basics.imp.title) || this.basics.name;
|
|
|
|
this.basics.imp.orgFormat = 'JRS';
|
|
|
|
}
|
|
|
|
// Parse dates, sort dates, and calculate computed values
|
|
|
|
(opts.date === undefined || opts.date) && _parseDates.call( this );
|
|
|
|
(opts.sort === undefined || opts.sort) && this.sort();
|
|
|
|
(opts.compute === undefined || opts.compute) && (this.basics.computed = {
|
|
|
|
numYears: this.duration(),
|
|
|
|
keywords: this.keywords()
|
|
|
|
});
|
|
|
|
return this;
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
2015-10-26 16:30:00 +00:00
|
|
|
/**
|
|
|
|
Save the sheet to disk (for environments that have disk access).
|
|
|
|
*/
|
2015-11-19 15:39:14 +00:00
|
|
|
JRSResume.prototype.save = function( filename ) {
|
2016-01-18 05:34:57 +00:00
|
|
|
this.basics.imp.file = filename || this.basics.imp.file;
|
|
|
|
FS.writeFileSync(this.basics.imp.file, this.stringify( this ), 'utf8');
|
2015-10-26 16:30:00 +00:00
|
|
|
return this;
|
|
|
|
};
|
|
|
|
|
2016-01-06 04:59:41 +00:00
|
|
|
|
|
|
|
|
2015-12-31 08:34:41 +00:00
|
|
|
/**
|
|
|
|
Save the sheet to disk in a specific format, either FRESH or JRS.
|
|
|
|
*/
|
|
|
|
JRSResume.prototype.saveAs = function( filename, format ) {
|
|
|
|
|
|
|
|
if( format === 'JRS' ) {
|
2016-01-18 05:34:57 +00:00
|
|
|
this.basics.imp.file = filename || this.basics.imp.file;
|
|
|
|
FS.writeFileSync( this.basics.imp.file, this.stringify(), 'utf8' );
|
2015-12-31 08:34:41 +00:00
|
|
|
}
|
|
|
|
else {
|
|
|
|
var newRep = CONVERTER.toFRESH( this );
|
|
|
|
var stringRep = CONVERTER.toSTRING( newRep );
|
|
|
|
FS.writeFileSync( filename, stringRep, 'utf8' );
|
|
|
|
}
|
|
|
|
return this;
|
|
|
|
|
|
|
|
};
|
|
|
|
|
2016-01-06 04:59:41 +00:00
|
|
|
|
|
|
|
|
2016-01-06 03:26:16 +00:00
|
|
|
/**
|
|
|
|
Return the resume format.
|
|
|
|
*/
|
|
|
|
JRSResume.prototype.format = function() {
|
|
|
|
return 'JRS';
|
|
|
|
};
|
|
|
|
|
2016-01-06 04:59:41 +00:00
|
|
|
|
|
|
|
|
2015-10-26 16:30:00 +00:00
|
|
|
/**
|
|
|
|
Convert this object to a JSON string, sanitizing meta-properties along the
|
|
|
|
way. Don't override .toString().
|
|
|
|
*/
|
2015-12-12 15:48:26 +00:00
|
|
|
JRSResume.stringify = function( obj ) {
|
2015-10-26 16:30:00 +00:00
|
|
|
function replacer( key,value ) { // Exclude these keys from stringification
|
2015-12-02 19:56:36 +00:00
|
|
|
return _.some(['imp', 'warnings', 'computed', 'filt', 'ctrl', 'index',
|
2015-10-26 16:30:00 +00:00
|
|
|
'safeStartDate', 'safeEndDate', 'safeDate', 'safeReleaseDate', 'result',
|
2016-01-06 04:59:41 +00:00
|
|
|
'isModified', 'htmlPreview', 'display_progress_bar'],
|
2015-10-26 16:30:00 +00:00
|
|
|
function( val ) { return key.trim() === val; }
|
|
|
|
) ? undefined : value;
|
|
|
|
}
|
2015-12-12 15:48:26 +00:00
|
|
|
return JSON.stringify( obj, replacer, 2 );
|
|
|
|
};
|
|
|
|
|
2016-01-06 04:59:41 +00:00
|
|
|
|
|
|
|
|
2015-12-12 15:48:26 +00:00
|
|
|
JRSResume.prototype.stringify = function() {
|
|
|
|
return JRSResume.stringify( this );
|
2015-10-26 16:30:00 +00:00
|
|
|
};
|
|
|
|
|
2015-12-12 15:48:26 +00:00
|
|
|
|
2015-10-26 16:30:00 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
Return a unique list of all keywords across all skills.
|
|
|
|
*/
|
2015-11-19 15:39:14 +00:00
|
|
|
JRSResume.prototype.keywords = function() {
|
2015-10-26 16:30:00 +00:00
|
|
|
var flatSkills = [];
|
|
|
|
if( this.skills && this.skills.length ) {
|
|
|
|
this.skills.forEach( function( s ) {
|
|
|
|
flatSkills = _.union( flatSkills, s.keywords );
|
|
|
|
});
|
|
|
|
}
|
|
|
|
return flatSkills;
|
2015-12-10 02:44:35 +00:00
|
|
|
};
|
2015-10-26 16:30:00 +00:00
|
|
|
|
2016-01-06 04:59:41 +00:00
|
|
|
|
|
|
|
|
2016-01-03 08:18:56 +00:00
|
|
|
/**
|
|
|
|
Return internal metadata. Create if it doesn't exist.
|
|
|
|
JSON Resume v0.0.0 doesn't allow additional properties at the root level,
|
|
|
|
so tuck this into the .basic sub-object.
|
|
|
|
*/
|
2016-01-06 03:32:46 +00:00
|
|
|
JRSResume.prototype.i = function() {
|
2016-01-18 05:34:57 +00:00
|
|
|
this.basics = this.basics || { };
|
|
|
|
this.basics.imp = this.basics.imp || { };
|
|
|
|
return this.basics.imp;
|
2016-01-03 08:18:56 +00:00
|
|
|
};
|
|
|
|
|
2016-01-06 04:59:41 +00:00
|
|
|
|
|
|
|
|
2015-10-26 16:30:00 +00:00
|
|
|
/**
|
|
|
|
Reset the sheet to an empty state.
|
|
|
|
*/
|
2015-11-19 15:39:14 +00:00
|
|
|
JRSResume.prototype.clear = function( clearMeta ) {
|
2015-10-26 16:30:00 +00:00
|
|
|
clearMeta = ((clearMeta === undefined) && true) || clearMeta;
|
2015-11-20 14:28:55 +00:00
|
|
|
clearMeta && (delete this.imp);
|
2015-12-12 15:48:26 +00:00
|
|
|
delete this.basics.computed; // Don't use Object.keys() here
|
2015-10-26 16:30:00 +00:00
|
|
|
delete this.work;
|
|
|
|
delete this.volunteer;
|
|
|
|
delete this.education;
|
|
|
|
delete this.awards;
|
|
|
|
delete this.publications;
|
|
|
|
delete this.interests;
|
|
|
|
delete this.skills;
|
2015-11-05 05:57:23 +00:00
|
|
|
delete this.basics.profiles;
|
2015-10-26 16:30:00 +00:00
|
|
|
};
|
|
|
|
|
2016-01-06 04:59:41 +00:00
|
|
|
|
|
|
|
|
2015-10-26 16:30:00 +00:00
|
|
|
/**
|
|
|
|
Get the default (empty) sheet.
|
|
|
|
*/
|
2015-11-19 15:39:14 +00:00
|
|
|
JRSResume.default = function() {
|
2016-01-06 04:59:41 +00:00
|
|
|
return new JRSResume().open(
|
|
|
|
PATH.join( __dirname, 'empty-jrs.json'), 'Empty'
|
|
|
|
);
|
2015-12-10 02:44:35 +00:00
|
|
|
};
|
2015-10-26 16:30:00 +00:00
|
|
|
|
2016-01-06 04:59:41 +00:00
|
|
|
|
|
|
|
|
2015-10-26 16:30:00 +00:00
|
|
|
/**
|
|
|
|
Add work experience to the sheet.
|
|
|
|
*/
|
2015-11-19 15:39:14 +00:00
|
|
|
JRSResume.prototype.add = function( moniker ) {
|
|
|
|
var defSheet = JRSResume.default();
|
2015-10-26 16:30:00 +00:00
|
|
|
var newObject = $.extend( true, {}, defSheet[ moniker ][0] );
|
|
|
|
this[ moniker ] = this[ moniker ] || [];
|
|
|
|
this[ moniker ].push( newObject );
|
|
|
|
return newObject;
|
|
|
|
};
|
|
|
|
|
2016-01-06 04:59:41 +00:00
|
|
|
|
|
|
|
|
2015-10-26 16:30:00 +00:00
|
|
|
/**
|
|
|
|
Determine if the sheet includes a specific social profile (eg, GitHub).
|
|
|
|
*/
|
2015-11-19 15:39:14 +00:00
|
|
|
JRSResume.prototype.hasProfile = function( socialNetwork ) {
|
2015-10-26 16:30:00 +00:00
|
|
|
socialNetwork = socialNetwork.trim().toLowerCase();
|
|
|
|
return this.basics.profiles && _.some( this.basics.profiles, function(p) {
|
|
|
|
return p.network.trim().toLowerCase() === socialNetwork;
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2016-01-06 04:59:41 +00:00
|
|
|
|
|
|
|
|
2015-10-26 16:30:00 +00:00
|
|
|
/**
|
|
|
|
Determine if the sheet includes a specific skill.
|
|
|
|
*/
|
2015-11-19 15:39:14 +00:00
|
|
|
JRSResume.prototype.hasSkill = function( skill ) {
|
2015-10-26 16:30:00 +00:00
|
|
|
skill = skill.trim().toLowerCase();
|
|
|
|
return this.skills && _.some( this.skills, function(sk) {
|
|
|
|
return sk.keywords && _.some( sk.keywords, function(kw) {
|
|
|
|
return kw.trim().toLowerCase() === skill;
|
|
|
|
});
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2016-01-06 04:59:41 +00:00
|
|
|
|
|
|
|
|
2015-10-26 16:30:00 +00:00
|
|
|
/**
|
|
|
|
Validate the sheet against the JSON Resume schema.
|
|
|
|
*/
|
2015-11-19 15:39:14 +00:00
|
|
|
JRSResume.prototype.isValid = function( ) { // TODO: ↓ fix this path ↓
|
2016-01-06 04:59:41 +00:00
|
|
|
var schema = FS.readFileSync( PATH.join( __dirname, 'resume.json' ),'utf8');
|
2015-10-26 16:30:00 +00:00
|
|
|
var schemaObj = JSON.parse( schema );
|
2015-12-10 02:44:35 +00:00
|
|
|
var validator = require('is-my-json-valid');
|
2015-12-12 15:48:26 +00:00
|
|
|
var validate = validator( schemaObj, { // Note [1]
|
|
|
|
formats: { date: /^\d{4}(?:-(?:0[0-9]{1}|1[0-2]{1})(?:-[0-9]{2})?)?$/ }
|
|
|
|
});
|
|
|
|
var ret = validate( this );
|
|
|
|
if( !ret ) {
|
|
|
|
this.basics.imp = this.basics.imp || { };
|
|
|
|
this.basics.imp.validationErrors = validate.errors;
|
|
|
|
}
|
|
|
|
return ret;
|
2015-10-26 16:30:00 +00:00
|
|
|
};
|
|
|
|
|
2016-01-06 04:59:41 +00:00
|
|
|
|
|
|
|
|
2015-10-26 16:30:00 +00:00
|
|
|
/**
|
|
|
|
Calculate the total duration of the sheet. Assumes this.work has been sorted
|
|
|
|
by start date descending, perhaps via a call to Sheet.sort().
|
|
|
|
@returns The total duration of the sheet's work history, that is, the number
|
|
|
|
of years between the start date of the earliest job on the resume and the
|
|
|
|
*latest end date of all jobs in the work history*. This last condition is for
|
|
|
|
sheets that have overlapping jobs.
|
|
|
|
*/
|
2016-01-08 17:13:54 +00:00
|
|
|
JRSResume.prototype.duration = function( unit ) {
|
|
|
|
unit = unit || 'years';
|
2015-10-26 16:30:00 +00:00
|
|
|
if( this.work && this.work.length ) {
|
|
|
|
var careerStart = this.work[ this.work.length - 1].safeStartDate;
|
|
|
|
if ((typeof careerStart === 'string' || careerStart instanceof String) &&
|
|
|
|
!careerStart.trim())
|
|
|
|
return 0;
|
|
|
|
var careerLast = _.max( this.work, function( w ) {
|
|
|
|
return w.safeEndDate.unix();
|
|
|
|
}).safeEndDate;
|
2016-01-08 17:13:54 +00:00
|
|
|
return careerLast.diff( careerStart, unit );
|
2015-10-26 16:30:00 +00:00
|
|
|
}
|
|
|
|
return 0;
|
|
|
|
};
|
|
|
|
|
2016-01-06 04:59:41 +00:00
|
|
|
|
|
|
|
|
2015-10-26 16:30:00 +00:00
|
|
|
/**
|
|
|
|
Sort dated things on the sheet by start date descending. Assumes that dates
|
|
|
|
on the sheet have been processed with _parseDates().
|
|
|
|
*/
|
2015-11-19 15:39:14 +00:00
|
|
|
JRSResume.prototype.sort = function( ) {
|
2015-10-26 16:30:00 +00:00
|
|
|
|
|
|
|
this.work && this.work.sort( byDateDesc );
|
|
|
|
this.education && this.education.sort( byDateDesc );
|
|
|
|
this.volunteer && this.volunteer.sort( byDateDesc );
|
|
|
|
|
|
|
|
this.awards && this.awards.sort( function(a, b) {
|
|
|
|
return( a.safeDate.isBefore(b.safeDate) ) ? 1
|
|
|
|
: ( a.safeDate.isAfter(b.safeDate) && -1 ) || 0;
|
|
|
|
});
|
|
|
|
this.publications && this.publications.sort( function(a, b) {
|
|
|
|
return( a.safeReleaseDate.isBefore(b.safeReleaseDate) ) ? 1
|
|
|
|
: ( a.safeReleaseDate.isAfter(b.safeReleaseDate) && -1 ) || 0;
|
|
|
|
});
|
|
|
|
|
|
|
|
function byDateDesc(a,b) {
|
|
|
|
return( a.safeStartDate.isBefore(b.safeStartDate) ) ? 1
|
|
|
|
: ( a.safeStartDate.isAfter(b.safeStartDate) && -1 ) || 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
};
|
|
|
|
|
2016-01-06 04:59:41 +00:00
|
|
|
|
|
|
|
|
2015-12-29 15:01:45 +00:00
|
|
|
JRSResume.prototype.dupe = function() {
|
|
|
|
var rnew = new JRSResume();
|
|
|
|
rnew.parse( this.stringify(), { } );
|
|
|
|
return rnew;
|
|
|
|
};
|
|
|
|
|
2016-01-06 04:59:41 +00:00
|
|
|
|
|
|
|
|
2015-12-29 15:01:45 +00:00
|
|
|
/**
|
|
|
|
Create a copy of this resume in which all fields have been interpreted as
|
|
|
|
Markdown.
|
|
|
|
*/
|
|
|
|
JRSResume.prototype.harden = function() {
|
|
|
|
|
|
|
|
var that = this;
|
|
|
|
var ret = this.dupe();
|
|
|
|
|
|
|
|
function HD(txt) {
|
|
|
|
return '@@@@~' + txt + '~@@@@';
|
|
|
|
}
|
|
|
|
|
|
|
|
function HDIN(txt){
|
|
|
|
//return MD(txt || '' ).replace(/^\s*<p>|<\/p>\s*$/gi, '');
|
|
|
|
return HD(txt);
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: refactor recursion
|
|
|
|
function hardenStringsInObject( obj, inline ) {
|
|
|
|
|
|
|
|
if( !obj ) return;
|
|
|
|
inline = inline === undefined || inline;
|
|
|
|
|
|
|
|
|
|
|
|
if( Object.prototype.toString.call( obj ) === '[object Array]' ) {
|
|
|
|
obj.forEach(function(elem, idx, ar){
|
|
|
|
if( typeof elem === 'string' || elem instanceof String )
|
|
|
|
ar[idx] = inline ? HDIN(elem) : HD( elem );
|
|
|
|
else
|
|
|
|
hardenStringsInObject( elem );
|
|
|
|
});
|
|
|
|
}
|
|
|
|
else if (typeof obj === 'object') {
|
|
|
|
Object.keys( obj ).forEach(function(key) {
|
|
|
|
var sub = obj[key];
|
|
|
|
if( typeof sub === 'string' || sub instanceof String ) {
|
2016-01-06 04:59:41 +00:00
|
|
|
if( _.contains(['skills','url','website','startDate','endDate',
|
|
|
|
'releaseDate','date','phone','email','address','postalCode',
|
|
|
|
'city','country','region'], key) )
|
2015-12-29 15:01:45 +00:00
|
|
|
return;
|
|
|
|
if( key === 'summary' )
|
|
|
|
obj[key] = HD( obj[key] );
|
|
|
|
else
|
|
|
|
obj[key] = inline ? HDIN( obj[key] ) : HD( obj[key] );
|
|
|
|
}
|
|
|
|
else
|
|
|
|
hardenStringsInObject( sub );
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
Object.keys( ret ).forEach(function(member){
|
|
|
|
hardenStringsInObject( ret[ member ] );
|
|
|
|
});
|
|
|
|
|
|
|
|
return ret;
|
|
|
|
};
|
|
|
|
|
2016-01-06 04:59:41 +00:00
|
|
|
|
|
|
|
|
2015-10-26 16:30:00 +00:00
|
|
|
/**
|
|
|
|
Convert human-friendly dates into formal Moment.js dates for all collections.
|
|
|
|
We don't want to lose the raw textual date as entered by the user, so we store
|
|
|
|
the Moment-ified date as a separate property with a prefix of .safe. For ex:
|
|
|
|
job.startDate is the date as entered by the user. job.safeStartDate is the
|
|
|
|
parsed Moment.js date that we actually use in processing.
|
|
|
|
*/
|
|
|
|
function _parseDates() {
|
|
|
|
|
|
|
|
var _fmt = require('./fluent-date').fmt;
|
|
|
|
|
|
|
|
this.work && this.work.forEach( function(job) {
|
|
|
|
job.safeStartDate = _fmt( job.startDate );
|
|
|
|
job.safeEndDate = _fmt( job.endDate );
|
|
|
|
});
|
|
|
|
this.education && this.education.forEach( function(edu) {
|
|
|
|
edu.safeStartDate = _fmt( edu.startDate );
|
|
|
|
edu.safeEndDate = _fmt( edu.endDate );
|
|
|
|
});
|
|
|
|
this.volunteer && this.volunteer.forEach( function(vol) {
|
|
|
|
vol.safeStartDate = _fmt( vol.startDate );
|
|
|
|
vol.safeEndDate = _fmt( vol.endDate );
|
|
|
|
});
|
|
|
|
this.awards && this.awards.forEach( function(awd) {
|
|
|
|
awd.safeDate = _fmt( awd.date );
|
|
|
|
});
|
|
|
|
this.publications && this.publications.forEach( function(pub) {
|
|
|
|
pub.safeReleaseDate = _fmt( pub.releaseDate );
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2016-01-06 04:59:41 +00:00
|
|
|
|
|
|
|
|
2015-10-26 16:30:00 +00:00
|
|
|
/**
|
2015-11-19 15:39:14 +00:00
|
|
|
Export the JRSResume function/ctor.
|
2015-10-26 16:30:00 +00:00
|
|
|
*/
|
2015-11-19 15:39:14 +00:00
|
|
|
module.exports = JRSResume;
|
2015-10-26 16:30:00 +00:00
|
|
|
|
2016-01-06 04:59:41 +00:00
|
|
|
|
|
|
|
|
2015-10-26 16:30:00 +00:00
|
|
|
}());
|