1
0
mirror of https://github.com/JuanCanham/HackMyResume.git synced 2024-10-05 23:15:12 +01:00
HackMyResume/src/core/jrs-resume.js

278 lines
9.1 KiB
JavaScript
Raw Normal View History

2015-10-26 16:30:00 +00:00
/**
Definition of the JRSResume class.
2015-12-17 15:15:59 +00:00
@license MIT. Copyright (c) 2015 James Devlin / FluentDesk.
@module jrs-resume.js
2015-10-26 16:30:00 +00:00
*/
(function() {
var FS = require('fs')
, extend = require('../utils/extend')
, validator = require('is-my-json-valid')
, _ = require('underscore')
, PATH = require('path')
, moment = require('moment');
/**
The JRSResume class represent a specific JSON character sheet. When Sheet.open
2015-10-26 16:30:00 +00:00
is called, we merge the loaded JSON sheet properties onto the Sheet instance
via extend(), so a full-grown sheet object will have all of the methods here,
plus a complement of JSON properties from the backing JSON file. That allows
us to treat Sheet objects interchangeably with the loaded JSON model.
@class JRSResume
2015-10-26 16:30:00 +00:00
*/
function JRSResume() {
2015-10-26 16:30:00 +00:00
}
/**
Open and parse the specified JSON resume sheet. 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.
*/
JRSResume.prototype.open = function( file, title ) {
2015-12-12 15:48:26 +00:00
//this.imp = { fileName: file }; <-- schema violation, tuck it into .basics instead
this.basics = {
imp: {
fileName: file,
raw: FS.readFileSync( file, 'utf8' )
}
};
return this.parse( this.basics.imp.raw, title );
2015-10-26 16:30:00 +00:00
};
/**
Save the sheet to disk (for environments that have disk access).
*/
JRSResume.prototype.save = function( filename ) {
2015-12-12 15:48:26 +00:00
this.basics.imp.fileName = filename || this.basics.imp.fileName;
FS.writeFileSync( this.basics.imp.fileName, this.stringify( this ), 'utf8' );
2015-10-26 16:30:00 +00:00
return this;
};
/**
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',
2015-11-05 05:57:10 +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 );
};
JRSResume.prototype.stringify = function() {
return JRSResume.stringify( this );
2015-10-26 16:30:00 +00:00
};
/**
Initialize the JRS Resume from string data.
2015-10-26 16:30:00 +00:00
Open and parse the specified JSON resume sheet. 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.
*/
JRSResume.prototype.parse = function( stringData, opts ) {
2015-10-26 16:30:00 +00:00
opts = opts || { };
var rep = JSON.parse( stringData );
return this.parseJSON( rep, opts );
};
2015-12-12 15:48:26 +00:00
/**
Initialize the JRSRume from JSON data.
*/
JRSResume.prototype.parseJSON = function( rep, opts ) {
opts = opts || { };
2015-10-26 16:30:00 +00:00
extend( true, this, rep );
// Set up metadata
2015-11-20 14:28:55 +00:00
if( opts.imp === undefined || opts.imp ) {
2015-12-12 15:48:26 +00:00
this.basics.imp = this.basics.imp || { };
this.basics.imp.title = (opts.title || this.basics.imp.title) || this.basics.name;
2015-10-26 16:30:00 +00:00
}
// Parse dates, sort dates, and calculate computed values
(opts.date === undefined || opts.date) && _parseDates.call( this );
(opts.sort === undefined || opts.sort) && this.sort();
2015-12-12 15:48:26 +00:00
(opts.compute === undefined || opts.compute) && (this.basics.computed = {
2015-10-26 16:30:00 +00:00
numYears: this.duration(),
keywords: this.keywords()
});
return this;
};
/**
Return a unique list of all keywords across all skills.
*/
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
/**
Reset the sheet to an empty state.
*/
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
};
/**
Get the default (empty) sheet.
*/
JRSResume.default = function() {
2015-12-02 19:56:36 +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
/**
Add work experience to the sheet.
*/
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;
};
/**
Determine if the sheet includes a specific social profile (eg, GitHub).
*/
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;
});
};
/**
Determine if the sheet includes a specific skill.
*/
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;
});
});
};
/**
Validate the sheet against the JSON Resume schema.
*/
JRSResume.prototype.isValid = function( ) { // TODO: ↓ fix this path ↓
2015-10-26 16:30:00 +00:00
var schema = FS.readFileSync( PATH.join( __dirname, 'resume.json' ), 'utf8' );
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
};
/**
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.
*/
JRSResume.prototype.duration = function() {
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;
return careerLast.diff( careerStart, 'years' );
}
return 0;
};
/**
Sort dated things on the sheet by start date descending. Assumes that dates
on the sheet have been processed with _parseDates().
*/
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;
}
};
/**
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 );
});
}
/**
Export the JRSResume function/ctor.
2015-10-26 16:30:00 +00:00
*/
module.exports = JRSResume;
2015-10-26 16:30:00 +00:00
}());