forms.py 57.5 KB
Newer Older
1
2
import csv
import io
3
import os
4
import random
5
import re
6
import string
7

8
import pandas as pd
9
from basetheme_bootstrap.user_preferences_utils import get_user_preferences_for_user
10
from crispy_forms import helper as crispy_forms_helper
11
from crispy_forms import layout as crispy_forms_layout
Bryan  BRANCOTTE's avatar
Bryan BRANCOTTE committed
12
from crispy_forms.helper import FormHelper
13
from crispy_forms.layout import HTML, Layout, Row, Column
14
from django import forms
15
from django.conf import settings
16
from django.contrib.auth import get_user_model
17
from django.core.exceptions import FieldDoesNotExist, ValidationError
18
from django.db.models import Q, Min, Max
19
from django.forms import widgets
20
from django.utils import timezone
21
from django.utils.datastructures import MultiValueDictKeyError
22
23
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _, ugettext
24

25
from viralhostrangedb import models, business_process, mixins
26
from viralhostrangedb.business_process import ImportationObserver, explicit_item
27
from viralhostrangedb.mixins import only_public_or_granted_or_owned_queryset_filter
28
29


30
31
32
33
34
class ByLastFirstNameModelMultipleChoiceField(forms.ModelMultipleChoiceField):
    def label_from_instance(self, obj):
        return obj.last_name.upper() + " " + obj.first_name.title()


35
36
37
38
class DateInput(forms.DateInput):
    input_type = 'date'


39
40
41
42
class ImportDataSourceForm(forms.ModelForm):
    file = forms.FileField(
        label=_('File'),
        help_text=_("The file to upload."),
43
        required=True,
44
    )
45
    make_upload_mandatory = True
46
47
48
49
50
51
52
53
54
55
56
57

    class Meta:
        model = models.DataSource
        fields = (
            'name',
            'description',
            'public',
        )

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.on_disk_file = None
58
        self.import_file_fcn = None
59
60
61

    def full_clean(self):
        super().full_clean()
62
        if not self.is_bound:  # Stop further processing.
63
            return
64
65
66
67
68
69
70
        if self.files is not None and len(self.files) == 1:
            file = list(self.files.values())[0]
            errors, self.import_file_fcn = business_process.import_file_later(file=file)
            for e in errors:
                self.add_error('file', e)
        # else:
        #     self.add_error('file', "No file provided")
71
72
73
74
        # if (self.files is None or len(self.files) == 0) and len(self.cleaned_data["url"]) == 0 or \
        #         (self.files is not None and len(self.files) > 0) and len(self.cleaned_data["url"]) > 0:
        #     self.add_error("url", _("You have to either provide a file or an URL, and not both."))
        #     self.add_error("file", _("You have to either provide a file or an URL, and not both."))
75

76
    def save(self, owner=None, commit=True, importation_observer: ImportationObserver = None):
77
        instance = super().save(commit=False)
78
79
        if owner is not None:
            instance.owner = owner
80
        instance.save()
81
82
83
84
85
86
87
        if (instance.raw_name or "") == "":
            instance.raw_name = list(self.files.values())[0].name
            instance.kind = "FILE"
        self.import_file_fcn(
            data_source=instance,
            importation_observer=importation_observer,
        )
88
89
        if commit:
            instance.save()
90
        return instance
Bryan  BRANCOTTE's avatar
Bryan BRANCOTTE committed
91
92


93
94
95
96
97
98
class UploadToUpdateDataSourceForm(ImportDataSourceForm):
    class Meta:
        model = models.DataSource
        fields = ()


Bryan  BRANCOTTE's avatar
Bryan BRANCOTTE committed
99
class DataSourceOwnershipTransferForm(forms.Form):
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
    ownership_transfer_recipient = forms.CharField(
        label=_("ownership_transfer_recipient__label"),
        help_text=_("ownership_transfer_recipient__help_text"),
        max_length=255,
    )
    recipient = forms.ModelChoiceField(
        queryset=get_user_model().objects.none(),
        required=False,
        widget=forms.HiddenInput,
    )

    def clean(self):
        f = super().clean()
        if "ownership_transfer_recipient" in self.cleaned_data:
            c = self.cleaned_data["ownership_transfer_recipient"].strip()
115
            self.cleaned_data["recipient"], rw = find_user(self, "ownership_transfer_recipient", c + ";;")
116
117
118
        return f


119
120
121
def find_user(form, field_name, user_str, sep=" "):
    if sep == " " and user_str.count(",") > 0:
        return find_user(form, field_name, user_str, ",")
122
123
124
125
    user_str = re.sub(r"[ ][ ]+", " ", user_str.replace('\t', ' ').strip())
    fields = user_str.split(';')
    assert len(fields) == 3
    user = fields[0]
126
    cs = user.strip().split(sep)
127
    if len(cs) == 1:
128
        users = get_user_model().objects.filter(email=cs[0].strip())
129
130
131
132
133
134
135
136
    elif len(cs) == 2:
        users = get_user_model().objects.filter(
            Q(first_name__iexact=cs[0].strip()) & Q(last_name__iexact=cs[1].strip())
            | Q(last_name__iexact=cs[0].strip()) & Q(first_name__iexact=cs[1].strip())
        )
    else:
        form.add_error(field_name,
                       _("Cannot determine first_name and last_name in \"%s\", "
137
138
139
140
                         "please separate them with a comma ','") % user)
        return None, False
    if fields[1] != '':
        users = users.filter(pk=fields[1])
141
142
143

    user_count = users.count()
    if user_count == 1:
144
        return users.first(), fields[2].upper() == "TRUE"
145
146
147
148
149
    elif user_count == 0:
        msg = _("Unknown user '%s'")
    elif len(cs) == 1:
        msg = _("Cannot determine of which user we are talking about, multiple user use this email, "
                "please provide first name and last name '%s'")
150
    else:  # if len(cs) == 2:
151
152
        msg = _("Cannot determine of which user we are talking about, "
                "please provide its email '%s'")
153
154
    form.add_error(field_name, msg % user)
    return None, False
155
156


157
class DataSourceUserCreateOrUpdateForm(forms.ModelForm):
158
159
160
161
    allowed_users_editor = forms.CharField(
        label=_("allowed_users_editor__label"),
        help_text=_("allowed_users_editor__help_text"),
        widget=widgets.Textarea(attrs={'rows': '4', 'class': 'allowed-users-editor-serialized'}),
162
163
        required=False,
    )
164
165
166
167
168

    class Meta:
        model = models.DataSource
        fields = (
            'name',
169
            # 'experiment_date',
170
            'publication_url',
171
            'life_domain',
172
            'description',
173
174
            'provider_first_name',
            'provider_last_name',
175
            'public',
176
            'allowed_users_editor',
177
178
179
        )
        widgets = dict(
            raw_name=widgets.TextInput(),
180
            # experiment_date=DateInput,
181
182
        )

183
    def __init__(self, owner=None, data=None, *args, **kwargs):
184
        self.owner = owner
185
186
187
        super().__init__(data=data, *args, **kwargs)
        if self.instance and data and "public" in data and data["public"]:
            self.fields["description"].required = True
188
            # self.fields["experiment_date"].required = True
189

190
        if self.instance and self.instance.pk is not None:
191
192
193
194
195
196
            if not mixins.only_owned_queryset_filter(
                    self=None,
                    request=None,
                    queryset=models.DataSource.objects.filter(pk=self.instance.pk),
                    user=owner,
            ).exists():
197
                try:
198
                    self.fields.pop("allowed_users_editor")
199
200
201
                except KeyError:
                    pass
            else:
202
203
                gus = []
                for gu in self.instance.granteduser_set.order_by("user__last_name", "user__first_name"):
204
205
206
                    gus.append("%(last_name)s %(first_name)s;%(pk)i;%(can_write)s" % dict(
                        last_name=gu.user.last_name,
                        first_name=gu.user.first_name,
207
208
209
210
                        can_write=gu.can_write,
                        pk=gu.user.pk,
                    ))
                self.fields["allowed_users_editor"].initial = '\n'.join(gus)
Bryan  BRANCOTTE's avatar
Bryan BRANCOTTE committed
211
212
213
214
        try:
            self.fields["description"].widget.attrs["placeholder"] = _("description_placeholder_in_form")
        except KeyError:
            pass
215
216
217
218
        # try:
        #     self.fields["public"].widget.attrs['data-onchange-js'] = 'update_allowed_user_visibility'
        # except KeyError:
        #     pass
219

220
221
    def clean(self):
        f = super().clean()
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
        if "name" in self.cleaned_data:
            name = self.cleaned_data["name"]
            homonyms = only_public_or_granted_or_owned_queryset_filter(
                None,
                request=None,
                queryset=self._meta.model.objects.filter(name=name),
                user=self.owner
            )
            if self.instance:
                homonyms = homonyms.filter(~Q(pk=self.instance.pk))
            if homonyms.exists():
                self.data = self.data.copy()
                try:
                    key = self.data['data_source_wizard-current_step'] + "-name"
                except MultiValueDictKeyError:
                    key = "name"
                suffix_numbered = ' (%i)'
                suffix_named = None
240
241
                if homonyms.filter(owner=self.owner):
                    pass
242
243
244
245
246
247
248
249
250
251
252
253
                else:
                    suffix_named = ' - %s %s' % (self.owner.last_name, self.owner.first_name)
                    suffix_numbered = suffix_named + suffix_numbered

                if suffix_named and not self._meta.model.objects.filter(name=name + suffix_named).exists():
                    suffix = suffix_named
                else:
                    i = 1
                    while self._meta.model.objects.filter(name=name + (suffix_numbered % i)).exists():
                        i += 1
                    suffix = suffix_numbered % i
                self.data[key] += suffix
Bryan  BRANCOTTE's avatar
Bryan BRANCOTTE committed
254
                self.add_error("name", mark_safe(_("homomys detected, suffixed by \"%s\"") % suffix))
255
256
257
258
259
260
261
262
263
264
265

        try:
            if self.cleaned_data.get("public", False) and "experiment_date" not in self.cleaned_data:
                self.add_error("experiment_date", _("experiment_date_required_when_public"))
        except ValueError:
            pass
        try:
            if self.cleaned_data.get("public", False) and len(self.cleaned_data.get("description", "")) < 10:
                self.add_error("description", _("description_required_when_public%i") % 10)
        except ValueError:
            pass
266
        if "allowed_users_editor" in self.cleaned_data:
267
            users = []
268
269
            self.cleaned_data["allowed_users_with_permission"] = users
            for c in self.cleaned_data["allowed_users_editor"].replace("\r", "\n").replace("\n\n", "\n").split("\n"):
270
271
                if c == "":
                    continue
Bryan  BRANCOTTE's avatar
Bryan BRANCOTTE committed
272
                users.append(find_user(self, "allowed_users_editor", c))
273
274
        return f

275
    def save(self, commit=True):
276
277
278
279
280
281
282
283
        instance = super().save(commit=False)
        try:
            # Do not allow to edit the owner with this form
            getattr(instance, 'owner')
        except models.DataSource.owner.RelatedObjectDoesNotExist as e:
            instance.owner = self.owner
        if commit:
            instance.save()
284
            self.save_m2m()
285
286
287
        if "allowed_users_editor" in self.cleaned_data:
            to_keep = list()
            for user, can_write in self.cleaned_data["allowed_users_with_permission"]:
Bryan  BRANCOTTE's avatar
Bryan BRANCOTTE committed
288
289
                if user is None:
                    continue
290
291
292
293
294
295
296
297
298
299
                gu, created = models.GrantedUser.objects.update_or_create(
                    data_source=instance,
                    user=user,
                    defaults=dict(
                        can_write=can_write,
                    )
                )
                to_keep.append(gu.pk)
            models.GrantedUser.objects.filter(data_source=instance).exclude(pk__in=to_keep).delete()

300
301
302
303
304
        if commit:
            instance.save()
        return instance


Bryan  BRANCOTTE's avatar
Bryan BRANCOTTE committed
305
306
307
308
class MappingForm(forms.Form):
    raw_response = forms.FloatField(
    )
    mapping = forms.ModelChoiceField(
309
        queryset=models.GlobalViralHostResponseValue.objects_mappable().order_by("value"),
Bryan  BRANCOTTE's avatar
Bryan BRANCOTTE committed
310
311
312
313
314
315
316
317
318
        required=True,
    )


MappingFormSet = forms.formset_factory(
    form=MappingForm,
    extra=0,
    can_delete=False,
)
319

320
321
322
323
324

class RangeMappingForm(forms.Form):
    def __init__(self, data_source=None, data=None, *args, **kwargs):
        super().__init__(data=data, *args, **kwargs)
        self.data_source = data_source
325
326
327
328
        use_default = not models.ViralHostResponseValueInDataSource.objects \
            .filter(data_source=data_source) \
            .filter(~Q(response__pk=models.GlobalViralHostResponseValue.get_not_mapped_yet_pk())) \
            .exists()
329
        min_maxs = []  # min_maxs will contains min and max for each GlobalViralHostResponseValue
330
331
332
333
334
335
336
337
338
339
340
        if use_default:
            try:
                from sklearn.cluster import KMeans
                import numpy as np
                responses = models.ViralHostResponseValueInDataSource.objects \
                    .filter(data_source=data_source) \
                    .values_list("raw_response", flat=True)
                clusters = KMeans(
                    n_clusters=models.GlobalViralHostResponseValue.objects_mappable().count(),
                    random_state=0,
                ).fit(np.array(responses).reshape(-1, 1)).cluster_centers_
341
342
343
344
                centroids = sorted([c[0] for c in clusters])
                # put in min_maxs the centroid as min and max for each GlobalViralHostResponseValue as we do not known
                # which element have been associated to each cluster (or don't want to compute it)
                min_maxs = [[x, x] for x in centroids]
345
            except ModuleNotFoundError:
346
347
348
349
                # Here when we do not have numpy
                pass
            except ValueError:
                # Here when there is no data to work on
350
351
                pass

352
353
354
355
        # if we do not use KMeans, of if it failed
        if not min_maxs:
            for m in models.GlobalViralHostResponseValue.objects_mappable().order_by("value"):
                min_max = models.ViralHostResponseValueInDataSource.objects \
356
357
                    .filter(data_source=data_source) \
                    .filter(response=m) \
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
                    .aggregate(Min('raw_response'), Max('raw_response'))
                min_maxs.append([min_max["raw_response__min"], min_max["raw_response__max"]])
            prev = None
            # Replace the None with an upper value
            for i in range(len(min_maxs) - 1, -1, -1):
                # Go backward from higher to lower GlobalViralHostResponseValue
                for j in range(1, -1, -1):
                    # Go backward from max (j=1) to min (j=0)
                    if min_maxs[i][j] is None:
                        # if the value is None replace it with the last seen values, and thus greater or equal of what
                        # we will see in the future
                        if prev is None:
                            # if prev is None, we find the overall maximum value, and increment it of one
                            prev = max(*[x for mm in min_maxs for x in mm if x is not None], *[-1000, -1000]) + 1
                        min_maxs[i][j] = prev
                    else:
                        # we see a value, so keeping it in memory for later None value to replace
                        prev = min_maxs[i][j]

        for pos, m in enumerate(models.GlobalViralHostResponseValue.objects_mappable().order_by("value")[1:], 1):
            key = 'mapping_for_%i_starts_with' % m.pk
            # val is the average between the maximum of the previous response and the minimum of the current response
            val = (min_maxs[pos][0] + min_maxs[pos - 1][1]) / 2
381
382
            self.fields[key] = forms.FloatField(
                initial=val,
383
                required=True,
384
385
386
            )
            self.fields[key].obj = m

387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
    def clean(self):
        f = super().clean()
        previous_key = None
        for m in models.GlobalViralHostResponseValue.objects_mappable().order_by("value")[1:]:
            key = 'mapping_for_%i_starts_with' % m.pk
            if previous_key is None:
                previous_key = key
                continue
            try:
                val = self.cleaned_data[key]
                try:
                    if val < self.cleaned_data[previous_key]:
                        self.add_error(previous_key, _('Value should not be higher than the next one'))
                except KeyError:
                    self.add_error(previous_key, _('Field is required'))
            except KeyError:
                self.add_error(key, _('Field is required'))
        return f

406
407
408
409
410
411
412
413
414
415
416
417
418
    @property
    def get_current_range_start(self):
        return {f.obj.pk: f.initial for f in self.fields.values()}

    def save(self):
        ceiling = None
        for m in models.GlobalViralHostResponseValue.objects_mappable().order_by("-value"):
            key = 'mapping_for_%i_starts_with' % m.pk
            try:
                floor = self.cleaned_data[key]
            except KeyError:
                floor = None
            qs = models.ViralHostResponseValueInDataSource.objects.filter(data_source=self.data_source)
419
            if floor is not None:
420
                qs = qs.filter(raw_response__gte=floor)
421
            if ceiling is not None:
422
423
424
425
                qs = qs.filter(raw_response__lt=ceiling)
            qs.update(response=m)

            ceiling = floor
426

427

428
429
430
431
class AutoMergeSplitUpdateFormSet(forms.BaseModelFormSet):
    def __init__(self, data_source=None, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.__data_source = data_source
432
433
434
435
436
        try:
            self.model._meta.get_field("her_identifier")
            self.has_her_identifier = True
        except FieldDoesNotExist:
            self.has_her_identifier = False
437
438
439
440
441
442
443
444
445
446
447

    def save(self, commit=True):
        assert (self.__data_source is not None)
        assert commit, "It has to be actually saved in DB"
        objects = super().save(commit=False)
        indexed_objects = dict([(o.pk, o) for o in objects])

        for obj, reason in self.changed_objects:
            # get an instance unchanged
            old_o = obj.__class__.objects.get(pk=obj.pk)
            # Find an object which have same identifier and names as what we are trying to save
448
            alter_egos = obj.__class__.objects \
449
450
                .filter(identifier=obj.identifier) \
                .filter(name=obj.name) \
451
452
453
                .filter(~Q(pk=obj.pk))
            if self.has_her_identifier:
                alter_egos = alter_egos.filter(her_identifier=obj.her_identifier)
454
            # alter_ego is the object to which we have to rename the obj
455
            alter_ego = alter_egos.first()
456
            # If there is no alter_ego we may have to build one new
457
            if alter_ego is None:
458
                # If we can get the is_ncbi_identifier_value elsewhere then we re-use it
459
                if 'identifier' in reason:
460
461
462
463
464
                    identifier_alter_ego = obj.__class__.objects.filter(identifier=obj.identifier).first()
                    if identifier_alter_ego is None:
                        obj.is_ncbi_identifier_value = None
                    else:
                        obj.is_ncbi_identifier_value = identifier_alter_ego.is_ncbi_identifier_value
465
                # If the object is not used by someone else then we just change it
466
467
468
                if obj.data_source.count() == 1:
                    obj.save()
                    continue
469
                # obj is used, we clone it
470
471
                obj.pk = None
                obj.save()
472
                # And use this copy as its alter_ego
473
                alter_ego = obj
474
475
476
477
            elif alter_ego.data_source.filter(pk=self.__data_source.pk).exists():
                raise ValidationError(
                    mark_safe(ugettext("Duplicated entry <b>%s</b> have been found but is not allowed.")
                              % alter_ego.explicit_name_html))
478
479
480
481
482
483
484

            # remove the old instance from the current data_source
            old_o.data_source.remove(self.__data_source)
            old_o.save()

            # in any case, replace the instance that was about to be returned by the alter ego/newly created one
            indexed_objects[old_o.pk] = alter_ego
485
            # obj is now a new db instance, while old_o is still pointing to the original db instance
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501

            # add the new instance to the data source
            alter_ego.data_source.add(self.__data_source)

            # get all the response associated to the original instance and the data source
            for r in old_o.responseindatasource.filter(data_source=self.__data_source):
                # and set the new instance in place of the old
                setattr(r, alter_ego.__class__.__name__.lower(), alter_ego)
                # don't forget to save !
                r.save()

            # If the old instance is not linked to anything anymore, we delete it
            if old_o.data_source.count() == 0:
                old_o.delete()
        # return all the instance changed
        return indexed_objects.values()
502

503
504

UpdateVirusFormSet = forms.modelformset_factory(
Bryan  BRANCOTTE's avatar
Bryan BRANCOTTE committed
505
506
507
    model=models.Virus,
    form=forms.modelform_factory(
        model=models.Virus,
508
        fields=("name", "identifier", "her_identifier", "id",),
Bryan  BRANCOTTE's avatar
Bryan BRANCOTTE committed
509
    ),
510
    formset=AutoMergeSplitUpdateFormSet,
Bryan  BRANCOTTE's avatar
Bryan BRANCOTTE committed
511
512
    extra=0,
    can_delete=False,
513

Bryan  BRANCOTTE's avatar
Bryan BRANCOTTE committed
514
515
516
517
518
519
520
521
)

UpdateHostFormSet = forms.modelformset_factory(
    model=models.Host,
    form=forms.modelform_factory(
        model=models.Host,
        fields=("name", "identifier", "id",),
    ),
522
    formset=AutoMergeSplitUpdateFormSet,
Bryan  BRANCOTTE's avatar
Bryan BRANCOTTE committed
523
524
525
526
527
    extra=0,
    can_delete=False,
)

DeleteVirusFormSet = forms.modelformset_factory(
528
529
530
    model=models.Virus,
    form=forms.modelform_factory(
        model=models.Virus,
Bryan  BRANCOTTE's avatar
Bryan BRANCOTTE committed
531
532
533
534
535
        fields=("name", "identifier", "id",),
        widgets={
            "name": forms.TextInput(attrs={"readonly": True}),
            "identifier": forms.TextInput(attrs={"readonly": True}),
        }
536
537
538
539
540
    ),
    extra=0,
    can_delete=True,
)

Bryan  BRANCOTTE's avatar
Bryan BRANCOTTE committed
541
DeleteHostFormSet = forms.modelformset_factory(
542
    model=models.Host,
543
    form=forms.modelform_factory(
544
        model=models.Host,
Bryan  BRANCOTTE's avatar
Bryan BRANCOTTE committed
545
546
547
548
549
        fields=("name", "identifier", "id",),
        widgets={
            "name": forms.TextInput(attrs={"readonly": True}),
            "identifier": forms.TextInput(attrs={"readonly": True}),
        }
550
551
552
553
    ),
    extra=0,
    can_delete=True,
)
554

555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
VirusForm = forms.modelform_factory(
    model=models.Virus,
    fields=(
        "name",
        "identifier",
    ),
)

HostForm = forms.modelform_factory(
    model=models.Host,
    fields=(
        "name",
        "identifier",
    ),
)

571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598

class BoostrapSelectMultiple(forms.SelectMultiple):
    def __init__(
            self,
            allSelectedText,
            nonSelectedText,
            SelectedText,
            onChangeFunctionName=None,
            attrs=None,
            *args,
            **kwargs,
    ):
        if attrs is not None:
            attrs = attrs.copy()
        else:
            attrs = {}
        if onChangeFunctionName is not None:
            attrs["data-on-change-fcn-name"] = onChangeFunctionName
        attrs.update({
            "data-all-selected-text": allSelectedText,
            "data-non-selected-text": nonSelectedText,
            "data-n-selected-text": SelectedText,
            "class": "bootstrap-multiselect-applicant",
            "style": "display:none;",
        })
        super().__init__(attrs=attrs, *args, **kwargs)


599
class ByNameModelMultipleChoiceField(forms.ModelMultipleChoiceField):
600
601
602
603
    def label_from_instance(self, obj):
        return obj.name


604
605
606
607
608
class ByExplicitNameModelMultipleChoiceField(forms.ModelMultipleChoiceField):
    def label_from_instance(self, obj):
        return obj.explicit_name


609
class CommonSearchPrefForm(forms.Form):
610
611
612
    class Meta:
        abstract = True

613
614
615
    only_published_data = forms.BooleanField(
        label=_("Only published data"),
        help_text=_("only_published_data.help_text"),
616
        widget=widgets.CheckboxInput(
617
            attrs={
618
                'data-toggle-css-class': 'only-published',
619
                'data-advanced-option': 'true',
620
621
                'data-advanced-option-family': 0,
                'data-onchange-js': 'load_data_wrapper',
622
623
624
625
626
            },
        ),
        required=False,
        initial=False,
    )
627
628
629
    only_host_ncbi_id = forms.BooleanField(
        label=_("only_host_ncbi_id.label"),
        help_text=_("only_host_ncbi_id.help_text"),
630
        widget=widgets.CheckboxInput(
631
            attrs={
632
                'data-toggle-css-class': 'only-nbci-id',
633
                'data-advanced-option': 'true',
634
635
                'data-advanced-option-family': 0,
                'data-onchange-js': 'load_data_wrapper',
636
637
638
639
640
            },
        ),
        required=False,
        initial=False,
    )
641
642
643
    only_virus_ncbi_id = forms.BooleanField(
        label=_("only_virus_ncbi_id.label"),
        help_text=_("only_virus_ncbi_id.help_text"),
644
        widget=widgets.CheckboxInput(
645
            attrs={
646
                'data-toggle-css-class': 'only-nbci-id',
647
                'data-advanced-option': 'true',
648
649
                'data-advanced-option-family': 0,
                'data-onchange-js': 'load_data_wrapper',
650
651
652
653
654
            },
        ),
        required=False,
        initial=False,
    )
655
    life_domain = forms.ChoiceField(
Bryan  BRANCOTTE's avatar
Bryan BRANCOTTE committed
656
        label=_("preference.life_domain.label"),
Bryan  BRANCOTTE's avatar
Bryan BRANCOTTE committed
657
        help_text=_("preference.life_domain.help_text"),
658
659
660
661
662
663
664
665
666
667
668
669
        choices=(('', _('All')),) + models.DataSource._meta.get_field("life_domain").choices,
        widget=widgets.RadioSelect(
            attrs={
                'data-toggle-css-class': 'only-nbci-id',
                'data-advanced-option': 'true',
                'data-advanced-option-family': 0,
                'data-onchange-js': 'load_data_wrapper',
            },
        ),
        initial='',
        required=False,
    )
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
    helper = FormHelper()


class CommonPrefBrowseForm(CommonSearchPrefForm):
    class Meta:
        abstract = True

    advanced_option_families = {
        "2": _("Analysis Tools"),
        "0": _("Data filtering options"),
        "1": _("Rendering options"),
    }
    display_all_at_once = forms.BooleanField(
        # label=_("Fit table to screen, scroll when needed"),
        # help_text=_("Otherwise display all the table at once but may not fit on screen"),
        label=_("Display all the table at once, even if it does not fit on screen"),
        help_text=_("Otherwise we fit the table to the screen and use scroll bar when needed."),
687
688
        widget=widgets.CheckboxInput(
            attrs={
689
                'data-toggle-css-class': 'freeze-col-header-v3 freeze-row-header-v3',
690
                'data-advanced-option': 'true',
691
                'data-advanced-option-family': 1,
692
693
694
695
696
            },
        ),
        required=False,
        initial=False,
    )
697
698
699
    horizontal_column_header = forms.BooleanField(
        label=_("horizontal_column_header_label"),
        help_text=_("horizontal_column_header_help_text"),
700
701
702
        widget=widgets.CheckboxInput(
            attrs={
                'data-advanced-option': 'true',
703
704
                'data-advanced-option-family': 1,
                'data-toggle-css-class': 'horizontal-col-header',
705
706
707
708
709
            },
        ),
        required=False,
        initial=False,
    )
710
711
712
    weak_infection = forms.BooleanField(
        label=_("Consider all positive response as a infection"),
        help_text=_("By default we consider that there is an infection only when the highest response is observed."),
713
        widget=widgets.CheckboxInput(
714
715
            attrs={
                'data-advanced-option': 'true',
716
717
                'data-advanced-option-family': 2,
                'data-onchange-js': 'refresh_all_infection_ratio',
718
                'data-container-additional-class': 'order-xl-4 order-7',
719
720
721
            },
        ),
        required=False,
722
        initial=False,
723
    )
724
725
726


class BrowseForm(CommonPrefBrowseForm):
727
728
729
730
731
732
733
734
    ds = ByNameModelMultipleChoiceField(
        label=_("Data source"),
        queryset=models.DataSource.objects.none(),
        widget=BoostrapSelectMultiple(
            allSelectedText=_("All data sources selected"),
            nonSelectedText=_("All relevant data sources") + "*",
            SelectedText=_(" data source selected"),
            onChangeFunctionName="filter_changed",
735
736
737
            attrs={
                "data-select-all-inv-nop": "nop",
            },
738
739
740
        ),
        required=False,
    )
741
    virus = ByExplicitNameModelMultipleChoiceField(
Bryan  BRANCOTTE's avatar
Bryan BRANCOTTE committed
742
        label=_("Virus"),
743
744
        queryset=models.Virus.objects.none(),
        widget=BoostrapSelectMultiple(
745
            allSelectedText=_("All viruses selected"),
746
            nonSelectedText=_("All relevant viruses") + "*",
747
            SelectedText=_(" viruses selected"),
748
            onChangeFunctionName="filter_changed",
749
750
751
            attrs={
                "data-select-all-inv-nop": True,
            },
752
753
754
        ),
        required=False,
    )
755
    host = ByExplicitNameModelMultipleChoiceField(
Bryan  BRANCOTTE's avatar
Bryan BRANCOTTE committed
756
        label=_("Host"),
757
        queryset=models.Host.objects.none(),
758
759
        widget=BoostrapSelectMultiple(
            allSelectedText=_("All hosts selected"),
760
            nonSelectedText=_("All relevant hosts") + "*",
761
762
            SelectedText=_(" hosts selected"),
            onChangeFunctionName="filter_changed",
763
764
765
            attrs={
                "data-select-all-inv-nop": True,
            },
766
767
768
        ),
        required=False,
    )
769
    focus_disagreements_toggle = forms.BooleanField(
770
771
        label=_("Render table focusing on disagreements"),
        help_text=_("Responses where data sources disagree are highlighted, others are faded."),
772
        widget=widgets.CheckboxInput(
773
774
775
            attrs={
                'data-toggle-css-class': 'only-disagree',
                'data-advanced-option': 'true',
776
                'data-advanced-option-family': 1,
777
778
779
780
781
            },
        ),
        required=False,
        initial=False,
    )
782
783
784
    render_actual_aggregated_responses = forms.BooleanField(
        label=_("render_actual_aggregated_responses_label"),
        help_text=_("render_actual_aggregated_responses_help_text"),
785
        widget=widgets.CheckboxInput(
786
787
788
            attrs={
                'data-advanced-option': 'true',
                'data-advanced-option-family': 1,
Bryan  BRANCOTTE's avatar
Bryan BRANCOTTE committed
789
                'data-onchange-js': 'load_data_wrapper',
790
791
792
793
794
            },
        ),
        required=False,
        initial=False,
    )
795
796
797
798
799
800
801
802
803
804
805
806
807
    full_length_vh_name_toggle = forms.BooleanField(
        label=_("Show names at full length"),
        help_text=_("To improve rendering, viruses and hosts names are capped."),
        widget=widgets.CheckboxInput(
            attrs={
                'data-toggle-css-class': 'full-length-vh-name',
                'data-advanced-option': 'true',
                'data-advanced-option-family': 1,
            },
        ),
        required=False,
        initial=False,
    )
808
809
810
    virus_infection_ratio = forms.BooleanField(
        label=_("Show the infection ratio for the viruses"),
        help_text=_("Note that it can deteriorate the overall response time."),
811
        widget=widgets.CheckboxInput(
Bryan  BRANCOTTE's avatar
Bryan BRANCOTTE committed
812
813
            attrs={
                'data-advanced-option': 'true',
814
                'data-advanced-option-family': 2,
815
                'data-onchange-js': 'refresh_virus_infection_ratio',
Bryan  BRANCOTTE's avatar
Bryan BRANCOTTE committed
816
                'data-container-additional-class': 'order-xl-2 order-3',
Bryan  BRANCOTTE's avatar
Bryan BRANCOTTE committed
817
818
819
            },
        ),
        required=False,
820
821
822
823
824
        initial=False,
    )
    host_infection_ratio = forms.BooleanField(
        label=_("Show the infection ratio for the hosts"),
        help_text=_("Note that it can deteriorate the overall response time."),
825
        widget=widgets.CheckboxInput(
826
827
828
            attrs={
                'data-advanced-option': 'true',
                'data-advanced-option-family': 2,
829
                'data-onchange-js': 'refresh_host_infection_ratio',
Bryan  BRANCOTTE's avatar
Bryan BRANCOTTE committed
830
                'data-container-additional-class': 'order-xl-3 order-5',
831
832
833
834
            },
        ),
        required=False,
        initial=False,
Bryan  BRANCOTTE's avatar
Bryan BRANCOTTE committed
835
    )
836
    hide_rows_with_no_infection = forms.BooleanField(
837
        label=_("hide_virus_with_no_infection_label"),
838
        help_text=_("hide_rows_with_no_infection_help_text"),
839
        widget=widgets.CheckboxInput(
840
841
842
843
            attrs={
                'data-advanced-option': 'true',
                'data-advanced-option-family': 2,
                'data-toggle-css-class': 'hide-rows-with-no-infection',
844
                # 'data-onchange-js': 'refresh_all_infection_ratio',
Bryan  BRANCOTTE's avatar
Bryan BRANCOTTE committed
845
                'data-container-additional-class': 'order-xl-5 order-4',
846
847
848
849
850
            },
        ),
        required=False,
        initial=False,
    )
851
    hide_cols_with_no_infection = forms.BooleanField(
852
        label=_("hide_host_with_no_infection_label"),
853
        help_text=_("hide_cols_with_no_infection_help_text"),
854
        widget=widgets.CheckboxInput(
855
856
857
858
            attrs={
                'data-advanced-option': 'true',
                'data-advanced-option-family': 2,
                'data-toggle-css-class': 'hide-cols-with-no-infection',
859
                # 'data-onchange-js': 'refresh_all_infection_ratio',
Bryan  BRANCOTTE's avatar
Bryan BRANCOTTE committed
860
                'data-container-additional-class': 'order-xl-6 order-6',
861
862
863
864
865
            },
        ),
        required=False,
        initial=False,
    )
Bryan  BRANCOTTE's avatar
Bryan BRANCOTTE committed
866
867
868
869
870
871
    agreed_infection = forms.BooleanField(
        label=_(
            "Consider that there is an infection only when <i>all data sources</i> "
            "documenting the interaction observed an infection"),
        help_text=_("By default we consider that there is an infection when in <i>at least one</i> "
                    "data source an infection was observed."),
872
        widget=widgets.CheckboxInput(
Bryan  BRANCOTTE's avatar
Bryan BRANCOTTE committed
873
874
            attrs={
                'data-advanced-option': 'true',
875
                'data-advanced-option-family': 2,
876
                'data-onchange-js': 'refresh_all_infection_ratio',
877
                'data-container-additional-class': 'order-xl-8 order-8',
Bryan  BRANCOTTE's avatar
Bryan BRANCOTTE committed
878
879
880
881
882
            },
        ),
        required=False,
        initial=False,
    )
883
    sort_rows = forms.ChoiceField(
884
        widget=widgets.HiddenInput(),
885
886
887
888
        choices=(('asc', 'asc'), ('desc', 'desc'),),
        required=False,
    )
    sort_cols = forms.ChoiceField(
889
        widget=widgets.HiddenInput(),
890
891
892
        choices=(('asc', 'asc'), ('desc', 'desc'),),
        required=False,
    )
893

894
    def __init__(self, data=None, user=None, initial=None, *args, **kwargs):
895
        use_pref = True
896
        initial = initial or dict()
Bryan  BRANCOTTE's avatar
Bryan BRANCOTTE committed
897
        if data is not None:
898
            data = data.copy()
899
900
901
            initial.setdefault('host', [])
            initial.setdefault('virus', [])
            initial.setdefault("ds", [])
902
903
            for k, v in data.items():
                initial[k] = v
904
            for k in ['host', 'ds', 'virus']:
Bryan  BRANCOTTE's avatar
Bryan BRANCOTTE committed
905
906
907
908
909
                list_for_k = data.getlist(k)
                if len(list_for_k) > 1:
                    initial[k] = list_for_k
                elif len(list_for_k) == 1:
                    initial[k] = list_for_k[0].split(',')
910
            use_pref = False
Bryan  BRANCOTTE's avatar
Bryan BRANCOTTE committed
911
        if use_pref or data and data.get('use_pref', 'False').lower() == 'true':
912
            pref = get_user_preferences_for_user(user)
913
914
915
916
917
            for group, fields in models.UserPreferences.preferences_groups.items():
                for f in fields:
                    initial[f] = getattr(pref, f)
                    if data:
                        data[f] = getattr(pref, f)
918
        super().__init__(initial=initial, data=data if data and len(data) > 0 else None, *args, **kwargs)
919
920
        if user is None:
            return
921
        self.fields["virus"].queryset = only_public_or_granted_or_owned_queryset_filter(
922
923
924
925
926
927
            None,
            request=None,
            user=user,
            queryset=models.Virus.objects,
            path_to_data_source="data_source__",
        )
928
        self.fields["host"].queryset = only_public_or_granted_or_owned_queryset_filter(
929
930
931
            None,
            request=None,
            user=user,
932
            queryset=models.Host.objects,
933
934
            path_to_data_source="data_source__",
        )
935
        self.fields["ds"].queryset = only_public_or_granted_or_owned_queryset_filter(
936
937
938
939
940
941
            None,
            request=None,
            user=user,
            queryset=models.DataSource.objects,
            path_to_data_source="",
        )
942
        self.fields["horizontal_column_header"].label = _("horizontal_host_header_label")
943
944


945
class VirusHostDetailViewForm(CommonPrefBrowseForm):
946
947
948
    infection_ratio = forms.BooleanField(
        label=_("Show the infection ratio"),
        help_text=_("Note that it can deteriorate the overall response time."),
949
        widget=widgets.CheckboxInput(
950
951
952
953
            attrs={
                'data-advanced-option': 'true',
                'data-advanced-option-family': 2,
                'data-onchange-js': 'refresh_all_infection_ratio',
Bryan  BRANCOTTE's avatar
Bryan BRANCOTTE committed
954
                'data-container-additional-class': 'order-0',
955
956
957
958
959
            },
        ),
        required=False,
        initial=False,
    )
960
961
962
    hide_rows_with_no_infection = forms.BooleanField(
        label=_("hide_rows_with_no_infection_label"),
        help_text=_("hide_rows_with_no_infection_help_text"),
963
        widget=widgets.CheckboxInput(
964
965
966
967
            attrs={
                'data-advanced-option': 'true',
                'data-advanced-option-family': 2,
                'data-toggle-css-class': 'hide-rows-with-no-infection',
Bryan  BRANCOTTE's avatar
Bryan BRANCOTTE committed
968
                'data-container-additional-class': 'order-3',
969
970
971
972
973
974
975
976
            },
        ),
        required=False,
        initial=False,
    )
    hide_cols_with_no_infection = forms.BooleanField(
        label=_("hide_ds_with_no_infection_label"),
        help_text=_("hide_cols_with_no_infection_help_text"),
977
        widget=widgets.CheckboxInput(
978
979
980
981
            attrs={
                'data-advanced-option': 'true',
                'data-advanced-option-family': 2,
                'data-toggle-css-class': 'hide-cols-with-no-infection',
Bryan  BRANCOTTE's avatar
Bryan BRANCOTTE committed
982
                'data-container-additional-class': 'order-4',
983
984
985
986
987
            },
        ),
        required=False,
        initial=False,
    )
988
    sort_rows = forms.ChoiceField(
989
        widget=widgets.HiddenInput(),
990
991
992
993
        choices=(('asc', 'asc'), ('desc', 'desc'),),
        required=False,
    )
    sort_cols = forms.ChoiceField(
994
        widget=widgets.HiddenInput(),
995
996
997
998
        choices=(('asc', 'asc'), ('desc', 'desc'),),
        required=False,
    )
    row_order = forms.CharField(
999
        widget=widgets.HiddenInput(),
1000
        required=False,