Skip to content

Commit c7ec40a

Browse files
committed
Edit the participant data in the bidseditor (GitHub issue #253)
1 parent 1cb1e1c commit c7ec40a

File tree

1 file changed

+130
-67
lines changed

1 file changed

+130
-67
lines changed

bidscoin/bidseditor.py

Lines changed: 130 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ def __init__(self, bidsfolder: Path, input_bidsmap: BidsMap, template_bidsmap: B
175175
tabwidget.setTabPosition(QtWidgets.QTabWidget.TabPosition.North)
176176
tabwidget.setTabShape(QtWidgets.QTabWidget.TabShape.Rounded)
177177

178-
self.subses_table = {}
178+
self.participant_table = {}
179179
self.samples_table = {}
180180
self.options_label = {}
181181
self.options_table = {}
@@ -250,7 +250,7 @@ def samples_menu(self, pos):
250250
"""Pops up a context-menu for deleting or editing the right-clicked sample in the samples_table"""
251251

252252
# Get the activated row-data
253-
dataformat = self.tabwidget.widget(self.tabwidget.currentIndex()).objectName()
253+
dataformat = self.tabwidget.currentWidget().objectName()
254254
table = self.samples_table[dataformat]
255255
colindex = table.currentColumn()
256256
rowindexes = [index.row() for index in table.selectedIndexes() if index.column() == colindex]
@@ -303,7 +303,7 @@ def samples_menu(self, pos):
303303
else:
304304
self.ordered_file_index[dataformat][datasource.path] = max(self.ordered_file_index[dataformat][fname] for fname in self.ordered_file_index[dataformat]) + 1
305305
if runitem:
306-
self.update_subses_samples(dataformat)
306+
self.fill_samples_table(dataformat)
307307

308308
elif action == delete:
309309
deleted = False
@@ -319,7 +319,7 @@ def samples_menu(self, pos):
319319
self.output_bidsmap.delete_run(provenance, datatype, dataformat)
320320
deleted = True
321321
if deleted:
322-
self.update_subses_samples(dataformat)
322+
self.fill_samples_table(dataformat)
323323

324324
elif action == compare:
325325
CompareWindow(runitems, subids, sesids)
@@ -353,7 +353,7 @@ def samples_menu(self, pos):
353353
self.output_bidsmap.update(datatype, templaterun)
354354
LOGGER.verbose(f"User sets run-item {datatype} -> {templaterun}")
355355

356-
self.update_subses_samples(dataformat)
356+
self.fill_samples_table(dataformat)
357357

358358
def set_menu_statusbar(self):
359359
"""Set up the menu and statusbar"""
@@ -427,25 +427,25 @@ def set_tab_bidsmap(self, dataformat: str):
427427
"""Set the SOURCE file sample listing tab"""
428428

429429
# Set the Participant table
430-
subses_label = QLabel('Participant data')
431-
subses_label.setToolTip('Subject/session mappings')
432-
433-
subses_table = MyQTable(ncols=2, nrows=2)
434-
subses_table.setToolTip(f"Use e.g. '<<filepath:/sub-(.*?)/>>' to parse the subject and (optional) session label from the pathname. NB: the () parentheses indicate the part that is extracted as the subject/session label\n"
435-
f"Use a dynamic {dataformat} attribute (e.g. '<<PatientName>>') to extract the subject and (optional) session label from the {dataformat} header")
436-
subses_table.setMouseTracking(True)
437-
header = subses_table.horizontalHeader()
430+
participant_label = QLabel('Participant data')
431+
participant_label.setToolTip('Data to parse the subject/session labels, and to populate the participants tsv- and json-files')
432+
433+
self.participant_table[dataformat] = participant_table = MyQTable(ncols=3)
434+
participant_table.setToolTip(f"Use e.g. '<<filepath:/sub-(.*?)/>>' to parse the subject and (optional) session label from the pathname. NB: the () parentheses indicate the part that is extracted as the subject/session label\n"
435+
f"Use a dynamic {dataformat} attribute (e.g. '<<PatientName>>') to extract the subject and (optional) session label from the {dataformat} header")
436+
participant_table.cellChanged.connect(self.participant_table2bidsmap)
437+
header = participant_table.horizontalHeader()
438438
header.setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents)
439439
header.setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch)
440-
subses_table.cellChanged.connect(self.subsescell2bidsmap)
441-
self.subses_table[dataformat] = subses_table
440+
441+
self.fill_participant_table(dataformat)
442442

443443
# Set the bidsmap table
444-
label = QLabel('Data samples')
445-
label.setToolTip('List of unique source-data samples')
444+
label = QLabel('Representative samples')
445+
label.setToolTip('List of unique source-data samples (datatypes)')
446446

447447
self.samples_table[dataformat] = samples_table = MyQTable(minsize=False, ncols=6)
448-
samples_table.setMouseTracking(True)
448+
samples_table.setMouseTracking(True) # Needed for showing filepath in the statusbar
449449
samples_table.setShowGrid(True)
450450
samples_table.setHorizontalHeaderLabels(['', f'{dataformat} input', 'BIDS data type', 'BIDS output', 'Action', 'Provenance'])
451451
samples_table.setSortingEnabled(True)
@@ -462,8 +462,8 @@ def set_tab_bidsmap(self, dataformat: str):
462462
header.setSectionResizeMode(3, QHeaderView.ResizeMode.Stretch)
463463

464464
layout = QVBoxLayout()
465-
layout.addWidget(subses_label)
466-
layout.addWidget(subses_table)
465+
layout.addWidget(participant_label)
466+
layout.addWidget(participant_table)
467467
layout.addWidget(label)
468468
layout.addWidget(samples_table)
469469
tab = QtWidgets.QWidget()
@@ -472,7 +472,7 @@ def set_tab_bidsmap(self, dataformat: str):
472472
self.tabwidget.addTab(tab, f"{dataformat} mappings")
473473
self.tabwidget.setCurrentWidget(tab)
474474

475-
self.update_subses_samples(dataformat)
475+
self.fill_samples_table(dataformat)
476476

477477
def set_tab_options(self):
478478
"""Set the options tab"""
@@ -556,24 +556,86 @@ def set_tab_filebrowser(self):
556556

557557
self.tabwidget.addTab(tab, 'Data browser')
558558

559-
def update_subses_samples(self, dataformat: str):
560-
"""(Re)populates the sample list with bidsnames according to the bidsmap"""
559+
def fill_participant_table(self, dataformat: str):
560+
"""(Re)populate the participant table with the new bidsmap data"""
561+
562+
# Populate the participant table
563+
participant_table = self.participant_table[dataformat]
564+
participant_table.blockSignals(True)
565+
participant_table.hide()
566+
participant_table.setRowCount(len(self.output_bidsmap.dataformat(dataformat).participant) + 1)
567+
participant_table.clearContents()
568+
for n, (key, item) in enumerate(self.output_bidsmap.dataformat(dataformat).participant.items()):
569+
tableitem = MyQTableItem(key, editable=key not in ('participant_id','session_id'))
570+
tableitem.setToolTip(get_columnhelp(key))
571+
participant_table = self.participant_table[dataformat]
572+
participant_table.setItem(n, 0, tableitem)
573+
participant_table.setItem(n, 1, MyQTableItem(item['value']))
574+
edit_button = QPushButton('Metadata')
575+
edit_button.setToolTip('Data for participants json sidecar-file')
576+
edit_button.clicked.connect(self.edit_metadata)
577+
participant_table.setCellWidget(n, 2, edit_button)
578+
579+
participant_table.show()
580+
participant_table.blockSignals(False)
581+
582+
def participant_table2bidsmap(self, rowindex: int, colindex: int):
583+
"""
584+
A value has been changed in the participant table. If it is valid, save it to the bidsmap and update the
585+
participant and samples table
586+
587+
:param rowindex:
588+
:param colindex:
589+
:return:
590+
"""
591+
592+
# Only if cell was actually clicked, update
593+
if self.tabwidget.currentIndex() < 0:
594+
return
595+
dataformat = self.tabwidget.currentWidget().objectName()
596+
if colindex == 1 and dataformat in self.dataformats:
597+
key = self.participant_table[dataformat].item(rowindex, 0).text().strip()
598+
value = self.participant_table[dataformat].item(rowindex, 1).text().strip()
599+
oldvalue = self.output_bidsmap.dataformat(dataformat).participant[key]['value']
600+
if oldvalue is None:
601+
oldvalue = ''
602+
603+
# Only if cell content was changed, update
604+
if key and value != oldvalue:
605+
LOGGER.verbose(f"User sets {dataformat}['{key}'] from '{oldvalue}' to '{value}'")
606+
self.output_bidsmap.dataformat(dataformat).participant[key]['value'] = value
607+
self.fill_participant_table(dataformat)
608+
self.fill_samples_table(dataformat)
609+
self.datachanged = True
610+
611+
def edit_metadata(self):
612+
"""Pop-up a text window to edit the sidecar metadata of participant item"""
613+
614+
dataformat = self.tabwidget.currentWidget().objectName()
615+
participant_table = self.participant_table[dataformat]
616+
clicked = self.focusWidget()
617+
rowindex = participant_table.indexAt(clicked.pos()).row()
618+
key = participant_table.item(rowindex, 0).text().strip()
619+
meta = self.output_bidsmap.dataformat(dataformat).participant[key]['meta']
620+
621+
text, ok = QtWidgets.QInputDialog.getMultiLineText(self, f"Edit sidecar metadata for {key}", 'json data', text=json.dumps(meta, indent=2))
622+
if ok:
623+
try:
624+
meta_ = json.loads(text)
625+
self.output_bidsmap.dataformat(dataformat).participant[key]['meta'] = meta_
626+
if meta_ != meta:
627+
self.datachanged = True
628+
except json.decoder.JSONDecodeError as jsonerror:
629+
QMessageBox.warning(self, f"Sidecar metadata parsing error", f"{text}\n\nPlease provide valid json metadata:\n{jsonerror}")
630+
self.edit_metadata()
631+
632+
def fill_samples_table(self, dataformat: str):
633+
"""(Re)populate the sample table with bidsnames according to the new bidsmap data"""
561634

562635
self.datachanged = True
563636
output_bidsmap = self.output_bidsmap
564637

565-
# Update the subject/session table
566-
subitem = MyQTableItem('subject', editable=False)
567-
subitem.setToolTip(get_entityhelp('sub'))
568-
sesitem = MyQTableItem('session', editable=False)
569-
sesitem.setToolTip(get_entityhelp('ses'))
570-
subses_table = self.subses_table[dataformat]
571-
subses_table.setItem(0, 0, subitem)
572-
subses_table.setItem(1, 0, sesitem)
573-
subses_table.setItem(0, 1, MyQTableItem(output_bidsmap.dataformat(dataformat).subject))
574-
subses_table.setItem(1, 1, MyQTableItem(output_bidsmap.dataformat(dataformat).session))
575-
576-
# Update the run samples table
638+
# Add runs to the samples table
577639
idx = 0
578640
num_files = self.set_ordered_file_index(dataformat)
579641
samples_table = self.samples_table[dataformat]
@@ -662,28 +724,10 @@ def set_ordered_file_index(self, dataformat: str) -> int:
662724

663725
return len(provenances)
664726

665-
def subsescell2bidsmap(self, rowindex: int, colindex: int):
666-
"""Subject or session value has been changed in subject-session table"""
667-
668-
# Only if cell was actually clicked, update
669-
dataformat = self.tabwidget.widget(self.tabwidget.currentIndex()).objectName()
670-
if colindex == 1 and dataformat in self.dataformats:
671-
key = self.subses_table[dataformat].item(rowindex, 0).text().strip()
672-
value = self.subses_table[dataformat].item(rowindex, 1).text().strip()
673-
oldvalue = getattr(self.output_bidsmap.dataformat(dataformat), key)
674-
if oldvalue is None:
675-
oldvalue = ''
676-
677-
# Only if cell content was changed, update
678-
if key and value != oldvalue:
679-
LOGGER.verbose(f"User sets {dataformat}['{key}'] from '{oldvalue}' to '{value}'")
680-
setattr(self.output_bidsmap.dataformat(dataformat), key, value)
681-
self.update_subses_samples(dataformat)
682-
683727
def open_editwindow(self, provenance: Path=Path(), datatype: str=''):
684728
"""Make sure that index map has been updated"""
685729

686-
dataformat = self.tabwidget.widget(self.tabwidget.currentIndex()).objectName()
730+
dataformat = self.tabwidget.currentWidget().objectName()
687731
if not datatype:
688732
samples_table = self.samples_table[dataformat]
689733
clicked = self.focusWidget()
@@ -702,7 +746,7 @@ def open_editwindow(self, provenance: Path=Path(), datatype: str=''):
702746
LOGGER.verbose(f'User is editing {provenance}')
703747
self.editwindow = EditWindow(runitem, self.output_bidsmap, self.template_bidsmap)
704748
self.editwindow_opened = str(provenance)
705-
self.editwindow.done_edit.connect(self.update_subses_samples)
749+
self.editwindow.done_edit.connect(self.fill_samples_table)
706750
self.editwindow.finished.connect(self.release_editwindow)
707751
self.editwindow.show()
708752
return
@@ -777,7 +821,7 @@ def options2bidsmap(self, rowindex: int, colindex: int):
777821
self.ignoredatatypes = newoptions.get('ignoretypes', [])
778822
self.bidsignore = newoptions.get('bidsignore', [])
779823
for dataformat in self.dataformats:
780-
self.update_subses_samples(dataformat)
824+
self.fill_samples_table(dataformat)
781825
else:
782826
self.output_bidsmap.plugins[plugin] = newoptions
783827

@@ -942,7 +986,7 @@ def save_options(self):
942986
def sample_doubleclicked(self, item):
943987
"""When source file is double-clicked in the samples_table, show the inspect- or edit-window"""
944988

945-
dataformat = self.tabwidget.widget(self.tabwidget.currentIndex()).objectName()
989+
dataformat = self.tabwidget.currentWidget().objectName()
946990
datatype = self.samples_table[dataformat].item(item.row(), 2).text()
947991
sourcefile = self.samples_table[dataformat].item(item.row(), 5).text()
948992
if item.column() == 1:
@@ -1135,7 +1179,7 @@ def __init__(self, runitem: RunItem, bidsmap: BidsMap, template_bidsmap: BidsMap
11351179
layout1_.addWidget(inspect_button, alignment=QtCore.Qt.AlignmentFlag.AlignRight)
11361180
layout1_.addWidget(log_table_label)
11371181
layout1_.addWidget(log_table)
1138-
self.events_inbox = events_inbox = QGroupBox(f"{self.dataformat} input")
1182+
self.events_inbox = events_inbox = QGroupBox(f"{self.dataformat} input data")
11391183
events_inbox.setSizePolicy(sizepolicy)
11401184
events_inbox.setLayout(layout1_)
11411185

@@ -1167,7 +1211,7 @@ def __init__(self, runitem: RunItem, bidsmap: BidsMap, template_bidsmap: BidsMap
11671211
layout2_.addWidget(events_time_label)
11681212
layout2_.addWidget(events_time)
11691213
layout2_.addStretch()
1170-
self.events_editbox = events_editbox = QGroupBox(' ')
1214+
self.events_editbox = events_editbox = QGroupBox('Mapping')
11711215
events_editbox.setSizePolicy(sizepolicy)
11721216
events_editbox.setLayout(layout2_)
11731217

@@ -1176,7 +1220,7 @@ def __init__(self, runitem: RunItem, bidsmap: BidsMap, template_bidsmap: BidsMap
11761220
layout3.addWidget(done_button, alignment=QtCore.Qt.AlignmentFlag.AlignRight)
11771221
layout3.addWidget(events_table_label)
11781222
layout3.addWidget(events_table)
1179-
self.eventsbox = eventsbox = QGroupBox('BIDS output')
1223+
self.eventsbox = eventsbox = QGroupBox('BIDS output data')
11801224
eventsbox.setSizePolicy(sizepolicy)
11811225
eventsbox.setLayout(layout3)
11821226

@@ -1446,7 +1490,7 @@ def run2data(self) -> tuple:
14461490
return properties_data, attributes_data, bids_data, meta_data, events_data
14471491

14481492
def properties2run(self, rowindex: int, colindex: int):
1449-
"""Source attribute value has been changed"""
1493+
"""Source attribute value has been changed. If OK, update the target run"""
14501494

14511495
# Only if cell was actually clicked, update (i.e. not when BIDS datatype changes)
14521496
if colindex == 1:
@@ -1470,7 +1514,7 @@ def properties2run(self, rowindex: int, colindex: int):
14701514
self.properties_table.blockSignals(False)
14711515

14721516
def attributes2run(self, rowindex: int, colindex: int):
1473-
"""Source attribute value has been changed"""
1517+
"""Source attribute value has been changed. If OK, update the target run"""
14741518

14751519
# Only if cell was actually clicked, update (i.e. not when BIDS datatype changes)
14761520
if colindex == 1:
@@ -1494,7 +1538,7 @@ def attributes2run(self, rowindex: int, colindex: int):
14941538
self.attributes_table.blockSignals(False)
14951539

14961540
def bids2run(self, rowindex: int, colindex: int):
1497-
"""BIDS attribute value has been changed"""
1541+
"""BIDS attribute value has been changed. If OK, update the target run"""
14981542

14991543
# Only if cell was actually clicked, update (i.e. not when BIDS datatype changes) and store the data in the target_run
15001544
if colindex == 1:
@@ -1533,7 +1577,7 @@ def bids2run(self, rowindex: int, colindex: int):
15331577
self.refresh_bidsname()
15341578

15351579
def meta2run(self, rowindex: int, colindex: int):
1536-
"""Meta value has been changed"""
1580+
"""Meta value has been changed. If OK, update the target run"""
15371581

15381582
key = self.meta_table.item(rowindex, 0).text().strip()
15391583
value = self.meta_table.item(rowindex, 1).text().strip()
@@ -1566,7 +1610,7 @@ def meta2run(self, rowindex: int, colindex: int):
15661610
self.fill_table(self.meta_table, meta_data)
15671611

15681612
def events_time2run(self, rowindex: int, colindex: int):
1569-
"""Events value has been changed. Read the data from the event 'time' table"""
1613+
"""Events value has been changed. Read the data from the event 'time' table and, if OK, update the target run"""
15701614

15711615
# events_data['time'] = [['columns', events.timecols],
15721616
# ['units/sec', events.timeunit],
@@ -1599,7 +1643,7 @@ def events_time2run(self, rowindex: int, colindex: int):
15991643
self.fill_table(self.events_table, events_data['table'])
16001644

16011645
def events_rows2run(self, rowindex: int, colindex: int):
1602-
"""Events value has been changed. Read the data from the event 'rows' table"""
1646+
"""Events value has been changed. Read the data from the event 'rows' table and, if OK, update the target run"""
16031647

16041648
# row: [[include, {column_in: regex}],
16051649
# [cast, {column_out: newvalue}]]
@@ -1627,7 +1671,7 @@ def events_rows2run(self, rowindex: int, colindex: int):
16271671
self.fill_table(self.events_rows, events_data['rows'])
16281672

16291673
def events_columns2run(self, rowindex: int, colindex: int):
1630-
"""Events value has been changed. Read the data from the event 'columns' table"""
1674+
"""Events value has been changed. Read the data from the event 'columns' table and, if OK, update the target run"""
16311675

16321676
# events_data['columns'] = [[{'source1': target1}],
16331677
# [{'source2': target2}],
@@ -2172,6 +2216,25 @@ def get_suffixhelp(suffix: str) -> str:
21722216
return f"{suffix}\nAn unknown/private suffix"
21732217

21742218

2219+
def get_columnhelp(column: str) -> str:
2220+
"""
2221+
Reads the description of a matching entity=entitykey in the bidsschema.objects.columns
2222+
2223+
:param column: The column name for which the help text is obtained
2224+
:return: The obtained help text
2225+
"""
2226+
2227+
if not column:
2228+
return "Please provide a column-name"
2229+
2230+
# Return the description from the entities or a default text
2231+
for _, entity in bids.bidsschema.objects.columns.items():
2232+
if entity.name == column:
2233+
return f"{entity.display_name}\n{entity.description}"
2234+
2235+
return f"{column}\nAn unknown/private column name"
2236+
2237+
21752238
def get_entityhelp(entitykey: str) -> str:
21762239
"""
21772240
Reads the description of a matching entity=entitykey in the schema/entities.yaml file

0 commit comments

Comments
 (0)