From cbc22d06e95cd30e781cc7f7003e8c85b69a1174 Mon Sep 17 00:00:00 2001 From: Diane Trout Date: Tue, 5 May 2015 14:41:10 -0700 Subject: [PATCH] adjust whitespace --- experiments/test_experiments.py | 24 +- samples/models.py | 617 +++++++++++++++++--------------- 2 files changed, 343 insertions(+), 298 deletions(-) diff --git a/experiments/test_experiments.py b/experiments/test_experiments.py index 1635560..4084a42 100644 --- a/experiments/test_experiments.py +++ b/experiments/test_experiments.py @@ -30,9 +30,9 @@ from htsworkflow.util.ethelp import validate_xhtml from htsworkflow.pipelines.test.simulate_runfolder import TESTDATA_DIR -LANE_SET = range(1,9) +LANE_SET = range(1, 9) -NSMAP = {'libns':'http://jumpgate.caltech.edu/wiki/LibraryOntology#'} +NSMAP = {'libns': 'http://jumpgate.caltech.edu/wiki/LibraryOntology#'} from django.db import connection @@ -63,7 +63,7 @@ class ExperimentsTestCases(TestCase): runxml = 'run_FC12150_2007-09-27.xml' shutil.copy(os.path.join(TESTDATA_DIR, runxml), os.path.join(self.fc1_dir, runxml)) - for i in range(1,9): + for i in range(1, 9): affiliation = self.affiliation_odd if i % 2 == 1 else self.affiliation_even library = LibraryFactory(id="1215" + str(i)) library.affiliations.add(affiliation) @@ -78,7 +78,7 @@ class ExperimentsTestCases(TestCase): self.fc42jtn = FlowCellFactory(flowcell_id='42JTNAAXX') self.fc42jtn_lanes = [] - for i in range(1,9): + for i in range(1, 9): affiliation = self.affiliation_odd if i % 2 == 1 else self.affiliation_even library_type = LibraryTypeFactory(can_multiplex=True) multiplex_index = MultiplexIndexFactory(adapter_type=library_type) @@ -187,16 +187,17 @@ class ExperimentsTestCases(TestCase): self.assertEqual(library_12153['library_id'], '12153') def test_raw_id_field(self): - """ - Test ticket:147 + """Test ticket:147 Library's have IDs, libraries also have primary keys, - we eventually had enough libraries that the drop down combo box was too - hard to filter through, unfortnately we want a field that uses our library - id and not the internal primary key, and raw_id_field uses primary keys. + we eventually had enough libraries that the drop down combo + box was too hard to filter through, unfortnately we want a + field that uses our library id and not the internal + primary key, and raw_id_field uses primary keys. + + This tests to make sure that the value entered in the raw + library id field matches the library id looked up. - This tests to make sure that the value entered in the raw library id field matches - the library id looked up. """ expected_ids = [ '1215{}'.format(i) for i in range(1,9) ] self.assertTrue(self.client.login(username=self.admin.username, password=self.password)) @@ -333,7 +334,6 @@ class ExperimentsTestCases(TestCase): fc.flowcell_id = flowcell_id + " (failed)" self.assertEqual(fc.get_raw_data_directory(), raw_dir) - def test_data_run_import(self): srf_file_type = FileType.objects.get(name='SRF') runxml_file_type = FileType.objects.get(name='run_xml') diff --git a/samples/models.py b/samples/models.py index eaa433b..c61c0d2 100644 --- a/samples/models.py +++ b/samples/models.py @@ -11,6 +11,7 @@ import six logger = logging.getLogger(__name__) + class Antibody(models.Model): antigene = models.CharField(max_length=500, db_index=True) # New field Aug/20/08 @@ -24,37 +25,46 @@ class Antibody(models.Model): ) catalog = models.CharField(max_length=50, blank=True, null=True) antibodies = models.CharField(max_length=500, db_index=True) - source = models.CharField(max_length=500, blank=True, null=True, db_index=True) + source = models.CharField(max_length=500, + blank=True, null=True, db_index=True) biology = models.TextField(blank=True, null=True) notes = models.TextField(blank=True, null=True) + def __str__(self): return '%s - %s' % (self.antigene, self.antibodies) + class Meta: verbose_name_plural = "antibodies" ordering = ["antigene"] + class Cellline(models.Model): - cellline_name = models.CharField(max_length=100, unique=True, db_index=True) - nickname = models.CharField(max_length=20, + cellline_name = models.CharField(max_length=100, + unique=True, db_index=True) + nickname = models.CharField( + max_length=20, blank=True, null=True, db_index=True) notes = models.TextField(blank=True) + def __str__(self): return str(self.cellline_name) class Meta: ordering = ["cellline_name"] + class Condition(models.Model): condition_name = models.CharField( max_length=2000, unique=True, db_index=True) - nickname = models.CharField(max_length=20, + nickname = models.CharField( + max_length=20, blank=True, null=True, db_index=True, - verbose_name = 'Short Name') + verbose_name='Short Name') notes = models.TextField(blank=True) def __str__(self): @@ -65,82 +75,92 @@ class Condition(models.Model): class ExperimentType(models.Model): - name = models.CharField(max_length=50, unique=True) + name = models.CharField(max_length=50, unique=True) + + def __str__(self): + return str(self.name) - def __str__(self): - return str(self.name) class Tag(models.Model): - tag_name = models.CharField(max_length=100, db_index=True,blank=False,null=False) - TAG_CONTEXT = ( - #('Antibody','Antibody'), - #('Cellline', 'Cellline'), - #('Condition', 'Condition'), - ('Library', 'Library'), - ('ANY','ANY'), - ) - context = models.CharField(max_length=50, - choices=TAG_CONTEXT, default='Library') - - def __str__(self): - return '%s' % (self.tag_name) - - class Meta: - ordering = ["context","tag_name"] + tag_name = models.CharField(max_length=100, + db_index=True,blank=False,null=False) + TAG_CONTEXT = ( + #('Antibody','Antibody'), + #('Cellline', 'Cellline'), + #('Condition', 'Condition'), + ('Library', 'Library'), + ('ANY','ANY'), + ) + context = models.CharField( + max_length=50, + choices=TAG_CONTEXT, default='Library') + + def __str__(self): + return '%s' % (self.tag_name,) + + class Meta: + ordering = ["context", "tag_name"] + class Species(models.Model): - scientific_name = models.CharField(max_length=256, - unique=False, - db_index=True - ) - common_name = models.CharField(max_length=256, blank=True) - #use_genome_build = models.CharField(max_length=100, blank=False, null=False) + scientific_name = models.CharField( + max_length=256, + unique=False, + db_index=True + ) + common_name = models.CharField(max_length=256, blank=True) + #use_genome_build = models.CharField(max_length=100, blank=False, null=False) - def __str__(self): - return '%s (%s)' % (self.scientific_name, self.common_name) + def __str__(self): + return '%s (%s)' % (self.scientific_name, self.common_name) + + class Meta: + verbose_name_plural = "species" + ordering = ["scientific_name"] - class Meta: - verbose_name_plural = "species" - ordering = ["scientific_name"] + @models.permalink + def get_absolute_url(self): + return ('samples.views.species', [str(self.id)]) - @models.permalink - def get_absolute_url(self): - return ('samples.views.species', [str(self.id)]) class Affiliation(models.Model): - name = models.CharField(max_length=256, db_index=True, verbose_name='Name') - contact = models.CharField(max_length=256, null=True, blank=True,verbose_name='Lab Name') - email = models.EmailField(null=True,blank=True) - users = models.ManyToManyField('HTSUser', null=True, blank=True) - users.admin_order_field = "username" - - def __str__(self): - name = str(self.name) - if self.contact is not None and len(self.contact) > 0: - name += ' ('+self.contact+')' - return name - - def Users(self): - users = self.users.all().order_by('username') - return ", ".join([str(a) for a in users ]) - - class Meta: - ordering = ["name","contact"] - unique_together = (("name", "contact"),) + name = models.CharField(max_length=256, db_index=True, verbose_name='Name') + contact = models.CharField(max_length=256, + null=True, blank=True, verbose_name='Lab Name') + email = models.EmailField(null=True, blank=True) + users = models.ManyToManyField('HTSUser', null=True, blank=True) + users.admin_order_field = "username" + + def __str__(self): + name = str(self.name) + if self.contact is not None and len(self.contact) > 0: + name += ' ('+self.contact+')' + return name + + def Users(self): + users = self.users.all().order_by('username') + return ", ".join([str(a) for a in users]) + + class Meta: + ordering = ["name", "contact"] + unique_together = (("name", "contact"),) + class LibraryType(models.Model): - name = models.CharField(max_length=255, unique=True, - verbose_name="Adapter Type") - is_paired_end = models.BooleanField(default=True, - help_text="can you do a paired end run with this adapter") - can_multiplex = models.BooleanField(default=True, - help_text="Does this adapter provide multiplexing?") + name = models.CharField(max_length=255, unique=True, + verbose_name="Adapter Type") + is_paired_end = models.BooleanField( + default=True, + help_text="can you do a paired end run with this adapter") + can_multiplex = models.BooleanField( + default=True, + help_text="Does this adapter provide multiplexing?") - def __str__(self): - return str(self.name) + def __str__(self): + return str(self.name) - class Meta: - ordering = ["-id"] + class Meta: + ordering = ["-id"] class MultiplexIndex(models.Model): @@ -153,227 +173,252 @@ class MultiplexIndex(models.Model): verbose_name_plural = "multiplex indicies" unique_together = ('adapter_type', 'multiplex_id') + class Library(models.Model): - id = models.CharField(max_length=10, primary_key=True) - library_name = models.CharField(max_length=100, unique=True) - library_species = models.ForeignKey(Species) - hidden = models.BooleanField(default=False) - account_number = models.CharField(max_length=100, null=True, blank=True) - cell_line = models.ForeignKey(Cellline, blank=True, null=True, - verbose_name="Background") - condition = models.ForeignKey(Condition, blank=True, null=True) - antibody = models.ForeignKey(Antibody,blank=True,null=True) - affiliations = models.ManyToManyField( - Affiliation,related_name='library_affiliations',null=True) - tags = models.ManyToManyField(Tag,related_name='library_tags', - blank=True,null=True) - REPLICATE_NUM = [(x,x) for x in range(1,7)] - replicate = models.PositiveSmallIntegerField(choices=REPLICATE_NUM, - blank=True,null=True) - experiment_type = models.ForeignKey(ExperimentType) - library_type = models.ForeignKey(LibraryType, blank=True, null=True, - verbose_name="Adapter Type") - multiplex_id = models.CharField(max_length=128, - blank=True, null=True, - verbose_name="Index ID") - creation_date = models.DateField(blank=True, null=True) - made_for = models.CharField(max_length=50, blank=True, - verbose_name='ChIP/DNA/RNA Made By') - made_by = models.CharField(max_length=50, blank=True, default="Lorian") - - PROTOCOL_END_POINTS = ( - ('?', 'Unknown'), - ('Sample', 'Raw sample'), - ('Progress', 'In progress'), - ('1A', 'Ligation, then gel'), - ('PCR', 'Ligation, then PCR'), - ('1Ab', 'Ligation, PCR, then gel'), - ('1Ac', 'Ligation, gel, then 12x PCR'), - ('1Aa', 'Ligation, gel, then 18x PCR'), - ('2A', 'Ligation, PCR, gel, PCR'), - ('Done', 'Completed'), + id = models.CharField(max_length=10, primary_key=True) + library_name = models.CharField(max_length=100, unique=True) + library_species = models.ForeignKey(Species) + hidden = models.BooleanField(default=False) + account_number = models.CharField(max_length=100, null=True, blank=True) + cell_line = models.ForeignKey(Cellline, blank=True, null=True, + verbose_name="Background") + condition = models.ForeignKey(Condition, blank=True, null=True) + antibody = models.ForeignKey(Antibody,blank=True,null=True) + affiliations = models.ManyToManyField( + Affiliation,related_name='library_affiliations',null=True) + tags = models.ManyToManyField(Tag,related_name='library_tags', + blank=True,null=True) + REPLICATE_NUM = [(x,x) for x in range(1,7)] + replicate = models.PositiveSmallIntegerField(choices=REPLICATE_NUM, + blank=True,null=True) + experiment_type = models.ForeignKey(ExperimentType) + library_type = models.ForeignKey(LibraryType, blank=True, null=True, + verbose_name="Adapter Type") + multiplex_id = models.CharField(max_length=128, + blank=True, null=True, + verbose_name="Index ID") + creation_date = models.DateField(blank=True, null=True) + made_for = models.CharField(max_length=50, blank=True, + verbose_name='ChIP/DNA/RNA Made By') + made_by = models.CharField(max_length=50, blank=True, default="Lorian") + + PROTOCOL_END_POINTS = ( + ('?', 'Unknown'), + ('Sample', 'Raw sample'), + ('Progress', 'In progress'), + ('1A', 'Ligation, then gel'), + ('PCR', 'Ligation, then PCR'), + ('1Ab', 'Ligation, PCR, then gel'), + ('1Ac', 'Ligation, gel, then 12x PCR'), + ('1Aa', 'Ligation, gel, then 18x PCR'), + ('2A', 'Ligation, PCR, gel, PCR'), + ('Done', 'Completed'), ) - PROTOCOL_END_POINTS_DICT = dict(PROTOCOL_END_POINTS) - stopping_point = models.CharField(max_length=25, - choices=PROTOCOL_END_POINTS, - default='Done') - - amplified_from_sample = models.ForeignKey('self', - related_name='amplified_into_sample', - blank=True, null=True) - - undiluted_concentration = models.DecimalField("Concentration", - max_digits=5, decimal_places=2, blank=True, null=True, - help_text = "Undiluted concentration (ng/\u00b5l)") - # note \u00b5 is the micro symbol in unicode - successful_pM = models.DecimalField(max_digits=9, - decimal_places=1, blank=True, null=True) - ten_nM_dilution = models.BooleanField(default=False) - gel_cut_size = models.IntegerField(default=225, blank=True, null=True) - insert_size = models.IntegerField(blank=True, null=True) - notes = models.TextField(blank=True) - - bioanalyzer_summary = models.TextField(blank=True,default="") - bioanalyzer_concentration = models.DecimalField(max_digits=5, - decimal_places=2, blank=True, null=True, - help_text="(ng/\u00b5l)") - bioanalyzer_image_url = models.URLField(blank=True,default="") - - def __str__(self): - return '#%s: %s' % (self.id, self.library_name) - - class Meta: - verbose_name_plural = "libraries" - #ordering = ["-creation_date"] - ordering = ["-id"] - - def antibody_name(self): - str =''+self.antibody.label+'' - return str - antibody_name.allow_tags = True - - def organism(self): - return self.library_species.common_name - - def index_sequences(self): - """Return a dictionary of multiplex index id to sequence - Return None if the library can't multiplex, - - """ - if self.library_type is None: - return None - if not self.library_type.can_multiplex: - return None - if self.multiplex_id is None or len(self.multiplex_id) == 0: - return 'Err: id empty' - sequences = {} - multiplex_expressions = self.multiplex_id.split(',') - for multiplex_term in multiplex_expressions: - pairs = multiplex_term.split('-') - if len(pairs) == 1: - key = pairs[0] - seq = self._lookup_index(pairs[0]) - elif len(pairs) == 2: - key = pairs[0] + '-' + pairs[1] - seq0 = self._lookup_index(pairs[0]) - seq1 = self._lookup_index(pairs[1]) - if seq0 is None or seq1 is None: - seq = None - else: - seq = seq0 + '-' + seq1 - else: - raise RuntimeError("Too many - seperated sequences") - if seq is None: - seq = 'Err: index not found' - sequences[key] = seq - return sequences - - def _lookup_index(self, multiplex_id): - try: - multiplex = MultiplexIndex.objects.get( - adapter_type = self.library_type.id, - multiplex_id = multiplex_id) - return multiplex.sequence - except MultiplexIndex.DoesNotExist as e: - return None - - def index_sequence_text(self, seperator=' '): - """Return formatted multiplex index sequences""" - sequences = self.index_sequences() - if sequences is None: - return "" - if isinstance(sequences, six.string_types): - return sequences - multiplex_ids = sequences.keys() - multiplex_ids.sort() - return seperator.join(( "%s:%s" %(i,sequences[i]) for i in multiplex_ids)) - index_sequence_text.short_description = "Index" - - - def affiliation(self): - affs = self.affiliations.all().order_by('name') - tstr = '' - ar = [] - for t in affs: - ar.append(t.__str__()) - return '%s' % (", ".join(ar)) - - def is_archived(self): - """ - returns True if archived else False - """ - if self.longtermstorage_set.count() > 0: - return True - else: - return False - - def stopping_point_name(self): - end_points = Library.PROTOCOL_END_POINTS_DICT - name = end_points.get(self.stopping_point, None) - if name is None: - name = "Lookup Error" - logger.error("protocol stopping point in database didn't match names in library model") - return name - - - def libtags(self): - affs = self.tags.all().order_by('tag_name') - ar = [] - for t in affs: - ar.append(t.__str__()) - return '%s' % ( ", ".join(ar)) - - def DataRun(self): - str ='Data Run' - return str - DataRun.allow_tags = True - - def aligned_m_reads(self): - return getLibReads(self.id) - - def aligned_reads(self): - res = getLibReads(self.id) - - # Check data sanity - if res[2] != "OK": - return '
'+res[2]+'
' - - rc = "%1.2f" % (res[1]/1000000.0) - # Color Scheme: green is more than 10M, blue is more than 5M, orange is more than 3M and red is less. For RNAseq, all those thresholds should be doubled - if res[0] > 0: - bgcolor = '#ff3300' # Red - rc_thr = [10000000,5000000,3000000] - if self.experiment_type == 'RNA-seq': - rc_thr = [20000000,10000000,6000000] - - if res[1] > rc_thr[0]: - bgcolor = '#66ff66' # Green - else: - if res[1] > rc_thr[1]: - bgcolor ='#00ccff' # Blue + PROTOCOL_END_POINTS_DICT = dict(PROTOCOL_END_POINTS) + stopping_point = models.CharField(max_length=25, + choices=PROTOCOL_END_POINTS, + default='Done') + + amplified_from_sample = models.ForeignKey( + 'self', + related_name='amplified_into_sample', + blank=True, null=True) + + undiluted_concentration = models.DecimalField( + "Concentration", + max_digits=5, decimal_places=2, blank=True, null=True, + help_text="Undiluted concentration (ng/\u00b5l)") + # note \u00b5 is the micro symbol in unicode + successful_pM = models.DecimalField( + max_digits=9, decimal_places=1, blank=True, null=True) + ten_nM_dilution = models.BooleanField(default=False) + gel_cut_size = models.IntegerField(default=225, blank=True, null=True) + insert_size = models.IntegerField(blank=True, null=True) + notes = models.TextField(blank=True) + + bioanalyzer_summary = models.TextField(blank=True, default="") + bioanalyzer_concentration = models.DecimalField( + max_digits=5, decimal_places=2, blank=True, null=True, + help_text="(ng/\u00b5l)") + bioanalyzer_image_url = models.URLField(blank=True, default="") + + def __str__(self): + return '#%s: %s' % (self.id, self.library_name) + + class Meta: + verbose_name_plural = "libraries" + # ordering = ["-creation_date"] + ordering = ["-id"] + + def antibody_name(self): + str =''+self.antibody.label+'' + return str + antibody_name.allow_tags = True + + def organism(self): + return self.library_species.common_name + + def index_sequences(self): + """Return a dictionary of multiplex index id to sequence + Return None if the library can't multiplex, + """ + if self.library_type is None: + return None + if not self.library_type.can_multiplex: + return None + if self.multiplex_id is None or len(self.multiplex_id) == 0: + return 'Err: id empty' + sequences = {} + multiplex_expressions = self.multiplex_id.split(',') + for multiplex_term in multiplex_expressions: + pairs = multiplex_term.split('-') + if len(pairs) == 1: + key = pairs[0] + seq = self._lookup_index(pairs[0]) + elif len(pairs) == 2: + key = pairs[0] + '-' + pairs[1] + seq0 = self._lookup_index(pairs[0]) + seq1 = self._lookup_index(pairs[1]) + if seq0 is None or seq1 is None: + seq = None + else: + seq = seq0 + '-' + seq1 + else: + raise RuntimeError("Too many - seperated sequences") + if seq is None: + seq = 'Err: index not found' + sequences[key] = seq + return sequences + + def _lookup_index(self, multiplex_id): + try: + multiplex = MultiplexIndex.objects.get( + adapter_type=self.library_type.id, + multiplex_id=multiplex_id) + return multiplex.sequence + except MultiplexIndex.DoesNotExist as e: + return None + + def index_sequence_text(self, seperator=' '): + """Return formatted multiplex index sequences""" + sequences = self.index_sequences() + if sequences is None: + return "" + if isinstance(sequences, six.string_types): + return sequences + multiplex_ids = sorted(sequences) + return seperator.join(("%s:%s" % (i,sequences[i]) for i in multiplex_ids)) + index_sequence_text.short_description = "Index" + + def affiliation(self): + affs = self.affiliations.all().order_by('name') + tstr = '' + ar = [] + for t in affs: + ar.append(t.__str__()) + return '%s' % (", ".join(ar)) + + def is_archived(self): + """returns True if archived else False + """ + if self.longtermstorage_set.count() > 0: + return True else: - if res[1] > rc_thr[2]: - bgcolor ='#ffcc33' # Orange - tstr = '
' - tstr += res[0].__str__()+' Lanes, '+rc+' M Reads' - tstr += '
' - else: tstr = 'not processed yet' - return tstr - aligned_reads.allow_tags = True - - def public(self): - SITE_ROOT = '/' - summary_url = self.get_absolute_url() - return 'S' % (summary_url,) - public.allow_tags = True - - @models.permalink - def get_absolute_url(self): - return ('samples.views.library_to_flowcells', [str(self.id)]) - - def get_admin_url(self): - return urlresolvers.reverse('admin:samples_library_change', - args=(self.id,)) + return False + + def lanes_sequenced(self): + """Count how many lanes of each type were run. + """ + single = 0 + paired = 1 + short_read = 0 + medium_read = 1 + long_read = 2 + counts = [[0, 0, 0], [0, 0, 0]] + + for lane in self.lane_set.all(): + if lane.flowcell.paired_end: + lane_type = paired + else: + lane_type = single + + if lane.flowcell.read_length < 40: + read_type = short_read + elif lane.flowcell.read_length < 100: + read_type = medium_read + else: + read_type = long_read + counts[lane_type][read_type] += 1 + + return counts + + def stopping_point_name(self): + end_points = Library.PROTOCOL_END_POINTS_DICT + name = end_points.get(self.stopping_point, None) + if name is None: + name = "Lookup Error" + logger.error("protocol stopping point in database didn't match names in library model") + return name + + def libtags(self): + affs = self.tags.all().order_by('tag_name') + ar = [] + for t in affs: + ar.append(t.__str__()) + return '%s' % (", ".join(ar)) + + def DataRun(self): + str ='Data Run' + return str + DataRun.allow_tags = True + + def aligned_m_reads(self): + return getLibReads(self.id) + + def aligned_reads(self): + res = getLibReads(self.id) + + # Check data sanity + if res[2] != "OK": + return '
'+res[2]+'
' + + rc = "%1.2f" % (res[1]/1000000.0) + # Color Scheme: green is more than 10M, blue is more than 5M, orange is more than 3M and red is less. For RNAseq, all those thresholds should be doubled + if res[0] > 0: + bgcolor = '#ff3300' # Red + rc_thr = [10000000, 5000000, 3000000] + if self.experiment_type == 'RNA-seq': + rc_thr = [20000000, 10000000, 6000000] + + if res[1] > rc_thr[0]: + bgcolor = '#66ff66' # Green + else: + if res[1] > rc_thr[1]: + bgcolor = '#00ccff' # Blue + else: + if res[1] > rc_thr[2]: + bgcolor = '#ffcc33' # Orange + tstr = '
' + tstr += res[0].__str__()+' Lanes, '+rc+' M Reads' + tstr += '
' + else: + tstr = 'not processed yet' + return tstr + aligned_reads.allow_tags = True + + def public(self): + summary_url = self.get_absolute_url() + return 'S' % (summary_url,) + public.allow_tags = True + + @models.permalink + def get_absolute_url(self): + return ('samples.views.library_to_flowcells', [str(self.id)]) + + def get_admin_url(self): + return urlresolvers.reverse('admin:samples_library_change', + args=(self.id,)) + class HTSUser(User): """ @@ -388,7 +433,7 @@ class HTSUser(User): return '/admin/%s/%s/%d' % (self._meta.app_label, self._meta.module_name, self.id) def __str__(self): - #return str(self.username) + " (" + str(self.get_full_name()) + u")" + # return str(self.username) + " (" + str(self.get_full_name()) + u")" return str(self.get_full_name()) + ' (' + str(self.username) + ')' def HTSUserInsertID(sender, instance, **kwargs): -- 2.30.2