Name
sn_skill_cfg_page.ManageSkillsUtils
Description
For use in skill matrix of Manage Skills configurable page
Script
var ManageSkillsUtils = Class.create();
ManageSkillsUtils.prototype = {
/*
simple usage for testing:
var s = new sn_skill_cfg_page.ManageSkillsUtils();
var parentDepartment = "221f79b7c6112284005d646b76ab978c";
var parentSkill = "2eb1c2029f100200a3bc1471367fcfe4";
var params = {
groupBy: department/group,
parentDepartment,
isRecursiveDepartment: true,
parentSkill,
isRecursiveSkill: true,
pageNumber: 0,
pageSize: 100, // number of user rows to display
skillCount: 50, // number of skill columns to display
showLocation: true
};
var result = s.getUserSkillRows(params);
*/
initialize: function() {
this.SKILL_RECURSION_LIMIT = 1000; // hardcoded limit on skills recursion
this.util = new global.UserSkillAPI();
this.skillManager = new global.SkillManager();
this.TABLE = {
DEPARTMENT: 'cmn_department',
SKILL: 'cmn_skill',
GROUP: 'sys_user_group',
GROUP_MEMBER: 'sys_user_grmember',
USER: 'sys_user'
};
},
type: 'ManageSkillsUtils',
/*
* params:
* - SkillFilter
* - UserFilter/search
* - parentDepartment (isRecursiveDepartment)
* - parentSkill (isResursiveSkill)
* -
* - assignmentGroup
* - pageNumber
* - pageSize (chooseWindow for users)
* - skillCount (how many columns)
* - showLocation (boolean for showing location from user profile)
* - filter (for filtering users and/or skills using sidepanel filter)
*/
getUserSkillRows(params) {
const colorMap = JSON.parse(gs.getProperty('com.snc.skills_management.skill_level_color_map'));
const skillCount = params.skillCount;
const parentSkill = params.parentSkill;
// load all skill levels and level types into map
var skillLevelTypesMap = this.loadSkillLevelTypes();
// search only skills in filter if provided, or choose window of all skills
var selectedSkillsMap;
if (params.filter && params.filter.cmn_skill && params.filter.cmn_skill.sys_id.length) {
selectedSkillsMap = this.getFilteredSkills(params.filter.cmn_skill.sys_id);
} else {
if(!this.getSkillGr(parentSkill)){
gs.warn("Could not find parent skill");
return {
error: true,
errorMessage: gs.getMessage("Could not find parent skill")
};
}
selectedSkillsMap = this.getSkillsByParentSkill(parentSkill, params.isRecursiveSkill, skillCount);
}
let usersList = [];
let totalRowCount = 0;
let grpMembers = {};
var implementations = new GlideScriptedExtensionPoint().getExtensions('sn_skill_cfg_page.ManageSkillsExtnPt');
if (implementations) {
for (let i = 0; i < implementations.length; i++) {
let implementation = implementations[i];
if (implementation.canHandle(params)) {
const result = implementation.getUsers(params);
if(!result || result.error){
return {
error: true,
errorMessage: result.errorMessage
};
}
usersList = result.usersList;
totalRowCount = result.totalRowCount;
grpMembers = result.grpMembers;
break;
}
}
} else {
gs.error("unable to find extension point for ManageSkillsExtnPt");
return {
error: true
};
}
var userGr = new GlideRecordSecure("sys_user");
userGr.addQuery("sys_id", "IN", usersList.join());
userGr.orderBy("name");
userGr.query();
var users = new Map();
while (userGr.next()) {
let record = {};
record.id = userGr.getUniqueValue();
record.label = userGr.getValue("name");
record.skills = {};
let userDepartment = userGr.getValue("department");
if (!gs.nil(userDepartment)) {
record.department = {
id: userDepartment,
label: userGr.getDisplayValue("department")
};
}
users.set(record.id, record);
}
users = this.getSkillLevelsForUsers(users, selectedSkillsMap, skillLevelTypesMap);
this.getUserCountBySkillPerGroup(grpMembers, selectedSkillsMap, skillLevelTypesMap); // updates grpMembers with userCount by skill for each group
var result = {
skills: Object.fromEntries(selectedSkillsMap),
skillLevelTypes: Object.fromEntries(skillLevelTypesMap),
userRows: Object.fromEntries(users),
colorMap,
totalRowCount,
grpMembers
};
return result;
},
/*
* Build a map
* Map of just the columns to display (these require more queries to get level types)
*/
getSkillsByParentSkill(parentSkill, isRecursive, skillCount) {
var selectedSkillsMap = new Map();
var parentGr = this.getSkillGr(parentSkill);
if (parentGr == null) {
gs.warn("Could not find parent Skill");
return;
}
this._getSkillsRecursive(selectedSkillsMap, parentSkill, isRecursive, skillCount);
return selectedSkillsMap;
},
/*
* helper for getSkillsByParentSkill() when recurive option is true
*/
_getSkillsRecursive(selectedSkillsMap, parentSkill, isRecursive, skillCount) {
var gr = new GlideAggregate("cmn_skill_contains");
gr.addQuery("contains", parentSkill);
gr.addQuery("skill.active", true);
gr.query();
while (gr.next()) {
if (selectedSkillsMap.size >= skillCount || selectedSkillsMap.size >= this.SKILL_RECURSION_LIMIT) { // hard cap on skills works if singlethreaded
return;
}
const skillId = gr.getValue("skill");
if (!selectedSkillsMap.get(skillId)) {
let skillRecord = {};
skillRecord = this.getSkillInfoById(skillId);
selectedSkillsMap.set(skillId, skillRecord);
if (isRecursive)
this._getSkillsRecursive(selectedSkillsMap, skillId, true, skillCount);
}
}
},
getSkillInfoById(skillId) {
var gr = new GlideRecordSecure("cmn_skill");
gr.get(skillId);
var record = {};
const skillLevelType = gr.getValue("level_type");
record.id = skillId;
record.name = gr.getValue("name");
record.levelType = skillLevelType;
record.description = gr.getDisplayValue("description");
return record;
},
_getBasicSkillInfoById(skillId) {
var gr = new GlideRecordSecure("cmn_skill");
gr.get(skillId);
var record = {};
record.id = skillId;
record.name = gr.getValue("name");
return record;
},
getSkillGr(skillId) {
var gr = new GlideRecordSecure("cmn_skill");
if (gr.get(skillId))
return gr;
return null;
},
getFilteredSkills(filteredSkills) {
var skillsMap = new Map();
var skillsGr = new GlideRecordSecure(this.TABLE.SKILL);
if (gs.nil(filteredSkills) || filteredSkills.length === 0)
return skillsMap;
skillsGr.addQuery('sys_id', 'IN', filteredSkills.join(','));
skillsGr.query();
while (skillsGr.next()) {
var skillId = skillsGr.getUniqueValue();
skillsMap.set(skillId, {
id: skillId,
name: skillsGr.getValue('name'),
levelType: skillsGr.getValue("level_type")
});
}
return skillsMap;
},
/*
* given the object of users and list of skills (rows x columns)
* find out whether the user has that skill, and at what level
*/
getSkillLevelsForUsers(users, skillsMap, levelTypeMap) {
let hasSkill = new GlideAggregate("sys_user_has_skill");
hasSkill.addQuery('active', true);
hasSkill.addQuery("user", "IN", Array.from(users.keys()).join());
hasSkill.addQuery("skill", "IN", Array.from(skillsMap.keys()).join());
hasSkill.query();
// for each sys_user_has_skill, see if it is the highest value skill that user has.
while (hasSkill.next()) {
let record = {};
let skillId = hasSkill.getValue("skill");
let levelId = hasSkill.getValue("skill_level");
let recordId = hasSkill.getValue("sys_id");
let userId = hasSkill.getValue("user");
record.level = levelId;
const user = users.get(hasSkill.getValue('user'));
let skills = user.skills;
record.sys_id = skillId;
let skillRecord = skillsMap.get(skillId);
if (levelId) { // Level exists
const levelInfo = this._getLevelInfoFromSkillLevelType(skillsMap, levelTypeMap, skillId, levelId);
if (!levelInfo) {
gs.warn("No information found for this level " + levelId + " and skill " + skillId);
skills[skillId] = {
value: null,
userId: userId,
userName: user.label,
level: null,
skillId: skillId,
skillName: skillRecord.name,
levelTypeId: skillRecord.levelType,
recordId: recordId
}; // value null
}
//if skill not inserted yet, had no level due to malformed data, or found a higher value
else if (!skills[skillId] || skills[skillId].value == null || skills[skillId].value < levelInfo.value) {
skills[skillId] = {
level: levelId,
value: levelInfo.value,
name: levelInfo.name,
userId: userId,
userName: user.label,
skillId: skillId,
skillName: skillRecord.name,
levelTypeId: skillRecord.levelType,
recordId: recordId
};
}
} else { // simply default value when there is no level
if (!skills[skillId]) {
skills[skillId] = {
value: null,
userId: userId,
userName: user.label,
level: null,
skillId: skillId,
skillName: skillRecord.name,
levelTypeId: skillRecord.levelType,
recordId: recordId
}; // value null
}
}
}
Array.from(users.keys()).forEach(function(userId) {
const user = users.get(userId);
let skills = user.skills;
Array.from(skillsMap.keys()).forEach(function(skillId) {
let skillRecord = skillsMap.get(skillId);
if (!skills[skillId]) {
skills[skillId] = {
userId: userId,
userName: user.label,
level: null,
value: null,
levelTypeId: skillRecord.levelType,
skillId: skillId,
skillName: skillRecord.name,
recordId: '-1'
}; // value null
}
});
});
return users;
},
/*
* helper to access our stored skill type information, to get the numerical value of a skill level
*/
_getLevelInfoFromSkillLevelType(skillsMap, skillLevelTypesMap, skill, level) {
let skillRecord = skillsMap.get(skill);
if (!skillRecord) {
gs.warn("sys_user_has_skill not within set of skills");
return null;
}
if (skillRecord.levelType) {
let levelType = skillLevelTypesMap.get(skillRecord.levelType);
if (!levelType) {
gs.warn("Level type expected but not saved in skilLevelTypes");
}
return levelType[level]; // numerical value for this Level sysId within this Level Type
} else {
gs.warn("expected level type to exist for level " + level);
return null; // no level type for this skill, so it is just a default value
}
},
loadSkillLevelTypes: function() {
var levelTypesMap = new Map();
var typeGr = new GlideRecordSecure("cmn_skill_level_type");
typeGr.query();
while (typeGr.next()) {
let levelTypeId = typeGr.getUniqueValue();
if (!levelTypesMap.get(levelTypeId)) {
levelTypesMap.set(levelTypeId, this.getLevelInfoByLevelType(levelTypeId));
}
}
return levelTypesMap;
},
getLevelInfoByLevelType: function(levelTypeId) {
var levelInfo = {};
var levelGR = new GlideRecordSecure("cmn_skill_level");
levelGR.addQuery("skill_level_type", levelTypeId);
levelGR.orderBy("value");
levelGR.query();
while (levelGR.next()) {
var level = {};
level.name = levelGR.getValue("name");
level.sys_id = levelGR.getValue("sys_id");
level.value = levelGR.getValue("value");
level.colorDisplayValue = levelGR.getDisplayValue('color');
level.colorValue = levelGR.getValue('color');
var type = levelGR.getValue("skill_level_type");
var levels = {};
if (levelInfo.hasOwnProperty(type)) {
levels = levelInfo[type];
}
levels[level.sys_id] = level; // Use key later to sort by skill level value instead of during query
levelInfo[type] = levels;
}
return levelInfo[type];
},
getDepartmentGr: function(department) {
var gr = new GlideRecordSecure("cmn_department");
if (gr.get(department))
return gr;
return null;
},
canReadTable: function(table) {
var gr = new GlideRecord(table);
return gr.canRead();
},
//[{sys_id: '46d44a23a9fe19810012d100cca80666', selectedLevel: '4e0ac4d6b3332300290ea943c6a8dc4e'}]
getUserSkillLevelsByUserId(users) {
const userSkillLevels = new Map();
for (let i = 0; i < users.length; i++) {
const user = users[i];
if (user.hasOwnProperty('sys_id') && user.sys_id) {
userSkillLevels.set(user.sys_id, user.selectedLevel);
}
}
return userSkillLevels;
},
//get existing user skill pairs
getSkilledUsers(skillId, userIds) {
const skilledUsersByUserId = new Map();
var gr = new GlideRecordSecure("sys_user_has_skill");
gr.addActiveQuery();
gr.addQuery("skill", skillId);
gr.addQuery("user", "IN", userIds.join(','));
gr.orderByDesc("skill_level.value");
gr.query();
while (gr.next()) {
let userId = gr.getValue("user");
if (!skilledUsersByUserId.has(userId)) {
let user = {};
user.userId = userId;
user.level = gr.getValue("skill_level");
skilledUsersByUserId.set(userId, user);
}
}
return skilledUsersByUserId;
},
//only update user skill if selected level is different from the existing skill level
//or insert if there is no this user skill pair
updateSkilledUsers(skillId, userSkillLevelsByUserId) {
const dbSkillLevelsBySkillId = this.getSkilledUsers(skillId, Array.from(userSkillLevelsByUserId.keys()));
for (let [key, value] of userSkillLevelsByUserId) {
const userId = key;
const selectedLevel = value;
if (!dbSkillLevelsBySkillId.has(userId) ||
selectedLevel != dbSkillLevelsBySkillId.get(userId).level) {
this.util.updateUserSkill(skillId, userId, selectedLevel);
}
}
},
//insert user skill or update skill level
//[{sys_id: '46d44a23a9fe19810012d100cca80666', selectedLevel: '4e0ac4d6b3332300290ea943c6a8dc4e'}]
updateSkill(skillId, users) {
const userSkillLevelsByUserId = this.getUserSkillLevelsByUserId(users);
this.updateSkilledUsers(skillId, userSkillLevelsByUserId);
},
deleteUserSkillsBySkillId(skillId, usersToBeDeleted) {
this.util.deleteUserSkill(usersToBeDeleted, [skillId]);
},
deleteUserSkillsByUserId(userId, skillsToBeDeleted) {
this.util.deleteUserSkill([userId], skillsToBeDeleted);
},
_findValidRecord: function(table, sysId) {
var gr = new GlideRecord(table);
return gr.get(sysId);
},
addParentSkillAndCategory(data) {
var newSkillId = data.newSkillId;
var skillCategories = data.skillCategories;
var parentSkill = data.parentSkill;
var response = {};
try {
if (this._findValidRecord('cmn_skill', parentSkill)) {
var res = this.skillManager.addParentSkill(newSkillId, parentSkill);
if (res.status === 'error') {
throw new Error(res.errorMessage);
}
skillCategories.forEach(function(category) {
var result = new global.SkillManager().createM2MSkillCategory(newSkillId, category.id);
if (result.status === 'error') {
throw new Error(result.errorMessage);
}
});
}
} catch (error) {
gs.warn(error.message);
response = {
error: error,
success: false
};
return response;
}
response = {
newSkillId: newSkillId,
success: true
};
return response;
},
/*
* Get details of all the departments that contain parentDepartment if isRecursive is true
* Else, get the department details of parentDepartment
*/
getAllDepartments(parentDepartment, isRecursiveDepartment) {
if (gs.nil(parentDepartment)) {
gs.warn("Parent department not provided");
}
let departmentMap = {};
let deptGR = new GlideRecordSecure(this.TABLE.DEPARTMENT);
deptGR.get(parentDepartment);
deptGR.query();
if (deptGR.next()) {
departmentMap[deptGR.getUniqueValue()] = deptGR.getValue('name');
}
this.getRecursiveDepartments(parentDepartment, departmentMap, isRecursiveDepartment);
return Object.keys(departmentMap);
},
getRecursiveDepartments(parentDepartment, departmentMap, isRecursiveDepartment) {
if (gs.nil(parentDepartment))
return;
let deptGR = new GlideRecordSecure(this.TABLE.DEPARTMENT);
deptGR.addActiveQuery();
deptGR.addQuery("parent", parentDepartment);
deptGR.query();
if (!deptGR) {
gs.warn("Parent department not found");
}
while (deptGR.next()) {
const depSysId = deptGR.getUniqueValue();
if (!departmentMap[depSysId]) {
departmentMap[depSysId] = deptGR.getValue('name');
if (isRecursiveDepartment)
this.getRecursiveDepartments(depSysId, departmentMap, isRecursiveDepartment);
}
}
},
// Get number of users in a group that have a skill
getUserCountBySkillPerGroup: function(grpMembers, skillMap) {
var userCountBySkills = {};
var skillAndUserKeysMap = {}; // to avoid duplicate count
var skills = Array.from(skillMap.keys());
//for each group calculate user count skills
for (var group in grpMembers) {
var users = grpMembers[group].users.map(user => {
return user.userID;
});
// Get skill levels associated with the skills of parentSkill that users in this group/department have
var skillLevels = this.getDistinctSkillLevels(users, skills);
userCountBySkills = {};
for (var i = 0; i < skillLevels.length; i++) {
//Get available skill level count for skills of parentSkill that the users in the group/department have
this.getUserCountBySkillLevel(users, skills, skillLevels[i], userCountBySkills, skillAndUserKeysMap);
}
var userCountBySkillWithNoLevelType = this.getUserCountBySkillWithNoLevelType(users, skills);
var userCountBySkillLevel = {};
//Construct skill level count for each skill level for a skill per group
for (var j in skills) {
var skill = skills[j];
if (!gs.nil(userCountBySkills[skill])) // get skill level count for each group if level type is associated with the skill
userCountBySkillLevel[skill] = this.flattenSkillLevelCounts(userCountBySkills[skill]);
}
grpMembers[group] = {
'userCountBySkillLevel': userCountBySkillLevel,
'userCountBySkillWithNoLevelType': userCountBySkillWithNoLevelType,
...grpMembers[group]
};
}
},
getDistinctSkillLevels: function(users, skills) {
if (gs.nil(users) || gs.nil(skills) || users.length === 0 || skills.length === 0)
return [];
var skillLevelIds = [];
var skillLevels = [];
var ga = new GlideAggregate("sys_user_has_skill");
ga.addQuery('user', 'IN', users.join(','));
ga.addQuery('skill', 'IN', skills.join(','));
ga.addQuery("active", true);
ga.groupBy("skill_level");
ga.query();
while (ga.next()) {
var skillLevel = ga.getValue("skill_level");
if (!gs.nil(skillLevel))
skillLevelIds.push(skillLevel);
}
var gr = new GlideRecordSecure("cmn_skill_level");
gr.addQuery("sys_id", "IN", skillLevelIds);
gr.orderByDesc("value");
gr.query();
while (gr.next()) {
skillLevels.push({
id: gr.getValue("sys_id"),
name: gr.getValue("name"),
rank: gr.getValue("value")
});
}
return skillLevels;
},
// Get skill level count for each skill in a group
getUserCountBySkillLevel: function(users, skills, skillLevel, userCountBySkills, skillAndUserKeysMap) {
var ga = new GlideAggregate("sys_user_has_skill");
ga.addQuery("skill_level", skillLevel.id);
ga.addQuery('user', 'IN', users.join(','));
ga.addQuery('skill', 'IN', skills.join(','));
ga.addQuery("active", true);
ga.orderBy("skill");
ga.orderBy("user");
ga.query();
while (ga.next()) {
var skillId = ga.getValue("skill");
var userId = ga.getValue("user");
var uniqueKey = skillId + "-" + userId;
// continue if user already has skill = skillId and skill_level = skillLevel.id
if (uniqueKey in skillAndUserKeysMap) {
continue;
}
skillAndUserKeysMap[uniqueKey] = true;
// begin to populate skill data to userCountBySkills
var skill = userCountBySkills[skillId];
if (!skill) {
userCountBySkills[skillId] = skill = {
skill_id: skillId,
skill_levels: [],
};
}
var lastSkillLevel = skill.skill_levels[skill.skill_levels.length - 1];
var nextSkillLevel = lastSkillLevel ?
lastSkillLevel.skill_level_id === skillLevel.id ?
lastSkillLevel :
null :
null;
if (!nextSkillLevel) {
nextSkillLevel = {
skill_level_id: skillLevel.id,
skill_level_name: skillLevel.name,
skill_level_rank: skillLevel.rank,
user_count: 0,
};
skill.skill_levels.push(nextSkillLevel);
}
nextSkillLevel.user_count++;
}
},
getUserCountBySkillWithNoLevelType: function(users, skills) {
var userSkillCount = {};
var userSkillGA = new GlideAggregate('sys_user_has_skill');
userSkillGA.addQuery('user', 'IN', users.join(','));
userSkillGA.addQuery('skill', 'IN', skills.join(','));
userSkillGA.addQuery('active', true);
userSkillGA.addNullQuery('skill_level');
userSkillGA.addAggregate('COUNT', 'user');
userSkillGA.groupBy('skill');
userSkillGA.query();
while (userSkillGA.next()) {
userSkillCount[userSkillGA.getValue('skill')] = userSkillGA.getAggregate('COUNT', 'user');
}
return userSkillCount;
},
/**
* Get user count per skill grouped by skill level
*/
flattenSkillLevelCounts: function(skillLevelCounts) {
var simplifiedResult = {};
if (gs.nil(skillLevelCounts) || gs.nil(skillLevelCounts.skill_levels) || skillLevelCounts.skill_levels.length === 0)
return simplifiedResult;
skillLevelCounts.skill_levels.forEach(skill_level => {
(simplifiedResult[skill_level.skill_level_id] =
skill_level.user_count);
});
return simplifiedResult;
},
groupsManagedByMe: function() {
var groups = [];
var groupGR = new GlideRecord(this.TABLE.GROUP);
groupGR.addQuery('manager', gs.getUserID());
groupGR.addActiveQuery();
groupGR.query();
while (groupGR.next())
groups.push(groupGR.getUniqueValue());
return groups;
},
getUsersIManage: function() {
var users = [];
var groups = this.groupsManagedByMe();
var userGR = new GlideRecord(this.TABLE.GROUP_MEMBER);
userGR.addActiveQuery();
userGR.addQuery('group', 'IN', groups.join(','));
userGR.query();
while (userGR.next())
users.push(userGR.getValue('user'));
return users;
}
};
Sys ID
0946b5a987fb51101bf7a64d0ebb35bd