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

Clinical-Genomics / cg / 10216797607

02 Aug 2024 01:55PM UTC coverage: 84.303%. First build
10216797607

Pull #3507

github

web-flow
Merge ef77614cb into e6390edbb
Pull Request #3507: Refactor cancel action

28 of 66 new or added lines in 8 files covered. (42.42%)

20801 of 24674 relevant lines covered (84.3%)

0.84 hits per line

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

77.31
/cg/server/admin.py
1
"""Module for Flask-Admin views"""
2

3
from gettext import gettext
1✔
4

5
from flask import flash, redirect, request, session, url_for
1✔
6
from flask_admin.actions import action
1✔
7
from flask_admin.contrib.sqla import ModelView
1✔
8
from flask_dance.contrib.google import google
1✔
9
from markupsafe import Markup
1✔
10

11
from cg.constants.constants import NG_UL_SUFFIX, CaseActions, DataDelivery, Workflow
1✔
12
from cg.server.ext import db, sample_service
1✔
13
from cg.utils.flask.enum import SelectEnumField
1✔
14

15

16
class BaseView(ModelView):
1✔
17
    """Base for the specific views."""
18

19
    def is_accessible(self):
1✔
20
        user = db.get_user_by_email(email=session.get("user_email"))
×
21
        return bool(google.authorized and user and user.is_admin)
×
22

23
    def inaccessible_callback(self, name, **kwargs):
1✔
24
        # redirect to login page if user doesn't have access
25
        return redirect(url_for("google.login", next=request.url))
×
26

27

28
def view_priority(unused1, unused2, model, unused3):
1✔
29
    """column formatter for priority"""
30
    del unused1, unused2, unused3
×
31
    return Markup("%s" % model.priority.name) if model else ""
×
32

33

34
def view_flow_cell_internal_id(unused1, unused2, model, unused3):
1✔
35
    """column formatter for priority"""
36
    del unused1, unused2, unused3
×
37
    return Markup("%s" % model.device.internal_id)
×
38

39

40
def view_flow_cell_model(unused1, unused2, model, unused3):
1✔
41
    """column formatter for priority"""
42
    del unused1, unused2, unused3
×
43
    return Markup("%s" % model.device.model)
×
44

45

46
def view_case_sample_link(unused1, unused2, model, unused3):
1✔
47
    """column formatter to open the case-sample view"""
48

49
    del unused1, unused2, unused3
×
50

51
    return Markup(
×
52
        "<a href='%s'>%s</a>"
53
        % (url_for("casesample.index_view", search=model.internal_id), model.internal_id)
54
    )
55

56

57
def is_external_application(unused1, unused2, model, unused3):
1✔
58
    """column formatter to open this view"""
59
    del unused1, unused2, unused3
×
60
    return model.application_version.application.is_external if model.application_version else ""
×
61

62

63
def view_sample_concentration_minimum(unused1, unused2, model, unused3):
1✔
64
    """Column formatter to append unit"""
65
    del unused1, unused2, unused3
×
66
    return (
×
67
        str(model.sample_concentration_minimum) + NG_UL_SUFFIX
68
        if model.sample_concentration_minimum
69
        else None
70
    )
71

72

73
def view_sample_concentration_maximum(unused1, unused2, model, unused3):
1✔
74
    """Column formatter to append unit"""
75
    del unused1, unused2, unused3
×
76
    return (
×
77
        str(model.sample_concentration_maximum) + NG_UL_SUFFIX
78
        if model.sample_concentration_maximum
79
        else None
80
    )
81

82

83
def view_sample_concentration_minimum_cfdna(unused1, unused2, model, unused3):
1✔
84
    """Column formatter to append unit"""
85
    del unused1, unused2, unused3
×
86
    return (
×
87
        str(model.sample_concentration_minimum_cfdna) + NG_UL_SUFFIX
88
        if model.sample_concentration_minimum_cfdna
89
        else None
90
    )
91

92

93
def view_sample_concentration_maximum_cfdna(unused1, unused2, model, unused3):
1✔
94
    """Column formatter to append unit"""
95
    del unused1, unused2, unused3
×
96
    return (
×
97
        str(model.sample_concentration_maximum_cfdna) + NG_UL_SUFFIX
98
        if model.sample_concentration_maximum_cfdna
99
        else None
100
    )
101

102

103
class ApplicationView(BaseView):
1✔
104
    """Admin view for Model.Application"""
105

106
    column_editable_list = [
1✔
107
        "description",
108
        "is_accredited",
109
        "target_reads",
110
        "percent_reads_guaranteed",
111
        "comment",
112
        "prep_category",
113
        "sequencing_depth",
114
        "min_sequencing_depth",
115
        "is_external",
116
        "turnaround_time",
117
        "sample_concentration",
118
        "sample_concentration_minimum",
119
        "sample_concentration_maximum",
120
        "sample_concentration_minimum_cfdna",
121
        "sample_concentration_maximum_cfdna",
122
        "priority_processing",
123
        "is_archived",
124
    ]
125
    column_exclude_list = [
1✔
126
        "minimum_order",
127
        "sample_amount",
128
        "sample_volume",
129
        "details",
130
        "limitations",
131
        "created_at",
132
        "updated_at",
133
        "category",
134
    ]
135
    column_formatters = {
1✔
136
        "sample_concentration_minimum": view_sample_concentration_minimum,
137
        "sample_concentration_maximum": view_sample_concentration_maximum,
138
        "sample_concentration_minimum_cfdna": view_sample_concentration_minimum_cfdna,
139
        "sample_concentration_maximum_cfdna": view_sample_concentration_maximum_cfdna,
140
    }
141
    column_filters = ["prep_category", "is_accredited"]
1✔
142
    column_searchable_list = ["tag", "prep_category"]
1✔
143
    form_excluded_columns = ["category", "versions"]
1✔
144

145
    @staticmethod
1✔
146
    def view_application_link(unused1, unused2, model, unused3):
1✔
147
        """column formatter to open this view"""
148
        del unused1, unused2, unused3
×
149
        return (
×
150
            Markup(
151
                "<a href='%s'>%s</a>"
152
                % (
153
                    url_for("application.index_view", search=model.application.tag),
154
                    model.application.tag,
155
                )
156
            )
157
            if model.application
158
            else ""
159
        )
160

161

162
class ApplicationVersionView(BaseView):
1✔
163
    """Admin view for Model.ApplicationVersion"""
164

165
    column_default_sort = ("valid_from", True)
1✔
166
    column_editable_list = [
1✔
167
        "valid_from",
168
        "price_standard",
169
        "price_priority",
170
        "price_express",
171
        "price_clinical_trials",
172
        "price_research",
173
    ]
174
    column_list = (
1✔
175
        "application",
176
        "version",
177
        "valid_from",
178
        "price_standard",
179
        "price_priority",
180
        "price_express",
181
        "price_clinical_trials",
182
        "price_research",
183
        "comment",
184
    )
185
    column_exclude_list = ["created_at", "updated_at"]
1✔
186
    column_filters = ["version", "application.tag"]
1✔
187
    column_formatters = {"application": ApplicationView.view_application_link}
1✔
188
    column_searchable_list = ["application.tag"]
1✔
189
    edit_modal = True
1✔
190
    create_modal = True
1✔
191
    form_excluded_columns = ["samples", "pools", "microbial_samples"]
1✔
192

193

194
class ApplicationLimitationsView(BaseView):
1✔
195
    """Admin view for Model.ApplicationLimitations."""
196

197
    column_list = (
1✔
198
        "application",
199
        "workflow",
200
        "limitations",
201
        "comment",
202
        "created_at",
203
        "updated_at",
204
    )
205
    column_formatters = {"application": ApplicationView.view_application_link}
1✔
206
    column_filters = ["application.tag", "workflow"]
1✔
207
    column_searchable_list = ["application.tag"]
1✔
208
    column_editable_list = ["comment"]
1✔
209
    form_excluded_columns = ["created_at", "updated_at"]
1✔
210
    form_extra_fields = {"workflow": SelectEnumField(enum_class=Workflow)}
1✔
211
    create_modal = True
1✔
212
    edit_modal = True
1✔
213

214

215
class BedView(BaseView):
1✔
216
    """Admin view for Model.Bed"""
217

218
    column_default_sort = "name"
1✔
219
    column_editable_list = ["comment"]
1✔
220
    column_exclude_list = ["created_at"]
1✔
221
    column_filters = []
1✔
222
    column_searchable_list = ["name"]
1✔
223
    form_excluded_columns = ["created_at", "updated_at"]
1✔
224

225
    @staticmethod
1✔
226
    def view_bed_link(unused1, unused2, model, unused3):
1✔
227
        """column formatter to open this view"""
228
        del unused1, unused2, unused3
×
229
        return (
×
230
            Markup(
231
                "<a href='%s'>%s</a>"
232
                % (url_for("bed.index_view", search=model.bed.name), model.bed.name)
233
            )
234
            if model.bed
235
            else ""
236
        )
237

238

239
class BedVersionView(BaseView):
1✔
240
    """Admin view for Model.BedVersion"""
241

242
    column_default_sort = ("updated_at", True)
1✔
243
    column_editable_list = ["shortname", "filename", "comment", "designer", "checksum"]
1✔
244
    column_exclude_list = ["created_at"]
1✔
245
    form_excluded_columns = ["created_at", "updated_at", "samples"]
1✔
246
    column_filters = []
1✔
247
    column_formatters = {"bed": BedView.view_bed_link}
1✔
248
    column_searchable_list = ["bed.name"]
1✔
249
    edit_modal = True
1✔
250

251

252
class CustomerView(BaseView):
1✔
253
    """Admin view for Model.Customer"""
254

255
    column_editable_list = [
1✔
256
        "collaborations",
257
        "comment",
258
        "delivery_contact",
259
        "lab_contact",
260
        "loqus_upload",
261
        "primary_contact",
262
        "priority",
263
        "return_samples",
264
        "scout_access",
265
    ]
266
    column_list = [
1✔
267
        "internal_id",
268
        "name",
269
        "data_archive_location",
270
        "comment",
271
        "primary_contact",
272
        "delivery_contact",
273
        "lab_contact",
274
        "priority",
275
        "project_account_KI",
276
        "project_account_kth",
277
        "return_samples",
278
        "scout_access",
279
    ]
280
    column_filters = ["priority", "scout_access", "data_archive_location"]
1✔
281
    column_searchable_list = ["internal_id", "name"]
1✔
282
    form_excluded_columns = ["families", "samples", "pools", "orders", "invoices"]
1✔
283

284

285
class CollaborationView(BaseView):
1✔
286
    """Admin view for Model.CustomerGroup"""
287

288
    column_editable_list = ["name"]
1✔
289
    column_filters = []
1✔
290
    column_hide_backrefs = False
1✔
291
    column_list = ("internal_id", "name", "customers")
1✔
292
    column_searchable_list = ["internal_id", "name"]
1✔
293

294

295
class CaseView(BaseView):
1✔
296
    """Admin view for Model.Case"""
297

298
    column_default_sort = ("created_at", True)
1✔
299
    column_editable_list = ["action", "comment"]
1✔
300
    column_exclude_list = ["created_at", "_cohorts", "synopsis"]
1✔
301
    column_filters = [
1✔
302
        "customer.internal_id",
303
        "priority",
304
        "action",
305
        "data_analysis",
306
        "data_delivery",
307
        "tickets",
308
    ]
309
    column_formatters = {
1✔
310
        "internal_id": view_case_sample_link,
311
        "priority": view_priority,
312
    }
313
    column_searchable_list = [
1✔
314
        "internal_id",
315
        "name",
316
        "customer.internal_id",
317
        "tickets",
318
    ]
319
    form_excluded_columns = [
1✔
320
        "analyses",
321
        "_cohorts",
322
        "links",
323
        "synopsis",
324
    ]
325
    form_extra_fields = {
1✔
326
        "data_analysis": SelectEnumField(enum_class=Workflow),
327
        "data_delivery": SelectEnumField(enum_class=DataDelivery),
328
    }
329

330
    @staticmethod
1✔
331
    def view_case_link(unused1, unused2, model, unused3):
1✔
332
        """column formatter to open this view"""
333
        del unused1, unused2, unused3
×
334
        markup = ""
×
335
        if model.case:
×
336
            markup += Markup(
×
337
                " <a href='%s'>%s</a>"
338
                % (url_for("case.index_view", search=model.case.internal_id), model.case)
339
            )
340

341
        return markup
×
342

343
    @action(
1✔
344
        "set_hold",
345
        "Set action to hold",
346
        "Are you sure you want to set the action for selected families to hold?",
347
    )
348
    def action_set_hold(self, ids: list[str]):
1✔
349
        self.set_action_for_cases(action=CaseActions.HOLD, case_entry_ids=ids)
×
350

351
    @action(
1✔
352
        "set_empty",
353
        "Set action to Empty",
354
        "Are you sure you want to set the action for selected families to Empty?",
355
    )
356
    def action_set_empty(self, ids: list[str]):
1✔
357
        self.set_action_for_cases(action=None, case_entry_ids=ids)
×
358

359
    def set_action_for_cases(self, action: CaseActions | None, case_entry_ids: list[str]):
1✔
360
        try:
×
361
            for entry_id in case_entry_ids:
×
362
                case = db.get_case_by_entry_id(entry_id=entry_id)
×
363
                if case:
×
364
                    case.action = action
×
365

366
            db.session.commit()
×
367

368
            num_families = len(case_entry_ids)
×
369
            action_message = (
×
370
                f"Families were set to {action}."
371
                if num_families == 1
372
                else f"{num_families} families were set to {action}."
373
            )
374
            flash(action_message)
×
375

376
        except Exception as ex:
×
377
            if not self.handle_view_exception(ex):
×
378
                raise
×
379

380
            flash(gettext(f"Failed to set case action. {str(ex)}"))
×
381

382

383
class InvoiceView(BaseView):
1✔
384
    """Admin view for Model.Invoice"""
385

386
    column_default_sort = ("created_at", True)
1✔
387
    column_editable_list = ["comment"]
1✔
388
    column_list = (
1✔
389
        "id",
390
        "customer",
391
        "created_at",
392
        "updated_at",
393
        "invoiced_at",
394
        "comment",
395
        "discount",
396
        "price",
397
    )
398
    column_searchable_list = ["customer.internal_id", "customer.name", "id"]
1✔
399

400
    @staticmethod
1✔
401
    def view_invoice_link(unused1, unused2, model, unused3):
1✔
402
        """column formatter to open this view"""
403
        del unused1, unused2, unused3
×
404
        return (
×
405
            Markup(
406
                "<a href='%s'>%s</a>"
407
                % (
408
                    url_for("invoice.index_view", search=model.invoice.id),
409
                    (
410
                        model.invoice.invoiced_at.date()
411
                        if model.invoice.invoiced_at
412
                        else "In progress"
413
                    ),
414
                )
415
            )
416
            if model.invoice
417
            else ""
418
        )
419

420

421
class AnalysisView(BaseView):
1✔
422
    """Admin view for Model.Analysis"""
423

424
    column_default_sort = ("created_at", True)
1✔
425
    column_editable_list = ["is_primary", "comment"]
1✔
426
    column_filters = ["workflow", "workflow_version", "is_primary"]
1✔
427
    column_formatters = {"case": CaseView.view_case_link}
1✔
428
    column_searchable_list = [
1✔
429
        "case.internal_id",
430
        "case.name",
431
    ]
432
    form_extra_fields = {"workflow": SelectEnumField(enum_class=Workflow)}
1✔
433

434

435
class IlluminaFlowCellView(BaseView):
1✔
436
    """Admin view for Model.IlluminaSequencingRun"""
437

438
    column_list = (
1✔
439
        "internal_id",
440
        "model",
441
        "sequencer_type",
442
        "sequencer_name",
443
        "data_availability",
444
        "has_backup",
445
        "total_reads",
446
        "total_undetermined_reads",
447
        "percent_undetermined_reads",
448
        "percent_q30",
449
        "mean_quality_score",
450
        "total_yield",
451
        "yield_q30",
452
        "cycles",
453
        "demultiplexing_software",
454
        "demultiplexing_software_version",
455
        "sequencing_started_at",
456
        "sequencing_completed_at",
457
        "demultiplexing_started_at",
458
        "demultiplexing_completed_at",
459
        "archived_at",
460
    )
461
    column_formatters = {"internal_id": view_flow_cell_internal_id, "model": view_flow_cell_model}
1✔
462
    column_default_sort = ("sequencing_completed_at", True)
1✔
463
    column_filters = ["sequencer_type", "sequencer_name", "data_availability"]
1✔
464
    column_editable_list = ["data_availability"]
1✔
465
    column_searchable_list = ["sequencer_type", "sequencer_name", "device.internal_id"]
1✔
466
    column_sortable_list = [
1✔
467
        ("internal_id", "device.internal_id"),
468
        "sequencer_type",
469
        "sequencer_name",
470
        "data_availability",
471
        "has_backup",
472
        "sequencing_started_at",
473
        "sequencing_completed_at",
474
        "demultiplexing_started_at",
475
        "demultiplexing_completed_at",
476
        "archived_at",
477
    ]
478

479
    @staticmethod
1✔
480
    def view_flow_cell_link(unused1, unused2, model, unused3):
1✔
481
        """column formatter to open this view"""
482
        del unused1, unused2, unused3
×
483
        return (
×
484
            Markup(
485
                "<a href='%s'>%s</a>"
486
                % (
487
                    url_for(
488
                        "illuminasequencingrun.index_view",
489
                        search=model.instrument_run.device.internal_id,
490
                    ),
491
                    model.instrument_run.device.internal_id,
492
                )
493
            )
494
            if model.instrument_run.device
495
            else ""
496
        )
497

498

499
class OrganismView(BaseView):
1✔
500
    """Admin view for Model.Organism"""
501

502
    column_default_sort = ("created_at", True)
1✔
503
    column_editable_list = ["internal_id", "name", "reference_genome", "comment"]
1✔
504
    column_searchable_list = ["internal_id", "name", "reference_genome"]
1✔
505

506

507
class OrderView(BaseView):
1✔
508
    """Admin view for Model.Order"""
509

510
    column_default_sort = ("order_date", True)
1✔
511
    column_editable_list = ["is_delivered"]
1✔
512
    column_searchable_list = ["id", "ticket_id"]
1✔
513
    column_display_pk = True
1✔
514
    create_modal = True
1✔
515
    edit_modal = True
1✔
516

517

518
class PanelView(BaseView):
1✔
519
    """Admin view for Model.Panel"""
520

521
    column_editable_list = ["current_version", "name"]
1✔
522
    column_filters = ["customer.internal_id"]
1✔
523
    column_searchable_list = ["customer.internal_id", "name", "abbrev"]
1✔
524
    create_modal = True
1✔
525
    edit_modal = True
1✔
526

527

528
class PoolView(BaseView):
1✔
529
    """Admin view for Model.Pool"""
530

531
    column_default_sort = ("created_at", True)
1✔
532
    column_editable_list = ["ticket"]
1✔
533
    column_filters = ["customer.internal_id", "application_version.application"]
1✔
534
    column_formatters = {"invoice": InvoiceView.view_invoice_link}
1✔
535
    column_searchable_list = ["name", "order", "ticket", "customer.internal_id"]
1✔
536

537

538
class SampleView(BaseView):
1✔
539
    """Admin view for Model.Sample"""
540

541
    column_exclude_list = [
1✔
542
        "age_at_sampling",
543
        "_phenotype_groups",
544
        "_phenotype_terms",
545
    ]
546
    column_default_sort = ("created_at", True)
1✔
547
    column_editable_list = [
1✔
548
        "comment",
549
        "downsampled_to",
550
        "is_tumour",
551
        "last_sequenced_at",
552
        "sex",
553
    ]
554
    column_filters = ["customer.internal_id", "priority", "sex", "application_version.application"]
1✔
555
    column_formatters = {
1✔
556
        "is_external": is_external_application,
557
        "internal_id": view_case_sample_link,
558
        "invoice": InvoiceView.view_invoice_link,
559
        "priority": view_priority,
560
    }
561
    column_searchable_list = [
1✔
562
        "internal_id",
563
        "name",
564
        "subject_id",
565
        "customer.internal_id",
566
        "original_ticket",
567
    ]
568
    form_excluded_columns = [
1✔
569
        "age_at_sampling",
570
        "deliveries",
571
        "father_links",
572
        "flowcells",
573
        "invoice",
574
        "_phenotype_groups",
575
        "_phenotype_terms",
576
        "links",
577
        "mother_links",
578
    ]
579

580
    @staticmethod
1✔
581
    def view_sample_link(unused1, unused2, model, unused3):
1✔
582
        """column formatter to open this view"""
583
        del unused1, unused2, unused3
×
584
        return (
×
585
            Markup(
586
                "<a href='%s'>%s</a>"
587
                % (url_for("sample.index_view", search=model.sample.internal_id), model.sample)
588
            )
589
            if model.sample
590
            else ""
591
        )
592

593
    @action(
1✔
594
        "cancel_samples",
595
        "Cancel samples",
596
        "Are you sure you want to cancel the selected samples?",
597
    )
598
    def cancel_samples(self, entry_ids: list[str]) -> None:
1✔
NEW
599
        user_email: str | None = session.get("user_email")
×
NEW
600
        message: str = sample_service.cancel_samples(
×
601
            sample_ids=entry_ids,
602
            user_email=user_email,
603
        )
NEW
604
        flash(message)
×
605

606

607
class DeliveryView(BaseView):
1✔
608
    """Admin view for Model.Delivery"""
609

610
    column_default_sort = ("id", True)
1✔
611
    column_filters = ["sample.internal_id"]
1✔
612
    column_formatters = {"sample": SampleView.view_sample_link}
1✔
613
    column_searchable_list = ["sample.internal_id"]
1✔
614
    create_modal = True
1✔
615
    edit_modal = True
1✔
616

617

618
class CaseSampleView(BaseView):
1✔
619
    """Admin view for Model.caseSample"""
620

621
    column_default_sort = ("created_at", True)
1✔
622
    column_editable_list = ["status"]
1✔
623
    column_filters = ["status"]
1✔
624
    column_formatters = {
1✔
625
        "case": CaseView.view_case_link,
626
        "sample": SampleView.view_sample_link,
627
    }
628
    column_searchable_list = ["case.internal_id", "case.name", "sample.internal_id"]
1✔
629
    create_modal = True
1✔
630
    edit_modal = True
1✔
631

632

633
class UserView(BaseView):
1✔
634
    """Admin view for Model.User"""
635

636
    column_default_sort = "name"
1✔
637
    column_editable_list = ["order_portal_login"]
1✔
638
    column_filters = ["is_admin", "order_portal_login", "customers"]
1✔
639
    column_hide_backrefs = False
1✔
640
    column_list = ("name", "email", "is_admin", "order_portal_login", "customers")
1✔
641
    column_searchable_list = ["name", "email"]
1✔
642
    create_modal = True
1✔
643
    edit_modal = True
1✔
644

645

646
class IlluminaSampleSequencingMetricsView(BaseView):
1✔
647
    column_list = [
1✔
648
        "flow_cell",
649
        "sample",
650
        "flow_cell_lane",
651
        "total_reads_in_lane",
652
        "base_passing_q30_percent",
653
        "base_mean_quality_score",
654
        "yield_",
655
        "yield_q30",
656
        "created_at",
657
    ]
658
    column_formatters = {
1✔
659
        "flow_cell": IlluminaFlowCellView.view_flow_cell_link,
660
        "sample": SampleView.view_sample_link,
661
    }
662
    column_searchable_list = ["sample.internal_id", "instrument_run.device.internal_id"]
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