@01-edu/shared 1.0.12 → 1.1.0

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.
Files changed (3) hide show
  1. package/attrs-defs.js +328 -62
  2. package/attrs.js +11 -3
  3. package/package.json +1 -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 { attrs, name } = object
76
- if (!attrs.requirements?.core) return name
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 = `${coreName}/${getProjectName(object)}`
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 { name, attrs } = object
931
- if (!attrs.requirements?.core) return name
932
- // only for projects with a core requirement set, and which name starts by the core name:
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
- const coreName = getCoreName(object)
935
- // TODO: couldn't we just display the key?
936
- return coreName && name.startsWith(coreName)
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
- types.moduleGraphViews = {
1632
- type: 'string',
1633
- options: ['Circular', 'Linear'],
1634
- editable: true,
1635
- value: '',
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
- attrs.graphView = {
1638
- module: {
1639
- value: [],
1640
- editable: true,
1641
- label: 'Graph views',
1642
- type: [types.moduleGraphViews],
1643
- check: value => {
1644
- if (value.length === 0) throw Error('Must contain at least one view')
1645
- if (new Set(value).size !== value.length)
1646
- throw Error('Should not contain duplicates')
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 sorted = Object.entries(object.parent.children).sort(byValueIdx)
2077
- const index = sorted.findIndex(([k]) => k === object.key)
2078
- if (index < 1) return sorted.map(([key, _]) => `../${key}`)
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 = { editable: true, label: 'Access conditions' }
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, core, ...rest } = requirements
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 && !core) {
2383
+ if (!skills && !objects) {
2108
2384
  throw Error('Empty requirements, should be removed')
2109
2385
  }
2110
2386
 
2111
- const allObjects = [...(objects || []), core || []].flat()
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
- // TODO: in the near future remove the core attribute
2976
- const { core, objects } = requirements
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 = [...(objects || []), core].filter(
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
- if (isObject && invalidType) {
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
- if (!def) {
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@01-edu/shared",
3
- "version": "1.0.12",
3
+ "version": "1.1.0",
4
4
  "type": "module",
5
5
  "description": "",
6
6
  "scripts": {