Skip to content
Commits on Source (6)
# Changelog
### [1.7.1](https://gitlab.mim-libre.fr/alphabet/mezig/compare/release/1.7.0...release/1.7.1) (2023-01-30)
### Performance Improvements
* **skills:** create collection for skills ([6a53fbb](https://gitlab.mim-libre.fr/alphabet/mezig/commit/6a53fbbe07398760723a53b2eefa46ad07e6c2be))
## [1.7.0](https://gitlab.mim-libre.fr/alphabet/mezig/compare/release/1.6.1...release/1.7.0) (2022-09-19)
......
......@@ -2,6 +2,8 @@
import { Meteor } from 'meteor/meteor';
import { Random } from 'meteor/random';
import Mezigs from './mezigs/mezigs';
import Skills from './skills/skills';
import { updateSkillsCollection } from './utils';
if (Meteor.settings.private.fillWithFakeData) {
let nbMezigsAdded = 0;
......@@ -15,23 +17,29 @@ if (Meteor.settings.private.fillWithFakeData) {
}
};
const allSkills = [
'python',
'react',
'svelte',
'html',
'javascript',
'css',
'java',
'C++',
'rust',
'go',
'cobol',
'SQL',
'docker',
'git',
'markdown',
];
const allSkills = Skills.find({}).fetch();
let allSkillsNames = [];
if (allSkills.length !== 0) {
allSkillsNames = allSkills.map((s) => s.name);
} else {
allSkillsNames = [
'python',
'react',
'svelte',
'html',
'javascript',
'css',
'java',
'C++',
'rust',
'go',
'cobol',
'SQL',
'docker',
'git',
'markdown',
];
}
const allLinks = [
{
......@@ -105,11 +113,16 @@ if (Meteor.settings.private.fillWithFakeData) {
blacklist: Random.fraction() * 100 >= 90, // génère 10% de blacklist = true
skills: [
...new Set([
Random.choice(allSkills),
Random.choice(allSkills),
Random.choice(allSkills),
Random.choice(allSkills),
Random.choice(allSkills),
Random.choice(allSkillsNames),
Random.choice(allSkillsNames),
Random.choice(allSkillsNames),
Random.choice(allSkillsNames),
Random.choice(allSkillsNames),
Random.choice(allSkillsNames),
Random.choice(allSkillsNames),
Random.choice(allSkillsNames),
Random.choice(allSkillsNames),
Random.choice(allSkillsNames),
]),
],
links: [
......@@ -121,10 +134,12 @@ if (Meteor.settings.private.fillWithFakeData) {
Random.choice(allLinks),
]),
],
isActive: Random.choice(Boolean),
profileChecked: true,
};
createMezig(mez);
});
console.log(`${nbMezigsAdded} Mezigs added`);
Skills.remove({}); // force un recalcul global des skills
updateSkillsCollection();
}
}
......@@ -97,6 +97,27 @@ export const updateMezig = new ValidatedMethod({
throw new Meteor.Error('api.mezigs.methods.updateMezig.adminNeeded', i18n.__('api.adminNeeded'));
}
// update skills collection
const { skills } = data;
const mezig = Mezigs.findOne({ _id: mezigId }, { fields: { skills: 1 } });
const oldSkills = mezig.skills;
const skillsToAdd = [];
const skillsToDelete = [];
skills.forEach((skill) => {
if (!oldSkills.includes(skill)) {
skillsToAdd.push(skill);
}
});
oldSkills.forEach((skill) => {
if (!skills.includes(skill)) {
skillsToDelete.push(skill);
}
});
Meteor.call('skills.updateSkills', { skillsToAdd, skillsToDelete });
// get favicon for links
// https://stackoverflow.com/questions/37576685/using-async-await-with-a-foreach-loop
const links = await Promise.all(
......@@ -155,6 +176,7 @@ export const removeMezig = new ValidatedMethod({
if (myzig === undefined) {
throw new Meteor.Error('api.mezigs.methods.removeMezig.notFound', i18n.__('api.mezigs.notFound'));
}
Meteor.call('skills.updateSkills', { skillsToDelete: myzig.skills });
return Mezigs.remove({ _id: mezigId });
},
});
......@@ -216,6 +238,13 @@ export const getMezigs = new ValidatedMethod({
},
});
Meteor.methods({
'mezigs.publicProfileCount': function getAllMezigsCount() {
const count = Mezigs.find({ blacklist: false }).count();
return count;
},
});
// Meteor.methods({
// 'mezigs.getAllData': function getAllData() {
// const data = Mezigs.find().fetch();
......
import { Meteor } from 'meteor/meteor';
import Mezigs from '../mezigs';
Meteor.methods({
'mezigs.getAllSkills': function getAllSkills() {
const allSkills = [];
const mezigs = Mezigs.find({}, { fields: { skills: 1 } }).fetch();
mezigs.forEach(function (u) {
u.skills.forEach(function (s) {
if (!allSkills.includes(s)) {
allSkills.push(s);
}
});
});
allSkills.sort();
return allSkills;
},
});
......@@ -10,6 +10,7 @@ import { Factory } from 'meteor/dburles:factory';
import Mezigs from '../mezigs';
import './publications';
import { createMezig, updateMezig, removeMezig, getMezigs } from '../methods';
import Skills from '../../skills/skills';
describe('mezig', function () {
describe('mutators', function () {
......@@ -219,6 +220,35 @@ describe('mezig', function () {
/api.mezigs.methods.removeMezig.notFound/,
);
});
it('does update skills when remove a mezig', function () {
Factory.create('skills', { name: 'tata', count: 3 });
Factory.create('skills', { name: 'titi', count: 2 });
Factory.create('skills', { name: 'toto', count: 1 });
let allSkills = Skills.find().fetch();
mezigId = Factory.create('mezigs', { ...mezigData, skills: ['tata', 'titi', 'toto'] })._id;
const mez = Mezigs.findOne({ _id: mezigId });
assert.isObject(mez);
assert.isArray(mez.skills);
assert.lengthOf(mez.skills, 3);
assert.lengthOf(allSkills, 3);
removeMezig._execute({ userId: adminId }, { mezigId });
assert.equal(Mezigs.findOne({ _id: mezigId }), undefined);
allSkills = Skills.find().fetch();
assert.lengthOf(allSkills, 2);
assert.equal(Skills.findOne({ name: 'tata' }).count, 2);
assert.equal(Skills.findOne({ name: 'titi' }).count, 1);
assert.isNotObject(Skills.findOne({ name: 'toto' }));
});
it('throw error if mezig to remove has unknown skill', function () {
assert.throws(
() => {
mezigId = Factory.create('mezigs', { ...mezigData, skills: ['toto'] })._id;
removeMezig._execute({ userId: adminId }, { mezigId });
},
Meteor.Error,
/api.skills.methods.updateSkills/,
);
});
});
describe('mezigs.getMezigs', function () {
beforeEach(function () {
......@@ -271,5 +301,15 @@ describe('mezig', function () {
assert.equal(res.data.length, 0);
});
});
describe('mezigs.publicProfileCount', function () {
it('does count not blacklist mezigs', function () {
_.times(4, () => {
Factory.create('mezigs');
});
Factory.create('mezigs', { blacklist: true });
const nbMezigs = Meteor.call('mezigs.publicProfileCount');
assert.equal(nbMezigs, 4);
});
});
});
});
import { Meteor } from 'meteor/meteor';
import Skills from './skills';
Meteor.methods({
// eslint-disable-next-line meteor/audit-argument-checks
'skills.updateSkills': function updateSkills(skillsToUpdate) {
const skillsToIncrement = skillsToUpdate.skillsToAdd || [];
const skillsToDecrement = skillsToUpdate.skillsToDelete || [];
if (skillsToIncrement.length > 0) {
skillsToIncrement.forEach((skill) => {
const skillFind = Skills.findOne({ name: skill.toString() });
if (!skillFind) return Skills.insert({ name: skill.toString(), count: 1 });
return Skills.update({ _id: skillFind._id }, { $inc: { count: 1 } });
});
}
if (skillsToDecrement.length > 0) {
skillsToDecrement.forEach((skill) => {
const skillFind = Skills.findOne({ name: skill.toString() });
if (!skillFind)
throw new Meteor.Error('api.skills.methods.updateSkills', `Unknown skill to remove : ${skill.toString()}`);
if (skillFind.count === 1) return Skills.remove({ name: skill.toString() });
return Skills.update({ name: skill.toString() }, { $inc: { count: -1 } });
});
}
},
});
import { FindFromPublication } from 'meteor/percolate:find-from-publication';
import Skills from '../skills';
FindFromPublication.publish('skills.all', function publishAllSkills() {
if (this.userId !== null) {
return Skills.find({});
}
return this.ready();
});
FindFromPublication.publish('skills.table.all', function publishAllSkillsTable() {
return Skills.find({}, { sort: { count: -1 } });
});
/* eslint-env mocha */
/* eslint-disable func-names, prefer-arrow-callback */
import { assert } from 'chai';
import { Meteor } from 'meteor/meteor';
import { Factory } from 'meteor/dburles:factory';
import Skills from '../skills';
import './publications';
import '../methods';
describe('skills', function () {
describe('mutators', function () {
it('builds correctly from factory', function () {
const skill = Factory.create('skills');
assert.typeOf(skill, 'object');
});
});
describe('updateSkills', function () {
beforeEach(function () {
Skills.remove({});
});
it('does update skill correctly with multiple skills', function () {
Factory.create('skills', { name: 'test1', count: 1 });
Factory.create('skills', { name: 'test2', count: 2 });
Factory.create('skills', { name: 'test3', count: 1 });
const skillsToAdd = ['test1', 'test4'];
const skillsToDelete = ['test2', 'test3'];
Meteor.call('skills.updateSkills', { skillsToAdd, skillsToDelete });
const skill1Find = Skills.findOne({ name: 'test1' });
const skill2Find = Skills.findOne({ name: 'test2' });
const skill3Find = Skills.findOne({ name: 'test3' });
const skill4Find = Skills.findOne({ name: 'test4' });
assert.typeOf(skill1Find, 'object');
assert.equal(skill1Find.name, 'test1');
assert.equal(skill1Find.count, 2);
assert.typeOf(skill2Find, 'object');
assert.equal(skill2Find.name, 'test2');
assert.equal(skill2Find.count, 1);
assert.typeOf(skill3Find, 'undefined');
assert.typeOf(skill4Find, 'object');
assert.equal(skill4Find.name, 'test4');
assert.equal(skill4Find.count, 1);
});
it('does throw an error if skill to decrement does not exists', function () {
assert.throws(
() => {
const skillsToAdd = [];
const skillsToDelete = ['test1'];
Meteor.call('skills.updateSkills', { skillsToAdd, skillsToDelete });
},
Meteor.Error,
'api.skills.methods.updateSkills',
'Ce skill n existe pas',
);
});
});
});
/* eslint-disable func-names */
import { Mongo } from 'meteor/mongo';
import SimpleSchema from 'simpl-schema';
import { Tracker } from 'meteor/tracker';
import { Factory } from 'meteor/dburles:factory';
import { Random } from 'meteor/random';
const Skills = new Mongo.Collection('skills');
// Deny all client-side updates since we will be using methods to manage this collection
Skills.deny({
insert() {
return true;
},
update() {
return true;
},
remove() {
return true;
},
});
Skills.schema = new SimpleSchema(
{
name: {
type: String,
index: true,
unique: true,
},
count: {
type: Number,
min: 0,
},
},
{ clean: { removeEmptyStrings: false }, tracker: Tracker },
);
Skills.publicFields = {
name: 1,
count: 1,
};
Factory.define('skills', Skills, {
name: () => Random.id(),
count: () => 2,
});
Skills.attachSchema(Skills.schema);
export default Skills;
import { Meteor } from 'meteor/meteor';
import i18n from 'meteor/universe:i18n';
import SimpleSchema from 'simpl-schema';
import Mezigs from './mezigs/mezigs';
import Skills from './skills/skills';
export function isActive(userId) {
if (!userId) return false;
......@@ -90,3 +92,22 @@ export function genRandomPassword(pwdlen = 16) {
return password;
}
export function updateSkillsCollection() {
const skills = Skills.find({}).fetch();
if (skills.length === 0) {
console.log('updating skills...');
const mezigs = Mezigs.find({}).fetch();
mezigs.forEach((u) => {
u.skills.forEach((s) => {
const skill = Skills.findOne({ name: s });
if (skill) {
Skills.update({ name: s }, { $inc: { count: 1 } });
} else {
Skills.insert({ name: s, count: 1 });
}
});
});
console.log(`...end updating ${Skills.find({}).count()} skills.`);
}
}
......@@ -5,23 +5,21 @@
import Tag from './Tag.svelte';
export let skillsTab;
let maxSkill;
let expend = false;
let tab = [];
let tabSkill;
$: {
if (expend) {
maxSkill = skillsTab.length;
tabSkill = $skillsTab;
} else {
maxSkill = 10;
tabSkill = $skillsTab.slice(0, 10);
}
}
function calcFontSize(skillUseNumber) {
if (expend) {
tab.push(skillUseNumber);
let fontSize = (skillUseNumber / Math.max(...tab)) * 3;
const max = tabSkill[0].count;
let fontSize = (skillUseNumber / max) * 2;
fontSize = fontSize.toFixed(2);
if (fontSize <= 1) fontSize = 1;
return `${fontSize}em`;
......@@ -32,12 +30,12 @@
</script>
<div class="divContainer">
{#each skillsTab.slice(0, maxSkill) as skill}
{#each tabSkill as skill}
<Tag
skill={`#${skill[0]}`}
skill={`#${skill.name}`}
on:clickSkills
textTooltip={`${$_('ui.tags.useNumber')} : ${skill[1]}`}
fontSizeProperty={calcFontSize(skill[1])}
textTooltip={`${$_('ui.tags.useNumber')} : ${skill.count}`}
fontSizeProperty={calcFontSize(skill.count)}
/>
{/each}
</div>
......
......@@ -4,6 +4,7 @@
import { link as routerLink, navigate } from 'svelte-routing';
import { _ } from 'svelte-i18n';
import Mezigs from '../../api/mezigs/mezigs';
import Skills from "../../api/skills/skills"
import Spinner from '../components/Spinner.svelte';
import EditTableLinks from '../components/EditTableLinks.svelte';
......@@ -50,10 +51,12 @@
let newSkillsTab = [];
let searchInput;
$: Meteor.call('mezigs.getAllSkills', (err, res) => {
if (!err) {
listTag = res;
$: allSkillsName = useTracker(() => {
const sub = Meteor.subscribe('skills.all');
if (sub.ready()) {
return Skills.find({}).fetch();
}
return []
});
const maxSkillsCar = 32;
......@@ -106,6 +109,7 @@
email: email || null,
profileChecked: true,
};
Meteor.call('mezigs.updateMezig', { mezigId: $currentMezig._id, data: userData }, (err) => {
if (err) {
error = err.message;
......@@ -159,9 +163,9 @@
const makeMatchBold = (str) => {
// replace part of (country name === inputValue) with strong tags
let matched = str.substring(0, newSkill.length);
let matched = str.name.substring(0, newSkill.length);
let makeBold = `<strong>${matched}</strong>`;
let boldedMatch = str.replace(matched, makeBold);
let boldedMatch = str.name.replace(matched, makeBold);
return boldedMatch;
};
......@@ -200,11 +204,11 @@
};
// TODO meteor call instead of countries
const filterTags = () => {
$: filterTags = () => {
let storageArr = [];
if (newSkill) {
listTag.forEach((tag) => {
if (tag.toLowerCase().startsWith(newSkill.toLowerCase())) {
$allSkillsName.forEach((tag) => {
if (tag.name.toLowerCase().startsWith(newSkill.toLowerCase())) {
storageArr = [...storageArr, makeMatchBold(tag)];
}
});
......
......@@ -6,7 +6,7 @@
import { useTracker } from 'meteor/rdb:svelte-meteor-data';
import SearchResult from '../components/SearchResult.svelte';
import Mezigs from '../../api/mezigs/mezigs';
import Skills from '../../api/skills/skills';
import { searchingStore } from '../../stores/stores';
import PackageJSON from '../../../package.json';
import TagGroup from '../components/TagGroup.svelte';
......@@ -25,65 +25,23 @@
let totalFoundMezigs = 0;
let timeout;
let ulMezigs = {};
let allMezigsCount;
$: allMezigs = useTracker(() => {
const sub = Meteor.subscribe('mezigs.table.all');
if (sub.ready()) {
return Mezigs.findFromPublication('mezigs.table.all', {}).fetch();
$: Meteor.call('mezigs.publicProfileCount', {}, (err, res) => {
if (err) {
console.log(err);
} else {
allMezigsCount = res;
}
return [];
});
$: skillsTab = getSkillTab($allMezigs);
function getSkillTab(mezigs) {
let allSkills = {};
let skills = [];
mezigs.forEach(function (u) {
u.skills.forEach(function (s) {
if (s in allSkills) {
allSkills[s] += 1;
} else {
allSkills[s] = 1;
}
});
});
Object.keys(allSkills).forEach((s) => {
skills.push([s, allSkills[s]]);
});
skills.sort(function (a, b) {
return b[1] - a[1];
});
return skills;
}
function getRandomSkills(sTab) {
if (sTab) {
let i = 0;
let min = 10;
let max = sTab.length;
let temp;
let res = [];
if (max <= 10) {
return res;
} else {
if (max < 20) {
res = sTab.slice(10, max);
} else {
while (i < 10) {
temp = sTab[Math.floor(Math.random() * (max - min) + min)];
if (!res.includes(temp)) {
res.push(temp);
i++;
}
}
}
return res;
}
$: skillsTab = useTracker(() => {
const sub = Meteor.subscribe('skills.table.all');
if (sub.ready()) {
return Skills.findFromPublication('skills.table.all', {}).fetch();
}
return [];
}
});
onMount(async () => {
searching = previousSearch;
......@@ -149,7 +107,7 @@
<title>Accueil | {$_('ui.appName')} {version}</title>
</svelte:head>
<h1 class="numberUsers">{$allMezigs.length} {$_('api.users.number')}</h1>
<h1 class="numberUsers">{allMezigsCount} {$_('api.users.number')}</h1>
<form on:submit|preventDefault role="search" style={usersScroll.length > 0 ? 'top: 15%;' : ''}>
<label for="search">{$_('ui.searchLabel')}</label>
<input
......
{
"name": "mezig",
"version": "1.7.0",
"version": "1.7.1",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
......
{
"name": "mezig",
"private": true,
"version": "1.7.0",
"version": "1.7.1",
"license": "EUPL-1.2",
"description": "Online biography",
"author": "EOLE/PCLL <team@eole.education> - DINUM",
......
......@@ -5,14 +5,16 @@ import '../imports/api/users/users';
import '../imports/api/users/methods';
import '../imports/api/users/server/publications';
import '../imports/api/mezigs/server/publications';
import '../imports/api/mezigs/server/methods';
import '../imports/api/mezigs/methods';
import '../imports/api/importFakeData';
import '../imports/api/appsettings/appsettings';
import '../imports/api/appsettings/server/publications';
import '../imports/api/skills/server/publications';
import '../imports/api/skills/methods';
import { ServiceConfiguration } from 'meteor/service-configuration';
import SimpleSchema from 'simpl-schema';
import { updateSkillsCollection } from '../imports/api/utils';
SimpleSchema.defineValidationErrorTransform((error) => {
const ddpError = new Meteor.Error(error.message);
......@@ -76,5 +78,7 @@ Meteor.startup(() => {
// and keycloak authentication is disabled
Accounts.config({ forbidClientAccountCreation: false });
}
updateSkillsCollection();
}
});