@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 +241 -94
- package/attrs.js +86 -5
- package/bin/check-definitions.js +1 -1
- package/onboarding.js +1 -0
- package/package.json +1 -1
- package/toolbox.js +8 -1
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
|
|
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
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
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
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
2520
|
+
const pathways = types.pathwaysRequirementObjects.value(...args)
|
|
2521
|
+
return option ? [option, pathways] : []
|
|
2434
2522
|
},
|
|
2435
|
-
type: [types.objectRootRelativePath],
|
|
2436
|
-
instruction: '
|
|
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
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
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
|
-
|
|
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
|
|
3146
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
|
3438
|
-
instruction:
|
|
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(
|
|
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
|
-
|
|
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:
|
|
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
|
package/bin/check-definitions.js
CHANGED
|
@@ -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', '
|
|
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
package/package.json
CHANGED
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: [
|
|
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'],
|