From efc17cd71239459830dfcef6a71cce412d791d81 Mon Sep 17 00:00:00 2001 From: David Manthey Date: Thu, 21 Nov 2024 12:34:40 -0500 Subject: [PATCH] Name template option Allow setting item names based on database entries. --- devops/wsi_deid/girder.local.conf | 1 + docs/CUSTOMIZING.rst | 14 +++++++++++-- wsi_deid/__init__.py | 21 ++++++++++++++++++++ wsi_deid/config.py | 1 + wsi_deid/jobs.py | 7 ++++--- wsi_deid/process.py | 12 +++++++++++ wsi_deid/web_client/templates/ConfigView.pug | 8 ++++++++ wsi_deid/web_client/views/HierarchyWidget.js | 16 +++++++-------- 8 files changed, 67 insertions(+), 13 deletions(-) diff --git a/devops/wsi_deid/girder.local.conf b/devops/wsi_deid/girder.local.conf index 585a1f3..051e451 100644 --- a/devops/wsi_deid/girder.local.conf +++ b/devops/wsi_deid/girder.local.conf @@ -22,6 +22,7 @@ cache_memcached_password = None enabled = True [wsi_deid] +name_template = "{tokenId}" redact_macro_square = False always_redact_label = False edit_metadata = False diff --git a/docs/CUSTOMIZING.rst b/docs/CUSTOMIZING.rst index 467ce1f..752ba17 100644 --- a/docs/CUSTOMIZING.rst +++ b/docs/CUSTOMIZING.rst @@ -332,7 +332,6 @@ If ``InputFileName`` is added to the list of export fields in the ``upload_metad import_text_association_columns = ["SurgPathNum", "First_Name", "Last_Name", "Date_of_Birth"] ... - Choosing Custom Metadata To Add to Exported Images ++++++++++++++++++++++++++++++++++++++++++++++++++ @@ -354,7 +353,6 @@ If the setting is absent or set to ``None``, then all metadata from the upload f upload_metadata_add_to_images = None ... - Creating New TokenIDs for Refiling Images +++++++++++++++++++++++++++++++++++++++++ @@ -371,6 +369,18 @@ For example, if the specified pattern is ``0123#@@1###``, a randomly generated T new_token_pattern = "####@@#####" ... +Naming Images Based on Database Values +++++++++++++++++++++++++++++++++++++++ + +By default, images filed based on database lookup are named based on their ``tokenId`` and a unique image number. This can be changed to include data from the database lookup, if it is exists. The ``name_template`` specifies how this is done; the default is ``{tokenId}``. You can include, for instance, a tumor record number by changing it to ``{tokenId}_{tumor_record_number}``. Note that if all fields are not present, it will default to the tokenId. + +.. code-block:: python + + [wsi_deid] + ... + name_templaye = "{tokenId}" + ... + An Example to Allow All Import Files ++++++++++++++++++++++++++++++++++++ diff --git a/wsi_deid/__init__.py b/wsi_deid/__init__.py index 7e9dc89..55af59c 100644 --- a/wsi_deid/__init__.py +++ b/wsi_deid/__init__.py @@ -1,6 +1,7 @@ import json import os import re +import string import girder import PIL.Image @@ -143,6 +144,26 @@ def validateNewTokenPattern(doc): doc['value'] = None +@setting_utilities.validator({ + PluginSettings.WSI_DEID_BASE + 'name_template', +}) +def validateStringTemplate(doc): + if doc.get('value', None) is not None: + doc['value'] = re.sub( + r'\{tokenid\}', '{tokenId}', str(doc['value']).strip(), + flags=re.IGNORECASE) + try: + names = [fn for _, fn, _, _ in string.Formatter().parse(doc['value']) if fn is not None] + except Exception: + names = [] + if not len(names): + msg = 'The template string must contain at least one reference in brases' + raise ValidationException(msg) + + if not doc['value']: + doc['value'] = None + + @setting_utilities.validator({ PluginSettings.WSI_DEID_BASE + 'hide_metadata_keys', PluginSettings.WSI_DEID_BASE + 'hide_metadata_keys_format_aperio', diff --git a/wsi_deid/config.py b/wsi_deid/config.py index 22f2179..004bf06 100644 --- a/wsi_deid/config.py +++ b/wsi_deid/config.py @@ -22,6 +22,7 @@ 'show_next_item': True, 'show_metadata_in_lists': True, 'show_next_folder': True, + 'name_template': '{tokenid}', 'no_redact_control_keys': { r'^internal;aperio_version$': '', r'^internal;openslide;openslide\.(?!comment$)': '', diff --git a/wsi_deid/jobs.py b/wsi_deid/jobs.py index f232195..0dc1544 100644 --- a/wsi_deid/jobs.py +++ b/wsi_deid/jobs.py @@ -9,7 +9,7 @@ from . import config, matching_api from .constants import TokenOnlyPrefix -from .process import get_image_barcode, get_image_text, refile_image +from .process import get_image_barcode, get_image_name, get_image_text, refile_image def start_ocr_item_job(job): @@ -163,8 +163,9 @@ def match_images_via_api(imageIds, userId, job, reportInfo): tokenId = result[0]['token_id'] info = {'fields': result[0].get('tumors')[0]} oldName = item['name'] - item = refile_image(item, user, tokenId, TokenOnlyPrefix + tokenId, - {TokenOnlyPrefix + tokenId: info}) + newNameRoot = get_image_name(tokenId, info) + item = refile_image(item, user, tokenId, TokenOnlyPrefix + newNameRoot, + {TokenOnlyPrefix + newNameRoot: info}) Job().updateJob( job, log=f'Moved item {oldName} to folder {tokenId} as {item["name"]} based on api lookup\n', diff --git a/wsi_deid/process.py b/wsi_deid/process.py index e19cfdd..c14e45c 100644 --- a/wsi_deid/process.py +++ b/wsi_deid/process.py @@ -1724,6 +1724,18 @@ def get_image_barcode(item): return results +def get_image_name(prefix, info): + template = config.getConfig('name_template') or '{tokenId}' + try: + name = template.format(tokenId=prefix, **info['fields']) + if name != template and name: + return name + except Exception: + logger.exception( + 'Could not fill name template (%r) with tokenId=%s, %r', template, prefix, info) + return prefix + + def refile_image(item, user, tokenId, imageId, uploadInfo=None): """ Refile an item to a new name and folder. diff --git a/wsi_deid/web_client/templates/ConfigView.pug b/wsi_deid/web_client/templates/ConfigView.pug index 313ce10..01345a6 100644 --- a/wsi_deid/web_client/templates/ConfigView.pug +++ b/wsi_deid/web_client/templates/ConfigView.pug @@ -192,6 +192,14 @@ form#g-wsi_deid-form(role="form") p.g-hui-description | All of the settings in this section can be specified through the Girder configuration file. If changed via these settings, these values override the values in the configuration file. + .form-group + label(for="g-wsi-deid-base_name_template") Image Name Template + p.g-hui-description + | By default, items imported and matched to a database are given a name of the returned tokenId and a unique image number. Additional data from the database record can be added as part of the name. The template is in the python format string style. + input#g-wsi-deid-base_name_template.form-control.input-sm( + type="text", value=settings['wsi_deid.base_name_template'] || '', + title='A python template string style, such as "{tokenId}"', + spellcheck="false") .form-group input#g-wsi-deid-base_add_title_to_label.input-sm(type="checkbox", checked=(settings['wsi_deid.base_add_title_to_label'] ? "checked" : undefined)) label(for="g-wsi-deid-base_add_title_to_label") Add File Name To Label diff --git a/wsi_deid/web_client/views/HierarchyWidget.js b/wsi_deid/web_client/views/HierarchyWidget.js index 263a3e7..c37edb7 100644 --- a/wsi_deid/web_client/views/HierarchyWidget.js +++ b/wsi_deid/web_client/views/HierarchyWidget.js @@ -196,19 +196,19 @@ function addControls(key, settings) { { key: 'redactlist', text: 'Redact Checked', - class: 'btn-info disabled', + class: 'btn-primary disabled', action: 'list/process', check: () => settings.show_metadata_in_lists }, { key: 'import', text: 'Import', - class: 'btn-info', + class: 'btn-primary', action: 'ingest', check: () => settings.show_import_button !== false }, { key: 'ocr', text: 'Find label text', - class: 'btn-info', + class: 'btn-primary', action: 'ocrall', check: _.constant(true) } @@ -217,13 +217,13 @@ function addControls(key, settings) { { key: 'export', text: 'Export Recent', - class: 'btn-info', + class: 'btn-primary', action: 'export', check: () => settings.show_export_button !== false }, { key: 'exportall', text: 'Export All', - class: 'btn-info', + class: 'btn-primary', action: 'exportall', check: () => settings.show_export_button !== false } @@ -232,7 +232,7 @@ function addControls(key, settings) { { key: 'finishlist', text: 'Approve Checked', - class: 'btn-info disabled', + class: 'btn-primary disabled', action: 'list/finish', check: () => settings.show_metadata_in_lists } @@ -241,7 +241,7 @@ function addControls(key, settings) { { key: 'redactlist', text: 'Redact Checked', - class: 'btn-info disabled', + class: 'btn-primary disabled', action: 'list/process', check: () => settings.show_metadata_in_lists } @@ -250,7 +250,7 @@ function addControls(key, settings) { { key: 'import', text: 'Import', - class: 'btn-info', + class: 'btn-primary', action: 'ingest', check: () => settings.show_import_button !== false }