@01-edu/shared 1.0.12 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/attrs-defs.js +328 -62
- package/attrs.js +11 -3
- package/graph.js +96 -0
- package/package.json +2 -1
package/attrs-defs.js
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
getQuestStartAt,
|
|
8
8
|
numTime,
|
|
9
9
|
} from './event-utils.js'
|
|
10
|
+
import { flatGraphContents, getCoreOfSattelite, limitations } from './graph.js'
|
|
10
11
|
import { onboardingTypes } from './onboarding.js'
|
|
11
12
|
import { getObjectFromRelativePath } from './path.js'
|
|
12
13
|
import { gamesScoring } from './score.js'
|
|
@@ -61,22 +62,12 @@ const firstOfGroup = (a, _i, children) => {
|
|
|
61
62
|
return children.find(b => b.attrs.group === group) === a
|
|
62
63
|
}
|
|
63
64
|
|
|
64
|
-
const getCoreName = object => {
|
|
65
|
-
const corePath = object.attrs.requirements?.core
|
|
66
|
-
const core = _children(object.parent).find(
|
|
67
|
-
child =>
|
|
68
|
-
child.id ===
|
|
69
|
-
getObjectFromRelativePath(corePath, object, { throwError: false })?.id,
|
|
70
|
-
)
|
|
71
|
-
return core?.name || ''
|
|
72
|
-
}
|
|
73
|
-
|
|
74
65
|
const getObjectPath = object => {
|
|
75
|
-
const
|
|
76
|
-
if (!
|
|
66
|
+
const core = getCoreOfSattelite(object)
|
|
67
|
+
if (!core) return object.name
|
|
77
68
|
// special case only for projects with a core requirement set
|
|
78
|
-
const coreName = getCoreName(object)
|
|
79
|
-
const path = `${
|
|
69
|
+
// const coreName = getCoreName(object)
|
|
70
|
+
const path = `${core.name}/${getProjectName(object)}`
|
|
80
71
|
return path
|
|
81
72
|
}
|
|
82
73
|
|
|
@@ -129,6 +120,7 @@ const TypeObject = def => ({
|
|
|
129
120
|
// required: // if the attribute is always there by default,
|
|
130
121
|
// editable: // if it can be override by schools in the db (=/= choice in between several functions),
|
|
131
122
|
// private: // if the attribute should not be seen in front end,
|
|
123
|
+
// hidden: // if the attribute should be hidden to admins but needed in front end,
|
|
132
124
|
// primary: // only used objects in arrays, allow to indicate what key must have a unique value
|
|
133
125
|
// options: // array containing literal possible values
|
|
134
126
|
// acceptDuplicates: // when array type can have duplicates
|
|
@@ -927,15 +919,13 @@ relationAttrs.difficultyMod = {
|
|
|
927
919
|
|
|
928
920
|
const getName = ({ name }) => name
|
|
929
921
|
const getProjectName = object => {
|
|
930
|
-
const
|
|
931
|
-
if (!
|
|
932
|
-
// only for projects with a core
|
|
922
|
+
const core = getCoreOfSattelite(object)
|
|
923
|
+
if (!core) return object.name
|
|
924
|
+
// only for projects with a core, and which name starts by the core name:
|
|
933
925
|
// remove the core name from the name to avoid repetition
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
? name.slice(coreName.length + 1)
|
|
938
|
-
: name
|
|
926
|
+
return core.name && object.name.startsWith(core.name)
|
|
927
|
+
? object.name.slice(core.name.length + 1)
|
|
928
|
+
: object.name
|
|
939
929
|
}
|
|
940
930
|
const sharedDisplayedName = Literal('', {
|
|
941
931
|
editable: true,
|
|
@@ -978,7 +968,7 @@ const sharedDuration = Literal(1, {
|
|
|
978
968
|
relationAttrs.duration = {
|
|
979
969
|
// TODO: rm, here just for campus comparison
|
|
980
970
|
module: {
|
|
981
|
-
exam: sharedDuration,
|
|
971
|
+
exam: { ...sharedDuration, hidden: true },
|
|
982
972
|
},
|
|
983
973
|
piscine: {
|
|
984
974
|
quest: sharedDuration,
|
|
@@ -1628,24 +1618,306 @@ attrs.grade = {
|
|
|
1628
1618
|
}),
|
|
1629
1619
|
}
|
|
1630
1620
|
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1621
|
+
/* MODULE GRAPH ATTRIBUTE */
|
|
1622
|
+
types.graphArcName = TypeObject({
|
|
1623
|
+
label: 'Name of the arc',
|
|
1624
|
+
type: {
|
|
1625
|
+
text: Literal('', {
|
|
1626
|
+
label: 'The text name of the arc',
|
|
1627
|
+
type: 'string',
|
|
1628
|
+
}),
|
|
1629
|
+
hidden: Literal(true, {
|
|
1630
|
+
label: 'Display the text name on the graph',
|
|
1631
|
+
}),
|
|
1632
|
+
},
|
|
1633
|
+
})
|
|
1634
|
+
|
|
1635
|
+
const checkGraphArcContentIsValid = (contentName, object) => {
|
|
1636
|
+
const matchingObject = object.children[contentName]
|
|
1637
|
+
if (!matchingObject) {
|
|
1638
|
+
throw Error(
|
|
1639
|
+
`Invalid object - no object found in the module for the following key name: ${contentName}`,
|
|
1640
|
+
)
|
|
1641
|
+
}
|
|
1636
1642
|
}
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1643
|
+
|
|
1644
|
+
types.graphArcContentName = Literal('', {
|
|
1645
|
+
label: 'The text name of a content placed on an arc',
|
|
1646
|
+
check: (contentName, object) =>
|
|
1647
|
+
checkGraphArcContentIsValid(contentName, object),
|
|
1648
|
+
})
|
|
1649
|
+
|
|
1650
|
+
types.graphArcContentWithSubContents = {
|
|
1651
|
+
value: {},
|
|
1652
|
+
label: 'Content with sub-contents placed on an arc',
|
|
1653
|
+
type: 'object',
|
|
1654
|
+
check: (value, object) => {
|
|
1655
|
+
if (isntObjectOrIsEmpty(value)) {
|
|
1656
|
+
throw Error('Should be a non empty object.')
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
const entries = Object.entries(value)
|
|
1660
|
+
if (entries.length > 1) {
|
|
1661
|
+
throw Error(
|
|
1662
|
+
"Should be an object with a single key-value pair; the key being the content's name, and the value being an array of sub-contents's names.",
|
|
1663
|
+
)
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
const [contentName, subContentsList] = entries[0]
|
|
1667
|
+
|
|
1668
|
+
// check content name key matches an existing object in the module
|
|
1669
|
+
checkGraphArcContentIsValid(contentName, object)
|
|
1670
|
+
|
|
1671
|
+
// check sub-contents' list is a non empty array
|
|
1672
|
+
if (!Array.isArray(subContentsList) || !subContentsList.length) {
|
|
1673
|
+
throw Error('Must be a non empty array')
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
// check there's no duplicates in sub-contents' list
|
|
1677
|
+
const uniques = new Set(subContentsList)
|
|
1678
|
+
if (subContentsList.length !== uniques.size) {
|
|
1679
|
+
throw Error('Duplicates are not allowed.')
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
const maxSattelitesAllowed =
|
|
1683
|
+
limitations.SLICE.innerCircle.maxSubContentsCount
|
|
1684
|
+
if (subContentsList.length > maxSattelitesAllowed) {
|
|
1685
|
+
throw Error('Max sattelites reached')
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
// check sub-contents' name keys match existing objects in the module
|
|
1689
|
+
for (const keyName of subContentsList) {
|
|
1690
|
+
checkGraphArcContentIsValid(keyName, object)
|
|
1691
|
+
}
|
|
1692
|
+
},
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
const graphArcBasicChecks = value => {
|
|
1696
|
+
const { name: _name, contents, type: _type, id: _id, ...rest } = value
|
|
1697
|
+
|
|
1698
|
+
if (Object.keys(rest).length) {
|
|
1699
|
+
throw Error(`Unsupported attribute "${Object.keys(rest)[0]}"`)
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
if (!Array.isArray(contents) || !contents?.length) {
|
|
1703
|
+
throw Error('Must be a non empty array')
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
types.graphArc = TypeObject({
|
|
1708
|
+
label: 'Name & contents of an arc',
|
|
1709
|
+
type: {
|
|
1710
|
+
id: Literal('', {
|
|
1711
|
+
label: 'A randomly-generated id to identify the arc',
|
|
1712
|
+
}),
|
|
1713
|
+
name: types.graphArcName,
|
|
1714
|
+
contents: {
|
|
1715
|
+
label: 'The list of contents to be placed on an arc',
|
|
1716
|
+
type: [types.graphArcContentName],
|
|
1717
|
+
},
|
|
1718
|
+
},
|
|
1719
|
+
check: graphArcBasicChecks,
|
|
1720
|
+
})
|
|
1721
|
+
|
|
1722
|
+
types.innerCircleSlice = TypeObject({
|
|
1723
|
+
label: 'Slice on the inner circle',
|
|
1724
|
+
type: {
|
|
1725
|
+
id: Literal('', {
|
|
1726
|
+
label: 'A randomly-generated id to identify the slice',
|
|
1727
|
+
}),
|
|
1728
|
+
name: types.graphArcName,
|
|
1729
|
+
|
|
1730
|
+
type: Literal('slice', {
|
|
1731
|
+
label: 'The slice type',
|
|
1732
|
+
}),
|
|
1733
|
+
|
|
1734
|
+
entryPoint: types.graphArcContentName,
|
|
1735
|
+
|
|
1736
|
+
innerArc: TypeObject({
|
|
1737
|
+
label: 'Inner arc of an inner circle slice',
|
|
1738
|
+
type: {
|
|
1739
|
+
...types.graphArc.type,
|
|
1740
|
+
contents: {
|
|
1741
|
+
label:
|
|
1742
|
+
'The list of contents to be placed on an inner arc of an inner circle slice',
|
|
1743
|
+
type: [
|
|
1744
|
+
types.graphArcContentName,
|
|
1745
|
+
types.graphArcContentWithSubContents, // for now, sub-contents are only displayed on the inner arc of an inner circle slice
|
|
1746
|
+
],
|
|
1747
|
+
},
|
|
1748
|
+
},
|
|
1749
|
+
check: graphArcBasicChecks,
|
|
1750
|
+
}),
|
|
1751
|
+
|
|
1752
|
+
outerArcs: {
|
|
1753
|
+
label: 'Outer arcs of an inner circle slice',
|
|
1754
|
+
type: [types.graphArc],
|
|
1755
|
+
check: outerArcs => {
|
|
1756
|
+
const { maxArcsCount, maxContentsCount } = limitations.SLICE.outerArc
|
|
1757
|
+
if (outerArcs.length > maxArcsCount) {
|
|
1758
|
+
throw Error('Max outerArcs reached')
|
|
1759
|
+
}
|
|
1760
|
+
if (
|
|
1761
|
+
outerArcs.reduce(
|
|
1762
|
+
(total, arc) => total + (arc.contents?.length || 0),
|
|
1763
|
+
0,
|
|
1764
|
+
) > maxContentsCount
|
|
1765
|
+
) {
|
|
1766
|
+
throw Error('Max contents spread over outerArcs of slice reached')
|
|
1767
|
+
}
|
|
1768
|
+
},
|
|
1647
1769
|
},
|
|
1648
1770
|
},
|
|
1771
|
+
|
|
1772
|
+
check: innerCircle => {
|
|
1773
|
+
const { name, entryPoint, type, innerArc, outerArcs, id, ...rest } =
|
|
1774
|
+
innerCircle
|
|
1775
|
+
|
|
1776
|
+
if (Object.keys(rest).length) {
|
|
1777
|
+
throw Error(`Unsupported attribute "${Object.keys(rest)[0]}"`)
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
if (!name && !entryPoint && !type && !innerArc && !outerArcs && !id) {
|
|
1781
|
+
throw Error('Empty inner circle, should be removed')
|
|
1782
|
+
}
|
|
1783
|
+
},
|
|
1784
|
+
})
|
|
1785
|
+
|
|
1786
|
+
types.innerCircleLine = {
|
|
1787
|
+
...types.graphArc,
|
|
1788
|
+
label: 'Line on the inner circle',
|
|
1789
|
+
type: {
|
|
1790
|
+
...types.graphArc.type,
|
|
1791
|
+
type: Literal('line', {
|
|
1792
|
+
label: 'The line type',
|
|
1793
|
+
}),
|
|
1794
|
+
},
|
|
1795
|
+
check: line => {
|
|
1796
|
+
if (line.contents.length > limitations.LINE.maxContentsCount) {
|
|
1797
|
+
throw Error('Max contents reached for a line')
|
|
1798
|
+
}
|
|
1799
|
+
},
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1802
|
+
const graphStructure = TypeObject({
|
|
1803
|
+
private: true, // not displayed in the admin settings; could use hidden too but private more efficient
|
|
1804
|
+
required: true,
|
|
1805
|
+
editable: true, // TODO: check if should be here?
|
|
1806
|
+
value: { innerCircle: [], middleCircle: [], outerCircle: [] },
|
|
1807
|
+
label: 'Graph structure',
|
|
1808
|
+
instruction: 'Structure of the visual graph',
|
|
1809
|
+
description:
|
|
1810
|
+
'Sets the visual structure & hierarchy of the graph of a module.',
|
|
1811
|
+
type: {
|
|
1812
|
+
centralPoint: types.graphArcContentName,
|
|
1813
|
+
|
|
1814
|
+
innerCircle: {
|
|
1815
|
+
value: [],
|
|
1816
|
+
required: true,
|
|
1817
|
+
editable: true,
|
|
1818
|
+
label: 'List of slices and/or lines spread on the inner circle',
|
|
1819
|
+
type: [types.innerCircleSlice, types.innerCircleLine],
|
|
1820
|
+
check: innerCircle => {
|
|
1821
|
+
const slices = innerCircle.filter(section => section.type === 'slice')
|
|
1822
|
+
const lines = innerCircle.filter(section => section.type === 'line')
|
|
1823
|
+
const { SLICE, LINE } = limitations
|
|
1824
|
+
if (slices.length > SLICE.maxSlicesCount) {
|
|
1825
|
+
throw Error('Max slices sections reached')
|
|
1826
|
+
}
|
|
1827
|
+
if (lines.length > LINE.maxLinesCount) {
|
|
1828
|
+
throw Error('Max lines sections reached')
|
|
1829
|
+
}
|
|
1830
|
+
if (
|
|
1831
|
+
slices.flatMap(({ innerArc }) =>
|
|
1832
|
+
innerArc.contents.flatMap(c =>
|
|
1833
|
+
typeof c === 'string' ? c : Object.keys(c)[0],
|
|
1834
|
+
),
|
|
1835
|
+
).length > SLICE.innerCircle.maxContentsCount
|
|
1836
|
+
) {
|
|
1837
|
+
throw Error('Max contents spread over innerArcs of slices reached')
|
|
1838
|
+
}
|
|
1839
|
+
},
|
|
1840
|
+
},
|
|
1841
|
+
|
|
1842
|
+
middleCircle: {
|
|
1843
|
+
value: [],
|
|
1844
|
+
required: true,
|
|
1845
|
+
editable: true,
|
|
1846
|
+
label: 'List of arcs spread on the middle circle',
|
|
1847
|
+
type: [types.graphArc],
|
|
1848
|
+
check: middleCircle => {
|
|
1849
|
+
const { maxArcsCount, maxContentsCount } = limitations.MIDDLE_CIRCLE
|
|
1850
|
+
if (middleCircle.length > maxArcsCount) {
|
|
1851
|
+
throw Error('Max arches reach on middleCircle')
|
|
1852
|
+
}
|
|
1853
|
+
if (
|
|
1854
|
+
middleCircle.reduce(
|
|
1855
|
+
(total, arc) => total + (arc.contents?.length || 0),
|
|
1856
|
+
0,
|
|
1857
|
+
) > maxContentsCount
|
|
1858
|
+
) {
|
|
1859
|
+
throw Error('Max contents reach on middleCircle')
|
|
1860
|
+
}
|
|
1861
|
+
},
|
|
1862
|
+
},
|
|
1863
|
+
|
|
1864
|
+
outerCircle: {
|
|
1865
|
+
value: [],
|
|
1866
|
+
required: true,
|
|
1867
|
+
editable: true,
|
|
1868
|
+
label: 'List of arcs spread on the outer circle',
|
|
1869
|
+
type: [types.graphArc],
|
|
1870
|
+
check: outerCircle => {
|
|
1871
|
+
const { maxArcsCount, maxContentsCount } = limitations.OUTER_CIRCLE
|
|
1872
|
+
if (outerCircle.length > maxArcsCount) {
|
|
1873
|
+
throw Error('Max arches reach on outerCircle')
|
|
1874
|
+
}
|
|
1875
|
+
if (
|
|
1876
|
+
outerCircle.reduce(
|
|
1877
|
+
(total, arc) => total + (arc.contents?.length || 0),
|
|
1878
|
+
0,
|
|
1879
|
+
) > maxContentsCount
|
|
1880
|
+
) {
|
|
1881
|
+
throw Error('Max contents reach on outerCircle')
|
|
1882
|
+
}
|
|
1883
|
+
},
|
|
1884
|
+
},
|
|
1885
|
+
},
|
|
1886
|
+
check: (graph, object) => {
|
|
1887
|
+
const { centralPoint, innerCircle, middleCircle, outerCircle, ...rest } =
|
|
1888
|
+
graph
|
|
1889
|
+
|
|
1890
|
+
if (Object.keys(rest).length) {
|
|
1891
|
+
throw Error(`Unsupported attribute "${Object.keys(rest)[0]}"`)
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1894
|
+
if (
|
|
1895
|
+
!centralPoint &&
|
|
1896
|
+
!innerCircle?.length &&
|
|
1897
|
+
!middleCircle?.length &&
|
|
1898
|
+
!outerCircle?.length
|
|
1899
|
+
) {
|
|
1900
|
+
throw Error('Empty graph, should be removed')
|
|
1901
|
+
}
|
|
1902
|
+
const flattenContents = flatGraphContents(graph)
|
|
1903
|
+
const flattenContentsSet = new Set(flattenContents)
|
|
1904
|
+
if (flattenContents.length !== flattenContentsSet.size) {
|
|
1905
|
+
throw Error('Graph should not contain duplicate contents keys')
|
|
1906
|
+
}
|
|
1907
|
+
const childrenKeys = Object.keys(object.children)
|
|
1908
|
+
if (flattenContentsSet.size !== childrenKeys.length) {
|
|
1909
|
+
// console.log(flattenContents.filter(c => !childrenKeys.some(k => k === c)))
|
|
1910
|
+
// console.log(childrenKeys.filter(c => !flattenContents.some(k => k === c)))
|
|
1911
|
+
// TODO: update for throw Error when fixed with content
|
|
1912
|
+
console.error(
|
|
1913
|
+
`Inconsistancy in between graph and children: different size (${flattenContentsSet.size} vs ${childrenKeys.length})`,
|
|
1914
|
+
)
|
|
1915
|
+
}
|
|
1916
|
+
},
|
|
1917
|
+
})
|
|
1918
|
+
|
|
1919
|
+
attrs.graph = {
|
|
1920
|
+
module: graphStructure,
|
|
1649
1921
|
}
|
|
1650
1922
|
|
|
1651
1923
|
// about to be refactored to define group base on exercise level?
|
|
@@ -1654,6 +1926,8 @@ relationAttrs.group = {
|
|
|
1654
1926
|
exercise: Literal(1, {
|
|
1655
1927
|
label: 'Exercise group',
|
|
1656
1928
|
editable: true,
|
|
1929
|
+
required: true,
|
|
1930
|
+
hidden: true,
|
|
1657
1931
|
options: arrayOf(100, 1),
|
|
1658
1932
|
check: value => checkIntegerInBetween(value, 1, 100),
|
|
1659
1933
|
}),
|
|
@@ -1728,6 +2002,7 @@ const sharedPrivateHasStarted = {
|
|
|
1728
2002
|
const sharedPublicHasStared = {
|
|
1729
2003
|
label: 'Starts when',
|
|
1730
2004
|
type: 'boolean',
|
|
2005
|
+
hidden: true,
|
|
1731
2006
|
required: true,
|
|
1732
2007
|
...Functions({
|
|
1733
2008
|
'temporal-window has started (in hackathon mode)': getHasStarted,
|
|
@@ -2073,11 +2348,9 @@ types.objectRootRelativePath = Literal('../', {
|
|
|
2073
2348
|
},
|
|
2074
2349
|
options: object => {
|
|
2075
2350
|
if (!object?.parent?.children) return []
|
|
2076
|
-
const
|
|
2077
|
-
const
|
|
2078
|
-
|
|
2079
|
-
const before = sorted.slice(0, index)
|
|
2080
|
-
return before.map(([key, _]) => `../${key}`).reverse()
|
|
2351
|
+
const entries = Object.entries(object.parent.children)
|
|
2352
|
+
const options = entries.filter(([k]) => k !== object.key)
|
|
2353
|
+
return options.map(([key]) => `../${key}`)
|
|
2081
2354
|
},
|
|
2082
2355
|
})
|
|
2083
2356
|
|
|
@@ -2092,7 +2365,10 @@ types.sharedObjectList = {
|
|
|
2092
2365
|
},
|
|
2093
2366
|
}
|
|
2094
2367
|
|
|
2095
|
-
const sharedRequirements = {
|
|
2368
|
+
const sharedRequirements = {
|
|
2369
|
+
editable: true,
|
|
2370
|
+
label: 'Access conditions',
|
|
2371
|
+
}
|
|
2096
2372
|
|
|
2097
2373
|
const contentRequirements = {
|
|
2098
2374
|
...sharedRequirements,
|
|
@@ -2100,16 +2376,15 @@ const contentRequirements = {
|
|
|
2100
2376
|
description:
|
|
2101
2377
|
'Sets the requirements that have to be met for a content to be accessible to a student.',
|
|
2102
2378
|
check: (requirements, object) => {
|
|
2103
|
-
const { skills, objects,
|
|
2379
|
+
const { skills, objects, ...rest } = requirements
|
|
2104
2380
|
if (Object.keys(rest).length) {
|
|
2105
2381
|
throw Error(`Unsupported attribute "${Object.keys(rest)[0]}"`)
|
|
2106
2382
|
}
|
|
2107
|
-
if (!skills && !objects
|
|
2383
|
+
if (!skills && !objects) {
|
|
2108
2384
|
throw Error('Empty requirements, should be removed')
|
|
2109
2385
|
}
|
|
2110
2386
|
|
|
2111
|
-
const
|
|
2112
|
-
const invalidObjectsRequirements = allObjects.filter(path => {
|
|
2387
|
+
const invalidObjectsRequirements = (objects || []).flat().filter(path => {
|
|
2113
2388
|
const objectFromPath = getObjectFromRelativePath(path, object, {
|
|
2114
2389
|
throwError: false,
|
|
2115
2390
|
})
|
|
@@ -2508,11 +2783,6 @@ const sharedContentRequirementsForMainAttr = TypeObject({
|
|
|
2508
2783
|
...contentRequirements,
|
|
2509
2784
|
type: {
|
|
2510
2785
|
skills: { ...skillsList, instruction: 'Expertises required' },
|
|
2511
|
-
core: {
|
|
2512
|
-
...types.objectRootRelativePath,
|
|
2513
|
-
label: 'Core content',
|
|
2514
|
-
instruction: 'Relative path of the core content to be succeeded',
|
|
2515
|
-
},
|
|
2516
2786
|
objects: {
|
|
2517
2787
|
...types.sharedObjectList,
|
|
2518
2788
|
value: (...args) => {
|
|
@@ -2972,16 +3242,11 @@ const isPathStatusSucceeded = (path, object) => {
|
|
|
2972
3242
|
*/
|
|
2973
3243
|
const hasSucceededRequiredObjects = (requirements, object) => {
|
|
2974
3244
|
if (!requirements) return true
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
if ((!objects || !objects.length) && !core) return true
|
|
3245
|
+
const { objects } = requirements
|
|
3246
|
+
if (!objects || !objects.length) return true
|
|
2978
3247
|
|
|
2979
|
-
// aggregate core object and regular required objects in the list of objects to check
|
|
2980
|
-
// only keep values that are not undefined
|
|
2981
3248
|
// required objects are the ones that need to be done to unblock the current object
|
|
2982
|
-
const requiredObjects =
|
|
2983
|
-
p => !Array.isArray(p) && Boolean(p),
|
|
2984
|
-
)
|
|
3249
|
+
const requiredObjects = objects.filter(p => !Array.isArray(p) && Boolean(p))
|
|
2985
3250
|
// pathway objects represent the choices a student can take to unblock the current object
|
|
2986
3251
|
// note: at least one object from each pathway must be successfully completed
|
|
2987
3252
|
const pathWayObjects = objects?.filter(p => Array.isArray(p) && Boolean(p))
|
|
@@ -3002,7 +3267,7 @@ const hasSucceededRequiredObjects = (requirements, object) => {
|
|
|
3002
3267
|
}
|
|
3003
3268
|
|
|
3004
3269
|
// check if the requirements are met for a given object
|
|
3005
|
-
const meetsRequirements = ({ requirements, object, user, progress }) => {
|
|
3270
|
+
export const meetsRequirements = ({ requirements, object, user, progress }) => {
|
|
3006
3271
|
// check if there's already a progress, to not block students who began the project before implementing the requirements feature
|
|
3007
3272
|
if ((progress && Object.keys(progress).length) || !requirements) return true
|
|
3008
3273
|
// check if the required skills have been earned & the required objects have been succeeded
|
|
@@ -3231,6 +3496,7 @@ types.meetRequirements = Literal(false, {
|
|
|
3231
3496
|
label: 'User meet requirement',
|
|
3232
3497
|
restrictive: true,
|
|
3233
3498
|
required: true,
|
|
3499
|
+
hidden: true,
|
|
3234
3500
|
...Functions({ 'by requirements': getMeetsRequirements }),
|
|
3235
3501
|
})
|
|
3236
3502
|
relationAttrs.meetsRequirements = {
|
package/attrs.js
CHANGED
|
@@ -14,7 +14,7 @@ const typeCheckers = {
|
|
|
14
14
|
|
|
15
15
|
const determinType = value => {
|
|
16
16
|
if (Array.isArray(value)) return 'array'
|
|
17
|
-
if (typeof value === 'object' && value !== null) return value.type
|
|
17
|
+
if (typeof value === 'object' && value !== null) return value.type || 'object'
|
|
18
18
|
return typeof value
|
|
19
19
|
}
|
|
20
20
|
const typeChecker = (defs, value, object, key) => {
|
|
@@ -309,16 +309,24 @@ const expandAttr = (key, value, defs, object, getUser) => {
|
|
|
309
309
|
let def
|
|
310
310
|
if (types) {
|
|
311
311
|
const isObject = typeof value[key][i] === 'object'
|
|
312
|
+
|
|
313
|
+
// when type is set to "object" for items that cannot be defined in attrs because
|
|
314
|
+
// the key cannot be known (like types.graphArcContentWithSubContents)
|
|
315
|
+
const typeCouldNotBeDefined = isObject && Boolean(types.object)
|
|
316
|
+
|
|
312
317
|
const invalidType =
|
|
313
318
|
!value[key][i].type || typeof value[key][i].type !== 'string'
|
|
314
319
|
const allowedTypes = Object.keys(types).join(',').slice(0, -1)
|
|
315
|
-
|
|
320
|
+
|
|
321
|
+
if (isObject && invalidType && !typeCouldNotBeDefined) {
|
|
316
322
|
console.warn(
|
|
317
323
|
`Type not allowed. Object item for ${key} array (#${i} item) must have one of these "type" property: ${allowedTypes}.`,
|
|
318
324
|
)
|
|
319
325
|
}
|
|
326
|
+
|
|
320
327
|
def = isObject ? types[value[key][i].type] : types[typeof value[key][i]]
|
|
321
|
-
|
|
328
|
+
|
|
329
|
+
if (!def && !typeCouldNotBeDefined) {
|
|
322
330
|
console.warn(
|
|
323
331
|
`Missing type definition. Object item for ${key} array (#${i} item) must have one of these "type" property: ${allowedTypes}.`,
|
|
324
332
|
)
|
package/graph.js
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
const isCoreObj = (contentShape, coreKey) =>
|
|
2
|
+
typeof contentShape === 'object' && Object.keys(contentShape)[0] === coreKey
|
|
3
|
+
const isSatteliteObj = (contentShape, satteliteKey) =>
|
|
4
|
+
typeof contentShape === 'object' &&
|
|
5
|
+
Object.values(contentShape)[0].some(sat => sat === satteliteKey)
|
|
6
|
+
|
|
7
|
+
export const getCoreSattelites = (coreObject, objectsList) => {
|
|
8
|
+
const { key, parent } = coreObject
|
|
9
|
+
const objects = objectsList || Object.values(parent.children)
|
|
10
|
+
const sattelitesSection = parent.attrs.graph.innerCircle.find(
|
|
11
|
+
s => s.type === 'slice' && s.innerArc.contents.find(c => isCoreObj(c, key)),
|
|
12
|
+
)
|
|
13
|
+
if (!sattelitesSection) return []
|
|
14
|
+
const sattelitesObject = sattelitesSection.innerArc.contents.find(c =>
|
|
15
|
+
isCoreObj(c, key),
|
|
16
|
+
)
|
|
17
|
+
const sattelitesList = new Set(...Object.values(sattelitesObject))
|
|
18
|
+
|
|
19
|
+
return objects.filter(o => sattelitesList.has(o.key))
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const getCoreOfSattelite = (satteliteObject, objectsList) => {
|
|
23
|
+
const { key, parent } = satteliteObject
|
|
24
|
+
const objects = objectsList || Object.values(parent.children)
|
|
25
|
+
const sattelitesSection = parent.attrs.graph?.innerCircle?.find(
|
|
26
|
+
s =>
|
|
27
|
+
s.type === 'slice' &&
|
|
28
|
+
s.innerArc?.contents.find(c => isSatteliteObj(c, key)),
|
|
29
|
+
)
|
|
30
|
+
if (!sattelitesSection) return undefined
|
|
31
|
+
const coreObject = sattelitesSection.innerArc.contents.find(c =>
|
|
32
|
+
isSatteliteObj(c, key),
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
return objects.filter(o => o.key === Object.keys(coreObject)[0])
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const flatGraphContents = graph => [
|
|
39
|
+
...(graph.centralPoint ? [graph.centralPoint] : []),
|
|
40
|
+
...(graph.innerCircle || []).flatMap(section =>
|
|
41
|
+
section.type === 'slice'
|
|
42
|
+
? [
|
|
43
|
+
...(section.entryPoint ? [section.entryPoint] : []),
|
|
44
|
+
...(section.innerArc?.contents || []).flatMap(c =>
|
|
45
|
+
typeof c === 'string' ? c : Object.entries(c)[0].flat(),
|
|
46
|
+
),
|
|
47
|
+
...(section.outerArcs || []).flatMap(arc => arc.contents || []),
|
|
48
|
+
]
|
|
49
|
+
: section.contents,
|
|
50
|
+
),
|
|
51
|
+
...(graph.middleCircle || []).flatMap(arc => arc.contents || []),
|
|
52
|
+
...(graph.outerCircle || []).flatMap(arc => arc.contents || []),
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
// WARNING: without central point (for linear graph, already destructured)
|
|
56
|
+
export const flatAtomeAndCoreContents = graph => [
|
|
57
|
+
...(graph.innerCircle || []).flatMap(section =>
|
|
58
|
+
section.type === 'slice'
|
|
59
|
+
? [
|
|
60
|
+
...(section.entryPoint ? [section.entryPoint] : []),
|
|
61
|
+
...(section.innerArc?.contents || []).flatMap(c =>
|
|
62
|
+
typeof c === 'string' ? c : Object.keys(c)[0],
|
|
63
|
+
),
|
|
64
|
+
...(section.outerArcs || []).flatMap(arc => arc.contents || []),
|
|
65
|
+
]
|
|
66
|
+
: section.contents,
|
|
67
|
+
),
|
|
68
|
+
...(graph.middleCircle || []).flatMap(arc => arc.contents || []),
|
|
69
|
+
...(graph.outerCircle || []).flatMap(arc => arc.contents || []),
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
export const limitations = {
|
|
73
|
+
LINE: {
|
|
74
|
+
maxLinesCount: 6,
|
|
75
|
+
maxContentsCount: 7, // 7 contents max split along a single line
|
|
76
|
+
},
|
|
77
|
+
SLICE: {
|
|
78
|
+
maxSlicesCount: 4,
|
|
79
|
+
innerCircle: {
|
|
80
|
+
maxSubContentsCount: 5, // 5 sub-contents max by content
|
|
81
|
+
maxContentsCount: 30, // 30 contents max spread evenly over the number of inner cirlces of slices
|
|
82
|
+
},
|
|
83
|
+
outerArc: {
|
|
84
|
+
maxArcsCount: 2,
|
|
85
|
+
maxContentsCount: 10, // 10 contents max spread evenly over the 1 or 2 outer arcs
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
MIDDLE_CIRCLE: {
|
|
89
|
+
maxArcsCount: 8,
|
|
90
|
+
maxContentsCount: 70, // 70 contents max spread evenly over the number of arcs
|
|
91
|
+
},
|
|
92
|
+
OUTER_CIRCLE: {
|
|
93
|
+
maxArcsCount: 9,
|
|
94
|
+
maxContentsCount: 90, // 90 contents max spread evenly over the number of arcs
|
|
95
|
+
},
|
|
96
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@01-edu/shared",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "",
|
|
6
6
|
"scripts": {
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
"./onboarding.js",
|
|
14
14
|
"./attrs.js",
|
|
15
15
|
"./attrs-defs.js",
|
|
16
|
+
"./graph.js",
|
|
16
17
|
"./languages.js",
|
|
17
18
|
"./definitions-checker.js",
|
|
18
19
|
"./path.js",
|