• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

IGVF-DACC / igvfd / #9236

14 Oct 2025 10:54PM UTC coverage: 91.398% (+0.03%) from 91.364%
#9236

push

coveralls-python

web-flow
IGVF-3021-library-prep-kit (#1773)

9680 of 10591 relevant lines covered (91.4%)

0.91 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

93.4
/src/igvfd/types/sample.py
1
from snovault import (
1✔
2
    abstract_collection,
3
    calculated_property,
4
    collection,
5
    load_schema,
6
)
7
from snovault.util import Path
1✔
8
from .base import (
1✔
9
    Item,
10
    paths_filtered_by_status
11
)
12

13

14
def collect_multiplexed_samples_prop(request, multiplexed_samples, property_name, skip_calculated=True):
1✔
15
    property_set = set()
1✔
16
    for sample in multiplexed_samples:
1✔
17
        # If to get a specific calculated property
18
        if skip_calculated is False:
1✔
19
            sample_props = request.embed(sample, f'@@object_with_select_calculated_properties?field={property_name}')
1✔
20
        # If to get non-calculated properties
21
        else:
22
            sample_props = request.embed(sample, '@@object?skip_calculated=true')
1✔
23
        property_contents = sample_props.get(property_name, None)
1✔
24
        if property_contents:
1✔
25
            if type(property_contents) == list:
1✔
26
                for item in property_contents:
1✔
27
                    property_set.add(item)
1✔
28
            else:
29
                property_set.add(property_contents)
×
30
    property_list = sorted(property_set)
1✔
31
    return property_list or None
1✔
32

33

34
def decompose_multiplexed_samples(request, samples, visited_multiplexed_samples=None):
1✔
35
    if visited_multiplexed_samples is None:
×
36
        visited_multiplexed_samples = set()
×
37
    decomposed_samples = set()
×
38
    for sample in samples:
×
39
        if sample.startswith('/multiplexed-samples/') and sample not in visited_multiplexed_samples:
×
40
            visited_multiplexed_samples.add(sample)
×
41
            multiplexed_samples = request.embed(sample, '@@object').get('multiplexed_samples')
×
42
            if multiplexed_samples:
×
43
                decomposed_samples.update(decompose_multiplexed_samples(
×
44
                    request, multiplexed_samples, visited_multiplexed_samples))
45
        else:
46
            decomposed_samples.add(sample)
×
47
    return list(decomposed_samples)
×
48

49

50
def concat_numeric_and_units(numeric, numeric_units, no_numeric_on_one=False):
1✔
51
    if str(numeric) == '1':
1✔
52
        if no_numeric_on_one:
1✔
53
            return f'{numeric_units}'
1✔
54
        else:
55
            return f'{numeric} {numeric_units}'
1✔
56
    else:
57
        return f'{numeric} {numeric_units}s'
1✔
58

59

60
def compose_summary_sample_term_phrase(sample_term_term_names: list, cap_number: int) -> str:
1✔
61
    """Compose a summary sample term phrase based on the number of terms and a max number of items.
62

63
    Args:
64
        sample_term_term_names (list): A list of unique sample term names
65
        cap_number (int): An item count number used to determine how to format the summary (e.g., num of samples)
66

67
    Returns:
68
        str: A formatted string of sample term names
69
    """
70
    sample_term_term_names = sorted(sample_term_term_names)
1✔
71
    if cap_number < 3:
1✔
72
        sample_term_phrase = ' and '.join(sample_term_term_names)
1✔
73
    elif 3 <= cap_number <= 5:
1✔
74
        sample_term_phrase = f"{', '.join(sample_term_term_names[0:-1])} and {sample_term_term_names[-1]}"
1✔
75
    else:
76
        sample_term_phrase = f'{", ".join(sample_term_term_names[0:4])} and {(cap_number - 5)} more'
1✔
77
    return sample_term_phrase
1✔
78

79

80
def get_filesets_samples_used_in(list_of_samples: list, starter_filesets: list, request) -> list:
1✔
81
    """Get a mapping of fileset to samples used in it.
82

83
    Args:
84
        list_of_samples (list): A list of sample objects
85

86
    Returns:
87
        dict: A dictionary mapping fileset @id to a list of sample accessions used in it
88
    """
89
    for sample in list_of_samples:
1✔
90
        sample_object = request.embed(
1✔
91
            sample, '@@object_with_select_calculated_properties?field=file_sets')
92
        sample_file_sets = sample_object.get('file_sets', [])
1✔
93
        for file_set in sample_file_sets:
1✔
94
            if file_set not in starter_filesets:
1✔
95
                starter_filesets.append(file_set)
1✔
96
    return starter_filesets
1✔
97

98

99
@abstract_collection(
1✔
100
    name='samples',
101
    unique_key='accession',
102
    properties={
103
        'title': 'Samples',
104
        'description': 'Listing of samples',
105
    }
106
)
107
class Sample(Item):
1✔
108
    item_type = 'sample'
1✔
109
    base_types = ['Sample'] + Item.base_types
1✔
110
    name_key = 'accession'
1✔
111
    schema = load_schema('igvfd:schemas/sample.json')
1✔
112
    rev = {
1✔
113
        'file_sets': ('FileSet', 'samples'),
114
        'multiplexed_in': ('MultiplexedSample', 'multiplexed_samples'),
115
        'sorted_fractions': ('Sample', 'sorted_from'),
116
        'origin_of': ('Sample', 'originated_from'),
117
        'institutional_certificates': ('InstitutionalCertificate', 'samples'),
118
        'superseded_by': ('Sample', 'supersedes')
119
    }
120
    embedded_with_frame = [
1✔
121
        Path('award', include=['@id', 'component']),
122
        Path('lab', include=['@id', 'title']),
123
        Path('sources', include=['@id', 'title', 'status']),
124
        Path('submitted_by', include=['@id', 'title']),
125
        Path('sorted_from', include=['@id', 'accession', 'status']),
126
        Path('file_sets', include=['@id', 'accession', 'summary', 'aliases',
127
             'lab', 'status', 'preferred_assay_titles', 'file_set_type']),
128
        Path('file_sets.lab', include=['title']),
129
        Path('multiplexed_in', include=['@id', 'accession', 'status']),
130
        Path('publications', include=['@id', 'publication_identifiers', 'status']),
131
        Path('sample_terms', include=['@id', 'term_name', 'status']),
132
        Path('disease_terms', include=['@id', 'term_name', 'status']),
133
        Path('treatments', include=['@id', 'purpose', 'treatment_type',
134
             'status', 'treatment_term_name', 'depletion']),
135
        Path('biomarkers.gene', include=['@id', 'name_quantification', 'classification', 'gene', 'symbol', 'status']),
136
        Path('modifications.tagged_proteins', include=[
137
             '@id', 'summary', 'status', 'tagged_proteins', 'modality', 'fused_domain', 'symbol', 'cas', 'cas_species', 'degron_system']),
138
        Path('institutional_certificates', include=[
139
             '@id', 'certificate_identifier', 'status', 'data_use_limitation', 'data_use_limitation_modifiers', 'data_use_limitation_summary', 'controlled_access']),
140
        Path('construct_library_sets.associated_phenotypes', include=[
141
             '@id', 'accession', 'file_set_type', 'term_name', 'associated_phenotypes', 'status']),
142
        Path('donors', include=['@id', 'accession', 'status', 'strain', 'ethnicities']),
143
    ]
144

145
    audit_inherit = [
1✔
146
        'award',
147
        'lab',
148
        'sources',
149
        'sample_terms',
150
        'construct_library_sets',
151
    ]
152

153
    set_status_up = [
1✔
154
        'biomarkers',
155
        'construct_library_sets',
156
        'documents',
157
        'modifications',
158
        'sorted_from',
159
        'treatments'
160
    ]
161
    set_status_down = []
1✔
162

163
    @calculated_property(schema={
1✔
164
        'title': 'File Sets',
165
        'type': 'array',
166
        'description': 'The file sets linked to this sample.',
167
        'minItems': 1,
168
        'uniqueItems': True,
169
        'items': {
170
            'title': 'File Set',
171
            'type': 'string',
172
            'linkFrom': 'FileSet.samples',
173
        },
174
        'notSubmittable': True,
175
    })
176
    def file_sets(self, request, file_sets, multiplexed_in=None, pooled_in=None):
1✔
177
        if multiplexed_in is None:
1✔
178
            multiplexed_in = []
×
179
        if pooled_in is None:
1✔
180
            pooled_in = []
1✔
181
        # This is required to get the analysis set reverse links since analysis set calculates samples
182
        for file_set in file_sets:
1✔
183
            file_set_object = request.embed(
1✔
184
                file_set, '@@object_with_select_calculated_properties?field=input_for')
185
            for input_for in file_set_object.get('input_for', []):
1✔
186
                if input_for.startswith('/analysis-sets/') and input_for not in file_sets:
1✔
187
                    file_sets.append(input_for)
1✔
188
        # file sets associated with a multiplexed sample that a sample was multiplexed in are included
189
        if multiplexed_in:
1✔
190
            file_sets = get_filesets_samples_used_in(multiplexed_in, file_sets, request)
1✔
191
        # file sets associated with a pooled sample that a sample was pooled in are included
192
        if pooled_in:
1✔
193
            file_sets = get_filesets_samples_used_in(pooled_in, file_sets, request)
1✔
194
        return paths_filtered_by_status(request, file_sets) or None
1✔
195

196
    @calculated_property(schema={
1✔
197
        'title': 'Multiplexed In',
198
        'type': 'array',
199
        'description': 'The multiplexed samples in which this sample is included.',
200
        'minItems': 1,
201
        'uniqueItems': True,
202
        'items': {
203
            'title': 'Multiplexed In',
204
            'type': 'string',
205
            'linkFrom': 'MultiplexedSample.multiplexed_samples',
206
        },
207
        'notSubmittable': True,
208
    })
209
    def multiplexed_in(self, request, multiplexed_in):
1✔
210
        return paths_filtered_by_status(request, multiplexed_in) or None
1✔
211

212
    @calculated_property(schema={
1✔
213
        'title': 'Sorted Fraction Samples',
214
        'type': 'array',
215
        'description': 'The fractions into which this sample has been sorted.',
216
        'minItems': 1,
217
        'uniqueItems': True,
218
        'items': {
219
            'title': 'Sorted Fraction Sample',
220
            'type': 'string',
221
            'linkFrom': 'Sample.sorted_from',
222
        },
223
        'notSubmittable': True,
224
    })
225
    def sorted_fractions(self, request, sorted_fractions):
1✔
226
        return paths_filtered_by_status(request, sorted_fractions) or None
1✔
227

228
    @calculated_property(schema={
1✔
229
        'title': 'Origin Sample Of',
230
        'type': 'array',
231
        'description': 'The samples which originate from this sample, such as through a process of cell differentiation.',
232
        'minItems': 1,
233
        'uniqueItems': True,
234
        'items': {
235
            'title': 'Originated Sample',
236
            'type': 'string',
237
            'linkFrom': 'InVitroSystem.originated_from',
238
        },
239
        'notSubmittable': True,
240
    })
241
    def origin_of(self, request, origin_of):
1✔
242
        return paths_filtered_by_status(request, origin_of) or None
1✔
243

244
    @calculated_property(schema={
1✔
245
        'title': 'Institutional Certificates',
246
        'type': 'array',
247
        'description': 'The institutional certificates under which use of this sample is approved.',
248
        'minItems': 1,
249
        'uniqueItems': True,
250
        'items': {
251
            'title': 'Institutional Certificate',
252
            'type': 'string',
253
            'linkFrom': 'InstitutionalCertificate.samples',
254
        },
255
        'notSubmittable': True,
256
    })
257
    def institutional_certificates(self, request, institutional_certificates):
1✔
258
        return paths_filtered_by_status(request, institutional_certificates) or None
1✔
259

260
    @calculated_property(schema={
1✔
261
        'title': 'Superseded By',
262
        'description': 'Sample(s) this sample is superseded by virtue of those sample(s) being newer, better, or a fixed version of etc. than this one.',
263
        'type': 'array',
264
        'minItems': 1,
265
        'uniqueItems': True,
266
        'items': {
267
            'title': 'Superseded By',
268
            'type': 'string',
269
            'linkFrom': 'Sample.supersedes',
270
        },
271
        'notSubmittable': True
272
    })
273
    def superseded_by(self, request, superseded_by):
1✔
274
        return paths_filtered_by_status(request, superseded_by) or None
1✔
275

276

277
@abstract_collection(
1✔
278
    name='biosamples',
279
    unique_key='accession',
280
    properties={
281
        'title': 'Biosamples',
282
        'description': 'Listing of biosamples',
283
    }
284
)
285
class Biosample(Sample):
1✔
286
    item_type = 'biosample'
1✔
287
    base_types = ['Biosample'] + Sample.base_types
1✔
288
    schema = load_schema('igvfd:schemas/biosample.json')
1✔
289
    rev = Sample.rev | {'parts': ('Biosample', 'part_of'),
1✔
290
                        'pooled_in': ('Biosample', 'pooled_from')}
291
    embedded_with_frame = Sample.embedded_with_frame
1✔
292

293
    audit_inherit = Sample.audit_inherit + [
1✔
294
        'disease_terms',
295
        'treatments',
296
        'modifications',
297
    ]
298

299
    set_status_up = Sample.set_status_up + [
1✔
300
        'biomarkers',
301
        'donors',
302
        'modifications',
303
        'originated_from',
304
        'part_of',
305
        'pooled_from',
306
        'sample_terms',
307
        'targeted_sample_term',
308
    ]
309
    set_status_down = Sample.set_status_down + []
1✔
310

311
    @calculated_property(
1✔
312
        define=True,
313
        schema={
314
            'title': 'Sex',
315
            'type': 'string',
316
            'enum': [
317
                'female',
318
                'male',
319
                'mixed',
320
                'unspecified'
321
            ],
322
            'notSubmittable': True,
323
        }
324
    )
325
    def sex(self, request, donors=None):
1✔
326
        sexes = set()
1✔
327
        if donors:
1✔
328
            for d in donors:
1✔
329
                donor_object = request.embed(d, '@@object')
1✔
330
                if donor_object.get('sex'):
1✔
331
                    sexes.add(donor_object.get('sex'))
1✔
332
        if len(sexes) == 1:
1✔
333
            return list(sexes).pop()
1✔
334
        elif len(sexes) > 1:
1✔
335
            return 'mixed'
1✔
336

337
    @calculated_property(
1✔
338
        define=True,
339
        schema={
340
            'title': 'Age',
341
            'description': 'Age of organism at the time of collection of the sample.',
342
            'type': 'string',
343
            'pattern': '^((\\d+(\\.[1-9])?(\\-\\d+(\\.[1-9])?)?)|(unknown)|([1-8]?\\d)|(90 or above))$',
344
            'notSubmittable': True,
345
        }
346
    )
347
    def age(self, lower_bound_age=None, upper_bound_age=None, age_units=None):
1✔
348
        if lower_bound_age and upper_bound_age:
1✔
349
            if lower_bound_age == upper_bound_age:
1✔
350
                if lower_bound_age == 90 and upper_bound_age == 90 and age_units == 'year':
1✔
351
                    return '90 or above'
1✔
352
                return str(lower_bound_age)
1✔
353
            return str(lower_bound_age) + '-' + str(upper_bound_age)
1✔
354
        else:
355
            return 'unknown'
1✔
356

357
    @calculated_property(
1✔
358
        define=True,
359
        schema={
360
            'title': 'Upper Bound Age In Hours',
361
            'description': 'Upper bound of age of organism in hours at the time of collection of the sample.',
362
            'type': 'number',
363
            'notSubmittable': True,
364
        }
365
    )
366
    def upper_bound_age_in_hours(self, upper_bound_age=None, age_units=None):
1✔
367
        conversion_factors = {
1✔
368
            'minute': 1/60,
369
            'hour': 1,
370
            'day': 24,
371
            'week': 168,
372
            'month': 720,
373
            'year': 8760
374
        }
375
        if upper_bound_age:
1✔
376
            return upper_bound_age*conversion_factors[age_units]
1✔
377

378
    @calculated_property(
1✔
379
        define=True,
380
        schema={
381
            'title': 'Lower Bound Age In Hours',
382
            'description': 'Lower bound of age of organism in hours at the time of collection of the sample .',
383
            'type': 'number',
384
            'notSubmittable': True,
385
        }
386
    )
387
    def lower_bound_age_in_hours(self, lower_bound_age=None, age_units=None):
1✔
388
        conversion_factors = {
1✔
389
            'minute': 1/60,
390
            'hour': 1,
391
            'day': 24,
392
            'week': 168,
393
            'month': 720,
394
            'year': 8760
395
        }
396
        if lower_bound_age:
1✔
397
            return lower_bound_age*conversion_factors[age_units]
1✔
398

399
    @calculated_property(
1✔
400
        define=True,
401
        schema={
402
            'title': 'Taxa',
403
            'type': 'string',
404
            'description': 'The species of the organism.',
405
            'enum': [
406
                    'Homo sapiens',
407
                    'Mus musculus'
408
            ],
409
            'notSubmittable': True
410
        }
411
    )
412
    def taxa(self, request, donors):
1✔
413
        taxas = set()
1✔
414
        if donors:
1✔
415
            for d in donors:
1✔
416
                donor_object = request.embed(d, '@@object?skip_calculated=true')
1✔
417
                if donor_object.get('taxa'):
1✔
418
                    taxas.add(donor_object.get('taxa'))
1✔
419

420
        if len(taxas) == 1:
1✔
421
            return list(taxas).pop()
1✔
422

423
    @calculated_property(
1✔
424
        schema={
425
            'title': 'Summary',
426
            'type': 'string',
427
            'description': 'A summary of the sample.',
428
            'notSubmittable': True,
429
        }
430
    )
431
    def summary(self, request, sample_terms, donors, sex, age, age_units=None, modifications=None, embryonic=None, virtual=None, classifications=None, time_post_change=None, time_post_change_units=None, targeted_sample_term=None, cellular_sub_pool=None, taxa=None, sorted_from_detail=None, disease_terms=None, biomarkers=None, treatments=None, construct_library_sets=None, moi=None, nucleic_acid_delivery=None, growth_medium=None, biosample_qualifiers=None, time_post_library_delivery=None, time_post_library_delivery_units=None):
1✔
432
        term_object = request.embed(sample_terms[0], '@@object?skip_calculated=true')
1✔
433
        term_name = term_object.get('term_name')
1✔
434
        biosample_type = self.item_type
1✔
435
        biosample_subschemas = ['primary_cell', 'in_vitro_system', 'tissue', 'whole_organism']
1✔
436

437
        # sample term customization based on biosample type
438
        if biosample_type == 'primary_cell':
1✔
439
            if 'cell' not in term_name:
1✔
440
                summary_terms = f'{term_name} cell'
1✔
441
            else:
442
                summary_terms = term_name
1✔
443
        elif biosample_type == 'in_vitro_system':
1✔
444
            if len(classifications) == 1:
1✔
445
                summary_terms = f'{term_name} {classifications[0]}'
1✔
446
                if classifications[0] in term_name:
1✔
447
                    summary_terms = term_name
1✔
448
                elif 'cell' in classifications[0] and 'cell' in term_name:
1✔
449
                    summary_terms = term_name.replace('cell', classifications[0])
1✔
450
                elif 'tissue/organ' in classifications[0] and ('tissue' in term_name or 'organ' in term_name):
1✔
451
                    summary_terms = term_name.replace('tissue', classifications[0])
×
452
                elif 'gastruloid' in classifications[0]:
1✔
453
                    summary_terms = term_name.replace('gastruloid', '')
1✔
454
            elif len(classifications) == 2:
1✔
455
                if 'differentiated cell specimen' in classifications and 'pooled cell specimen' in classifications:
1✔
456
                    summary_terms = f'{term_name} pooled differentiated cell specimen'
1✔
457
                    if 'cell' in term_name:
1✔
458
                        summary_terms = term_name.replace('cell', 'pooled differentiated cell specimen')
×
459
                elif 'reprogrammed cell specimen' in classifications and 'pooled cell specimen' in classifications:
1✔
460
                    summary_terms = f'{term_name} pooled reprogrammed cell specimen'
×
461
                    if 'cell' in term_name:
×
462
                        summary_terms = term_name.replace('cell', 'pooled reprogrammed cell specimen')
×
463
                elif 'cell line' in classifications and 'pooled cell specimen' in classifications:
1✔
464
                    summary_terms = f'{term_name} pooled cell specimen'
1✔
465
                    if 'cell' in term_name:
1✔
466
                        summary_terms = term_name.replace('cell', 'pooled cell specimen')
×
467
        elif biosample_type == 'tissue':
1✔
468
            if 'tissue' not in term_name and 'organ' not in term_name:
1✔
469
                summary_terms = f'{term_name} tissue/organ'
1✔
470
            else:
471
                summary_terms = term_name
1✔
472
        elif biosample_type == 'whole_organism':
1✔
473
            summary_terms = concat_numeric_and_units(len(donors), term_name, no_numeric_on_one=True)
1✔
474
        elif len(sample_terms) > 1:
×
475
            summary_terms = 'mixed'
×
476

477
        # Prepend biosample qualifiers if they exist
478
        if biosample_qualifiers:
1✔
479
            qualifiers_string = ', '.join(biosample_qualifiers)
1✔
480
            summary_terms = f'{qualifiers_string} {summary_terms}'
1✔
481

482
        # embryonic is prepended to the start of the summary
483
        if (embryonic and
1✔
484
                biosample_type in ['primary_cell', 'tissue']):
485
            summary_terms = f'embryonic {summary_terms}'
1✔
486

487
        # sex and age are prepended to the start of the summary
488
        sex_and_age_ethnicity = [None, None, None]
1✔
489
        if (sex and
1✔
490
                biosample_type in biosample_subschemas):
491
            if sex != 'unspecified':
1✔
492
                if sex == 'mixed':
1✔
493
                    sex_and_age_ethnicity[0] = 'mixed sex'
1✔
494
                else:
495
                    sex_and_age_ethnicity[0] = sex
1✔
496

497
        if (age != 'unknown' and
1✔
498
                biosample_type in ['primary_cell', 'tissue', 'whole_organism']):
499
            age = concat_numeric_and_units(age, age_units)
1✔
500
            sex_and_age_ethnicity[1] = age
1✔
501

502
        if (donors and taxa == 'Homo sapiens'):
1✔
503
            ethnicity_set = set()
1✔
504
            for donor in donors:
1✔
505
                donor_object = request.embed(donor, '@@object?skip_calculated=true')
1✔
506
                ethnicity_set = ethnicity_set | set(donor_object.get('ethnicities', []))
1✔
507
            ethnicity_set = ', '.join(sorted(ethnicity_set))
1✔
508
            if ethnicity_set:
1✔
509
                sex_and_age_ethnicity[2] = ethnicity_set
1✔
510

511
        if any(x is not None for x in sex_and_age_ethnicity):
1✔
512
            summary_terms = f'({", ".join([x for x in sex_and_age_ethnicity if x is not None])}) {summary_terms}'
1✔
513

514
        # taxa of the donor(s) is prepended to the start of the summary
515
        if (donors and
1✔
516
                biosample_type in biosample_subschemas):
517
            if not taxa or taxa == 'Mus musculus':
1✔
518
                taxa_set = set()
1✔
519
                strains_set = set()
1✔
520
                for donor in donors:
1✔
521
                    donor_object = request.embed(donor, '@@object?skip_calculated=true')
1✔
522
                    taxa_set.add(donor_object['taxa'])
1✔
523
                    if donor_object['taxa'] == 'Mus musculus':
1✔
524
                        strains_set.add(donor_object.get('strain', ''))
1✔
525
                strains = ', '.join(sorted(strains_set))
1✔
526
                taxa_list = sorted(taxa_set)
1✔
527
                if 'Mus musculus' in taxa_list:
1✔
528
                    mouse_index = taxa_list.index('Mus musculus')
1✔
529
                    taxa_list[mouse_index] += f' {strains}'
1✔
530
                taxa = ' and '.join(taxa_list)
1✔
531
            summary_terms = f'{taxa} {summary_terms}'
1✔
532

533
        # virtual is prepended to the start of the summary
534
        if (virtual and
1✔
535
                biosample_type in ['primary_cell', 'in_vitro_system', 'tissue']):
536
            summary_terms = f'virtual {summary_terms}'
1✔
537

538
        if donors and classifications is not None and 'pooled cell specimen' in classifications:
1✔
539
            suffix = 's'
1✔
540
            if len(donors) == 1:
1✔
541
                suffix = ''
1✔
542
            summary_terms += f' ({len(donors)} donor{suffix})'
1✔
543

544
        # time post change and targeted term are appended to the end of the summary
545
        if (time_post_change and
1✔
546
                biosample_type in ['in_vitro_system']):
547
            time_post_change = concat_numeric_and_units(time_post_change, time_post_change_units)
1✔
548
            if targeted_sample_term:
1✔
549
                targeted_term_object = request.embed(targeted_sample_term, '@@object?skip_calculated=true')
1✔
550
                targeted_term_name = targeted_term_object.get('term_name')
1✔
551
                summary_terms += f' induced to {targeted_term_name} for {time_post_change},'
1✔
552
            else:
553
                summary_terms += f' induced for {time_post_change}'
×
554

555
        # cellular sub pool is appended to the end of the summary in parentheses
556
        if (cellular_sub_pool and
1✔
557
                biosample_type in ['primary_cell', 'in_vitro_system', 'tissue']):
558
            summary_terms += f' (cellular sub pool: {cellular_sub_pool})'
1✔
559

560
        # sorted from detail is appended to the end of the summary
561
        if (sorted_from_detail and
1✔
562
                biosample_type in ['primary_cell', 'in_vitro_system']):
563
            summary_terms += f' (sorting details: {sorted_from_detail})'
1✔
564

565
        # biomarker summaries are appended to the end of the summary
566
        if (biomarkers and
1✔
567
                biosample_type in ['primary_cell', 'in_vitro_system']):
568
            biomarker_summaries = []
1✔
569
            for biomarker in biomarkers:
1✔
570
                biomarker_object = request.embed(biomarker)
1✔
571
                if biomarker_object['quantification'] in ['positive', 'negative']:
1✔
572
                    biomarker_summary = f'{biomarker_object["quantification"]} detection of {biomarker_object["name"]}'
1✔
573
                elif biomarker_object['quantification'] in ['high', 'intermediate', 'low']:
1✔
574
                    if biomarker_object.get('classification') == 'marker gene':
1✔
575
                        biomarker_summary = f'{biomarker_object["quantification"]} expression of {biomarker_object["name"]}'
×
576
                    else:
577
                        biomarker_summary = f'{biomarker_object["quantification"]} level of {biomarker_object["name"]}'
1✔
578
                biomarker_summaries.append(biomarker_summary)
1✔
579
                biomarker_summaries = sorted(biomarker_summaries)
1✔
580
            summary_terms += f' characterized by {", ".join(biomarker_summaries)},'
1✔
581

582
        # disease terms are appended to the end of the summary
583
        if (disease_terms and
1✔
584
                biosample_type in biosample_subschemas):
585
            phenotype_term_names = sorted([request.embed(disease_term).get('term_name')
1✔
586
                                          for disease_term in disease_terms])
587
            summary_terms += f' associated with {", ".join(phenotype_term_names)},'
1✔
588

589
        # treatment summaries are appended to the end of the summary
590
        if (treatments and
1✔
591
                biosample_type in biosample_subschemas):
592
            treatment_objects = [request.embed(treatment) for treatment in treatments]
1✔
593
            depleted_treatment_summaries = sorted([treatment.get('summary')[13:]
1✔
594
                                                  for treatment in treatment_objects if treatment.get('depletion')])
595
            perturbation_treatment_summaries = sorted([treatment.get('summary')[13:]
1✔
596
                                                      for treatment in treatment_objects if not treatment.get('depletion')])
597
            if depleted_treatment_summaries:
1✔
598
                summary_terms += f' depleted of {", ".join(depleted_treatment_summaries)},'
1✔
599
            if perturbation_treatment_summaries:
1✔
600
                summary_terms += f' treated with {", ".join(perturbation_treatment_summaries)},'
1✔
601

602
        # modification summaries are appended to the end of the summary
603
        if (modifications and
1✔
604
                biosample_type in biosample_subschemas):
605
            modification_objects = [request.embed(modification) for modification in modifications]
1✔
606
            modification_summaries = sorted([modification.get('summary') for modification in modification_objects])
1✔
607
            if modification_summaries:
1✔
608
                summary_terms += f' modified with {", ".join(modification_summaries)},'
1✔
609

610
        # construct library set overview is appended to the end of the summary
611
        if (construct_library_sets and
1✔
612
                biosample_type in biosample_subschemas):
613
            verb = 'transfected with'
1✔
614
            if time_post_library_delivery:
1✔
615
                verb = f'{time_post_library_delivery} {time_post_library_delivery_units}(s) after transfection with'
1✔
616
            library_summaries = set()
1✔
617
            for construct_library_set in construct_library_sets:
1✔
618
                construct_library_set_object = request.embed(
1✔
619
                    construct_library_set, '@@object_with_select_calculated_properties?field=summary')
620
                library_summaries.add(construct_library_set_object['summary'])
1✔
621
            if nucleic_acid_delivery:
1✔
622
                if nucleic_acid_delivery == 'lentiviral transduction':
1✔
623
                    verb = 'transduced (lentivirus) with'
×
624
                elif nucleic_acid_delivery == 'adenoviral transduction':
1✔
625
                    verb = 'transduced (adenovirus) with'
×
626
            if len(library_summaries) == 1:
1✔
627
                library_summaries = ', '.join(library_summaries)
1✔
628
                if moi:
1✔
629
                    summary_terms += f' {verb} a {library_summaries} (MOI of {moi}),'
1✔
630
                else:
631
                    summary_terms += f' {verb} a {library_summaries},'
1✔
632
            else:
633
                if moi:
1✔
634
                    summary_terms += f' {verb} multiple libraries (MOI of {moi}),'
×
635
                else:
636
                    summary_terms += f' {verb} multiple libraries,'
1✔
637

638
        # growth media is appended to the end of the summary
639
        if (growth_medium and biosample_type in ['in_vitro_system']):
1✔
640
            summary_terms += f' grown in {growth_medium}'
1✔
641

642
        return summary_terms.strip(',')
1✔
643

644
    @calculated_property(schema={
1✔
645
        'title': 'Biosample Parts',
646
        'type': 'array',
647
        'description': 'The parts into which this sample has been divided.',
648
        'minItems': 1,
649
        'uniqueItems': True,
650
        'items': {
651
            'title': 'Biosample Part',
652
            'type': 'string',
653
            'linkFrom': 'Biosample.part_of',
654
        },
655
        'notSubmittable': True,
656
    })
657
    def parts(self, request, parts):
1✔
658
        return paths_filtered_by_status(request, parts) or None
1✔
659

660
    @calculated_property(schema={
1✔
661
        'title': 'Pooled In',
662
        'type': 'array',
663
        'description': 'The pooled samples in which this sample is included.',
664
        'minItems': 1,
665
        'uniqueItems': True,
666
        'items': {
667
            'title': 'Biosample Pooled In',
668
            'type': 'string',
669
            'linkFrom': 'Biosample.pooled_from',
670
        },
671
        'notSubmittable': True,
672
    })
673
    def pooled_in(self, request, pooled_in):
1✔
674
        return paths_filtered_by_status(request, pooled_in) or None
1✔
675

676

677
@collection(
1✔
678
    name='primary-cells',
679
    unique_key='accession',
680
    properties={
681
        'title': 'Primary Cells',
682
        'description': 'Listing of primary cells',
683
    }
684
)
685
class PrimaryCell(Biosample):
1✔
686
    item_type = 'primary_cell'
1✔
687
    schema = load_schema('igvfd:schemas/primary_cell.json')
1✔
688
    embedded_with_frame = Biosample.embedded_with_frame
1✔
689
    audit_inherit = Biosample.audit_inherit
1✔
690
    set_status_up = Biosample.set_status_up + []
1✔
691
    set_status_down = Biosample.set_status_down + []
1✔
692

693
    @calculated_property(
1✔
694
        schema={
695
            'title': 'Classifications',
696
            'description': 'The general category of this type of sample.',
697
            'type': 'array',
698
            'minItems': 1,
699
            'uniqueItems': True,
700
            'items': {
701
                'title': 'Classification',
702
                'type': 'string'
703
            },
704
            'notSubmittable': True,
705
        }
706
    )
707
    def classifications(self):
1✔
708
        return [self.item_type.replace('_', ' ')]
1✔
709

710

711
@collection(
1✔
712
    name='in-vitro-systems',
713
    unique_key='accession',
714
    properties={
715
         'title': 'In Vitro Systems',
716
         'description': 'Listing of in vitro systems',
717
    })
718
class InVitroSystem(Biosample):
1✔
719
    item_type = 'in_vitro_system'
1✔
720
    schema = load_schema('igvfd:schemas/in_vitro_system.json')
1✔
721
    rev = Biosample.rev | {'demultiplexed_to': ('InVitroSystem', 'demultiplexed_from')}
1✔
722
    embedded_with_frame = Biosample.embedded_with_frame + [
1✔
723
        Path('targeted_sample_term', include=['@id', 'term_name', 'status']),
724
        Path('originated_from', include=['@id', 'accession', 'status']),
725
    ]
726
    audit_inherit = Biosample.audit_inherit
1✔
727
    set_status_up = Biosample.set_status_up + [
1✔
728
        'cell_fate_change_protocol'
729
    ]
730
    set_status_down = Biosample.set_status_down + []
1✔
731

732
    @calculated_property(schema={
1✔
733
        'title': 'Demultiplexed To',
734
        'type': 'array',
735
        'description': 'The parts into which this sample has been demultiplexed.',
736
        'minItems': 1,
737
        'uniqueItems': True,
738
        'items': {
739
            'title': 'Demultiplexed To',
740
            'type': 'string',
741
            'linkFrom': 'InVitroSystem.demultiplexed_from',
742
        },
743
        'notSubmittable': True,
744
    })
745
    def demultiplexed_to(self, request, demultiplexed_to):
1✔
746
        return paths_filtered_by_status(request, demultiplexed_to) or None
1✔
747

748

749
@collection(
1✔
750
    name='tissues',
751
    unique_key='accession',
752
    properties={
753
        'title': 'Tissues/Organs',
754
        'description': 'Listing of tissues or organs.',
755
    }
756
)
757
class Tissue(Biosample):
1✔
758
    item_type = 'tissue'
1✔
759
    schema = load_schema('igvfd:schemas/tissue.json')
1✔
760
    embedded_with_frame = Biosample.embedded_with_frame
1✔
761
    audit_inherit = Biosample.audit_inherit
1✔
762
    set_status_up = Biosample.set_status_up + []
1✔
763
    set_status_down = Biosample.set_status_down + []
1✔
764

765
    @calculated_property(
1✔
766
        schema={
767
            'title': 'Classifications',
768
            'description': 'The general category of this type of sample.',
769
            'type': 'array',
770
            'minItems': 1,
771
            'uniqueItems': True,
772
            'items': {
773
                'title': 'Classification',
774
                'type': 'string'
775
            },
776
            'notSubmittable': True,
777
        }
778
    )
779
    def classifications(self):
1✔
780
        return ['tissue/organ']
1✔
781

782

783
@collection(
1✔
784
    name='technical-samples',
785
    unique_key='accession',
786
    properties={
787
        'title': 'Technical Samples',
788
        'description': 'Listing of technical samples',
789
    }
790
)
791
class TechnicalSample(Sample):
1✔
792
    item_type = 'technical_sample'
1✔
793
    schema = load_schema('igvfd:schemas/technical_sample.json')
1✔
794
    embedded_with_frame = [
1✔
795
        Path('award', include=['@id', 'component']),
796
        Path('lab', include=['@id', 'title']),
797
        Path('sources', include=['@id', 'title', 'status']),
798
        Path('submitted_by', include=['@id', 'title']),
799
        Path('sorted_from', include=['@id', 'accession', 'status']),
800
        Path('file_sets', include=['@id', 'accession', 'summary', 'aliases',
801
             'lab', 'status', 'preferred_assay_titles', 'file_set_type']),
802
        Path('file_sets.lab', include=['title']),
803
        Path('publications', include=['@id', 'publication_identifiers', 'status']),
804
        Path('sample_terms', include=['@id', 'term_name', 'status']),
805
        Path('construct_library_sets.associated_phenotypes', include=[
806
             '@id', 'accession', 'file_set_type', 'term_name', 'status'])
807
    ]
808
    audit_inherit = Sample.audit_inherit
1✔
809
    set_status_up = Biosample.set_status_up + []
1✔
810
    set_status_down = Biosample.set_status_down + []
1✔
811

812
    @calculated_property(
1✔
813
        schema={
814
            'title': 'Summary',
815
            'type': 'string',
816
            'description': 'A summary of this sample.',
817
            'notSubmittable': True,
818
        }
819
    )
820
    def summary(self, request, sample_terms, sample_material, virtual=None, construct_library_sets=None, moi=None, nucleic_acid_delivery=None):
1✔
821
        if len(sample_terms) > 1:
1✔
822
            summary_terms = 'mixed'
×
823
        else:
824
            term_object = request.embed(sample_terms[0], '@@object?skip_calculated=true')
1✔
825
            summary_terms = term_object.get('term_name')
1✔
826

827
        summary_terms = f'{sample_material} {summary_terms}'
1✔
828

829
        if virtual:
1✔
830
            summary_terms = f'virtual {summary_terms}'
1✔
831

832
        if construct_library_sets:
1✔
833
            verb = 'transfected with'
1✔
834
            library_summaries = set()
1✔
835
            for construct_library_set in construct_library_sets:
1✔
836
                construct_library_set_object = request.embed(
1✔
837
                    construct_library_set, '@@object_with_select_calculated_properties?field=summary')
838
                library_summaries.add(construct_library_set_object['summary'])
1✔
839
            if nucleic_acid_delivery:
1✔
840
                if nucleic_acid_delivery == 'lentiviral transduction':
1✔
841
                    verb = 'transduced (lentivirus) with'
1✔
842
                elif nucleic_acid_delivery == 'adenoviral transduction':
×
843
                    verb = 'transduced (adenovirus) with'
×
844
            if len(library_summaries) == 1:
1✔
845
                library_summaries = ', '.join(library_summaries)
1✔
846
                summary_terms = f'{summary_terms} {verb} a {library_summaries}'
1✔
847
            else:
848
                summary_terms = f'{summary_terms} {verb} multiple libraries'
1✔
849
        if moi:
1✔
850
            summary_terms = f'{summary_terms} (MOI of {moi})'
1✔
851
        return summary_terms
1✔
852

853
    @calculated_property(
1✔
854
        schema={
855
            'title': 'Classifications',
856
            'description': 'The general category of this type of sample.',
857
            'type': 'array',
858
            'minItems': 1,
859
            'uniqueItems': True,
860
            'items': {
861
                'title': 'Classification',
862
                'type': 'string'
863
            },
864
            'notSubmittable': True,
865
        }
866
    )
867
    def classifications(self):
1✔
868
        return [self.item_type.replace('_', ' ')]
1✔
869

870

871
@collection(
1✔
872
    name='whole-organisms',
873
    unique_key='accession',
874
    properties={
875
        'title': 'Whole Organism Samples',
876
        'description': 'Listing of whole organism samples',
877
    }
878
)
879
class WholeOrganism(Biosample):
1✔
880
    item_type = 'whole_organism'
1✔
881
    schema = load_schema('igvfd:schemas/whole_organism.json')
1✔
882
    embedded_with_frame = Biosample.embedded_with_frame
1✔
883
    audit_inherit = Biosample.audit_inherit
1✔
884
    set_status_up = Biosample.set_status_up + []
1✔
885
    set_status_down = Biosample.set_status_down + []
1✔
886

887
    @calculated_property(
1✔
888
        schema={
889
            'title': 'Classifications',
890
            'description': 'The general category of this type of sample.',
891
            'type': 'array',
892
            'minItems': 1,
893
            'uniqueItems': True,
894
            'items': {
895
                'title': 'Classification',
896
                'type': 'string'
897
            },
898
            'notSubmittable': True,
899
        }
900
    )
901
    def classifications(self):
1✔
902
        return [self.item_type.replace('_', ' ')]
1✔
903

904

905
@collection(
1✔
906
    name='multiplexed-samples',
907
    unique_key='accession',
908
    properties={
909
        'title': 'Multiplexed Samples',
910
        'description': 'Listing of multiplexed samples',
911
    }
912
)
913
class MultiplexedSample(Sample):
1✔
914
    item_type = 'multiplexed_sample'
1✔
915
    schema = load_schema('igvfd:schemas/multiplexed_sample.json')
1✔
916
    embedded_with_frame = Sample.embedded_with_frame + [
1✔
917
        Path('multiplexed_samples', include=['@id', 'accession', '@type',
918
             'summary', 'sample_terms', 'construct_library_sets', 'disease_terms', 'donors', 'status']),
919
        Path('multiplexed_samples.sample_terms', include=['@id', 'term_name', 'status']),
920
        Path('multiplexed_samples.disease_terms', include=['@id', 'term_name', 'status']),
921
        Path('multiplexed_samples.donors', include=['@id', 'accession', 'status']),
922
    ]
923
    audit_inherit = Biosample.audit_inherit + [
1✔
924
        'multiplexed_samples'
925
    ]
926
    set_status_up = Biosample.set_status_up + [
1✔
927
        'barcode_map',
928
        'multiplexed_samples'
929
    ]
930
    set_status_down = Biosample.set_status_down + [
1✔
931
        'multiplexed_samples'
932
    ]
933

934
    @calculated_property(
1✔
935
        schema={
936
            'title': 'Summary',
937
            'type': 'string',
938
            'description': 'A summary of this sample.',
939
            'notSubmittable': True,
940
        }
941
    )
942
    def summary(self, request, multiplexed_samples=None, donors=None, sample_terms=None):
1✔
943
        # Generate sample term phrase
944
        sample_term_phrase = None
1✔
945
        if sample_terms:
1✔
946
            sample_term_term_names = []
1✔
947
            for term in sample_terms:
1✔
948
                term_object = request.embed(term, '@@object?skip_calculated=true')
1✔
949
                sample_term_term_names.append(term_object['term_name'])
1✔
950
        sample_term_phrase = compose_summary_sample_term_phrase(sample_term_term_names=sample_term_term_names,
1✔
951
                                                                cap_number=len(sample_terms))
952

953
        # Generate num of unique donors
954
        donors_phrase = None
1✔
955
        if donors:
1✔
956
            suffix = 's'
1✔
957
            if len(donors) == 1:
1✔
958
                suffix = ''
1✔
959
            donors_phrase = f'{len(donors)} donor{suffix}'
1✔
960

961
        # Generate num of samples and targeted_sample_terms in multiplexed_samples
962
        samples_phrase = None
1✔
963
        if multiplexed_samples:
1✔
964
            suffix = 's'
1✔
965
            if len(multiplexed_samples) == 1:
1✔
966
                suffix = ''
×
967
            samples_phrase = f'{len(multiplexed_samples)} sample{suffix}'
1✔
968

969
        # Generate unique targeted sample term phrase
970
        # Could nest under the same if condition above, but this is more readable
971
        targeted_sample_term_phrase = None
1✔
972
        if multiplexed_samples:
1✔
973
            targeted_sample_term_names = set()
1✔
974
            for sample in multiplexed_samples:
1✔
975
                # First, get sample object to see if it has a targeted sample term
976
                sample_object = request.embed(sample, '@@object?skip_calculated=true')
1✔
977
                if sample_object.get('targeted_sample_term'):
1✔
978
                    # Get targeted sample term object if it exists
979
                    targeted_sample_term_object = request.embed(
1✔
980
                        sample_object['targeted_sample_term'], '@@object?skip_calculated=true')
981
                    targeted_sample_term_names.add(targeted_sample_term_object['term_name'])
1✔
982
            targeted_sample_term_phrase = compose_summary_sample_term_phrase(
1✔
983
                sample_term_term_names=targeted_sample_term_names,
984
                cap_number=len(targeted_sample_term_names)
985
            )
986

987
        # Make final phrase
988
        phrases = [
1✔
989
            sample_term_phrase,
990
            donors_phrase,
991
            samples_phrase,
992
            f'induced to {targeted_sample_term_phrase}' if targeted_sample_term_phrase else None
993
        ]
994
        return f"multiplexed {', '.join([x for x in phrases if x is not None])}"
1✔
995

996
    @calculated_property(
1✔
997
        define=True,
998
        schema={
999
            'title': 'Sample Terms',
1000
            'type': 'array',
1001
            'description': 'The sample terms of the samples included in this multiplexed sample.',
1002
            'minItems': 1,
1003
            'uniqueItems': True,
1004
            'notSubmittable': True,
1005
            'items': {
1006
                'title': 'Sample Term',
1007
                'type': 'string',
1008
                'linkTo': 'SampleTerm',
1009
            }
1010
        }
1011
    )
1012
    def sample_terms(self, request, multiplexed_samples):
1✔
1013
        return collect_multiplexed_samples_prop(request, multiplexed_samples, 'sample_terms')
1✔
1014

1015
    @calculated_property(
1✔
1016
        define=True,
1017
        schema={
1018
            'title': 'Taxa',
1019
            'type': 'string',
1020
            'description': 'The species of the organism.',
1021
            'enum': [
1022
                    'Homo sapiens',
1023
                    'Mus musculus',
1024
                    'Mixed species',
1025
                    'Saccharomyces cerevisiae'
1026
            ],
1027
            'notSubmittable': True
1028
        }
1029
    )
1030
    def taxa(self, request, multiplexed_samples):
1✔
1031
        taxas = set()
1✔
1032
        if multiplexed_samples:
1✔
1033
            for sample in multiplexed_samples:
1✔
1034
                sample_object = request.embed(sample, '@@object_with_select_calculated_properties?field=taxa')
1✔
1035
                taxas.add(sample_object.get('taxa'))
1✔
1036

1037
        if len(taxas) == 1:
1✔
1038
            return list(taxas).pop()
1✔
1039
        elif len(taxas) > 1:
1✔
1040
            return 'Mixed species'
1✔
1041
        else:
1042
            return None
×
1043

1044
    @calculated_property(
1✔
1045
        schema={
1046
            'title': 'Disease Terms',
1047
            'type': 'array',
1048
            'description': 'The disease terms of the samples included in this multiplexed sample.',
1049
            'minItems': 1,
1050
            'uniqueItems': True,
1051
            'notSubmittable': True,
1052
            'items': {
1053
                'title': 'Disease Term',
1054
                'type': 'string',
1055
                'linkTo': 'PhenotypeTerm'
1056
            }
1057
        }
1058
    )
1059
    def disease_terms(self, request, multiplexed_samples):
1✔
1060
        return collect_multiplexed_samples_prop(request, multiplexed_samples, 'disease_terms')
1✔
1061

1062
    @calculated_property(
1✔
1063
        schema={
1064
            'title': 'Treatments',
1065
            'type': 'array',
1066
            'description': 'The treatments of the samples included in this multiplexed sample.',
1067
            'minItems': 1,
1068
            'uniqueItems': True,
1069
            'notSubmittable': True,
1070
            'items': {
1071
                'title': 'Treatment',
1072
                'type': 'string',
1073
                'linkTo': 'Treatment'
1074
            }
1075
        }
1076
    )
1077
    def treatments(self, request, multiplexed_samples):
1✔
1078
        return collect_multiplexed_samples_prop(request, multiplexed_samples, 'treatments')
1✔
1079

1080
    @calculated_property(
1✔
1081
        schema={
1082
            'title': 'Modifications',
1083
            'type': 'array',
1084
            'description': 'The modifications of the samples included in this multiplexed sample.',
1085
            'minItems': 1,
1086
            'uniqueItems': True,
1087
            'notSubmittable': True,
1088
            'items': {
1089
                'title': 'Modification',
1090
                'type': 'string',
1091
                'linkTo': ['CrisprModification', 'DegronModification']
1092
            }
1093
        }
1094
    )
1095
    def modifications(self, request, multiplexed_samples):
1✔
1096
        return collect_multiplexed_samples_prop(request, multiplexed_samples, 'modifications')
1✔
1097

1098
    @calculated_property(
1✔
1099
        define=True,
1100
        schema={
1101
            'title': 'Donors',
1102
            'type': 'array',
1103
            'description': 'The donors of the samples included in this multiplexed sample.',
1104
            'minItems': 1,
1105
            'uniqueItems': True,
1106
            'notSubmittable': True,
1107
            'items': {
1108
                'title': 'Donor',
1109
                'type': 'string',
1110
                'linkTo': 'Donor'
1111
            }
1112
        }
1113
    )
1114
    def donors(self, request, multiplexed_samples):
1✔
1115
        return collect_multiplexed_samples_prop(request, multiplexed_samples, 'donors')
1✔
1116

1117
    @calculated_property(
1✔
1118
        schema={
1119
            'title': 'Biomarkers',
1120
            'type': 'array',
1121
            'description': 'The biomarkers of the samples included in this multiplexed sample.',
1122
            'minItems': 1,
1123
            'uniqueItems': True,
1124
            'notSubmittable': True,
1125
            'items': {
1126
                'title': 'Biomarker',
1127
                'type': 'string',
1128
                'linkTo': 'Biomarker'
1129
            }
1130
        }
1131
    )
1132
    def biomarkers(self, request, multiplexed_samples):
1✔
1133
        return collect_multiplexed_samples_prop(request, multiplexed_samples, 'biomarkers')
1✔
1134

1135
    @calculated_property(
1✔
1136
        schema={
1137
            'title': 'Sources',
1138
            'type': 'array',
1139
            'description': 'The sources of the samples included in this multiplexed sample.',
1140
            'minItems': 1,
1141
            'uniqueItems': True,
1142
            'notSubmittable': True,
1143
            'items': {
1144
                'title': 'Source',
1145
                'type': 'string',
1146
                'linkTo': [
1147
                    'Source',
1148
                    'Lab'
1149
                ]
1150
            }
1151
        }
1152
    )
1153
    def sources(self, request, multiplexed_samples):
1✔
1154
        return collect_multiplexed_samples_prop(request, multiplexed_samples, 'sources')
1✔
1155

1156
    @calculated_property(
1✔
1157
        schema={
1158
            'title': 'Construct Library Sets',
1159
            'type': 'array',
1160
            'description': 'The construct library sets of the samples included in this multiplexed sample.',
1161
            'minItems': 1,
1162
            'uniqueItems': True,
1163
            'notSubmittable': True,
1164
            'items': {
1165
                'title': 'Construct Library Set',
1166
                'type': 'string',
1167
                'linkTo': 'ConstructLibrarySet'
1168
            }
1169
        }
1170
    )
1171
    def construct_library_sets(self, request, multiplexed_samples):
1✔
1172
        return collect_multiplexed_samples_prop(
1✔
1173
            request, multiplexed_samples, 'construct_library_sets')
1174

1175
    @calculated_property(
1✔
1176
        schema={
1177
            'title': 'Institutional Certificates',
1178
            'type': 'array',
1179
            'description': 'The institutional certificates of the samples included in this multiplexed sample.',
1180
            'minItems': 1,
1181
            'uniqueItems': True,
1182
            'notSubmittable': True,
1183
            'items': {
1184
                'title': 'Institutional Certificate',
1185
                'type': 'string',
1186
                'linkTo': 'InstitutionalCertificate'
1187
            }
1188
        }
1189
    )
1190
    def institutional_certificates(self, request, multiplexed_samples):
1✔
1191
        return collect_multiplexed_samples_prop(request, multiplexed_samples, 'institutional_certificates', skip_calculated=False)
1✔
1192

1193
    @calculated_property(
1✔
1194
        schema={
1195
            'title': 'Classifications',
1196
            'description': 'The general category of this type of sample.',
1197
            'minItems': 1,
1198
            'uniqueItems': True,
1199
            'type': 'array',
1200
            'items': {
1201
                'title': 'Classification',
1202
                'type': 'string'
1203
            },
1204
            'notSubmittable': True,
1205
        }
1206
    )
1207
    def classifications(self, request, multiplexed_samples):
1✔
1208
        # Get unique properties of individual samples' item types
1209
        sample_classfications = collect_multiplexed_samples_prop(
1✔
1210
            request, multiplexed_samples, 'classifications', skip_calculated=False)
1211
        self_classification = [self.item_type.replace('_', ' ')]
1✔
1212
        return sorted(sample_classfications + self_classification)
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc