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

IGVF-DACC / igvfd / #9533

13 Jan 2026 12:37AM UTC coverage: 91.437% (+0.06%) from 91.373%
#9533

push

coveralls-python

keenangraham
Add subfacet example

9803 of 10721 relevant lines covered (91.44%)

0.91 hits per line

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

93.74
/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
    @calculated_property(
1✔
277
        define=True,
278
        schema={
279
            'title': 'Is On AnVIL',
280
            'type': 'boolean',
281
            'description': 'Indicates whether the sample has been submitted to AnVIL.',
282
            'notSubmittable': True
283
        })
284
    def is_on_anvil(self, request, file_sets=[]):
1✔
285
        if not file_sets:
1✔
286
            return False
1✔
287
        for file_set in file_sets:
1✔
288
            file_set_object = request.embed(
1✔
289
                file_set, '@@object_with_select_calculated_properties?field=is_on_anvil')
290
            if file_set_object.get('is_on_anvil', False):
1✔
291
                return True
1✔
292
        return False
1✔
293

294

295
@abstract_collection(
1✔
296
    name='biosamples',
297
    unique_key='accession',
298
    properties={
299
        'title': 'Biosamples',
300
        'description': 'Listing of biosamples',
301
    }
302
)
303
class Biosample(Sample):
1✔
304
    item_type = 'biosample'
1✔
305
    base_types = ['Biosample'] + Sample.base_types
1✔
306
    schema = load_schema('igvfd:schemas/biosample.json')
1✔
307
    rev = Sample.rev | {'parts': ('Biosample', 'part_of'),
1✔
308
                        'pooled_in': ('Biosample', 'pooled_from')}
309
    embedded_with_frame = Sample.embedded_with_frame
1✔
310

311
    audit_inherit = Sample.audit_inherit + [
1✔
312
        'disease_terms',
313
        'treatments',
314
        'modifications',
315
    ]
316

317
    set_status_up = Sample.set_status_up + [
1✔
318
        'biomarkers',
319
        'donors',
320
        'modifications',
321
        'originated_from',
322
        'part_of',
323
        'pooled_from',
324
        'sample_terms',
325
        'targeted_sample_term',
326
    ]
327
    set_status_down = Sample.set_status_down + []
1✔
328

329
    @calculated_property(
1✔
330
        define=True,
331
        schema={
332
            'title': 'Sex',
333
            'type': 'string',
334
            'enum': [
335
                'female',
336
                'male',
337
                'mixed',
338
                'unspecified'
339
            ],
340
            'notSubmittable': True,
341
        }
342
    )
343
    def sex(self, request, donors=None):
1✔
344
        sexes = set()
1✔
345
        if donors:
1✔
346
            for d in donors:
1✔
347
                donor_object = request.embed(d, '@@object')
1✔
348
                if donor_object.get('sex'):
1✔
349
                    sexes.add(donor_object.get('sex'))
1✔
350
        if len(sexes) == 1:
1✔
351
            return list(sexes).pop()
1✔
352
        elif len(sexes) > 1:
1✔
353
            return 'mixed'
1✔
354

355
    @calculated_property(
1✔
356
        define=True,
357
        schema={
358
            'title': 'Age',
359
            'description': 'Age of organism at the time of collection of the sample.',
360
            'type': 'string',
361
            'pattern': '^((\\d+(\\.[1-9])?(\\-\\d+(\\.[1-9])?)?)|(unknown)|([1-8]?\\d)|(90 or above))$',
362
            'notSubmittable': True,
363
        }
364
    )
365
    def age(self, lower_bound_age=None, upper_bound_age=None, age_units=None):
1✔
366
        if lower_bound_age and upper_bound_age:
1✔
367
            if lower_bound_age == upper_bound_age:
1✔
368
                if lower_bound_age == 90 and upper_bound_age == 90 and age_units == 'year':
1✔
369
                    return '90 or above'
1✔
370
                return str(lower_bound_age)
1✔
371
            return str(lower_bound_age) + '-' + str(upper_bound_age)
1✔
372
        else:
373
            return 'unknown'
1✔
374

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

396
    @calculated_property(
1✔
397
        define=True,
398
        schema={
399
            'title': 'Lower Bound Age In Hours',
400
            'description': 'Lower bound of age of organism in hours at the time of collection of the sample .',
401
            'type': 'number',
402
            'notSubmittable': True,
403
        }
404
    )
405
    def lower_bound_age_in_hours(self, lower_bound_age=None, age_units=None):
1✔
406
        conversion_factors = {
1✔
407
            'minute': 1 / 60,
408
            'hour': 1,
409
            'day': 24,
410
            'week': 168,
411
            'month': 720,
412
            'year': 8760
413
        }
414
        if lower_bound_age:
1✔
415
            return lower_bound_age * conversion_factors[age_units]
1✔
416

417
    @calculated_property(
1✔
418
        define=True,
419
        schema={
420
            'title': 'Taxa',
421
            'type': 'string',
422
            'description': 'The species of the organism.',
423
            'enum': [
424
                    'Homo sapiens',
425
                    'Mus musculus'
426
            ],
427
            'notSubmittable': True
428
        }
429
    )
430
    def taxa(self, request, donors):
1✔
431
        taxas = set()
1✔
432
        if donors:
1✔
433
            for d in donors:
1✔
434
                donor_object = request.embed(d, '@@object?skip_calculated=true')
1✔
435
                if donor_object.get('taxa'):
1✔
436
                    taxas.add(donor_object.get('taxa'))
1✔
437

438
        if len(taxas) == 1:
1✔
439
            return list(taxas).pop()
1✔
440

441
    @calculated_property(
1✔
442
        schema={
443
            'title': 'Summary',
444
            'type': 'string',
445
            'description': 'A summary of the sample.',
446
            'notSubmittable': True,
447
        }
448
    )
449
    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✔
450
        term_object = request.embed(sample_terms[0], '@@object?skip_calculated=true')
1✔
451
        term_name = term_object.get('term_name')
1✔
452
        biosample_type = self.item_type
1✔
453
        biosample_subschemas = ['primary_cell', 'in_vitro_system', 'tissue', 'whole_organism']
1✔
454

455
        # sample term customization based on biosample type
456
        if biosample_type == 'primary_cell':
1✔
457
            if 'cell' not in term_name:
1✔
458
                summary_terms = f'{term_name} cell'
1✔
459
            else:
460
                summary_terms = term_name
1✔
461
        elif biosample_type == 'in_vitro_system':
1✔
462
            if len(classifications) == 1:
1✔
463
                summary_terms = f'{term_name} {classifications[0]}'
1✔
464
                if classifications[0] in term_name:
1✔
465
                    summary_terms = term_name
1✔
466
                elif 'cell' in classifications[0] and 'cell' in term_name:
1✔
467
                    summary_terms = term_name.replace('cell', classifications[0])
1✔
468
                elif 'tissue/organ' in classifications[0] and ('tissue' in term_name or 'organ' in term_name):
1✔
469
                    summary_terms = term_name.replace('tissue', classifications[0])
×
470
                elif 'gastruloid' in classifications[0]:
1✔
471
                    summary_terms = term_name.replace('gastruloid', '')
1✔
472
            elif len(classifications) == 2:
1✔
473
                if 'differentiated cell specimen' in classifications and 'pooled cell specimen' in classifications:
1✔
474
                    summary_terms = f'{term_name} pooled differentiated cell specimen'
1✔
475
                    if 'cell' in term_name:
1✔
476
                        summary_terms = term_name.replace('cell', 'pooled differentiated cell specimen')
×
477
                elif 'reprogrammed cell specimen' in classifications and 'pooled cell specimen' in classifications:
1✔
478
                    summary_terms = f'{term_name} pooled reprogrammed cell specimen'
×
479
                    if 'cell' in term_name:
×
480
                        summary_terms = term_name.replace('cell', 'pooled reprogrammed cell specimen')
×
481
                elif 'cell line' in classifications and 'pooled cell specimen' in classifications:
1✔
482
                    summary_terms = f'{term_name} pooled cell specimen'
1✔
483
                    if 'cell' in term_name:
1✔
484
                        summary_terms = term_name.replace('cell', 'pooled cell specimen')
×
485
        elif biosample_type == 'tissue':
1✔
486
            if 'tissue' not in term_name and 'organ' not in term_name:
1✔
487
                summary_terms = f'{term_name} tissue/organ'
1✔
488
            else:
489
                summary_terms = term_name
1✔
490
        elif biosample_type == 'whole_organism':
1✔
491
            summary_terms = concat_numeric_and_units(len(donors), term_name, no_numeric_on_one=True)
1✔
492
        elif len(sample_terms) > 1:
×
493
            summary_terms = 'mixed'
×
494

495
        # Prepend biosample qualifiers if they exist
496
        if biosample_qualifiers:
1✔
497
            qualifiers_string = ', '.join(biosample_qualifiers)
1✔
498
            summary_terms = f'{qualifiers_string} {summary_terms}'
1✔
499

500
        # embryonic is prepended to the start of the summary
501
        if (embryonic and
1✔
502
                biosample_type in ['primary_cell', 'tissue']):
503
            summary_terms = f'embryonic {summary_terms}'
1✔
504

505
        # sex and age are prepended to the start of the summary
506
        sex_and_age_ethnicity = [None, None, None]
1✔
507
        if (sex and
1✔
508
                biosample_type in biosample_subschemas):
509
            if sex != 'unspecified':
1✔
510
                if sex == 'mixed':
1✔
511
                    sex_and_age_ethnicity[0] = 'mixed sex'
1✔
512
                else:
513
                    sex_and_age_ethnicity[0] = sex
1✔
514

515
        if (age != 'unknown' and
1✔
516
                biosample_type in ['primary_cell', 'tissue', 'whole_organism']):
517
            age = concat_numeric_and_units(age, age_units)
1✔
518
            sex_and_age_ethnicity[1] = age
1✔
519

520
        if (donors and taxa == 'Homo sapiens'):
1✔
521
            ethnicity_set = set()
1✔
522
            for donor in donors:
1✔
523
                donor_object = request.embed(donor, '@@object?skip_calculated=true')
1✔
524
                ethnicity_set = ethnicity_set | set(donor_object.get('ethnicities', []))
1✔
525
            ethnicity_set = ', '.join(sorted(ethnicity_set))
1✔
526
            if ethnicity_set:
1✔
527
                sex_and_age_ethnicity[2] = ethnicity_set
1✔
528

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

532
        # taxa of the donor(s) is prepended to the start of the summary
533
        if (donors and
1✔
534
                biosample_type in biosample_subschemas):
535
            if not taxa or taxa == 'Mus musculus':
1✔
536
                taxa_set = set()
1✔
537
                strains_set = set()
1✔
538
                for donor in donors:
1✔
539
                    donor_object = request.embed(donor, '@@object?skip_calculated=true')
1✔
540
                    taxa_set.add(donor_object['taxa'])
1✔
541
                    if donor_object['taxa'] == 'Mus musculus':
1✔
542
                        strains_set.add(donor_object.get('strain', ''))
1✔
543
                strains = ', '.join(sorted(strains_set))
1✔
544
                taxa_list = sorted(taxa_set)
1✔
545
                if 'Mus musculus' in taxa_list:
1✔
546
                    mouse_index = taxa_list.index('Mus musculus')
1✔
547
                    taxa_list[mouse_index] += f' {strains}'
1✔
548
                taxa = ' and '.join(taxa_list)
1✔
549
            summary_terms = f'{taxa} {summary_terms}'
1✔
550

551
        # virtual is prepended to the start of the summary
552
        if (virtual and
1✔
553
                biosample_type in ['primary_cell', 'in_vitro_system', 'tissue']):
554
            summary_terms = f'virtual {summary_terms}'
1✔
555

556
        if donors and classifications is not None and 'pooled cell specimen' in classifications:
1✔
557
            suffix = 's'
1✔
558
            if len(donors) == 1:
1✔
559
                suffix = ''
1✔
560
            summary_terms += f' ({len(donors)} donor{suffix})'
1✔
561

562
        # time post change and targeted term are appended to the end of the summary
563
        if (time_post_change and
1✔
564
                biosample_type in ['in_vitro_system']):
565
            time_post_change = concat_numeric_and_units(time_post_change, time_post_change_units)
1✔
566
            if targeted_sample_term:
1✔
567
                targeted_term_object = request.embed(targeted_sample_term, '@@object?skip_calculated=true')
1✔
568
                targeted_term_name = targeted_term_object.get('term_name')
1✔
569
                summary_terms += f' induced to {targeted_term_name} for {time_post_change},'
1✔
570
            else:
571
                summary_terms += f' induced for {time_post_change}'
×
572

573
        # cellular sub pool is appended to the end of the summary in parentheses
574
        if (cellular_sub_pool and
1✔
575
                biosample_type in ['primary_cell', 'in_vitro_system', 'tissue']):
576
            summary_terms += f' (cellular sub pool: {cellular_sub_pool})'
1✔
577

578
        # sorted from detail is appended to the end of the summary
579
        if (sorted_from_detail and
1✔
580
                biosample_type in ['primary_cell', 'in_vitro_system']):
581
            summary_terms += f' (sorting details: {sorted_from_detail})'
1✔
582

583
        # biomarker summaries are appended to the end of the summary
584
        if (biomarkers and
1✔
585
                biosample_type in ['primary_cell', 'in_vitro_system']):
586
            biomarker_summaries = []
1✔
587
            for biomarker in biomarkers:
1✔
588
                biomarker_object = request.embed(biomarker)
1✔
589
                if biomarker_object['quantification'] in ['positive', 'negative']:
1✔
590
                    biomarker_summary = f'{biomarker_object["quantification"]} detection of {biomarker_object["name"]}'
1✔
591
                elif biomarker_object['quantification'] in ['high', 'intermediate', 'low']:
1✔
592
                    if biomarker_object.get('classification') == 'marker gene':
1✔
593
                        biomarker_summary = f'{biomarker_object["quantification"]} expression of {biomarker_object["name"]}'
×
594
                    else:
595
                        biomarker_summary = f'{biomarker_object["quantification"]} level of {biomarker_object["name"]}'
1✔
596
                biomarker_summaries.append(biomarker_summary)
1✔
597
                biomarker_summaries = sorted(biomarker_summaries)
1✔
598
            summary_terms += f' characterized by {", ".join(biomarker_summaries)},'
1✔
599

600
        # disease terms are appended to the end of the summary
601
        if (disease_terms and
1✔
602
                biosample_type in biosample_subschemas):
603
            phenotype_term_names = sorted([request.embed(disease_term).get('term_name')
1✔
604
                                          for disease_term in disease_terms])
605
            summary_terms += f' associated with {", ".join(phenotype_term_names)},'
1✔
606

607
        # treatment summaries are appended to the end of the summary
608
        if (treatments and
1✔
609
                biosample_type in biosample_subschemas):
610
            treatment_objects = [request.embed(treatment) for treatment in treatments]
1✔
611

612
            depleted_treatment_summaries = sorted([
1✔
613
                treatment.get('summary')[13:]
614
                for treatment in treatment_objects
615
                if treatment.get('depletion')
616
            ])
617
            if depleted_treatment_summaries:
1✔
618
                summary_terms += f' depleted of {", ".join(depleted_treatment_summaries)},'
1✔
619

620
            purpose_to_verb = {
1✔
621
                'activation': 'activated',
622
                'acute activation': 'acutely activated',
623
                'chronic activation': 'chronically activated',
624
                'agonist': 'agonized',
625
                'antagonist': 'antagonized',
626
                'control': 'treated with a control',
627
                'differentiation': 'differentiated',
628
                'de-differentiation': 'de-differentiated',
629
                'perturbation': 'perturbed',
630
                'selection': 'selected',
631
                'stimulation': 'stimulated'
632
            }
633
            purpose_to_summaries = {}
1✔
634
            for treatment in treatment_objects:
1✔
635
                if not (treatment.get('depletion')):
1✔
636
                    purpose = treatment['purpose']
1✔
637
                    if purpose not in purpose_to_summaries:
1✔
638
                        purpose_to_summaries[purpose] = []
1✔
639
                    purpose_to_summaries[purpose].append(treatment['summary'][13:])
1✔
640

641
            for purpose in sorted(purpose_to_summaries):
1✔
642
                verb = purpose_to_verb.get(purpose, 'treated with')
1✔
643
                unique_summaries = sorted(set(purpose_to_summaries[purpose]))
1✔
644
                perturbation_summaries = ', '.join(unique_summaries)
1✔
645
                summary_terms += f' {verb} with {perturbation_summaries},'
1✔
646

647
        # modification summaries are appended to the end of the summary
648
        if (modifications and
1✔
649
                biosample_type in biosample_subschemas):
650
            modification_objects = [request.embed(modification) for modification in modifications]
1✔
651
            modification_summaries = sorted([modification.get('summary') for modification in modification_objects])
1✔
652
            if modification_summaries:
1✔
653
                summary_terms += f' modified with {", ".join(modification_summaries)},'
1✔
654

655
        # construct library set overview is appended to the end of the summary
656
        if (construct_library_sets and
1✔
657
                biosample_type in biosample_subschemas):
658

659
            if nucleic_acid_delivery == 'lentiviral transduction':
1✔
660
                verb = 'transduced (lentivirus) with'
×
661
                noun = 'transduction (lentivirus) with'
×
662
            elif nucleic_acid_delivery == 'adenoviral transduction':
1✔
663
                verb = 'transduced (adenovirus) with'
1✔
664
                noun = 'transduction (adenovirus) with'
1✔
665
            else:
666
                verb = 'transfected with'
1✔
667
                noun = 'transfection with'
1✔
668

669
            if time_post_library_delivery is not None:
1✔
670
                verb = (
1✔
671
                    f'{time_post_library_delivery} '
672
                    f'{time_post_library_delivery_units}(s) after {noun}'
673
                )
674

675
            library_summaries = set()
1✔
676
            for construct_library_set in construct_library_sets:
1✔
677
                construct_library_set_object = request.embed(
1✔
678
                    construct_library_set, '@@object_with_select_calculated_properties?field=summary')
679
                library_summaries.add(construct_library_set_object['summary'])
1✔
680

681
            if len(library_summaries) == 1:
1✔
682
                library_summaries = ', '.join(library_summaries)
1✔
683
                if moi:
1✔
684
                    summary_terms += f' {verb} a {library_summaries} (MOI of {moi}),'
1✔
685
                else:
686
                    summary_terms += f' {verb} a {library_summaries},'
1✔
687
            else:
688
                if moi:
1✔
689
                    summary_terms += f' {verb} multiple libraries (MOI of {moi}),'
×
690
                else:
691
                    summary_terms += f' {verb} multiple libraries,'
1✔
692

693
        # growth media is appended to the end of the summary
694
        if (growth_medium and biosample_type in ['in_vitro_system']):
1✔
695
            summary_terms += f' grown in {growth_medium}'
1✔
696

697
        return summary_terms.strip(',')
1✔
698

699
    @calculated_property(schema={
1✔
700
        'title': 'Biosample Parts',
701
        'type': 'array',
702
        'description': 'The parts into which this sample has been divided.',
703
        'minItems': 1,
704
        'uniqueItems': True,
705
        'items': {
706
            'title': 'Biosample Part',
707
            'type': 'string',
708
            'linkFrom': 'Biosample.part_of',
709
        },
710
        'notSubmittable': True,
711
    })
712
    def parts(self, request, parts):
1✔
713
        return paths_filtered_by_status(request, parts) or None
1✔
714

715
    @calculated_property(schema={
1✔
716
        'title': 'Pooled In',
717
        'type': 'array',
718
        'description': 'The pooled samples in which this sample is included.',
719
        'minItems': 1,
720
        'uniqueItems': True,
721
        'items': {
722
            'title': 'Biosample Pooled In',
723
            'type': 'string',
724
            'linkFrom': 'Biosample.pooled_from',
725
        },
726
        'notSubmittable': True,
727
    })
728
    def pooled_in(self, request, pooled_in):
1✔
729
        return paths_filtered_by_status(request, pooled_in) or None
1✔
730

731

732
@collection(
1✔
733
    name='primary-cells',
734
    unique_key='accession',
735
    properties={
736
        'title': 'Primary Cells',
737
        'description': 'Listing of primary cells',
738
    }
739
)
740
class PrimaryCell(Biosample):
1✔
741
    item_type = 'primary_cell'
1✔
742
    schema = load_schema('igvfd:schemas/primary_cell.json')
1✔
743
    embedded_with_frame = Biosample.embedded_with_frame
1✔
744
    audit_inherit = Biosample.audit_inherit
1✔
745
    set_status_up = Biosample.set_status_up + []
1✔
746
    set_status_down = Biosample.set_status_down + []
1✔
747

748
    @calculated_property(
1✔
749
        schema={
750
            'title': 'Classifications',
751
            'description': 'The general category of this type of sample.',
752
            'type': 'array',
753
            'minItems': 1,
754
            'uniqueItems': True,
755
            'items': {
756
                'title': 'Classification',
757
                'type': 'string'
758
            },
759
            'notSubmittable': True,
760
        }
761
    )
762
    def classifications(self):
1✔
763
        return [self.item_type.replace('_', ' ')]
1✔
764

765

766
@collection(
1✔
767
    name='in-vitro-systems',
768
    unique_key='accession',
769
    properties={
770
         'title': 'In Vitro Systems',
771
         'description': 'Listing of in vitro systems',
772
    })
773
class InVitroSystem(Biosample):
1✔
774
    item_type = 'in_vitro_system'
1✔
775
    schema = load_schema('igvfd:schemas/in_vitro_system.json')
1✔
776
    rev = Biosample.rev | {'demultiplexed_to': ('InVitroSystem', 'demultiplexed_from')}
1✔
777
    embedded_with_frame = Biosample.embedded_with_frame + [
1✔
778
        Path('targeted_sample_term', include=['@id', 'term_name', 'status']),
779
        Path('originated_from', include=['@id', 'accession', 'status']),
780
    ]
781
    audit_inherit = Biosample.audit_inherit
1✔
782
    set_status_up = Biosample.set_status_up + [
1✔
783
        'cell_fate_change_protocol'
784
    ]
785
    set_status_down = Biosample.set_status_down + []
1✔
786

787
    @calculated_property(schema={
1✔
788
        'title': 'Demultiplexed To',
789
        'type': 'array',
790
        'description': 'The parts into which this sample has been demultiplexed.',
791
        'minItems': 1,
792
        'uniqueItems': True,
793
        'items': {
794
            'title': 'Demultiplexed To',
795
            'type': 'string',
796
            'linkFrom': 'InVitroSystem.demultiplexed_from',
797
        },
798
        'notSubmittable': True,
799
    })
800
    def demultiplexed_to(self, request, demultiplexed_to):
1✔
801
        return paths_filtered_by_status(request, demultiplexed_to) or None
1✔
802

803

804
@collection(
1✔
805
    name='tissues',
806
    unique_key='accession',
807
    properties={
808
        'title': 'Tissues/Organs',
809
        'description': 'Listing of tissues or organs.',
810
    }
811
)
812
class Tissue(Biosample):
1✔
813
    item_type = 'tissue'
1✔
814
    schema = load_schema('igvfd:schemas/tissue.json')
1✔
815
    embedded_with_frame = Biosample.embedded_with_frame
1✔
816
    audit_inherit = Biosample.audit_inherit
1✔
817
    set_status_up = Biosample.set_status_up + []
1✔
818
    set_status_down = Biosample.set_status_down + []
1✔
819

820
    @calculated_property(
1✔
821
        schema={
822
            'title': 'Classifications',
823
            'description': 'The general category of this type of sample.',
824
            'type': 'array',
825
            'minItems': 1,
826
            'uniqueItems': True,
827
            'items': {
828
                'title': 'Classification',
829
                'type': 'string'
830
            },
831
            'notSubmittable': True,
832
        }
833
    )
834
    def classifications(self):
1✔
835
        return ['tissue/organ']
1✔
836

837

838
@collection(
1✔
839
    name='technical-samples',
840
    unique_key='accession',
841
    properties={
842
        'title': 'Technical Samples',
843
        'description': 'Listing of technical samples',
844
    }
845
)
846
class TechnicalSample(Sample):
1✔
847
    item_type = 'technical_sample'
1✔
848
    schema = load_schema('igvfd:schemas/technical_sample.json')
1✔
849
    embedded_with_frame = [
1✔
850
        Path('award', include=['@id', 'component']),
851
        Path('lab', include=['@id', 'title']),
852
        Path('sources', include=['@id', 'title', 'status']),
853
        Path('submitted_by', include=['@id', 'title']),
854
        Path('sorted_from', include=['@id', 'accession', 'status']),
855
        Path('file_sets', include=['@id', 'accession', 'summary', 'aliases',
856
             'lab', 'status', 'preferred_assay_titles', 'file_set_type']),
857
        Path('file_sets.lab', include=['title']),
858
        Path('publications', include=['@id', 'publication_identifiers', 'status']),
859
        Path('sample_terms', include=['@id', 'term_name', 'status']),
860
        Path('construct_library_sets.associated_phenotypes', include=[
861
             '@id', 'accession', 'file_set_type', 'term_name', 'status'])
862
    ]
863
    audit_inherit = Sample.audit_inherit
1✔
864
    set_status_up = Biosample.set_status_up + []
1✔
865
    set_status_down = Biosample.set_status_down + []
1✔
866
    rev = Sample.rev | {'parts': ('TechnicalSample', 'part_of')}
1✔
867

868
    @calculated_property(
1✔
869
        schema={
870
            'title': 'Summary',
871
            'type': 'string',
872
            'description': 'A summary of this sample.',
873
            'notSubmittable': True,
874
        }
875
    )
876
    def summary(self, request, sample_terms, sample_material, virtual=None, construct_library_sets=None, moi=None, nucleic_acid_delivery=None):
1✔
877
        if len(sample_terms) > 1:
1✔
878
            summary_terms = 'mixed'
×
879
        else:
880
            term_object = request.embed(sample_terms[0], '@@object?skip_calculated=true')
1✔
881
            summary_terms = term_object.get('term_name')
1✔
882

883
        summary_terms = f'{sample_material} {summary_terms}'
1✔
884

885
        if virtual:
1✔
886
            summary_terms = f'virtual {summary_terms}'
1✔
887

888
        if construct_library_sets:
1✔
889
            verb = 'transfected with'
1✔
890
            library_summaries = set()
1✔
891
            for construct_library_set in construct_library_sets:
1✔
892
                construct_library_set_object = request.embed(
1✔
893
                    construct_library_set, '@@object_with_select_calculated_properties?field=summary')
894
                library_summaries.add(construct_library_set_object['summary'])
1✔
895
            if nucleic_acid_delivery:
1✔
896
                if nucleic_acid_delivery == 'lentiviral transduction':
1✔
897
                    verb = 'transduced (lentivirus) with'
1✔
898
                elif nucleic_acid_delivery == 'adenoviral transduction':
×
899
                    verb = 'transduced (adenovirus) with'
×
900
            if len(library_summaries) == 1:
1✔
901
                library_summaries = ', '.join(library_summaries)
1✔
902
                summary_terms = f'{summary_terms} {verb} a {library_summaries}'
1✔
903
            else:
904
                summary_terms = f'{summary_terms} {verb} multiple libraries'
1✔
905
        if moi:
1✔
906
            summary_terms = f'{summary_terms} (MOI of {moi})'
1✔
907
        return summary_terms
1✔
908

909
    @calculated_property(
1✔
910
        schema={
911
            'title': 'Classifications',
912
            'description': 'The general category of this type of sample.',
913
            'type': 'array',
914
            'minItems': 1,
915
            'uniqueItems': True,
916
            'items': {
917
                'title': 'Classification',
918
                'type': 'string'
919
            },
920
            'notSubmittable': True,
921
        }
922
    )
923
    def classifications(self):
1✔
924
        return [self.item_type.replace('_', ' ')]
1✔
925

926
    @calculated_property(
1✔
927
        schema={
928
            'title': 'Technical Sample Parts',
929
            'type': 'array',
930
            'description': 'The parts into which this sample has been divided.',
931
            'minItems': 1,
932
            'uniqueItems': True,
933
            'items': {
934
                'title': 'Technical Sample Part',
935
                'type': 'string',
936
                'linkFrom': 'TechnicalSample.part_of',
937
            },
938
            'notSubmittable': True,
939
        }
940
    )
941
    def parts(self, request, parts):
1✔
942
        return paths_filtered_by_status(request, parts) or None
1✔
943

944

945
@collection(
1✔
946
    name='whole-organisms',
947
    unique_key='accession',
948
    properties={
949
        'title': 'Whole Organism Samples',
950
        'description': 'Listing of whole organism samples',
951
    }
952
)
953
class WholeOrganism(Biosample):
1✔
954
    item_type = 'whole_organism'
1✔
955
    schema = load_schema('igvfd:schemas/whole_organism.json')
1✔
956
    embedded_with_frame = Biosample.embedded_with_frame
1✔
957
    audit_inherit = Biosample.audit_inherit
1✔
958
    set_status_up = Biosample.set_status_up + []
1✔
959
    set_status_down = Biosample.set_status_down + []
1✔
960

961
    @calculated_property(
1✔
962
        schema={
963
            'title': 'Classifications',
964
            'description': 'The general category of this type of sample.',
965
            'type': 'array',
966
            'minItems': 1,
967
            'uniqueItems': True,
968
            'items': {
969
                'title': 'Classification',
970
                'type': 'string'
971
            },
972
            'notSubmittable': True,
973
        }
974
    )
975
    def classifications(self):
1✔
976
        return [self.item_type.replace('_', ' ')]
1✔
977

978

979
@collection(
1✔
980
    name='multiplexed-samples',
981
    unique_key='accession',
982
    properties={
983
        'title': 'Multiplexed Samples',
984
        'description': 'Listing of multiplexed samples',
985
    }
986
)
987
class MultiplexedSample(Sample):
1✔
988
    item_type = 'multiplexed_sample'
1✔
989
    schema = load_schema('igvfd:schemas/multiplexed_sample.json')
1✔
990
    embedded_with_frame = Sample.embedded_with_frame + [
1✔
991
        Path('multiplexed_samples', include=['@id', 'accession', '@type',
992
             'summary', 'sample_terms', 'construct_library_sets', 'disease_terms', 'donors', 'status']),
993
        Path('multiplexed_samples.sample_terms', include=['@id', 'term_name', 'status']),
994
        Path('multiplexed_samples.disease_terms', include=['@id', 'term_name', 'status']),
995
        Path('multiplexed_samples.donors', include=['@id', 'accession', 'status']),
996
    ]
997
    audit_inherit = Biosample.audit_inherit + [
1✔
998
        'multiplexed_samples'
999
    ]
1000
    set_status_up = Biosample.set_status_up + [
1✔
1001
        'barcode_map',
1002
        'multiplexed_samples'
1003
    ]
1004
    set_status_down = Biosample.set_status_down + [
1✔
1005
        'multiplexed_samples'
1006
    ]
1007

1008
    @calculated_property(
1✔
1009
        schema={
1010
            'title': 'Summary',
1011
            'type': 'string',
1012
            'description': 'A summary of this sample.',
1013
            'notSubmittable': True,
1014
        }
1015
    )
1016
    def summary(self, request, multiplexed_samples=None, donors=None, sample_terms=None):
1✔
1017
        # Generate sample term phrase
1018
        sample_term_phrase = None
1✔
1019
        if sample_terms:
1✔
1020
            sample_term_term_names = []
1✔
1021
            for term in sample_terms:
1✔
1022
                term_object = request.embed(term, '@@object?skip_calculated=true')
1✔
1023
                sample_term_term_names.append(term_object['term_name'])
1✔
1024
        sample_term_phrase = compose_summary_sample_term_phrase(sample_term_term_names=sample_term_term_names,
1✔
1025
                                                                cap_number=len(sample_terms))
1026

1027
        # Generate num of unique donors
1028
        donors_phrase = None
1✔
1029
        if donors:
1✔
1030
            suffix = 's'
1✔
1031
            if len(donors) == 1:
1✔
1032
                suffix = ''
1✔
1033
            donors_phrase = f'{len(donors)} donor{suffix}'
1✔
1034

1035
        # Generate num of samples and targeted_sample_terms in multiplexed_samples
1036
        samples_phrase = None
1✔
1037
        if multiplexed_samples:
1✔
1038
            suffix = 's'
1✔
1039
            if len(multiplexed_samples) == 1:
1✔
1040
                suffix = ''
×
1041
            samples_phrase = f'{len(multiplexed_samples)} sample{suffix}'
1✔
1042

1043
        # Generate unique targeted sample term phrase
1044
        # Could nest under the same if condition above, but this is more readable
1045
        targeted_sample_term_phrase = None
1✔
1046
        if multiplexed_samples:
1✔
1047
            targeted_sample_term_names = set()
1✔
1048
            for sample in multiplexed_samples:
1✔
1049
                # First, get sample object to see if it has a targeted sample term
1050
                sample_object = request.embed(sample, '@@object?skip_calculated=true')
1✔
1051
                if sample_object.get('targeted_sample_term'):
1✔
1052
                    # Get targeted sample term object if it exists
1053
                    targeted_sample_term_object = request.embed(
1✔
1054
                        sample_object['targeted_sample_term'], '@@object?skip_calculated=true')
1055
                    targeted_sample_term_names.add(targeted_sample_term_object['term_name'])
1✔
1056
            targeted_sample_term_phrase = compose_summary_sample_term_phrase(
1✔
1057
                sample_term_term_names=targeted_sample_term_names,
1058
                cap_number=len(targeted_sample_term_names)
1059
            )
1060

1061
        # Make final phrase
1062
        phrases = [
1✔
1063
            sample_term_phrase,
1064
            donors_phrase,
1065
            samples_phrase,
1066
            f'induced to {targeted_sample_term_phrase}' if targeted_sample_term_phrase else None
1067
        ]
1068
        return f"multiplexed {', '.join([x for x in phrases if x is not None])}"
1✔
1069

1070
    @calculated_property(
1✔
1071
        define=True,
1072
        schema={
1073
            'title': 'Sample Terms',
1074
            'type': 'array',
1075
            'description': 'The sample terms of the samples included in this multiplexed sample.',
1076
            'minItems': 1,
1077
            'uniqueItems': True,
1078
            'notSubmittable': True,
1079
            'items': {
1080
                'title': 'Sample Term',
1081
                'type': 'string',
1082
                'linkTo': 'SampleTerm',
1083
            }
1084
        }
1085
    )
1086
    def sample_terms(self, request, multiplexed_samples):
1✔
1087
        return collect_multiplexed_samples_prop(request, multiplexed_samples, 'sample_terms')
1✔
1088

1089
    @calculated_property(
1✔
1090
        define=True,
1091
        schema={
1092
            'title': 'Taxa',
1093
            'type': 'string',
1094
            'description': 'The species of the organism.',
1095
            'enum': [
1096
                    'Homo sapiens',
1097
                    'Mus musculus',
1098
                    'Mixed species',
1099
                    'Saccharomyces cerevisiae'
1100
            ],
1101
            'notSubmittable': True
1102
        }
1103
    )
1104
    def taxa(self, request, multiplexed_samples):
1✔
1105
        taxas = set()
1✔
1106
        if multiplexed_samples:
1✔
1107
            for sample in multiplexed_samples:
1✔
1108
                sample_object = request.embed(sample, '@@object_with_select_calculated_properties?field=taxa')
1✔
1109
                taxas.add(sample_object.get('taxa'))
1✔
1110

1111
        if len(taxas) == 1:
1✔
1112
            return list(taxas).pop()
1✔
1113
        elif len(taxas) > 1:
1✔
1114
            return 'Mixed species'
1✔
1115
        else:
1116
            return None
×
1117

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

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

1154
    @calculated_property(
1✔
1155
        schema={
1156
            'title': 'Modifications',
1157
            'type': 'array',
1158
            'description': 'The modifications of the samples included in this multiplexed sample.',
1159
            'minItems': 1,
1160
            'uniqueItems': True,
1161
            'notSubmittable': True,
1162
            'items': {
1163
                'title': 'Modification',
1164
                'type': 'string',
1165
                'linkTo': ['CrisprModification', 'DegronModification']
1166
            }
1167
        }
1168
    )
1169
    def modifications(self, request, multiplexed_samples):
1✔
1170
        return collect_multiplexed_samples_prop(request, multiplexed_samples, 'modifications')
1✔
1171

1172
    @calculated_property(
1✔
1173
        define=True,
1174
        schema={
1175
            'title': 'Donors',
1176
            'type': 'array',
1177
            'description': 'The donors of the samples included in this multiplexed sample.',
1178
            'minItems': 1,
1179
            'uniqueItems': True,
1180
            'notSubmittable': True,
1181
            'items': {
1182
                'title': 'Donor',
1183
                'type': 'string',
1184
                'linkTo': 'Donor'
1185
            }
1186
        }
1187
    )
1188
    def donors(self, request, multiplexed_samples):
1✔
1189
        return collect_multiplexed_samples_prop(request, multiplexed_samples, 'donors')
1✔
1190

1191
    @calculated_property(
1✔
1192
        schema={
1193
            'title': 'Biomarkers',
1194
            'type': 'array',
1195
            'description': 'The biomarkers of the samples included in this multiplexed sample.',
1196
            'minItems': 1,
1197
            'uniqueItems': True,
1198
            'notSubmittable': True,
1199
            'items': {
1200
                'title': 'Biomarker',
1201
                'type': 'string',
1202
                'linkTo': 'Biomarker'
1203
            }
1204
        }
1205
    )
1206
    def biomarkers(self, request, multiplexed_samples):
1✔
1207
        return collect_multiplexed_samples_prop(request, multiplexed_samples, 'biomarkers')
1✔
1208

1209
    @calculated_property(
1✔
1210
        schema={
1211
            'title': 'Sources',
1212
            'type': 'array',
1213
            'description': 'The sources of the samples included in this multiplexed sample.',
1214
            'minItems': 1,
1215
            'uniqueItems': True,
1216
            'notSubmittable': True,
1217
            'items': {
1218
                'title': 'Source',
1219
                'type': 'string',
1220
                'linkTo': [
1221
                    'Source',
1222
                    'Lab'
1223
                ]
1224
            }
1225
        }
1226
    )
1227
    def sources(self, request, multiplexed_samples):
1✔
1228
        return collect_multiplexed_samples_prop(request, multiplexed_samples, 'sources')
1✔
1229

1230
    @calculated_property(
1✔
1231
        schema={
1232
            'title': 'Construct Library Sets',
1233
            'type': 'array',
1234
            'description': 'The construct library sets of the samples included in this multiplexed sample.',
1235
            'minItems': 1,
1236
            'uniqueItems': True,
1237
            'notSubmittable': True,
1238
            'items': {
1239
                'title': 'Construct Library Set',
1240
                'type': 'string',
1241
                'linkTo': 'ConstructLibrarySet'
1242
            }
1243
        }
1244
    )
1245
    def construct_library_sets(self, request, multiplexed_samples):
1✔
1246
        return collect_multiplexed_samples_prop(
1✔
1247
            request, multiplexed_samples, 'construct_library_sets')
1248

1249
    @calculated_property(
1✔
1250
        schema={
1251
            'title': 'Institutional Certificates',
1252
            'type': 'array',
1253
            'description': 'The institutional certificates of the samples included in this multiplexed sample.',
1254
            'minItems': 1,
1255
            'uniqueItems': True,
1256
            'notSubmittable': True,
1257
            'items': {
1258
                'title': 'Institutional Certificate',
1259
                'type': 'string',
1260
                'linkTo': 'InstitutionalCertificate'
1261
            }
1262
        }
1263
    )
1264
    def institutional_certificates(self, request, multiplexed_samples):
1✔
1265
        return collect_multiplexed_samples_prop(request, multiplexed_samples, 'institutional_certificates', skip_calculated=False)
1✔
1266

1267
    @calculated_property(
1✔
1268
        schema={
1269
            'title': 'Classifications',
1270
            'description': 'The general category of this type of sample.',
1271
            'minItems': 1,
1272
            'uniqueItems': True,
1273
            'type': 'array',
1274
            'items': {
1275
                'title': 'Classification',
1276
                'type': 'string'
1277
            },
1278
            'notSubmittable': True,
1279
        }
1280
    )
1281
    def classifications(self, request, multiplexed_samples):
1✔
1282
        # Get unique properties of individual samples' item types
1283
        sample_classfications = collect_multiplexed_samples_prop(
1✔
1284
            request, multiplexed_samples, 'classifications', skip_calculated=False)
1285
        self_classification = [self.item_type.replace('_', ' ')]
1✔
1286
        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