@01-edu/shared 1.0.9 → 1.0.12

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 CHANGED
@@ -125,7 +125,7 @@ const TypeObject = def => ({
125
125
  // label: // text displayed in front end, instead of attribute name (client friendly),
126
126
  // restrictive: // if true, the attribute will be defined only on the object itself and cannot be overridden in the relation to the parent object
127
127
  // instruction: // mention displayed under the label in the front end - to give precision that need to be seen,
128
- // description: complete explanation of what the attributes does - description is displayed only when the used hover the ℹ️ icon in the front end,
128
+ // description: complete explanation of what the attributes does - description is displayed only when the used hover the i icon in the front end,
129
129
  // required: // if the attribute is always there by default,
130
130
  // editable: // if it can be override by schools in the db (=/= choice in between several functions),
131
131
  // private: // if the attribute should not be seen in front end,
@@ -1782,33 +1782,63 @@ const sharedInput = {
1782
1782
  ...translatable,
1783
1783
  instruction: 'Required option: "type"',
1784
1784
  }
1785
- attrs.input = {
1786
- 'upload-step': {
1787
- ...sharedInput,
1788
- check: value => {
1789
- const [inputValues] = Object.values(value)
1790
- isntObjectOrIsEmpty(inputValues)
1791
- if (!inputValues.type || inputValues.type !== 'file') {
1792
- throw Error(
1793
- '"type":"file" property must be defined in the upload input.',
1794
- )
1795
- }
1796
- if (
1797
- inputValues.accept !== undefined &&
1798
- typeof inputValues.accept !== 'string'
1799
- ) {
1800
- throw Error(
1801
- '"accept" property (if added) must be a text. Example: "image/png, image/jpeg"',
1802
- )
1803
- }
1804
- if (
1805
- inputValues.required !== undefined &&
1806
- typeof inputValues.required !== 'boolean'
1807
- ) {
1808
- throw Error('"required" property (if added) must be a true or false.')
1809
- }
1810
- },
1785
+ const uploadInput = {
1786
+ ...sharedInput,
1787
+ editable: false,
1788
+ check: value => {
1789
+ const [inputValues] = Object.values(value)
1790
+ isntObjectOrIsEmpty(inputValues)
1791
+ if (!inputValues.type || inputValues.type !== 'file') {
1792
+ throw Error('"type":"file" property must be defined in the upload input.')
1793
+ }
1794
+ if (
1795
+ inputValues.accept !== undefined &&
1796
+ typeof inputValues.accept !== 'string'
1797
+ ) {
1798
+ throw Error(
1799
+ '"accept" property (if added) must be a text. Example: "image/png, image/jpeg"',
1800
+ )
1801
+ }
1802
+ if (
1803
+ inputValues.required !== undefined &&
1804
+ typeof inputValues.required !== 'boolean'
1805
+ ) {
1806
+ throw Error('"required" property (if added) must be a true or false.')
1807
+ }
1808
+ },
1809
+ }
1810
+
1811
+ const avatarInput = TypeObject({
1812
+ label: 'Avatar input',
1813
+ required: true,
1814
+ editable: false,
1815
+ value: {
1816
+ type: 'file',
1817
+ accept: 'image/png, image/jpeg',
1818
+ required: true,
1819
+ },
1820
+ type: {
1821
+ type: Literal('file', {
1822
+ label: 'Input type',
1823
+ required: true,
1824
+ editable: false,
1825
+ }),
1826
+ accept: Literal('image/png, image/jpeg', {
1827
+ label: 'Accepted file types',
1828
+ editable: false,
1829
+ required: true,
1830
+ }),
1831
+ required: Literal(true, {
1832
+ label: 'Required',
1833
+ editable: false,
1834
+ required: true,
1835
+ }),
1811
1836
  },
1837
+ })
1838
+
1839
+ attrs.input = {
1840
+ 'upload-step': uploadInput,
1841
+ 'avatar-step': avatarInput,
1812
1842
  'contact-validation-step': {
1813
1843
  ...sharedInput,
1814
1844
  check: value => {
@@ -1999,8 +2029,38 @@ types.objectChildRelativePath = Literal('./', {
1999
2029
  .map(([key, _]) => `./${key}`),
2000
2030
  })
2001
2031
 
2032
+ const checkRelativePaths = (objects, object) => {
2033
+ if (!Array.isArray(objects) || !objects.length) {
2034
+ const error = new Error('Must be a non empty array')
2035
+ error.userFeedback =
2036
+ 'This list cannot be empty! Please add an item or remove the setting.'
2037
+ throw error
2038
+ }
2039
+ const uniques = [...new Set(objects)]
2040
+ if (objects.length !== uniques.length) {
2041
+ throw Error('Duplicates are not allowed.')
2042
+ }
2043
+
2044
+ const invalidObjectsRequirements = objects.filter(path => {
2045
+ const objectFromPath = getObjectFromRelativePath(path, object, {
2046
+ throwError: false,
2047
+ })
2048
+ return !objectFromPath
2049
+ })
2050
+
2051
+ if (invalidObjectsRequirements.length) {
2052
+ const paths = invalidObjectsRequirements.map(p => `'${p}'`).join(', ')
2053
+ const error = new Error(
2054
+ `Invalid objects requirements - no object found for the following relative paths: ${paths}`,
2055
+ )
2056
+ error.userFeedback = 'Some Contents are misconfigured, please update them!'
2057
+ console.error(error.message)
2058
+ throw error
2059
+ }
2060
+ }
2061
+
2002
2062
  types.objectRootRelativePath = Literal('../', {
2003
- label: 'Content relative path',
2063
+ label: 'Content relative path (Mandatory)',
2004
2064
  instruction: 'In same parent',
2005
2065
  editable: true,
2006
2066
  check: (path, object) => {
@@ -2020,38 +2080,15 @@ types.objectRootRelativePath = Literal('../', {
2020
2080
  return before.map(([key, _]) => `../${key}`).reverse()
2021
2081
  },
2022
2082
  })
2083
+
2084
+ // NOTE: objects requirements is declared here
2023
2085
  types.sharedObjectList = {
2024
- label: 'Contents', // synonyms: item, material
2086
+ label: 'Contents required', // synonyms: item, material
2025
2087
  editable: true,
2026
2088
  check: (objects, object) => {
2027
- if (!Array.isArray(objects) || !objects.length) {
2028
- const error = new Error('Must be a non empty array')
2029
- error.userFeedback =
2030
- 'This list cannot be empty! Please add an item or remove the setting.'
2031
- throw error
2032
- }
2033
- const uniques = [...new Set(objects)]
2034
- if (objects.length !== uniques.length) {
2035
- throw Error('Duplicates are not allowed.')
2036
- }
2037
-
2038
- const invalidObjectsRequirements = objects.filter(path => {
2039
- const objectFromPath = getObjectFromRelativePath(path, object, {
2040
- throwError: false,
2041
- })
2042
- return !objectFromPath
2043
- })
2044
-
2045
- if (invalidObjectsRequirements.length) {
2046
- const paths = invalidObjectsRequirements.map(p => `'${p}'`).join(', ')
2047
- const error = new Error(
2048
- `Invalid objects requirements - no object found for the following relative paths: ${paths}`,
2049
- )
2050
- error.userFeedback =
2051
- 'Some Contents are misconfigured, please update them!'
2052
- console.error(error.message)
2053
- throw error
2054
- }
2089
+ // objects are split into alternative paths or mandatory paths
2090
+ // for the check we will just flat the array so that we check every path the same way
2091
+ checkRelativePaths(objects.flat(), object)
2055
2092
  },
2056
2093
  }
2057
2094
 
@@ -2071,7 +2108,7 @@ const contentRequirements = {
2071
2108
  throw Error('Empty requirements, should be removed')
2072
2109
  }
2073
2110
 
2074
- const allObjects = core ? [...(objects || []), core] : objects || []
2111
+ const allObjects = [...(objects || []), core || []].flat()
2075
2112
  const invalidObjectsRequirements = allObjects.filter(path => {
2076
2113
  const objectFromPath = getObjectFromRelativePath(path, object, {
2077
2114
  throwError: false,
@@ -2108,6 +2145,9 @@ const levelRequirements = {
2108
2145
  },
2109
2146
  }
2110
2147
 
2148
+ // NOTE: does level definition support the multiple pathways??
2149
+ // i think it should not for now, the multiple pathways was defined for the students to get to a content throughout different ways
2150
+ // not for the level be defined different ways
2111
2151
  types.levelDefinition = TypeObject({
2112
2152
  label: 'Level definition',
2113
2153
  type: {
@@ -2207,6 +2247,27 @@ attrs.link = {
2207
2247
  },
2208
2248
  ...translatable, // in case there are different versions of the doc to dl
2209
2249
  }),
2250
+ 'avatar-step': TypeObject({
2251
+ label: 'Link to the legal page',
2252
+ editable: false,
2253
+ required: true,
2254
+ value: {
2255
+ href: '/legal',
2256
+ label: '> Privacy policy',
2257
+ target: '_blank',
2258
+ },
2259
+ type: {
2260
+ href: Literal('/legal', {
2261
+ editable: false,
2262
+ }),
2263
+ label: Literal('> Privacy policy', {
2264
+ editable: false,
2265
+ }),
2266
+ target: Literal('_blank', {
2267
+ editable: false,
2268
+ }),
2269
+ },
2270
+ }),
2210
2271
  }
2211
2272
 
2212
2273
  relationAttrs.mandatory = {
@@ -2242,7 +2303,9 @@ const sharedName = Literal('', {
2242
2303
  })
2243
2304
  const attrsNameObj = {}
2244
2305
  for (const type of onboardingTypes) {
2245
- attrsNameObj[type] = sharedName
2306
+ if (type !== 'avatar-step') {
2307
+ attrsNameObj[type] = sharedName
2308
+ }
2246
2309
  }
2247
2310
  attrs.name = {
2248
2311
  signup: sharedName,
@@ -2417,6 +2480,30 @@ attrs.requiredAuditRatio = {
2417
2480
  }),
2418
2481
  }
2419
2482
 
2483
+ types.pathwaysRequirementObjects = {
2484
+ label: 'Multiple content choices',
2485
+ instruction:
2486
+ 'Adding this will create a new path way for the project being edit.',
2487
+ check: (objects, object) => {
2488
+ // objects are split into alternative paths or mandatory paths
2489
+ // for the check we will just flat the array so that we check every path the same way
2490
+ checkRelativePaths(objects.flat(), object)
2491
+ },
2492
+ required: false,
2493
+ editable: true,
2494
+ type: [
2495
+ {
2496
+ ...types.objectRootRelativePath,
2497
+ label: 'Content relative path (optional)',
2498
+ },
2499
+ ],
2500
+ value: (...args) => {
2501
+ const option = types.objectRootRelativePath.options(...args)?.[0]
2502
+ return option ? [option] : []
2503
+ },
2504
+ }
2505
+
2506
+ // NOTE: relation attribute requirements objects
2420
2507
  const sharedContentRequirementsForMainAttr = TypeObject({
2421
2508
  ...contentRequirements,
2422
2509
  type: {
@@ -2430,10 +2517,11 @@ const sharedContentRequirementsForMainAttr = TypeObject({
2430
2517
  ...types.sharedObjectList,
2431
2518
  value: (...args) => {
2432
2519
  const option = types.objectRootRelativePath.options(...args)?.[0]
2433
- return option ? [option] : []
2520
+ const pathways = types.pathwaysRequirementObjects.value(...args)
2521
+ return option ? [option, pathways] : []
2434
2522
  },
2435
- type: [types.objectRootRelativePath],
2436
- instruction: 'Items to be succeeded',
2523
+ type: [types.objectRootRelativePath, types.pathwaysRequirementObjects],
2524
+ instruction: 'Content required to unlock the current one',
2437
2525
  },
2438
2526
  },
2439
2527
  })
@@ -2863,6 +2951,20 @@ const questExerciseStatus = object => {
2863
2951
  return 'available'
2864
2952
  }
2865
2953
 
2954
+ const isPathStatusSucceeded = (path, object) => {
2955
+ try {
2956
+ const obj = getObjectFromRelativePath(path, object)
2957
+ return obj?.attrs.status === 'succeeded'
2958
+ } catch {
2959
+ // NOTE: should we make the requirement unblocked if the admin did not set the right requirement ????
2960
+
2961
+ // consider the requirement unlocked if an error is thrown by getObjectFromRelativePath,
2962
+ // because it would mean an admin wrongly set the requirement, probably manually in the db
2963
+ // (it is not possible to set an invalid requirement from the admin configuration interface)
2964
+ return true
2965
+ }
2966
+ }
2967
+
2866
2968
  /**
2867
2969
  * @throws This function throws an error if any of the required objects is invalid
2868
2970
  * @returns {bool} true if all the required objects are succeeded, false otherwise
@@ -2870,30 +2972,39 @@ const questExerciseStatus = object => {
2870
2972
  */
2871
2973
  const hasSucceededRequiredObjects = (requirements, object) => {
2872
2974
  if (!requirements) return true
2975
+ // TODO: in the near future remove the core attribute
2873
2976
  const { core, objects } = requirements
2874
2977
  if ((!objects || !objects.length) && !core) return true
2875
2978
 
2876
2979
  // aggregate core object and regular required objects in the list of objects to check
2877
- const requiredObjects = [...(objects || []), core].filter(Boolean) // only keep values that are not undefined
2878
- let objectFromPath
2879
- return requiredObjects.every(relativePath => {
2880
- try {
2881
- objectFromPath = getObjectFromRelativePath(relativePath, object)
2882
- } catch {
2883
- // consider the requirement unlocked if an error is thrown by getObjectFromRelativePath,
2884
- // because it would mean an admin wrongly set the requirement, probably manually in the db
2885
- // (it is not possible to set an invalid requirement from the admin configuration interface)
2886
- return true
2887
- }
2888
- return objectFromPath?.attrs.status === 'succeeded'
2889
- })
2980
+ // only keep values that are not undefined
2981
+ // 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
+ )
2985
+ // pathway objects represent the choices a student can take to unblock the current object
2986
+ // note: at least one object from each pathway must be successfully completed
2987
+ const pathWayObjects = objects?.filter(p => Array.isArray(p) && Boolean(p))
2988
+
2989
+ const hasSeccededRequiredObjects = requiredObjects.every(path =>
2990
+ isPathStatusSucceeded(path, object),
2991
+ )
2992
+
2993
+ // it's possible to have a pathway with just one relative path
2994
+ // in this case it will be considered as a required object
2995
+ const hasSucceededPathWays = pathWayObjects?.every(paths =>
2996
+ paths.some(path => isPathStatusSucceeded(path, object)),
2997
+ )
2998
+
2999
+ return hasSucceededPathWays !== undefined
3000
+ ? hasSeccededRequiredObjects && hasSucceededPathWays
3001
+ : hasSeccededRequiredObjects
2890
3002
  }
2891
3003
 
2892
3004
  // check if the requirements are met for a given object
2893
3005
  const meetsRequirements = ({ requirements, object, user, progress }) => {
2894
3006
  // check if there's already a progress, to not block students who began the project before implementing the requirements feature
2895
3007
  if ((progress && Object.keys(progress).length) || !requirements) return true
2896
-
2897
3008
  // check if the required skills have been earned & the required objects have been succeeded
2898
3009
  const hasSkills = hasRequiredSkills(requirements.skills, user.skills)
2899
3010
  const hasSucceededObjects = hasSucceededRequiredObjects(requirements, object)
@@ -2959,17 +3070,20 @@ const questStatus = object => {
2959
3070
  : getProgressStatus(progress) || 'available'
2960
3071
  }
2961
3072
 
3073
+ const getMeetsRequirements = (object, user) => {
3074
+ const { progress, attrs } = object
3075
+ const { requirements } = attrs
3076
+
3077
+ return meetsRequirements({ object, requirements, user, progress })
3078
+ }
2962
3079
  const getPiscineStatus = (object, user) => {
2963
3080
  const { progress, event, attrs } = object
2964
3081
  const { requirements } = attrs
2965
3082
  const progressStatus = getProgressStatus(progress)
2966
3083
  if (progressStatus) return progressStatus
2967
-
2968
- if (!event) return 'blocked'
2969
- const registrationNotStarted = Date.now() < event.registrationStartAt
2970
3084
  if (
2971
- !meetsRequirements({ object, requirements, user, progress }) ||
2972
- registrationNotStarted
3085
+ !meetsRequirements({ object, requirements, user, progress }) &&
3086
+ !event?.registeredPosition // in case a learner was force added to a registration through hasura by an admin
2973
3087
  ) {
2974
3088
  return 'blocked'
2975
3089
  }
@@ -3113,6 +3227,19 @@ relationAttrs.status = {
3113
3227
  },
3114
3228
  }
3115
3229
 
3230
+ types.meetRequirements = Literal(false, {
3231
+ label: 'User meet requirement',
3232
+ restrictive: true,
3233
+ required: true,
3234
+ ...Functions({ 'by requirements': getMeetsRequirements }),
3235
+ })
3236
+ relationAttrs.meetsRequirements = {
3237
+ module: {
3238
+ piscine: types.meetRequirements,
3239
+ project: types.meetRequirements,
3240
+ },
3241
+ }
3242
+
3116
3243
  const getSubject = object => {
3117
3244
  const path = `subjects/${getObjectPath(object)}/README.md`
3118
3245
  return `/markdown/root/public/${path}`
@@ -3130,10 +3257,7 @@ attrs.subject = {
3130
3257
  raid: sharedSubject,
3131
3258
  }
3132
3259
 
3133
- const sharedText = Literal('', {
3134
- editable: true,
3135
- ...translatable,
3136
- })
3260
+ const sharedText = Literal('', { editable: true, ...translatable })
3137
3261
 
3138
3262
  types.teamworkRankName = Literal('', {
3139
3263
  label: 'Rank name',
@@ -3142,9 +3266,8 @@ types.teamworkRankName = Literal('', {
3142
3266
  required: true,
3143
3267
  primary: true,
3144
3268
  check: (name, object) => {
3145
- const definitionsWithSameName = object.attrs.teamworkRanks?.filter(
3146
- rankDefinition => rankDefinition.name === name,
3147
- )
3269
+ const { teamworkRanks } = object.attrs
3270
+ const definitionsWithSameName = teamworkRanks?.filter(r => r.name === name)
3148
3271
  if (definitionsWithSameName?.length > 1) {
3149
3272
  throw Error(
3150
3273
  `Name "${name}" is already set for a teamwork rank definition! A given name can only be attributed once to a rank.`,
@@ -3159,12 +3282,11 @@ types.teamworkRankParticipations = Literal(0, {
3159
3282
  'The number of users the student has to work with, to unlock this rank.',
3160
3283
  editable: true,
3161
3284
  required: true,
3162
- options: arrayOf(150, 1),
3285
+ options: arrayOf(150, 0),
3163
3286
  type: 'number',
3164
3287
  check: (groups, object) => {
3165
3288
  if (!Number.isInteger(groups)) throw Error('Must be a whole number')
3166
-
3167
- const definitionsWithSameLevel = object.attrs.teamworkRanks.filter(
3289
+ const definitionsWithSameLevel = object.attrs.teamworkRanks?.filter(
3168
3290
  rankDefinition => rankDefinition.groups === groups,
3169
3291
  )
3170
3292
  if (definitionsWithSameLevel?.length > 1) {
@@ -3187,6 +3309,10 @@ attrs.teamworkRanks = {
3187
3309
  label: 'Teamwork ranks',
3188
3310
  instruction: 'List of teamwork ranks',
3189
3311
  type: [types.teamworkRanks],
3312
+ // this setting is required for practical UX reasons. It is not
3313
+ // required for platform to work properly, but as there are no other
3314
+ // settings for the campus, it avoid hiding it in "more settings to add"
3315
+ // section and make it more visible/easy to configure for admins (as displayed by default)
3190
3316
  required: true,
3191
3317
  editable: true,
3192
3318
  value: (...args) => [
@@ -3217,6 +3343,10 @@ attrs.text = {
3217
3343
  ...sharedText,
3218
3344
  label: 'Resume', // TODO: mv it to attrs.resume
3219
3345
  },
3346
+ 'avatar-step': {
3347
+ ...sharedText,
3348
+ label: 'Resume', // TODO: mv it to attrs.resume
3349
+ },
3220
3350
  }
3221
3351
 
3222
3352
  types.timelineChunk = TypeObject({
@@ -3428,14 +3558,19 @@ const noAttribution = () => isUser01
3428
3558
 
3429
3559
  export const getAuditPath = object => {
3430
3560
  const path = `subjects/${getObjectPath(object)}/audit/README.md`
3431
- return `/markdown/root/public/${path}`
3561
+ // TODO: the new content system will no longer use public, in the future we should change this
3562
+ // now we have dedicated repos on gitea for: modules, piscine, ....
3563
+ // so the repo should be something more dynamic depending on the object.type?
3564
+ // for now lets use the raw markdown
3565
+ return `/markdown/raw/root/public/${path}`
3432
3566
  }
3433
3567
  const sharedTypesForm = {
3434
3568
  required: true,
3435
3569
  editable: true,
3436
3570
  type: 'string',
3437
- label: 'Audit form url',
3438
- instruction: 'List of questions asked by the auditor during the audit.',
3571
+ label: 'Audit form URL',
3572
+ instruction:
3573
+ 'List of questions asked by the auditor during the audit. The URL should return raw markdown',
3439
3574
  ...Functions({ 'README in audit folder': getAuditPath }),
3440
3575
  }
3441
3576
  types.adminAuditValidationDelay = Literal(0, {
@@ -3669,6 +3804,18 @@ attrs.videos = {
3669
3804
  }),
3670
3805
  }
3671
3806
 
3807
+ attrs.legalText = {
3808
+ 'avatar-step': Literal(
3809
+ "Please make sure to upload a photograph that complies with the training center's internal regulations and standards of decency. This photo will be visible to the teaching staff to support essential individual academic monitoring, as well as to other learners to facilitate peer-to-peer collaboration. Any request for deletion or modification must be submitted to the management.",
3810
+ {
3811
+ type: 'string',
3812
+ label: 'Legal text',
3813
+ editable: false,
3814
+ required: true,
3815
+ },
3816
+ ),
3817
+ }
3818
+
3672
3819
  const getWeek = ({ attrs }) => {
3673
3820
  if (!attrs.startDay) return undefined // for exams in module for example
3674
3821
  const diff = attrs.startDay / 7
package/attrs.js CHANGED
@@ -12,6 +12,11 @@ const typeCheckers = {
12
12
  array: Array.isArray,
13
13
  }
14
14
 
15
+ const determinType = value => {
16
+ if (Array.isArray(value)) return 'array'
17
+ if (typeof value === 'object' && value !== null) return value.type
18
+ return typeof value
19
+ }
15
20
  const typeChecker = (defs, value, object, key) => {
16
21
  const { type, check, options } = defs
17
22
 
@@ -61,16 +66,23 @@ const typeChecker = (defs, value, object, key) => {
61
66
  // every value have to match one of the type definition
62
67
 
63
68
  const uniqueDef = type.length === 1 && type[0]
69
+ // convert array type into object for better accessibility
64
70
  const types =
65
71
  !uniqueDef &&
66
- Object.fromEntries(type.map(t => [t.type?.type?.value || t.type, t]))
72
+ Object.fromEntries(
73
+ type.map(t => [
74
+ Array.isArray(t.type) ? 'array' : t.type?.type?.value || t.type,
75
+ t,
76
+ ]),
77
+ )
78
+
67
79
  for (const [index, v] of value.entries()) {
68
80
  const err = Error('checks failed for all types')
69
81
  err.index = index
70
82
  err.key = key
71
83
  err.label = defs.label
72
- const subdefs =
73
- uniqueDef || (typeof v === 'object' ? types[v.type] : types[typeof v])
84
+ const subdefs = uniqueDef || types[determinType(v)]
85
+
74
86
  if (!subdefs) {
75
87
  err.details = {
76
88
  label: 'Unknown structure',
@@ -85,7 +97,6 @@ const typeChecker = (defs, value, object, key) => {
85
97
  label: subdefs.label || error.label,
86
98
  err: error,
87
99
  }
88
-
89
100
  throw err
90
101
  }
91
102
  }
@@ -136,6 +147,68 @@ export const relationAttributes = mapEntries(
136
147
  ],
137
148
  )
138
149
 
150
+ // white list of attributes that can be applied in bulk
151
+ const allowedBulkAttrs = {
152
+ codeEditor: { enabled: {} },
153
+ validations: {
154
+ // this is for the different options of validation,
155
+ // we know that raids for now is the only one that contains multiple validations
156
+ // and we **don't** want admin_selection
157
+ type: [
158
+ 'admin_audit',
159
+ 'tester',
160
+ 'dedicated_auditors_for_event',
161
+ 'user_audit',
162
+ ],
163
+ ratio: {},
164
+ required: {},
165
+ matchInfluence: {},
166
+ cooldown: {},
167
+ preQuestions: {},
168
+ postQuestions: {},
169
+ matchWhere: {},
170
+ },
171
+ }
172
+ export const getAllBulkAttrs = (parentType, childType) => {
173
+ const relationAttrs = getDefaultRelAttrs(parentType, childType)
174
+ const attrs = getDefaultAttrs({ type: childType })
175
+ const allAttrs = { ...relationAttrs, ...attrs }
176
+ return filterBulkAttrs(allAttrs, allowedBulkAttrs)
177
+ }
178
+ export const filterBulkAttrs = (attrs, allowedAttrs) => {
179
+ if (!attrs) return {}
180
+ if (Array.isArray(attrs.type)) {
181
+ const { type, ...rest } = attrs
182
+ // if it is type array and from the validation attribute we need to filter the validation type
183
+ const types = type
184
+ .map(a => filterBulkAttrs(a, allowedAttrs))
185
+ .filter(({ type }) => allowedAttrs.type.includes(type.type.value))
186
+ return { ...rest, type: types }
187
+ }
188
+ if (attrs.type && typeof attrs.type === 'object') {
189
+ const { type, ...rest } = attrs
190
+ const filtered = { type: {}, ...rest }
191
+ const typesKeys = Object.keys(type)
192
+ for (const t of typesKeys) {
193
+ if (allowedAttrs?.[t]) {
194
+ filtered.type[t] = type[t]
195
+ }
196
+ }
197
+ return filtered
198
+ }
199
+ if (typeof attrs === 'object' && !attrs.type) {
200
+ const filtered = {}
201
+ const keys = Object.keys(attrs)
202
+ for (const k of keys) {
203
+ if (allowedAttrs[k]) {
204
+ filtered[k] = filterBulkAttrs(attrs[k], allowedAttrs[k])
205
+ }
206
+ }
207
+ return filtered
208
+ }
209
+ return null
210
+ }
211
+
139
212
  // map from attrs[name][type] to attrs[type][name]
140
213
  export const attrsByType = {}
141
214
 
@@ -147,7 +220,7 @@ for (const [name, matches] of Object.entries(attributes)) {
147
220
  // handle translations: generate translation attrs and required status
148
221
  const { label, ...restDefs } = defs
149
222
  if (defs.functionsByName?.translate) {
150
- // perf measures done: inscrease from 0.671ms loadtime to 13.261ms
223
+ // perf measures done: increase from 0.671ms loadtime to 13.261ms
151
224
  // should not impact the perfs
152
225
  for (const [code, language] of languagesEntries) {
153
226
  const newLabel = `${label} - ${language}`
@@ -265,6 +338,14 @@ const expandAttr = (key, value, defs, object, getUser) => {
265
338
 
266
339
  export const expandAttrs = (object, getUser) => {
267
340
  if (!object.children) return (object.children = {})
341
+
342
+ for (const [key, defs] of getDefaultAttrsEntries(object)) {
343
+ if (object.attrs[key] === null) {
344
+ console.warn(`value is null for ${object.name} - ${key} - ${object.id}`)
345
+ }
346
+ expandAttr(key, object.attrs, defs, object, getUser)
347
+ }
348
+
268
349
  let prev
269
350
  for (const child of Object.values(object.children)) {
270
351
  child.parent = object
@@ -4,7 +4,7 @@ import { readFile, stat, watch } from 'node:fs/promises'
4
4
 
5
5
  import { checkAndBuildDefinitions } from '../definitions-checker.js'
6
6
 
7
- const rootTypes = ['module', 'piscine', 'sign-up', 'onboarding']
7
+ const rootTypes = ['module', 'piscine', 'signup', 'onboarding']
8
8
  const isAudit = validation => validation.type.endsWith('_audit')
9
9
  const readDef = async key => {
10
10
  const path = key == null ? 'content/def.json' : `content/${key}/def.json`
package/onboarding.js CHANGED
@@ -9,6 +9,7 @@ export const onboardingTypes = new Set([
9
9
  'sign-step',
10
10
  'upload-step',
11
11
  'contact-validation-step',
12
+ 'avatar-step',
12
13
  ])
13
14
 
14
15
  export const prevValidated = (key, object) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@01-edu/shared",
3
- "version": "1.0.9",
3
+ "version": "1.0.12",
4
4
  "type": "module",
5
5
  "description": "",
6
6
  "scripts": {
package/toolbox.js CHANGED
@@ -27,7 +27,13 @@ export const objectTypes = new Set([...contentObjects, ...onboardingTypes])
27
27
 
28
28
  export const childTypes = {
29
29
  campus: ['signup', 'onboarding', 'piscine', 'module'],
30
- signup: ['form-step', 'sign-step', 'upload-step', 'contact-validation-step'],
30
+ signup: [
31
+ 'form-step',
32
+ 'sign-step',
33
+ 'upload-step',
34
+ 'contact-validation-step',
35
+ 'avatar-step',
36
+ ],
31
37
  onboarding: [
32
38
  'games',
33
39
  'administration',
@@ -40,6 +46,7 @@ export const childTypes = {
40
46
  'sign-step',
41
47
  'upload-step',
42
48
  'contact-validation-step',
49
+ 'avatar-step',
43
50
  ],
44
51
  piscine: ['quest', 'exam', 'raid', 'project'],
45
52
  exam: ['exercise'],