Skip to content

Commit 7eebc1b

Browse files
Image reading annotation bugfixes (#129)
* Start properly saving annotations * Start storing marker locations * Validate annotations form * Add validation for annotations form * Be clear we're adding another for the same breast * Increase number of older clinics to generate so we have urgent cases to read
1 parent 21a338d commit 7eebc1b

File tree

8 files changed

+718
-390
lines changed

8 files changed

+718
-390
lines changed

app/config.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@ module.exports = {
3434
targetAttendancePercent: 100, // 100% of original capacity (not overbooking)
3535

3636
// Date range for generating data
37-
daysToGenerate: 8,
38-
daysBeforeToday: 5,
37+
daysToGenerate: 13,
38+
daysBeforeToday: 11,
3939
historicPeriodCount: 1, // Number of historic periods to generate
4040

4141
simulatedTime: '10:30', // 24h format

app/data/breast-screening-units.js

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -69,15 +69,15 @@ module.exports = [
6969
],
7070
registration: 'JA1 CP7',
7171
// Override BSU session patterns for this location
72-
sessionPatterns: [
73-
{
74-
name: 'full_day',
75-
type: 'single',
76-
sessions: [
77-
{ startTime: '09:00', endTime: '17:00' },
78-
],
79-
},
80-
],
72+
// sessionPatterns: [
73+
// {
74+
// name: 'full_day',
75+
// type: 'single',
76+
// sessions: [
77+
// { startTime: '09:00', endTime: '17:00' },
78+
// ],
79+
// },
80+
// ],
8181
},
8282
// {
8383
// id: 'acxcdcnj', // Must be hardcoded so it matches generated data

app/filters/nunjucks.js

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,80 @@ const log = (a, description = null) => {
77
return `<script>${description || ''}console.log(${JSON.stringify(a, null, '\t')});</script>`
88
}
99

10+
11+
/**
12+
* Safely join array elements with proper undefined/null handling
13+
* @param {any} input - Array-like input to join
14+
* @param {string} [delimiter=''] - String to use as delimiter between elements
15+
* @param {string} [attribute] - Optional object property to extract before joining
16+
* @param {Object} [options] - Additional options for join behavior
17+
* @param {boolean} [options.filterEmpty=true] - Whether to filter out empty/null/undefined values
18+
* @param {boolean} [options.toString=true] - Whether to convert values to strings before joining
19+
* @returns {string} Joined string or empty string if invalid input
20+
*
21+
* @example
22+
* join(['a', 'b', 'c'], ', ') // 'a, b, c'
23+
* join([{name: 'John'}, {name: 'Jane'}], ', ', 'name') // 'John, Jane'
24+
* join(['a', null, 'b'], ', ') // 'a, b' (filters nulls by default)
25+
* join(null) // ''
26+
* join(undefined) // ''
27+
*/
28+
const join = (input, delimiter = '', attribute = null, options = {}) => {
29+
const {
30+
filterEmpty = true,
31+
toString = true
32+
} = options
33+
34+
// Handle null, undefined, or non-array inputs
35+
if (!input) {
36+
return ''
37+
}
38+
39+
// Convert to array if it's array-like but not an array
40+
let array
41+
if (Array.isArray(input)) {
42+
array = input
43+
} else if (input.length !== undefined) {
44+
// Array-like object (NodeList, etc.)
45+
array = Array.from(input)
46+
} else {
47+
// Single value, wrap in array
48+
array = [input]
49+
}
50+
51+
// Extract attribute values if specified
52+
if (attribute) {
53+
array = array.map(item => {
54+
if (!item || typeof item !== 'object') {
55+
return undefined
56+
}
57+
return item[attribute]
58+
})
59+
}
60+
61+
// Filter out empty values if requested
62+
if (filterEmpty) {
63+
array = array.filter(item => {
64+
return item !== null &&
65+
item !== undefined &&
66+
item !== '' &&
67+
(!toString || String(item).trim() !== '')
68+
})
69+
}
70+
71+
// Convert to strings if requested
72+
if (toString) {
73+
array = array.map(item => {
74+
if (item === null || item === undefined) {
75+
return ''
76+
}
77+
return String(item)
78+
})
79+
}
80+
81+
return array.join(delimiter)
82+
}
83+
1084
/**
1185
* Get user name by user ID with format options
1286
* @param {string} userId - ID of the user
@@ -58,6 +132,7 @@ const getContext = function() {
58132

59133
module.exports = {
60134
log,
135+
join,
61136
getUsername,
62137
getContext,
63138
}

app/lib/generators/participant-generator.js

Lines changed: 0 additions & 198 deletions
Original file line numberDiff line numberDiff line change
@@ -146,102 +146,6 @@ const generateNHSNumber = () => {
146146
return `${baseNumber}${finalCheckDigit}`
147147
}
148148

149-
// New medical history generators
150-
const generateMedicalHistorySurvey = () => {
151-
// 50% chance of having completed the survey
152-
if (Math.random() > 0.5) {
153-
return null
154-
}
155-
156-
return {
157-
completedAt: faker.date.past({ years: 1 }).toISOString(),
158-
159-
// Non-cancerous procedures/diagnoses
160-
nonCancerousProcedures: generateNonCancerousProcedures(),
161-
162-
// Current hormone therapy (if they had previous cancer)
163-
onHormoneTherapy: Math.random() < 0.3, // 30% of those with cancer history
164-
165-
// Other medical history
166-
otherMedicalHistory: Math.random() < 0.3
167-
? faker.helpers.arrayElements([
168-
'Type 2 diabetes - diet controlled',
169-
'High blood pressure - medication',
170-
'Osteoarthritis',
171-
'Previous shoulder surgery',
172-
'Rheumatoid arthritis',
173-
], { min: 1, max: 2 })
174-
: null,
175-
}
176-
}
177-
178-
const generateNonCancerousProcedures = () => {
179-
const procedures = []
180-
181-
const possibleProcedures = {
182-
benign_lump: {
183-
probability: 0.15,
184-
details: () => ({
185-
dateDiscovered: faker.date.past({ years: 5 }).toISOString(),
186-
position: faker.helpers.arrayElement(['Left breast', 'Right breast']),
187-
wasRemoved: Math.random() < 0.7,
188-
pathology: 'Fibroadenoma',
189-
}),
190-
},
191-
cyst_aspiration: {
192-
probability: 0.1,
193-
details: () => ({
194-
date: faker.date.past({ years: 3 }).toISOString(),
195-
location: faker.helpers.arrayElement(['Left breast', 'Right breast']),
196-
notes: 'Simple cyst, fluid aspirated',
197-
}),
198-
},
199-
non_implant_augmentation: {
200-
probability: 0.02,
201-
details: () => ({
202-
date: faker.date.past({ years: 5 }).toISOString(),
203-
procedure: 'Fat transfer procedure',
204-
hospital: 'General Hospital',
205-
}),
206-
},
207-
breast_reduction: {
208-
probability: 0.03,
209-
details: () => ({
210-
date: faker.date.past({ years: 5 }).toISOString(),
211-
notes: 'Bilateral breast reduction',
212-
hospital: 'City Hospital',
213-
}),
214-
},
215-
previous_biopsy: {
216-
probability: 0.08,
217-
details: () => ({
218-
date: faker.date.past({ years: 2 }).toISOString(),
219-
result: 'Benign',
220-
location: faker.helpers.arrayElement(['Left breast', 'Right breast']),
221-
}),
222-
},
223-
skin_lesion: {
224-
probability: 0.05,
225-
details: () => ({
226-
date: faker.date.past({ years: 3 }).toISOString(),
227-
type: faker.helpers.arrayElement(['Seborrheic keratosis', 'Dermatofibroma']),
228-
location: faker.helpers.arrayElement(['Left breast', 'Right breast']),
229-
}),
230-
},
231-
}
232-
233-
Object.entries(possibleProcedures).forEach(([type, config]) => {
234-
if (Math.random() < config.probability) {
235-
procedures.push({
236-
type,
237-
...config.details(),
238-
})
239-
}
240-
})
241-
242-
return procedures
243-
}
244-
245149
const generateParticipant = ({
246150
ethnicities,
247151
breastScreeningUnits,
@@ -286,17 +190,11 @@ const generateParticipant = ({
286190
},
287191
medicalInformation: {
288192
nhsNumber: generateNHSNumber(),
289-
riskFactors: generateRiskFactors(),
290-
familyHistory: generateFamilyHistory(),
291-
previousCancerHistory: generatePreviousCancerHistory(),
292-
medicalHistorySurvey: generateMedicalHistorySurvey(),
293193
},
294194
currentHealthInformation: {
295195
isPregnant: false,
296196
onHRT: Math.random() < 0.1,
297-
hasBreastImplants: Math.random() < 0.05,
298197
recentBreastSymptoms: generateRecentSymptoms(),
299-
medications: generateMedications(),
300198
},
301199
}
302200

@@ -309,91 +207,6 @@ const generateParticipant = ({
309207
return _.merge({}, baseParticipant, participantOverrides)
310208
}
311209

312-
// Modified family history generator to add more detail
313-
const generateFamilyHistory = () => {
314-
if (Math.random() > 0.15) return null // 15% chance of family history
315-
316-
const affectedRelatives = faker.helpers.arrayElements(
317-
[
318-
{ relation: 'mother', age: faker.number.int({ min: 35, max: 75 }) },
319-
{ relation: 'sister', age: faker.number.int({ min: 30, max: 70 }) },
320-
{ relation: 'daughter', age: faker.number.int({ min: 25, max: 45 }) },
321-
{ relation: 'grandmother', age: faker.number.int({ min: 45, max: 85 }) },
322-
{ relation: 'aunt', age: faker.number.int({ min: 40, max: 80 }) },
323-
],
324-
{ min: 1, max: 3 }
325-
)
326-
327-
return {
328-
hasFirstDegreeHistory: affectedRelatives.some(r =>
329-
['mother', 'sister', 'daughter'].includes(r.relation)
330-
),
331-
affectedRelatives,
332-
additionalDetails: Math.random() < 0.3
333-
? 'Multiple occurrences on maternal side'
334-
: null,
335-
}
336-
}
337-
338-
const generateRiskFactors = () => {
339-
const factors = []
340-
const possibleFactors = {
341-
family_history: 0.15,
342-
dense_breast_tissue: 0.1,
343-
previous_radiation_therapy: 0.05,
344-
obesity: 0.2,
345-
alcohol_consumption: 0.15,
346-
}
347-
348-
Object.entries(possibleFactors).forEach(([factor, probability]) => {
349-
if (Math.random() < probability) {
350-
factors.push(factor)
351-
}
352-
})
353-
354-
return factors
355-
}
356-
357-
// Modified previous cancer history to include more detail
358-
const generatePreviousCancerHistory = () => {
359-
if (Math.random() > 0.02) return null // 2% chance of previous cancer
360-
361-
const treatments = faker.helpers.arrayElements(
362-
[
363-
{ type: 'surgery', details: 'Wide local excision' },
364-
{ type: 'radiotherapy', details: '15 fractions' },
365-
{ type: 'chemotherapy', details: '6 cycles' },
366-
{ type: 'hormone_therapy', details: '5 years tamoxifen' },
367-
],
368-
{ min: 1, max: 3 }
369-
)
370-
371-
return {
372-
yearDiagnosed: faker.date.past({ years: 20 }).getFullYear(),
373-
type: faker.helpers.arrayElement([
374-
'ductal_carcinoma_in_situ',
375-
'invasive_ductal_carcinoma',
376-
'invasive_lobular_carcinoma',
377-
]),
378-
position: faker.helpers.arrayElement([
379-
'Left breast - upper outer quadrant',
380-
'Right breast - upper outer quadrant',
381-
'Left breast - lower inner quadrant',
382-
'Right breast - lower inner quadrant',
383-
]),
384-
treatments,
385-
hospital: faker.helpers.arrayElement([
386-
'City General Hospital',
387-
'Royal County Hospital',
388-
'Memorial Cancer Centre',
389-
'University Teaching Hospital',
390-
]),
391-
additionalNotes: Math.random() < 0.3
392-
? 'Regular follow-up completed'
393-
: null,
394-
}
395-
}
396-
397210
const generateRecentSymptoms = () => {
398211
if (Math.random() > 0.1) return null // 10% chance of recent symptoms
399212

@@ -406,17 +219,6 @@ const generateRecentSymptoms = () => {
406219
], { min: 1, max: 2 })
407220
}
408221

409-
const generateMedications = () => {
410-
if (Math.random() > 0.3) return [] // 30% chance of medications
411-
412-
return faker.helpers.arrayElements([
413-
'hormone_replacement_therapy',
414-
'blood_pressure_medication',
415-
'diabetes_medication',
416-
'cholesterol_medication',
417-
], { min: 1, max: 3 })
418-
}
419-
420222
module.exports = {
421223
generateParticipant,
422224
}

0 commit comments

Comments
 (0)