Skip to content
2 changes: 1 addition & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ workflows:
only:
- develop
- PM-3893
- pm-3847_1
- pm-4068

# Production builds are exectuted only on tagged commits to the
# master branch.
Expand Down
3 changes: 2 additions & 1 deletion config/default.js
Original file line number Diff line number Diff line change
Expand Up @@ -158,5 +158,6 @@ module.exports = {
API_KEY: process.env.MAILCHIMP_API_KEY,
SERVER_PREFIX: process.env.MAILCHIMP_SERVER_PREFIX,
LIST_FETCH_COUNT: process.env.MAILCHIMP_LIST_FETCH_COUNT ? Number(process.env.MAILCHIMP_LIST_FETCH_COUNT) : 1000
}
},
PDF_SKILLS_PER_CATEGORY: process.env.PDF_SKILLS_PER_CATEGORY ? Number(process.env.PDF_SKILLS_PER_CATEGORY) : 5,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ correctness]
Consider using parseInt with a radix of 10 instead of Number for parsing process.env.PDF_SKILLS_PER_CATEGORY. This ensures that the conversion is always done in base 10, which can prevent unexpected results if the environment variable is prefixed with a non-decimal number.

}
61 changes: 38 additions & 23 deletions src/common/profileTemplate.js
Original file line number Diff line number Diff line change
Expand Up @@ -283,13 +283,26 @@ function createSkillsSubsection (title, verified, notVerified) {
)
}

/**
* Create one category line in same style as verified/not verified: bullet label + skills on same line
*/
function createCategorySkillsBlock (categoryName, skillNames) {
if (!skillNames || skillNames.length === 0) return null

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[💡 readability]
The check !skillNames || skillNames.length === 0 is redundant since skillNames.length === 0 will suffice to check for an empty array. Consider simplifying the condition to if (skillNames.length === 0) return null.

return React.createElement(
Text,
{ key: `category-${categoryName}`, style: styles.skillsList },
React.createElement(Text, { style: styles.skillsLabel }, `• ${categoryName}: `),
skillNames.join(', ')
)
}

/**
* Build the PDF template for member profile
* @param {Object} pdfData the aggregated PDF data
* @returns {Object} React element tree
*/
function buildProfileTemplate (pdfData) {
const { member, workExperience, education, languages, basicInfo, skills, topcoderActivity, certifications, courses } = pdfData
const { member, workExperience, education, languages, basicInfo, skills, skillsByCategory, topcoderActivity, certifications, courses } = pdfData

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ correctness]
The destructuring of pdfData now includes skillsByCategory. Ensure that this property is always present in the pdfData object to avoid potential runtime errors if it is undefined.


const children = []

Expand Down Expand Up @@ -387,30 +400,32 @@ function buildProfileTemplate (pdfData) {
)
}

// Technical Skills Section
const hasSkills = skills.principal.verified.length > 0 || skills.principal.notVerified.length > 0 ||
skills.additional.verified.length > 0 || skills.additional.notVerified.length > 0
if (hasSkills) {
const skillsContent = [
createSectionHeader('TECHNICAL SKILLS')
]

const principalSubsection = createSkillsSubsection(
'Principal Skills:',
skills.principal.verified,
skills.principal.notVerified
)
if (principalSubsection) {
skillsContent.push(principalSubsection)
const hasPrincipalSkills = skills && skills.principal && (skills.principal.verified.length > 0 || skills.principal.notVerified.length > 0)
const hasAdditionalByCategory = skillsByCategory && skillsByCategory.length > 0

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ correctness]
The condition skillsByCategory && skillsByCategory.length > 0 is used to check for additional skills by category. Ensure that skillsByCategory is always an array to avoid potential runtime errors.

if (hasPrincipalSkills || hasAdditionalByCategory) {
const skillsContent = [createSectionHeader('TECHNICAL SKILLS')]

if (hasPrincipalSkills) {
const principalSubsection = createSkillsSubsection(
'Principal Skills:',
skills.principal.verified,
skills.principal.notVerified
)
if (principalSubsection) skillsContent.push(principalSubsection)
}

const additionalSubsection = createSkillsSubsection(
'Additional Skills:',
skills.additional.verified,
skills.additional.notVerified
)
if (additionalSubsection) {
skillsContent.push(additionalSubsection)
if (hasAdditionalByCategory) {
const additionalItems = skillsByCategory
.map(item => createCategorySkillsBlock(item.categoryName, item.skills))
.filter(Boolean)
skillsContent.push(
React.createElement(
View,
{ key: 'additional-skills-subsection', style: styles.skillsSubsection },
React.createElement(Text, { style: styles.skillsSubsectionTitle }, 'Additional Skills:'),
...additionalItems
)
)
}

children.push(
Expand Down
71 changes: 40 additions & 31 deletions src/services/MemberService.js
Original file line number Diff line number Diff line change
Expand Up @@ -489,7 +489,7 @@ async function getProfileCompleteness (currentUser, handle, query) {
const memberTraits = await memberTraitService.getTraits(currentUser, handle, {})
// Avoid getting the member stats, since we don't need them here, and performance is
// better without them
const memberFields = { 'fields': 'userId,handle,handleLower,photoURL,description,skills,verified,availableForGigs,availableForGigsLastUpdateDate,lastProfileConfirmationDate,updatedAt,addresses' }
const memberFields = { 'fields': 'userId,handle,handleLower,photoURL,description,skills,verified,lastProfileConfirmationDate,updatedAt,addresses' }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ correctness]
The removal of availableForGigs and availableForGigsLastUpdateDate from memberFields might impact other parts of the code that rely on these fields. Ensure that these fields are not required elsewhere in the application logic.

const member = await getMemberData(handle, memberFields)

// Used for calculating the percentComplete
Expand All @@ -508,7 +508,7 @@ async function getProfileCompleteness (currentUser, handle, query) {
// TODO: Turn this back on once we have verification flow implemented elsewhere
// data.verified = false
data.skills = false
data.gigAvailability = false
data.engagementAvailability = false
data.bio = false
data.workHistory = false
data.education = false
Expand All @@ -517,19 +517,13 @@ async function getProfileCompleteness (currentUser, handle, query) {
const totalItems = Object.keys(data).length

data.skillsLastUpdateDate = undefined
data.gigAvailabilityLastUpdateDate = undefined
data.engagementAvailabilityLastUpdateDate = undefined
data.workHistoryLastUpdateDate = undefined
data.educationLastUpdateDate = undefined
data.locationLastUpdateDate = undefined
data.profileLastUpdateDate = new Date(member.updatedAt).toISOString()
data.lastProfileConfirmationDate = member.lastProfileConfirmationDate ? new Date(member.lastProfileConfirmationDate).toISOString() : undefined

if (member.availableForGigs != null) {
completeItems += 1
data.gigAvailability = true
data.gigAvailabilityLastUpdateDate = member.availableForGigsLastUpdateDate || undefined
}

_.forEach(memberTraits, (item) => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ correctness]
The removal of the if block checking member.availableForGigs may affect the completeness calculation logic. Verify that this change aligns with the intended behavior and does not omit necessary checks.

if (item.traitId === 'education' && item.traits.data.length > 0 && !data.education) {
completeItems += 1
Expand All @@ -542,6 +536,20 @@ async function getProfileCompleteness (currentUser, handle, query) {
data.workHistory = true
data.workHistoryLastUpdateDate = new Date(item.updatedAt).toISOString()
}

if (item.traitId === 'personalization' && item.traits.data.length > 0 && !data.engagementAvailability) {
const openToWorkTrait = item.traits.data.find(r => Object.keys(r).includes('openToWork')) || {};
const openToWorkData = openToWorkTrait.openToWork || {};

if (openToWorkData && (
!openToWorkData.availability ||
(openToWorkData.preferredRoles && openToWorkData.preferredRoles.length)
)) {
completeItems += 1
data.engagementAvailability = true
data.engagementAvailabilityLastUpdateDate = new Date(item.updatedAt).toISOString()
}
}
})
// Push on the incomplete traits for picking a random toast to show
if (!data.education) {
Expand All @@ -550,8 +558,8 @@ async function getProfileCompleteness (currentUser, handle, query) {
if (!data.workHistory) {
showToast.push('workHistory')
}
if (!data.gigAvailability) {
showToast.push('gigAvailability')
if (!data.engagementAvailability) {
showToast.push('engagementAvailability')
}

// TODO: Do we use the short bio or the "description" field of the member object?
Expand Down Expand Up @@ -1783,30 +1791,33 @@ async function aggregatePDFData (currentUser, handle) {
// Fetch skills from standardized-skills-api
const skills = await getMemberSkills(memberData.userId)

// Separate skills by display mode and verification status
// Principal skills: same as before (verified / not verified lists)
const principalSkills = { verified: [], notVerified: [] }
const additionalSkills = { verified: [], notVerified: [] }

skills.forEach(skill => {
const isPrincipal = _.get(skill, 'displayMode.name') === 'principal'
if (_.get(skill, 'displayMode.name') !== 'principal') return
const isVerified = _.some(_.get(skill, 'levels', []), level => level.name === 'verified')
const skillName = skill.name

if (isPrincipal) {
if (isVerified) {
principalSkills.verified.push(skillName)
} else {
principalSkills.notVerified.push(skillName)
}
if (isVerified) {
principalSkills.verified.push(skillName)
} else {
if (isVerified) {
additionalSkills.verified.push(skillName)
} else {
additionalSkills.notVerified.push(skillName)
}
principalSkills.notVerified.push(skillName)
}
})

// Additional skills: group by category, sort by name, take up to limit per category (env PDF_SKILLS_PER_CATEGORY, default 5)
const additionalSkills = skills.filter(skill => _.get(skill, 'displayMode.name') !== 'principal')
const skillsPerCategoryLimit = Math.max(1, parseInt(config.PDF_SKILLS_PER_CATEGORY, 10) || 5)
const categoryKey = (skill) => (skill.category && skill.category.name) ? skill.category.name : 'Other'
const byCategory = _.groupBy(additionalSkills, categoryKey)
const skillsByCategory = _.map(byCategory, (skillList, categoryName) => {
const names = _.map(skillList, 'name')
.filter(Boolean)
.sort((a, b) => a.localeCompare(b, undefined, { sensitivity: 'base' }))
.slice(0, skillsPerCategoryLimit)
return { categoryName, skills: names }
}).filter(item => item.skills.length > 0)
skillsByCategory.sort((a, b) => a.categoryName.localeCompare(b.categoryName, undefined, { sensitivity: 'base' }))

const specialRoles = []
const roleMap = {
'copilot': 'Copilot',
Expand Down Expand Up @@ -1913,10 +1924,8 @@ async function aggregatePDFData (currentUser, handle) {
shortBio: shortBio
},
// Skills
skills: {
principal: principalSkills,
additional: additionalSkills
},
skills: { principal: principalSkills },

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ maintainability]
The restructuring of skills into skillsByCategory without retaining the additionalSkills object may affect any logic that previously relied on additionalSkills. Ensure that this change is consistent with the rest of the application and that no functionality is lost.

skillsByCategory,
// Topcoder activity
topcoderActivity: {
specialRole: specialRoles.length > 0 ? `Topcoder Special Role: ${specialRoles.join(', ')}` : null,
Expand Down
Loading