Skip to content

Commit

Permalink
refactor column type detection (bihealth#730), make link and unit ren…
Browse files Browse the repository at this point in the history
…dering dynamic (bihealth#708)
  • Loading branch information
mikkonie committed Nov 29, 2019
1 parent 7077a28 commit 16a99ac
Show file tree
Hide file tree
Showing 4 changed files with 113 additions and 51 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ Changed
- Move UI notifications to ``NotifyBadge.vue`` (#718)
- Refactor column data retrieval in ``ColumnToggleModal`` (#710)
- Rename ``getGridOptions()`` to ``initGridOptions()`` (#721)
- Dynamically add cell tooltip in rendering if value is defined (#708)
- Dynamically add/omit cell unit, link and tooltip in rendering (#708)
- Improve column type detection (#730)

Fixed
-----
Expand Down
153 changes: 107 additions & 46 deletions samplesheets/rendering.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,15 +88,20 @@ class SampleSheetTableBuilder:
headers, to be rendered as HTML on the site"""

def __init__(self):
self._study = None
self._assay = None
self._row = []
self._top_header = []
self._field_header = []
self._table_data = []
self._first_row = True
self._col_values = []
self._col_idx = 0
self._node_idx = 0
self._field_idx = 0
self._parser_version = None
self._edit = False
self._sheet_config = None

# General data and cell functions ------------------------------------------

Expand Down Expand Up @@ -177,6 +182,8 @@ def _add_top_header(self, obj, colspan):
self._top_header.append(
{'value': value.strip(), 'colour': colour, 'colspan': colspan}
)
self._node_idx += 1
self._field_idx = 0

def _add_header(self, name, header_type=None, obj=None):
"""
Expand All @@ -195,13 +202,50 @@ def _add_header(self, name, header_type=None, obj=None):
'num_col': False, # Will be checked for sorting later
'align': 'left',
}
field_config = None

# Column type (the ones we can determine at this point)
# Override by config setting if present
if self._sheet_config:
study_config = self._sheet_config['studies'][
str(self._study.sodar_uuid)
]

if not self._assay or self._node_idx < len(study_config['nodes']):
field_config = study_config['nodes'][self._node_idx]['fields'][
self._field_idx
]

else: # Assay
a_node_idx = self._node_idx - len(study_config['nodes'])
field_config = study_config['assays'][
str(self._assay.sodar_uuid)
]['nodes'][a_node_idx]['fields'][self._field_idx]

if field_config and field_config.get('format') == 'integer':
header['col_type'] = 'UNIT'
header['align'] = 'right' # TODO: Should not be required for UNIT

# Else detect type without config
elif 'contact' in name.lower():
header['col_type'] = 'CONTACT'

elif name.lower() == 'external links':
header['col_type'] = 'EXTERNAL_LINKS'

elif name.lower() == 'name' and header['item_type'] == 'DATA':
header['col_type'] = 'LINK_FILE'

else:
header['col_type'] = None # Default / to be determined later

# Add extra data for editing
if self._edit:
header['type'] = header_type
header['name'] = name # Store original field name

self._field_header.append(header)
self._field_idx += 1

def _add_cell(
self,
Expand All @@ -212,6 +256,7 @@ def _add_cell(
header_type=None,
obj=None,
tooltip=None,
basic_val=True,
# attrs=None, # :param attrs: Optional attributes (dict)
):
"""
Expand All @@ -225,6 +270,7 @@ def _add_cell(
:param header_type: Header type (string)
:param obj: Original Django model object
:param tooltip: Tooltip to be shown on mouse hover (string)
:param basic_val: Whether the value is a basic string (HACK for #730)
"""

# Add header if first row
Expand All @@ -235,11 +281,13 @@ def _add_cell(
if isinstance(value, dict):
value = self._get_value(value)

cell = {
'value': value.strip() if isinstance(value, str) else value,
'unit': unit.strip() if isinstance(unit, str) else unit,
'link': link,
}
cell = {'value': value.strip() if isinstance(value, str) else value}

if unit:
cell['unit'] = unit.strip() if isinstance(unit, str) else unit

if link:
cell['link'] = link

if tooltip:
cell['tooltip'] = tooltip
Expand All @@ -259,6 +307,23 @@ def _add_cell(
elif col_value == 1 and self._col_values[self._col_idx] == 0:
self._col_values[self._col_idx] = 1

# Modify column type according to data
if not self._field_header[self._col_idx]['col_type']:
if (
not basic_val
or cell.get('link')
or isinstance(cell['value'], dict)
or (
isinstance(cell['value'], list)
and len(cell['value']) > 0
and isinstance(cell['value'][0], dict)
)
):
self._field_header[self._col_idx]['col_type'] = 'ONTOLOGY'

elif cell.get('unit'):
self._field_header[self._col_idx]['col_type'] = 'UNIT'

self._col_idx += 1

def _add_ordered_element(self, obj):
Expand Down Expand Up @@ -369,11 +434,13 @@ def _add_annotation(self, ann, header, header_type, obj):
unit = None
link = None
tooltip = None
basic_val = False

# Special case: Comments as parsed in SODAR v0.5.2 (see #629)
# TODO: TBD: Should these be added in this function at all?
if isinstance(ann, str):
val = ann
basic_val = True

# Ontology reference
# TODO: add original accession and ontology name when editing
Expand Down Expand Up @@ -431,6 +498,7 @@ def _add_annotation(self, ann, header, header_type, obj):
# Basic value string
else:
val = ann['value']
basic_val = True

# Add unit if present
if isinstance(ann, dict) and 'unit' in ann:
Expand All @@ -448,6 +516,7 @@ def _add_annotation(self, ann, header, header_type, obj):
header_type=header_type,
obj=obj,
tooltip=tooltip,
basic_val=basic_val,
)

# Table building functions -------------------------------------------------
Expand All @@ -458,15 +527,25 @@ def _append_row(self):
self._row = []
self._first_row = False
self._col_idx = 0
self._node_idx = 0
self._field_idx = 0

def _build_table(self, table_refs, node_map):
def _build_table(self, table_refs, node_map, study=None, assay=None):
"""
Function for building a table for rendering.
:param table_refs: Object unique_name:s in a list of lists
:param node_map: Lookup dictionary containing objects
:param study: Study object (optional, required if rendering study)
:param assay: Assay object (optional, required if rendering assay)
:raise: ValueError if both study and assay are None
:return: Dict
"""
if not study and not assay:
raise ValueError('Either study or assay must be defined')

self._study = study or assay.study
self._assay = assay
self._row = []
self._top_header = []
self._field_header = []
Expand Down Expand Up @@ -526,42 +605,15 @@ def _is_num(value):
for i in range(len(self._field_header)):
header_name = self._field_header[i]['value'].lower()

# Column type
if 'contact' in header_name:
col_type = 'CONTACT'

elif header_name == 'external links':
col_type = 'EXTERNAL_LINKS'

elif (
header_name == 'name'
and self._field_header[i]['item_type'] == 'DATA'
):
col_type = 'LINK_FILE'

# TODO: Refactor this?
elif any(x[i]['link'] for x in self._table_data) or (
any(
isinstance(x[i]['value'], list)
and len(x[i]['value']) > 0
and isinstance(x[i]['value'][0], dict)
# Right align if values are all numbers or empty (except if name)
# Skip check if column is already defined as UNIT
if (
self._field_header[i]['col_type'] != 'UNIT'
and any(_is_num(x[i]['value']) for x in self._table_data)
and all(
(_is_num(x[i]['value']) or not x[i]['value'])
for x in self._table_data
)
):
col_type = 'ONTOLOGY'

elif any([x[i]['unit'] for x in self._table_data]):
col_type = 'UNIT'

else:
col_type = None

self._field_header[i]['col_type'] = col_type

# Right align if values are all numbers or empty (except if name)
if any(_is_num(x[i]['value']) for x in self._table_data) and all(
(_is_num(x[i]['value']) or not x[i]['value'])
for x in self._table_data
):
self._field_header[i]['num_col'] = True

Expand All @@ -570,6 +622,7 @@ def _is_num(value):

# Maximum column value length for column width estimate
header_len = round(_get_length(self._field_header[i]['value']))
col_type = self._field_header[i]['col_type']

if col_type == 'CONTACT':
max_cell_len = max(
Expand Down Expand Up @@ -602,7 +655,7 @@ def _is_num(value):
max_cell_len = max(
[
_get_length(x[i]['value'], col_type)
+ _get_length(x[i]['unit'], col_type)
+ _get_length(x[i].get('unit'), col_type)
+ 1
for x in self._table_data
]
Expand Down Expand Up @@ -677,6 +730,17 @@ def build_study_tables(self, study, edit=False):
)
)

# Get study config for column type detection
self._sheet_config = app_settings.get_app_setting(
'samplesheets', 'sheet_config', project=study.get_project()
)

if self._sheet_config:
logger.debug('Using sheet configuration from app settings')

else:
logger.debug('No sheet configuration found in app settings')

self._edit = edit
self._parser_version = (
version.parse(study.investigation.parser_version)
Expand All @@ -693,10 +757,8 @@ def build_study_tables(self, study, edit=False):
)

ret = {'study': None, 'assays': {}}

nodes = study.get_nodes()
all_refs = self.build_study_reference(study, nodes)

sample_pos = [
i for i, col in enumerate(all_refs[0]) if '-sample-' in col
][0]
Expand All @@ -706,8 +768,7 @@ def build_study_tables(self, study, edit=False):
sr = [row[: sample_pos + 1] for row in all_refs]
study_refs = list(sr for sr, _ in itertools.groupby(sr))

ret['study'] = self._build_table(study_refs, node_map)

ret['study'] = self._build_table(study_refs, node_map, study=study)
logger.debug(
'Building study OK ({:.1f}s)'.format(time.time() - s_start)
)
Expand All @@ -734,7 +795,7 @@ def build_study_tables(self, study, edit=False):
assay_refs.append(row)

ret['assays'][str(assay.sodar_uuid)] = self._build_table(
assay_refs, node_map
assay_refs, node_map, assay=assay
)

assay_count += 1
Expand Down
7 changes: 4 additions & 3 deletions samplesheets/src/components/renderers/DataCellRenderer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
</span>
<!-- Value with unit -->
<span v-if="colType === 'UNIT'">
{{ value.value }} <span v-if="value.unit" class="text-muted">{{ value.unit }}</span>
{{ value.value }} <span v-if="value.hasOwnProperty('unit') && value.unit" class="text-muted">{{ value.unit }}</span>
</span>
<!-- Ontology links -->
<span v-else-if="colType === 'ONTOLOGY' && renderData">
Expand Down Expand Up @@ -109,14 +109,15 @@ export default Vue.extend(
})
}
} else if (this.value.value.indexOf(';') !== -1 &&
this.value.hasOwnProperty('link') &&
this.value.link.indexOf(';') !== -1) { // Legacy altamISA implementation
let values = this.value.value.split(';')
let urls = this.value.link.split(';')
for (let i = 0; i < values.length; i++) {
links.push({value: values[i], url: urls[i]})
}
} else {
} else if (this.value.hasOwnProperty('link')) {
links.push({value: this.value.value, url: this.value.link})
}
return {'links': links}
Expand Down Expand Up @@ -158,7 +159,7 @@ export default Vue.extend(
// Get file link
return {
value: this.value.value,
url: this.value.link
url: this.value.link ? this.value.hasOwnProperty('link') : '#'
}
},
getTooltip () {
Expand Down
1 change: 0 additions & 1 deletion samplesheets/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,6 @@ def _get_val(c_val):
wb.save(output)


# TODO: Updating existing config based on sheet struture changes/replace
def build_sheet_config(investigation):
"""
Build basic sample sheet configuration for editing configuration.
Expand Down

0 comments on commit 16a99ac

Please sign in to comment.