From 3b79c8e68a5359d0b90eb98cd089bae79d972a5a Mon Sep 17 00:00:00 2001 From: Adam Rauch Date: Mon, 16 Mar 2026 11:46:24 -0700 Subject: [PATCH 1/7] Move query-based specimen import to Specimen module --- .../org/labkey/specimen/SpecimenModule.java | 11 +- .../specimen/actions/SpecimenController.java | 143 +++++++++++++++- .../specimen/importer/QueryBasedExport.java | 97 +++++++++++ .../QueryBasedSpecimenImportUploadTask.java | 81 +++++++++ .../QueryBasedSpecimenReloadConfig.java | 58 +++++++ .../importer/QueryBasedSpecimenTransform.java | 128 ++++++++++++++ .../importer/QueryBasedTransformTask.java | 160 ++++++++++++++++++ .../specimen/view/configureQueryImport.jsp | 132 +++++++++++++++ .../test/sampledata/import/100_specimens.xlsx | Bin 0 -> 20576 bytes .../ConfigureSpecimenImportPage.java | 96 +++++++++++ .../QueryBasedSpecimenImportTest.java | 115 +++++++++++++ .../SpecimenImporterProviderBannerTest.java | 95 +++++++++++ 12 files changed, 1114 insertions(+), 2 deletions(-) create mode 100644 specimen/src/org/labkey/specimen/importer/QueryBasedExport.java create mode 100644 specimen/src/org/labkey/specimen/importer/QueryBasedSpecimenImportUploadTask.java create mode 100644 specimen/src/org/labkey/specimen/importer/QueryBasedSpecimenReloadConfig.java create mode 100644 specimen/src/org/labkey/specimen/importer/QueryBasedSpecimenTransform.java create mode 100644 specimen/src/org/labkey/specimen/importer/QueryBasedTransformTask.java create mode 100644 specimen/src/org/labkey/specimen/view/configureQueryImport.jsp create mode 100644 specimen/test/sampledata/import/100_specimens.xlsx create mode 100644 specimen/test/src/org/labkey/test/pages/professional/ConfigureSpecimenImportPage.java create mode 100644 specimen/test/src/org/labkey/test/tests/professional/QueryBasedSpecimenImportTest.java create mode 100644 specimen/test/src/org/labkey/test/tests/professional/SpecimenImporterProviderBannerTest.java diff --git a/specimen/src/org/labkey/specimen/SpecimenModule.java b/specimen/src/org/labkey/specimen/SpecimenModule.java index f4baed4e00a..833af41d63d 100644 --- a/specimen/src/org/labkey/specimen/SpecimenModule.java +++ b/specimen/src/org/labkey/specimen/SpecimenModule.java @@ -57,6 +57,7 @@ import org.labkey.api.study.importer.SimpleStudyImporterRegistry; import org.labkey.api.study.writer.SimpleStudyWriterRegistry; import org.labkey.api.usageMetrics.UsageMetricsService; +import org.labkey.api.util.SystemMaintenance; import org.labkey.api.util.emailTemplate.EmailTemplateService; import org.labkey.api.util.logging.LogHelper; import org.labkey.api.view.ActionURL; @@ -66,6 +67,8 @@ import org.labkey.specimen.actions.SpecimenApiController; import org.labkey.specimen.actions.SpecimenController; import org.labkey.specimen.importer.AbstractSpecimenTask; +import org.labkey.specimen.importer.QueryBasedSpecimenImportUploadTask; +import org.labkey.specimen.importer.QueryBasedSpecimenTransform; import org.labkey.specimen.importer.RequestabilityManager; import org.labkey.specimen.importer.SpecimenImporter; import org.labkey.specimen.importer.SpecimenSchemaImporter; @@ -232,7 +235,9 @@ public void addSpecimenPivotTableNames(Set names) return null; } }); - } + + SystemMaintenance.addTask(new QueryBasedSpecimenImportUploadTask()); + } @Override protected void startupAfterSpringConfig(ModuleContext moduleContext) @@ -254,6 +259,10 @@ protected void startupAfterSpringConfig(ModuleContext moduleContext) new SpecimenSettingsImporter() )); + SpecimenService specimenService = SpecimenService.get(); + if (null != specimenService) + specimenService.registerSpecimenTransform(new QueryBasedSpecimenTransform()); + UsageMetricsService svc = UsageMetricsService.get(); if (null != svc) { diff --git a/specimen/src/org/labkey/specimen/actions/SpecimenController.java b/specimen/src/org/labkey/specimen/actions/SpecimenController.java index 2d05110bb03..e2fd64a0701 100644 --- a/specimen/src/org/labkey/specimen/actions/SpecimenController.java +++ b/specimen/src/org/labkey/specimen/actions/SpecimenController.java @@ -64,8 +64,10 @@ import org.labkey.api.module.FolderType; import org.labkey.api.module.Module; import org.labkey.api.pipeline.PipeRoot; +import org.labkey.api.pipeline.PipelineJob; import org.labkey.api.pipeline.PipelineService; import org.labkey.api.pipeline.PipelineStatusUrls; +import org.labkey.api.pipeline.PipelineValidationException; import org.labkey.api.pipeline.browse.PipelinePathForm; import org.labkey.api.query.AbstractQueryImportAction; import org.labkey.api.query.BatchValidationException; @@ -152,6 +154,7 @@ import org.labkey.specimen.SpecimenRequestException; import org.labkey.specimen.SpecimenRequestManager; import org.labkey.specimen.SpecimenRequestStatus; +import org.labkey.specimen.importer.QueryBasedSpecimenTransform; import org.labkey.specimen.importer.RequestabilityManager; import org.labkey.specimen.importer.SimpleSpecimenImporter; import org.labkey.specimen.model.ExtendedSpecimenRequestView; @@ -195,7 +198,6 @@ import java.io.BufferedOutputStream; import java.io.ByteArrayOutputStream; -import java.io.File; import java.io.IOException; import java.io.OutputStream; import java.nio.charset.StandardCharsets; @@ -5759,4 +5761,143 @@ public void addNavTrail(NavTree root) root.addChild("Insert " + _form.getQueryName()); } } + + public static class ConfigForm + { + private String _schemaName; + private String _queryName; + private String _viewName; + private String _enabled; + private int _userId; + + public int getUserId() + { + return _userId; + } + + public void setUserId(int userId) + { + _userId = userId; + } + + public String getSchemaName() + { + return _schemaName; + } + + public void setSchemaName(String schemaName) + { + _schemaName = schemaName; + } + + public String getQueryName() + { + return _queryName; + } + + public void setQueryName(String queryName) + { + _queryName = queryName; + } + + public String getViewName() + { + return _viewName; + } + + public void setViewName(String viewName) + { + _viewName = viewName; + } + + public String getEnabled() + { + return _enabled; + } + + public void setEnabled(String enabled) + { + _enabled = enabled; + } + + public Map getOptions() + { + Map valueMap = new HashMap<>(); + valueMap.put("schemaName", _schemaName); + valueMap.put("queryName", _queryName); + valueMap.put("viewName", _viewName); + valueMap.put("enabled", _enabled); + valueMap.put("userId", String.valueOf(_userId)); + return valueMap; + } + } + + @RequiresPermission(AdminPermission.class) + public static class ConfigureQueryImportAction extends FormViewAction + { + @Override + public void addNavTrail(NavTree root) + { + root.addChild("Query-based specimen import"); + } + + @Override + public void validateCommand(ConfigForm target, Errors errors) + { + } + + @Override + public ModelAndView getView(ConfigForm form, boolean reshow, BindException errors) + { + return new JspView<>("/org/labkey/specimen/view/configureQueryImport.jsp", form, errors); + } + + @Override + public boolean handlePost(ConfigForm form, BindException errors) + { + String QBSpecimenImportKey = QueryBasedSpecimenTransform.PROPERTY_MAP_KEY; + WritablePropertyMap props = PropertyManager.getWritableProperties(getContainer(), QBSpecimenImportKey, true); + form.setUserId(getUser().getUserId()); + Map valuesToPersist = form.getOptions(); + + if (!valuesToPersist.isEmpty()) + { + props.putAll(valuesToPersist); + props.save(); + return true; + } + + return false; + } + + @Override + public URLHelper getSuccessURL(ConfigForm configForm) + { + return PageFlowUtil.urlProvider(StudyUrls.class).getManageStudyURL(getContainer()); + } + } + + @RequiresPermission(AdminPermission.class) + public static class ReloadQueryBasedImportAction extends MutatingApiAction + { + @Override + public Object execute(ConfigForm form, BindException errors) throws Exception + { + ApiSimpleResponse response = new ApiSimpleResponse(); + + try + { + SpecimenTransform transform = SpecimenService.get().getSpecimenTransform(QueryBasedSpecimenTransform.NAME); + PipelineJob job = SpecimenService.get().createSpecimenReloadJob(getContainer(), getUser(), transform, getViewContext().getActionURL()); + + PipelineService.get().queueJob(job); + response.put("success", true); + } + catch (PipelineValidationException e) + { + throw new IOException(e); + } + return response; + } + } } diff --git a/specimen/src/org/labkey/specimen/importer/QueryBasedExport.java b/specimen/src/org/labkey/specimen/importer/QueryBasedExport.java new file mode 100644 index 00000000000..7dde054b175 --- /dev/null +++ b/specimen/src/org/labkey/specimen/importer/QueryBasedExport.java @@ -0,0 +1,97 @@ +package org.labkey.specimen.importer; + +import org.labkey.api.action.NullSafeBindException; +import org.labkey.api.data.ColumnHeaderType; +import org.labkey.api.data.Container; +import org.labkey.api.data.ShowRows; +import org.labkey.api.data.TSVGridWriter; +import org.labkey.api.data.TSVWriter; +import org.labkey.api.pipeline.PipelineJob; +import org.labkey.api.pipeline.PipelineJobException; +import org.labkey.api.query.QueryService; +import org.labkey.api.query.QuerySettings; +import org.labkey.api.query.QueryView; +import org.labkey.api.query.UserSchema; +import org.labkey.api.security.User; +import org.labkey.api.view.ActionURL; +import org.labkey.api.view.ViewContext; +import org.labkey.specimen.actions.SpecimenController.ConfigureQueryImportAction; +import org.labkey.vfs.FileLike; +import org.springframework.validation.BindException; +import org.springframework.validation.ObjectError; + +import java.io.IOException; +import java.util.List; + +public class QueryBasedExport +{ + private final QueryBasedSpecimenReloadConfig _config; + private final PipelineJob _job; + private final FileLike _archive; + + public QueryBasedExport (QueryBasedSpecimenReloadConfig config, PipelineJob job, FileLike archive) + { + _config = config; + _job = job; + _archive = archive; + } + + public QueryBasedSpecimenReloadConfig getConfig() + { + return _config; + } + + public void exportRepository() throws PipelineJobException + { + _job.info("Starting query-based specimen export"); + + User user = _job.getUser(); + Container c = _job.getContainer(); + String schemaName = _config.getSchemaName(); + String queryName = _config.getQueryName(); + String viewName = _config.getViewName(); + + // We fake up a ViewContext here as we're in a background job, and and no request-based ViewContext is available + try (ViewContext.StackResetter reset = ViewContext.pushMockViewContext(user, c, new ActionURL(ConfigureQueryImportAction.class, c))) + { + ViewContext viewContext = reset.getContext(); + + UserSchema userSchema = QueryService.get().getUserSchema(user, c, schemaName); + if (null == userSchema) + throw new PipelineJobException(String.format("Schema %s is either inaccessible or deleted.", schemaName)); + + QuerySettings querySettings = new QuerySettings(viewContext, "query", queryName); + querySettings.setSchemaName(schemaName); + querySettings.setViewName(viewName); + querySettings.setShowRows(ShowRows.ALL); + + _job.info(String.format("Requesting data from query '%s' of schema '%s'", queryName, schemaName)); + BindException errors = new NullSafeBindException(new Object(), "form"); + QueryView queryView = userSchema.createView(viewContext, querySettings, errors); + + if (errors.hasErrors()) + { + List allErrors = errors.getAllErrors(); + for (ObjectError error : allErrors) + { + _job.error(error.toString()); + } + return; + } + + _job.info("creating the exported data .csv file"); + try (TSVGridWriter tsvWriter = queryView.getTsvWriter()) + { + tsvWriter.setDelimiterCharacter(TSVWriter.DELIM.COMMA); + tsvWriter.setColumnHeaderType(ColumnHeaderType.DisplayFieldKey); + tsvWriter.write(_archive.openOutputStream()); + _job.info("finished writing data file: " + _archive.getName()); + } + catch (IOException e) + { + _job.error("Error writing TSV: " + e.getMessage()); + } + } + _job.info("Finished writing data file: " + _archive.getName()); // use Format + } +} diff --git a/specimen/src/org/labkey/specimen/importer/QueryBasedSpecimenImportUploadTask.java b/specimen/src/org/labkey/specimen/importer/QueryBasedSpecimenImportUploadTask.java new file mode 100644 index 00000000000..bca831d6c68 --- /dev/null +++ b/specimen/src/org/labkey/specimen/importer/QueryBasedSpecimenImportUploadTask.java @@ -0,0 +1,81 @@ +package org.labkey.specimen.importer; + +import org.apache.logging.log4j.Logger; +import org.labkey.api.data.Container; +import org.labkey.api.data.PropertyManager; +import org.labkey.api.module.ModuleLoader; +import org.labkey.api.pipeline.PipelineJob; +import org.labkey.api.pipeline.PipelineService; +import org.labkey.api.security.User; +import org.labkey.api.security.UserManager; +import org.labkey.api.study.SpecimenService; +import org.labkey.api.study.SpecimenTransform; +import org.labkey.api.study.StudyService; +import org.labkey.api.util.SystemMaintenance; +import org.labkey.specimen.SpecimenModule; + +import java.util.stream.Stream; + +public class QueryBasedSpecimenImportUploadTask implements SystemMaintenance.MaintenanceTask +{ + @Override + public String getDescription() + { + return "Query-Based Specimen Import Upload Task"; + } + + @Override + public String getName() + { + return "QueryBasedSpecimenUpload"; + } + + @Override + public void run(Logger log) + { + String QBSpecimenImportKey = QueryBasedSpecimenTransform.PROPERTY_MAP_KEY; + try (Stream stream = PropertyManager.getNormalStore().streamMatchingContainers(PropertyManager.SHARED_USER, QBSpecimenImportKey)) + { + stream.forEach(c -> { + try + { + // Study must have been deleted + if (null == StudyService.get().getStudy(c)) + { + log.error("Query-based specimen import failed: Study does not exist in folder " + c.getPath()); + return; + } + + if (!c.getActiveModules().contains(ModuleLoader.getInstance().getModule(SpecimenModule.class))) + return; + + SpecimenTransform transform = SpecimenService.get().getSpecimenTransform(QueryBasedSpecimenTransform.NAME); + PropertyManager.PropertyMap props = PropertyManager.getProperties(c, QBSpecimenImportKey); + boolean enabled = ("on").equals(props.get("enabled")); + + if (!enabled) + { + log.info(String.format("Prohibiting queuing specimen import for %s. Query-based specimen import is not enabled.", c.getName())); + return; + } + if (!transform.isActive(c)) + { + log.info(String.format("Prohibiting queuing specimen import for %s. Query-based specimen import is not the active import mechanism.", c.getName())); + return; + } + log.info("Queuing specimen import for " + c.getName()); + + int userId = Integer.parseInt(props.get("userId")); + User reloadUser = UserManager.getUser(userId); + PipelineJob job = SpecimenService.get().createSpecimenReloadJob(c, reloadUser, transform, null); + + PipelineService.get().queueJob(job); + } + catch (Exception e) + { + log.error("Query-based specimen import failed", e); + } + }); + } + } +} diff --git a/specimen/src/org/labkey/specimen/importer/QueryBasedSpecimenReloadConfig.java b/specimen/src/org/labkey/specimen/importer/QueryBasedSpecimenReloadConfig.java new file mode 100644 index 00000000000..396d63c6859 --- /dev/null +++ b/specimen/src/org/labkey/specimen/importer/QueryBasedSpecimenReloadConfig.java @@ -0,0 +1,58 @@ +package org.labkey.specimen.importer; + +import org.labkey.api.study.SpecimenTransform; + +public class QueryBasedSpecimenReloadConfig implements SpecimenTransform.ExternalImportConfig +{ + private String schemaName; + private String queryName; + private String viewName; + + public String getSchemaName() + { + return schemaName; + } + + public void setSchemaName(String schemaName) + { + this.schemaName = schemaName; + } + + public String getQueryName() + { + return queryName; + } + + public void setQueryName(String queryName) + { + this.queryName = queryName; + } + + public String getViewName() + { + return viewName; + } + + public void setViewName(String viewName) + { + this.viewName = viewName; + } + + @Override + public String getBaseServerUrl() + { + return null; + } + + @Override + public String getUsername() + { + return null; + } + + @Override + public String getPassword() + { + return null; + } +} diff --git a/specimen/src/org/labkey/specimen/importer/QueryBasedSpecimenTransform.java b/specimen/src/org/labkey/specimen/importer/QueryBasedSpecimenTransform.java new file mode 100644 index 00000000000..1e92c7b54bf --- /dev/null +++ b/specimen/src/org/labkey/specimen/importer/QueryBasedSpecimenTransform.java @@ -0,0 +1,128 @@ +package org.labkey.specimen.importer; + +import org.jetbrains.annotations.Nullable; +import org.labkey.api.data.Container; +import org.labkey.api.data.PropertyManager; +import org.labkey.api.data.TSVMapWriter; +import org.labkey.api.module.ModuleLoader; +import org.labkey.api.pipeline.PipelineJob; +import org.labkey.api.pipeline.PipelineJobException; +import org.labkey.api.security.User; +import org.labkey.api.study.SpecimenService; +import org.labkey.api.study.SpecimenTransform; +import org.labkey.api.util.FileType; +import org.labkey.api.view.ActionURL; +import org.labkey.specimen.SpecimenModule; +import org.labkey.specimen.actions.SpecimenController.ConfigureQueryImportAction; +import org.labkey.vfs.FileLike; + +import java.util.ArrayList; +import java.util.Map; + +public class QueryBasedSpecimenTransform implements SpecimenTransform +{ + public static final String NAME = "QueryBased"; + public static final String PROPERTY_MAP_KEY = "queryBasedSpecimenLoader"; + + @Override + public String getName() + { + return NAME; + } + + @Override + public boolean isValid(Container container) + { + return container.getActiveModules().contains(ModuleLoader.getInstance().getModule(SpecimenModule.class)); + } + + @Override + public boolean isActive(Container container) + { + PropertyManager.PropertyMap props = PropertyManager.getProperties(container, PROPERTY_MAP_KEY); + boolean enabled = ("on").equals(props.get("enabled")); + // If selected is null, the container has only one transform + String selected = SpecimenService.get().getActiveSpecimenImporter(container); + + return enabled && (selected == null || NAME.equals(selected)); + } + + @Override + public FileType getFileType() + { + return new FileType(".qbst.csv"); + } + + protected static class QueryBasedTSVWriter extends TSVMapWriter + { + private int _rowCount; + public QueryBasedTSVWriter(Iterable> rows) + { + super(new ArrayList<>(), rows); // start with empty column list + } + + public int getRowCount() + { + return _rowCount; + } + + @Override + public void writeRow(Map row) + { + if (_rowCount == 0) + { + writeFileHeader(); + setColumns(row.keySet()); + writeColumnHeaders(); + } + super.writeRow(row); + _rowCount++; + } + } + + @Override + public void transform(@Nullable PipelineJob job, FileLike input, FileLike output) throws PipelineJobException + { + QueryBasedTransformTask task = new QueryBasedTransformTask(job); + task.transform(input, output); + } + + @Override + public void postTransform(@Nullable PipelineJob job, FileLike input, FileLike outputArchive) + { + // noop + } + + @Override + public @Nullable ActionURL getManageAction(Container c, User user) + { + return new ActionURL(ConfigureQueryImportAction.class, c); + } + + @Override + public ExternalImportConfig getExternalImportConfig(Container c, User user) + { + Map props = PropertyManager.getProperties(c, PROPERTY_MAP_KEY); + QueryBasedSpecimenReloadConfig config = new QueryBasedSpecimenReloadConfig(); + + String schemaName = props.get("schemaName"); + String queryName = props.get("queryName"); + String viewName = props.get("viewName"); + + config.setSchemaName(schemaName); + config.setQueryName(queryName); + config.setViewName(viewName); + + return config; + } + + @Override + public void importFromExternalSource(@Nullable PipelineJob job, ExternalImportConfig importConfig, FileLike inputArchive) throws PipelineJobException + { + if (importConfig instanceof QueryBasedSpecimenReloadConfig queryConfig) + { + QueryBasedExport export = new QueryBasedExport(queryConfig, job, inputArchive); + export.exportRepository(); + } + } +} diff --git a/specimen/src/org/labkey/specimen/importer/QueryBasedTransformTask.java b/specimen/src/org/labkey/specimen/importer/QueryBasedTransformTask.java new file mode 100644 index 00000000000..90a61d1c2b5 --- /dev/null +++ b/specimen/src/org/labkey/specimen/importer/QueryBasedTransformTask.java @@ -0,0 +1,160 @@ +package org.labkey.specimen.importer; + +import org.apache.commons.lang3.StringUtils; +import org.jetbrains.annotations.Nullable; +import org.labkey.api.collections.CaseInsensitiveHashSet; +import org.labkey.api.pipeline.AbstractSpecimenTransformTask; +import org.labkey.api.pipeline.PipelineJob; +import org.labkey.api.pipeline.PipelineJobException; +import org.labkey.api.reader.DataLoader; +import org.labkey.api.reader.DataLoaderFactory; +import org.labkey.api.writer.PrintWriters; +import org.labkey.vfs.FileLike; + +import java.io.IOException; +import java.io.PrintWriter; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +public class QueryBasedTransformTask extends AbstractSpecimenTransformTask +{ + public QueryBasedTransformTask(@Nullable PipelineJob job) + { + super(job); + } + + private final Map _primaryIds = new LinkedHashMap<>(); + private final Map _derivativeIds = new LinkedHashMap<>(); + private final Map _additiveIds = new LinkedHashMap<>(); + + @Nullable + private String getPrimaryType(Map row) + { + String colNameLK = getNonNullValue(row, "primary_type"); + return ("".equals(colNameLK)) ? getNonNullValue(row, "PrimaryType") : colNameLK; + } + + @Nullable + private String getDerivative(Map row) + { + String colNameLK = getNonNullValue(row, "derivative_type"); + return ("".equals(colNameLK)) ? getNonNullValue(row, "DerivativeType") : colNameLK; + } + + @Nullable + private String getAdditive(Map row) + { + String colNameLK = getNonNullValue(row, "additive_type"); + return ("".equals(colNameLK)) ? getNonNullValue(row, "AdditiveType") : colNameLK; + } + + private Integer getType(String type, Map typeMap) + { + Integer id = typeMap.get(type); + if (id == null && !StringUtils.isEmpty(type)) + { + id = typeMap.size() + 1; + typeMap.put(type, id); + } + return id; + } + + public void transform(FileLike input, FileLike output) throws PipelineJobException + { + info("Starting to transform input file " + input + " to output file " + output); + + try + { + DataLoaderFactory df = DataLoader.get().findFactory(input, null); + if (null == df) + throw new PipelineJobException("Unable to create a data loader factory for the file: " + input.getName()); + DataLoader loader = df.createLoader(input.openInputStream(), true, _job.getContainer()); + loader.setInferTypes(false); + + try (ZipOutputStream zOut = new ZipOutputStream(output.openOutputStream())) + { + zOut.putNextEntry(new ZipEntry("specimens.tsv")); + PrintWriter writer = PrintWriters.getPrintWriter(zOut); + final QueryBasedSpecimenTransform.QueryBasedTSVWriter tsvWriter = new QueryBasedSpecimenTransform.QueryBasedTSVWriter(Collections.emptyList()); + + tsvWriter.setFileHeader(Collections.singletonList("# " + "specimens")); + tsvWriter.setPrintWriter(writer); + try + { + loader.forEach(row -> { + if (MapFilter.getMapFilter(this).test(row)) + { + if (!row.containsKey("records_id")) + row.put("record_id", tsvWriter.getRowCount()); + + row.put("primary_specimen_type_id", getType(getPrimaryType(row), _primaryIds)); + row.put("derivative_type_id", getType(getDerivative(row), _derivativeIds)); + row.put("additive_type_id", getType(getAdditive(row), _additiveIds)); + + tsvWriter.writeRow(row); + } + }); + } + finally + { + writer.flush(); + zOut.closeEntry(); + } + + if (tsvWriter.getRowCount() > 0) + info("After removing duplicates, there are " + tsvWriter.getRowCount() + " rows of data"); + else + throw new PipelineJobException("There are no rows of data"); + + writePrimaries(getPrimaryIds(), zOut); + writeDerivatives(getDerivativeIds(), zOut); + writeAdditives(getAdditiveIds(), zOut); + } + } + + catch (IOException e) + { + throw new PipelineJobException(e); + } + } + + @Override + protected Map transformRow(Map inputRow, int rowIndex, Map labIds, Map primaryIds, Map derivativeIds) + { + return null; + } + + @Override + protected Set getIgnoredHashColumns() + { + return new CaseInsensitiveHashSet(); + } + + @Override + protected Map getLabIds() + { + return new LinkedHashMap<>(); + } + + @Override + protected Map getPrimaryIds() + { + return _primaryIds; + } + + @Override + protected Map getDerivativeIds() + { + return _derivativeIds; + } + + @Override + protected Map getAdditiveIds() + { + return _additiveIds; + } +} diff --git a/specimen/src/org/labkey/specimen/view/configureQueryImport.jsp b/specimen/src/org/labkey/specimen/view/configureQueryImport.jsp new file mode 100644 index 00000000000..f62ceaeaa04 --- /dev/null +++ b/specimen/src/org/labkey/specimen/view/configureQueryImport.jsp @@ -0,0 +1,132 @@ +<% +/* + * Copyright (c) 2013-2016 LabKey Corporation. All rights reserved. No portion of this work may be reproduced in + * any form or by any electronic or mechanical means without written permission from LabKey Corporation. + */ +%> +<%@ page import="org.labkey.api.data.Container" %> +<%@ page import="org.labkey.api.data.PropertyManager" %> +<%@ page import="org.labkey.api.pipeline.PipelineStatusUrls" %> +<%@ page import="org.labkey.api.study.SpecimenService" %> +<%@ page import="org.labkey.api.study.StudyUrls" %> +<%@ page import="org.labkey.api.util.ButtonBuilder" %> +<%@ page import="org.labkey.api.util.InputBuilder" %> +<%@ page import="org.labkey.api.util.InputBuilder.Input" %> +<%@ page import="org.labkey.api.util.PageFlowUtil" %> +<%@ page import="org.labkey.api.util.SelectBuilder" %> +<%@ page import="org.labkey.specimen.actions.SpecimenController.ReloadQueryBasedImportAction" %> +<%@ page import="org.labkey.specimen.importer.QueryBasedSpecimenTransform" %> +<%@ page extends="org.labkey.api.jsp.JspBase" %> +<%@ taglib prefix="labkey" uri="http://www.labkey.org/taglib" %> + +<% + Container c = getContainer(); + String selected = SpecimenService.get().getActiveSpecimenImporter(c); + boolean active = selected == null || selected.equals(QueryBasedSpecimenTransform.NAME); + String QBSpecimenImportKey = QueryBasedSpecimenTransform.PROPERTY_MAP_KEY; + + PropertyManager.PropertyMap props = PropertyManager.getProperties(c, QBSpecimenImportKey); + + String schemaName = props.get("schemaName"); + String queryName = props.get("queryName"); + String viewName = props.get("viewName"); + boolean enabled = ("on").equals(props.get("enabled")); +%> + + + + + +
+

+ Specimen data can be automatically loaded from an existing query using the specimen import configuration + defined on this page. The schema you specify must already be defined as an external schema, and the query + and view selected must map and filter rows as expected by the LabKey specimen repository. +

+ +

+ Learn more about configuring <%=helpLink("querySpecimenImport", "Query-based specimen reload")%>. +

+ +
+ + +

Configure connection

+ +
+ <%=InputBuilder.checkbox() + .name("enabled") + .label("Enable reload") + .layout(Input.Layout.HORIZONTAL) + .checked(enabled) + .formGroup(true) + %> + + <%= new SelectBuilder() + .name("schemaName") + .id("schemaName") + .label("Schema") + .layout(Input.Layout.HORIZONTAL) + .formGroup(true) + %> + + <%= new SelectBuilder() + .name("queryName") + .id("queryName") + .label("Query") + .layout(Input.Layout.HORIZONTAL) + .formGroup(true) + %> + + <%= new SelectBuilder() + .name("viewName") + .id("viewName") + .label("View") + .layout(Input.Layout.HORIZONTAL) + .formGroup(true) + %> +
+
+ + <%= new ButtonBuilder("Cancel") + .href(PageFlowUtil.urlProvider(StudyUrls.class).getManageStudyURL(getContainer())) + .build() + %> + + <%= new ButtonBuilder("Save") + .submit(true) + .build() + %> + + <%= new ButtonBuilder("Reload Now") + .id("reloadNow") + .enabled(enabled && active) + .onClick("reloadNow();") + .build() + %> +
+ + +
diff --git a/specimen/test/sampledata/import/100_specimens.xlsx b/specimen/test/sampledata/import/100_specimens.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..75b54324a2ed3fa67c30763cb2b3fb5ee02f8bd1 GIT binary patch literal 20576 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 literal 0 HcmV?d00001 diff --git a/specimen/test/src/org/labkey/test/pages/professional/ConfigureSpecimenImportPage.java b/specimen/test/src/org/labkey/test/pages/professional/ConfigureSpecimenImportPage.java new file mode 100644 index 00000000000..da7389387b9 --- /dev/null +++ b/specimen/test/src/org/labkey/test/pages/professional/ConfigureSpecimenImportPage.java @@ -0,0 +1,96 @@ +package org.labkey.test.pages.professional; + +import org.labkey.test.Locator; +import org.labkey.test.WebDriverWrapper; +import org.labkey.test.WebTestHelper; +import org.labkey.test.pages.LabKeyPage; +import org.labkey.test.pages.study.ManageStudyPage; +import org.labkey.test.util.DataRegionTable; +import org.labkey.test.util.PipelineStatusTable; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; + +import java.util.Optional; + +public class ConfigureSpecimenImportPage extends LabKeyPage +{ + public ConfigureSpecimenImportPage(WebDriver driver) + { + super(driver); + } + + public static ConfigureSpecimenImportPage beginAt(WebDriverWrapper webDriverWrapper) + { + return beginAt(webDriverWrapper, webDriverWrapper.getCurrentContainerPath()); + } + + public static ConfigureSpecimenImportPage beginAt(WebDriverWrapper webDriverWrapper, String containerPath) + { + webDriverWrapper.beginAt(WebTestHelper.buildURL("specimen", containerPath, "configureQueryImport")); + return new ConfigureSpecimenImportPage(webDriverWrapper.getDriver()); + } + + @Override + protected void waitForPage() + { + WebDriverWrapper.waitFor(()-> elementCache().schemaSelect.isEnabled(), + "The page did not become ready in time", WAIT_FOR_JAVASCRIPT); + } + + public ConfigureSpecimenImportPage configureQueryBasedImport(boolean enableReload, String schema, String query, String view) + { + setCheckbox(elementCache().reloadBoxElement, enableReload); + setFormElement(elementCache().schemaSelect, schema); + setFormElement(elementCache().querySelect, query); + setFormElement(elementCache().viewSelect, view); + return this; + } + + public ManageStudyPage clickSave() + { + clickAndWait(elementCache().saveButton); + return new ManageStudyPage(getDriver()); + } + + public ManageStudyPage clickCancel() + { + clickAndWait(elementCache().cancelButton); + return new ManageStudyPage(getDriver()); + } + + public boolean isReloadNowButtonPresent() + { + return elementCache().reloadNowButton().isPresent(); + } + + public DataRegionTable clickReloadNow() + { + doAndWaitForPageToLoad(()-> elementCache().reloadNowButton().get().click()); + return PipelineStatusTable.finder(getDriver()).waitFor(); + } + + @Override + protected ElementCache newElementCache() + { + return new ElementCache(); + } + + protected class ElementCache extends LabKeyPage.ElementCache + { + WebElement cancelButton = Locator.linkWithSpan("Cancel").findWhenNeeded(getDriver()); + WebElement saveButton = Locator.linkWithSpan("Save").findWhenNeeded(getDriver()); + Optional reloadNowButton() + { + return Locator.linkWithSpan("Reload Now").findOptionalElement(getDriver()); + } + + WebElement formContainer = Locator.tagWithClass("div", "QBSpecimenImportFormFields") + .findWhenNeeded(getDriver()).withTimeout(WAIT_FOR_JAVASCRIPT); + + WebElement reloadBoxElement = Locator.checkbox().withAttribute("name", "enabled") + .findWhenNeeded(formContainer); + WebElement schemaSelect = Locator.id("schemaName").findWhenNeeded(formContainer); + WebElement querySelect = Locator.id("queryName").findWhenNeeded(formContainer); + WebElement viewSelect = Locator.id("viewName").findWhenNeeded(formContainer); + } +} diff --git a/specimen/test/src/org/labkey/test/tests/professional/QueryBasedSpecimenImportTest.java b/specimen/test/src/org/labkey/test/tests/professional/QueryBasedSpecimenImportTest.java new file mode 100644 index 00000000000..de6206e7e93 --- /dev/null +++ b/specimen/test/src/org/labkey/test/tests/professional/QueryBasedSpecimenImportTest.java @@ -0,0 +1,115 @@ +package org.labkey.test.tests.professional; + +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.labkey.test.BaseWebDriverTest; +import org.labkey.test.TestFileUtils; +import org.labkey.test.categories.Git; +import org.labkey.test.pages.professional.ConfigureSpecimenImportPage; + +import java.io.File; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +import static org.hamcrest.CoreMatchers.hasItems; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; + +@Category({Git.class}) +public class QueryBasedSpecimenImportTest extends BaseWebDriverTest +{ + String TEST_SCHEMA = "lists"; + String TEST_QUERY = "test_specimen_data"; + String TEST_VIEW = "Default"; + File SPECIMENS_FILE = TestFileUtils.getSampleData("import/100_specimens.xlsx"); + + @BeforeClass + public static void setupProject() + { + QueryBasedSpecimenImportTest init = getCurrentTest(); + init.doSetup(); + } + + private void doSetup() + { + _containerHelper.createProject(getProjectName(), "Study (ITN)"); + _studyHelper.startCreateStudy() + .createStudy(); + goToFolderManagement() + .goToFolderTypeTab() + .enableModule("Specimen") + .save(); + + // create a list of specimens to import from + _listHelper.createListFromFile(getProjectName(), TEST_QUERY, SPECIMENS_FILE); + } + + @Before + public void preTest() + { + goToProjectHome(); + } + + @Test + public void testQueryBasedSpecimenImport() + { + // the test configures one query-based importer + goToManageStudy() + .clickConfigureSpecimenImport(); + new ConfigureSpecimenImportPage(getDriver()) + .configureQueryBasedImport(true, TEST_SCHEMA, TEST_QUERY, TEST_VIEW) + .clickSave(); + + // navigate directly to configure query-based import, user 'reload now' button to load the specimens from the query + ConfigureSpecimenImportPage.beginAt(this, getProjectName()) + .clickReloadNow(); + waitForPipelineJobsToComplete(1, false); + + // navigate to Specimens tab, verify + clickTab("Specimens", true); + + // iterate over the list, ensure that for each there is a matching specimen in specimenDetails + List> sourceSpecimenData =executeSelectRowCommand(TEST_SCHEMA, TEST_QUERY).getRows(); + List> specimenDetailsList = executeSelectRowCommand("study", "SpecimenDetail").getRows(); + List> specimenPrimaryTypes = executeSelectRowCommand("study", "SpecimenPrimaryType").getRows(); + + List primaryTypes = specimenPrimaryTypes.stream().map(a-> a.get("PrimaryType").toString()).collect(Collectors.toList()); + assertThat("expect 4 kinds of primary vial types", + primaryTypes, hasItems("PBMC-Na Hep", "Serum-Clot", "urine super", "whole blood")); + + for (Map specimenRow : sourceSpecimenData) + { + String globalUniqueSpecimenId = specimenRow.get("global_unique_specimen_id").toString(); + + Optional> matchingSpecimenDetail = specimenDetailsList.stream() + .filter(a-> a.get("GlobalUniqueId").equals(globalUniqueSpecimenId)).findFirst(); + assertTrue("Expect to find matching SpecimenDetail for [" +globalUniqueSpecimenId+ "]", + matchingSpecimenDetail.isPresent()); + } + } + + + + @Override + protected BrowserType bestBrowser() + { + return BrowserType.CHROME; + } + + @Override + protected String getProjectName() + { + return "SpecimenImportTest Project"; + } + + @Override + public List getAssociatedModules() + { + return Arrays.asList("Specimen"); + } +} diff --git a/specimen/test/src/org/labkey/test/tests/professional/SpecimenImporterProviderBannerTest.java b/specimen/test/src/org/labkey/test/tests/professional/SpecimenImporterProviderBannerTest.java new file mode 100644 index 00000000000..48ee2fd2c0c --- /dev/null +++ b/specimen/test/src/org/labkey/test/tests/professional/SpecimenImporterProviderBannerTest.java @@ -0,0 +1,95 @@ +package org.labkey.test.tests.professional; + +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.labkey.test.BaseWebDriverTest; +import org.labkey.test.categories.Git; +import org.labkey.test.pages.study.ConfigureImporterPage; + +import java.util.Arrays; +import java.util.List; + +import static org.junit.Assert.assertTrue; + +@Category({Git.class}) +public class SpecimenImporterProviderBannerTest extends BaseWebDriverTest +{ + + @BeforeClass + public static void setupProject() + { + SpecimenImporterProviderBannerTest init = getCurrentTest(); + + init.doSetup(); + } + + private void doSetup() + { + _containerHelper.createProject(getProjectName(), "Study (ITN)"); + _studyHelper.startCreateStudy() + .createStudy(); + } + + @Before + public void preTest() + { + goToProjectHome(); + } + + /** + * This verifies that if no importer is enabled for the study folder, but a module is available that could be enabled, + * the user will be notified of their options when they click on the 'configure specimen import' link in the Manage Study page. + */ + @Test + public void testEnableModulePageIsServedIfPrimaryNotEnabled() + { + ConfigureImporterPage configPage = goToManageStudy() + .clickConfigureSpecimenImport(); + + assertTrue("If Specimen is not enabled in page but module is present, call to action banner should appear", + configPage.isEnableModuleBannerShown()); + } + + @Test + public void testQueryBasedImportConfigurePageIsShownWhenEnabled() + { + // arrange + goToFolderManagement() + .goToFolderTypeTab() + .enableModule("Specimen") + .save(); + + // assert + ConfigureImporterPage configPage = goToManageStudy() + .clickConfigureSpecimenImport(); + + assertTrue("If Specimen is enabled in page, query-based configuration should appear", + configPage.isQueryConfigurationShown()); + + // clean up after + goToFolderManagement() + .goToFolderTypeTab() + .disableModule("Specimen") + .save(); + } + + @Override + protected BrowserType bestBrowser() + { + return BrowserType.CHROME; + } + + @Override + protected String getProjectName() + { + return "ProfessionalImporterProviderBannerTest Project"; + } + + @Override + public List getAssociatedModules() + { + return Arrays.asList(); + } +} From b70e993bbc9236ee2e0a5fc5648fa951b196fd7f Mon Sep 17 00:00:00 2001 From: Adam Rauch Date: Mon, 16 Mar 2026 13:00:50 -0700 Subject: [PATCH 2/7] Remove SpecimenImporterProviderBannerTest (no longer relevant). Repackage remaining test and page. --- .../ConfigureSpecimenImportPage.java | 2 +- .../SpecimenImporterProviderBannerTest.java | 95 ------------------- .../QueryBasedSpecimenImportTest.java | 6 +- 3 files changed, 3 insertions(+), 100 deletions(-) rename specimen/test/src/org/labkey/test/pages/{professional => specimen}/ConfigureSpecimenImportPage.java (98%) delete mode 100644 specimen/test/src/org/labkey/test/tests/professional/SpecimenImporterProviderBannerTest.java rename specimen/test/src/org/labkey/test/tests/{professional => specimen}/QueryBasedSpecimenImportTest.java (97%) diff --git a/specimen/test/src/org/labkey/test/pages/professional/ConfigureSpecimenImportPage.java b/specimen/test/src/org/labkey/test/pages/specimen/ConfigureSpecimenImportPage.java similarity index 98% rename from specimen/test/src/org/labkey/test/pages/professional/ConfigureSpecimenImportPage.java rename to specimen/test/src/org/labkey/test/pages/specimen/ConfigureSpecimenImportPage.java index da7389387b9..7114172d0f5 100644 --- a/specimen/test/src/org/labkey/test/pages/professional/ConfigureSpecimenImportPage.java +++ b/specimen/test/src/org/labkey/test/pages/specimen/ConfigureSpecimenImportPage.java @@ -1,4 +1,4 @@ -package org.labkey.test.pages.professional; +package org.labkey.test.pages.specimen; import org.labkey.test.Locator; import org.labkey.test.WebDriverWrapper; diff --git a/specimen/test/src/org/labkey/test/tests/professional/SpecimenImporterProviderBannerTest.java b/specimen/test/src/org/labkey/test/tests/professional/SpecimenImporterProviderBannerTest.java deleted file mode 100644 index 48ee2fd2c0c..00000000000 --- a/specimen/test/src/org/labkey/test/tests/professional/SpecimenImporterProviderBannerTest.java +++ /dev/null @@ -1,95 +0,0 @@ -package org.labkey.test.tests.professional; - -import org.junit.Before; -import org.junit.BeforeClass; -import org.junit.Test; -import org.junit.experimental.categories.Category; -import org.labkey.test.BaseWebDriverTest; -import org.labkey.test.categories.Git; -import org.labkey.test.pages.study.ConfigureImporterPage; - -import java.util.Arrays; -import java.util.List; - -import static org.junit.Assert.assertTrue; - -@Category({Git.class}) -public class SpecimenImporterProviderBannerTest extends BaseWebDriverTest -{ - - @BeforeClass - public static void setupProject() - { - SpecimenImporterProviderBannerTest init = getCurrentTest(); - - init.doSetup(); - } - - private void doSetup() - { - _containerHelper.createProject(getProjectName(), "Study (ITN)"); - _studyHelper.startCreateStudy() - .createStudy(); - } - - @Before - public void preTest() - { - goToProjectHome(); - } - - /** - * This verifies that if no importer is enabled for the study folder, but a module is available that could be enabled, - * the user will be notified of their options when they click on the 'configure specimen import' link in the Manage Study page. - */ - @Test - public void testEnableModulePageIsServedIfPrimaryNotEnabled() - { - ConfigureImporterPage configPage = goToManageStudy() - .clickConfigureSpecimenImport(); - - assertTrue("If Specimen is not enabled in page but module is present, call to action banner should appear", - configPage.isEnableModuleBannerShown()); - } - - @Test - public void testQueryBasedImportConfigurePageIsShownWhenEnabled() - { - // arrange - goToFolderManagement() - .goToFolderTypeTab() - .enableModule("Specimen") - .save(); - - // assert - ConfigureImporterPage configPage = goToManageStudy() - .clickConfigureSpecimenImport(); - - assertTrue("If Specimen is enabled in page, query-based configuration should appear", - configPage.isQueryConfigurationShown()); - - // clean up after - goToFolderManagement() - .goToFolderTypeTab() - .disableModule("Specimen") - .save(); - } - - @Override - protected BrowserType bestBrowser() - { - return BrowserType.CHROME; - } - - @Override - protected String getProjectName() - { - return "ProfessionalImporterProviderBannerTest Project"; - } - - @Override - public List getAssociatedModules() - { - return Arrays.asList(); - } -} diff --git a/specimen/test/src/org/labkey/test/tests/professional/QueryBasedSpecimenImportTest.java b/specimen/test/src/org/labkey/test/tests/specimen/QueryBasedSpecimenImportTest.java similarity index 97% rename from specimen/test/src/org/labkey/test/tests/professional/QueryBasedSpecimenImportTest.java rename to specimen/test/src/org/labkey/test/tests/specimen/QueryBasedSpecimenImportTest.java index de6206e7e93..45617148e6f 100644 --- a/specimen/test/src/org/labkey/test/tests/professional/QueryBasedSpecimenImportTest.java +++ b/specimen/test/src/org/labkey/test/tests/specimen/QueryBasedSpecimenImportTest.java @@ -1,4 +1,4 @@ -package org.labkey.test.tests.professional; +package org.labkey.test.tests.specimen; import org.junit.Before; import org.junit.BeforeClass; @@ -7,7 +7,7 @@ import org.labkey.test.BaseWebDriverTest; import org.labkey.test.TestFileUtils; import org.labkey.test.categories.Git; -import org.labkey.test.pages.professional.ConfigureSpecimenImportPage; +import org.labkey.test.pages.specimen.ConfigureSpecimenImportPage; import java.io.File; import java.util.Arrays; @@ -93,8 +93,6 @@ public void testQueryBasedSpecimenImport() } } - - @Override protected BrowserType bestBrowser() { From 843c44ecbd537bf690ffbeda461cd55eb523d3da Mon Sep 17 00:00:00 2001 From: Adam Rauch Date: Mon, 16 Mar 2026 13:16:35 -0700 Subject: [PATCH 3/7] Simplify choose importer page --- .../labkey/specimen/view/chooseImporter.jsp | 30 +++++-------------- 1 file changed, 7 insertions(+), 23 deletions(-) diff --git a/specimen/src/org/labkey/specimen/view/chooseImporter.jsp b/specimen/src/org/labkey/specimen/view/chooseImporter.jsp index 6cc910506b2..999007ea2de 100644 --- a/specimen/src/org/labkey/specimen/view/chooseImporter.jsp +++ b/specimen/src/org/labkey/specimen/view/chooseImporter.jsp @@ -5,9 +5,7 @@ */ %> <%@ page import="com.google.common.collect.Iterables" %> -<%@ page import="org.labkey.api.admin.AdminUrls" %> <%@ page import="org.labkey.api.data.Container" %> -<%@ page import="org.labkey.api.module.ModuleLoader" %> <%@ page import="org.labkey.api.security.User" %> <%@ page import="org.labkey.api.study.SpecimenService" %> <%@ page import="org.labkey.api.study.SpecimenTransform" %> @@ -18,7 +16,6 @@ <%@ page import="org.labkey.api.view.ActionURL" %> <%@ page import="org.labkey.specimen.actions.ShowUploadSpecimensAction" %> <%@ page import="java.util.Collection" %> -<%@ page import="static org.labkey.api.util.HtmlString.NBSP" %> <%@ page extends="org.labkey.api.jsp.JspBase" %> <%@ taglib prefix="labkey" uri="http://www.labkey.org/taglib" %> <% @@ -34,9 +31,6 @@ int rowNumber = 0; String selected = SpecimenService.get().getActiveSpecimenImporter(c); - HtmlString manageFoldersLink = h(urlProvider(AdminUrls.class).getFolderTypeURL(c)); - HtmlString labkeyEditionsLink = h("https://www.labkey.com/products-services/labkey-server/labkey-server-editions-feature-comparison/"); - HtmlString contactUsLink = h("https://www.labkey.com/about/contact/"); HtmlString manuallyImportSpecimensLink = h(urlFor(ShowUploadSpecimensAction.class)); %> @@ -49,6 +43,12 @@ +<% + // At the moment, there's exactly one SpecimenTransform, QueryBasedSpecimenTransform, which is provided by the + // Specimen module. As a result, this "choose importer" page is never linked (if a single transform exists, the + // "Configure Specimen Import" link navigates straight to the query-based import configuration page. This probably + // won't ever change, but we'll leave this page in place just in case. +%>
<% if (numberOfTransforms > 1) { %>

@@ -114,8 +114,7 @@ %> - <% } else if (numberOfTransforms == 1) { %> - <% + <% } else if (numberOfTransforms == 1) { SpecimenTransform transform = Iterables.get(specimenTransforms, 0); ActionURL manageAction = transform.getManageAction(c, user); %> @@ -138,21 +137,6 @@ <% } else { %> - <% if (ModuleLoader.getInstance().hasModule("professional")) { %> -

- External Specimen Import is not currently available for this folder. - To use External import, > enable the Professional Module for this folder. -
- <% } else { %> -
-

<%=NBSP%>

- External Specimen Import is a Premium LabKey feature. > Learn more or - > contact LabKey . -
- <% - } - %> - > Import specimens manually <% } From b56ab0e8c113a86f1e41163566daf05b6ca448fd Mon Sep 17 00:00:00 2001 From: Adam Rauch Date: Mon, 16 Mar 2026 13:46:24 -0700 Subject: [PATCH 4/7] Update comment. Tag test as Specimen category. --- specimen/src/org/labkey/specimen/view/chooseImporter.jsp | 6 +++--- .../test/tests/specimen/QueryBasedSpecimenImportTest.java | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/specimen/src/org/labkey/specimen/view/chooseImporter.jsp b/specimen/src/org/labkey/specimen/view/chooseImporter.jsp index 999007ea2de..0fc794dd987 100644 --- a/specimen/src/org/labkey/specimen/view/chooseImporter.jsp +++ b/specimen/src/org/labkey/specimen/view/chooseImporter.jsp @@ -45,9 +45,9 @@ <% // At the moment, there's exactly one SpecimenTransform, QueryBasedSpecimenTransform, which is provided by the - // Specimen module. As a result, this "choose importer" page is never linked (if a single transform exists, the - // "Configure Specimen Import" link navigates straight to the query-based import configuration page. This probably - // won't ever change, but we'll leave this page in place just in case. + // Specimen module. As a result, this "choose importer" page is never linked (if a single transform exists, the + // "Configure Specimen Import" link navigates straight its configuration page. This probably won't ever change, + // but we'll leave this page in place just in case. %>
<% if (numberOfTransforms > 1) { %> diff --git a/specimen/test/src/org/labkey/test/tests/specimen/QueryBasedSpecimenImportTest.java b/specimen/test/src/org/labkey/test/tests/specimen/QueryBasedSpecimenImportTest.java index 45617148e6f..890f703ff25 100644 --- a/specimen/test/src/org/labkey/test/tests/specimen/QueryBasedSpecimenImportTest.java +++ b/specimen/test/src/org/labkey/test/tests/specimen/QueryBasedSpecimenImportTest.java @@ -7,6 +7,7 @@ import org.labkey.test.BaseWebDriverTest; import org.labkey.test.TestFileUtils; import org.labkey.test.categories.Git; +import org.labkey.test.categories.Specimen; import org.labkey.test.pages.specimen.ConfigureSpecimenImportPage; import java.io.File; @@ -20,7 +21,7 @@ import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; -@Category({Git.class}) +@Category({Git.class, Specimen.class}) public class QueryBasedSpecimenImportTest extends BaseWebDriverTest { String TEST_SCHEMA = "lists"; From 2354b57530ce95b81bc2fbf014719aad8e18e098 Mon Sep 17 00:00:00 2001 From: Adam Rauch Date: Mon, 16 Mar 2026 14:27:06 -0700 Subject: [PATCH 5/7] Use "Study" folder type --- .../importer/QueryBasedSpecimenImportUploadTask.java | 8 ++++---- .../test/tests/specimen/QueryBasedSpecimenImportTest.java | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/specimen/src/org/labkey/specimen/importer/QueryBasedSpecimenImportUploadTask.java b/specimen/src/org/labkey/specimen/importer/QueryBasedSpecimenImportUploadTask.java index bca831d6c68..af9b7f64b7f 100644 --- a/specimen/src/org/labkey/specimen/importer/QueryBasedSpecimenImportUploadTask.java +++ b/specimen/src/org/labkey/specimen/importer/QueryBasedSpecimenImportUploadTask.java @@ -42,7 +42,7 @@ public void run(Logger log) // Study must have been deleted if (null == StudyService.get().getStudy(c)) { - log.error("Query-based specimen import failed: Study does not exist in folder " + c.getPath()); + log.error("Query-based specimen import failed: Study does not exist in folder {}", c.getPath()); return; } @@ -55,15 +55,15 @@ public void run(Logger log) if (!enabled) { - log.info(String.format("Prohibiting queuing specimen import for %s. Query-based specimen import is not enabled.", c.getName())); + log.info("Prohibiting queuing specimen import for {}. Query-based specimen import is not enabled.", c.getName()); return; } if (!transform.isActive(c)) { - log.info(String.format("Prohibiting queuing specimen import for %s. Query-based specimen import is not the active import mechanism.", c.getName())); + log.info("Prohibiting queuing specimen import for {}. Query-based specimen import is not the active import mechanism.", c.getName()); return; } - log.info("Queuing specimen import for " + c.getName()); + log.info("Queuing specimen import for {}", c.getName()); int userId = Integer.parseInt(props.get("userId")); User reloadUser = UserManager.getUser(userId); diff --git a/specimen/test/src/org/labkey/test/tests/specimen/QueryBasedSpecimenImportTest.java b/specimen/test/src/org/labkey/test/tests/specimen/QueryBasedSpecimenImportTest.java index 890f703ff25..1a07a916dc0 100644 --- a/specimen/test/src/org/labkey/test/tests/specimen/QueryBasedSpecimenImportTest.java +++ b/specimen/test/src/org/labkey/test/tests/specimen/QueryBasedSpecimenImportTest.java @@ -38,7 +38,7 @@ public static void setupProject() private void doSetup() { - _containerHelper.createProject(getProjectName(), "Study (ITN)"); + _containerHelper.createProject(getProjectName(), "Study"); _studyHelper.startCreateStudy() .createStudy(); goToFolderManagement() From 387bdeb9419770a800634d0e487d5aba7441e3f5 Mon Sep 17 00:00:00 2001 From: Adam Rauch Date: Mon, 16 Mar 2026 17:30:55 -0700 Subject: [PATCH 6/7] Correct tab name --- .../test/tests/specimen/QueryBasedSpecimenImportTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specimen/test/src/org/labkey/test/tests/specimen/QueryBasedSpecimenImportTest.java b/specimen/test/src/org/labkey/test/tests/specimen/QueryBasedSpecimenImportTest.java index 1a07a916dc0..8e6df16477a 100644 --- a/specimen/test/src/org/labkey/test/tests/specimen/QueryBasedSpecimenImportTest.java +++ b/specimen/test/src/org/labkey/test/tests/specimen/QueryBasedSpecimenImportTest.java @@ -72,7 +72,7 @@ public void testQueryBasedSpecimenImport() waitForPipelineJobsToComplete(1, false); // navigate to Specimens tab, verify - clickTab("Specimens", true); + clickTab("Specimen Data", true); // iterate over the list, ensure that for each there is a matching specimen in specimenDetails List> sourceSpecimenData =executeSelectRowCommand(TEST_SCHEMA, TEST_QUERY).getRows(); From c8ae0d643077def7a87f2f249a1245b0c9979d1b Mon Sep 17 00:00:00 2001 From: Adam Rauch Date: Mon, 16 Mar 2026 17:48:07 -0700 Subject: [PATCH 7/7] Null check on inputFile --- .../importer/AbstractSpecimenTask.java | 29 ++++++++++--------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/specimen/src/org/labkey/specimen/importer/AbstractSpecimenTask.java b/specimen/src/org/labkey/specimen/importer/AbstractSpecimenTask.java index 3acf4e95dda..951b261b880 100644 --- a/specimen/src/org/labkey/specimen/importer/AbstractSpecimenTask.java +++ b/specimen/src/org/labkey/specimen/importer/AbstractSpecimenTask.java @@ -132,22 +132,25 @@ public static void doImport(@Nullable FileLike inputFile, PipelineJob job, Simpl SpecimenImporter importer = new SpecimenImporter(ctx.getContainer(), ctx.getUser()); importer.process(specimenDir, merge, ctx, job, syncParticipantVisit); - // perform any tasks after the transform and import has been completed - String activeImporter = SpecimenService.get().getActiveSpecimenImporter(ctx.getContainer()); - if (null != activeImporter) - { - SpecimenTransform activeTransformer = SpecimenService.get().getSpecimenTransform(activeImporter); - if (activeTransformer != null && activeTransformer.getFileType().isType(inputFile)) - doPostTransform(activeTransformer, inputFile, job); - } - else + // if there's an inputFile, perform any tasks after the transform and import has been completed + if (inputFile != null) { - for (SpecimenTransform transformer : SpecimenService.get().getSpecimenTransforms(ctx.getContainer())) + String activeImporter = SpecimenService.get().getActiveSpecimenImporter(ctx.getContainer()); + if (null != activeImporter) { - if (transformer.getFileType().isType(inputFile)) + SpecimenTransform activeTransformer = SpecimenService.get().getSpecimenTransform(activeImporter); + if (activeTransformer != null && activeTransformer.getFileType().isType(inputFile)) + doPostTransform(activeTransformer, inputFile, job); + } + else + { + for (SpecimenTransform transformer : SpecimenService.get().getSpecimenTransforms(ctx.getContainer())) { - doPostTransform(transformer, inputFile, job); - break; + if (transformer.getFileType().isType(inputFile)) + { + doPostTransform(transformer, inputFile, job); + break; + } } } }