From 1c3e56b61d8822c72aab63c6c731ad89b6a76f35 Mon Sep 17 00:00:00 2001 From: Karl Lum Date: Fri, 23 May 2025 08:50:06 -0700 Subject: [PATCH 1/6] Move study design features into a new module Original PR: https://github.com/LabKey/platform/pull/6686 --- studydesign/build.gradle | 11 + studydesign/module.properties | 5 + .../studydesign/StudyDesignController.java | 949 ++++++++++++++ .../labkey/studydesign/StudyDesignModule.java | 83 ++ .../studydesign/StudyDesignServiceImpl.java | 68 + .../model/AssaySpecimenConfigImpl.java | 302 +++++ .../model/AssaySpecimenVisitImpl.java | 112 ++ .../studydesign/model/DoseAndRoute.java | 169 +++ .../studydesign/model/ProductAntigenImpl.java | 171 +++ .../labkey/studydesign/model/ProductImpl.java | 205 +++ .../studydesign/model/StudyAssaySchedule.java | 77 ++ .../studydesign/model/StudyDesignCohort.java | 94 ++ .../model/StudyTreatmentSchedule.java | 174 +++ .../studydesign/model/TreatmentImpl.java | 226 ++++ .../studydesign/model/TreatmentManager.java | 1151 +++++++++++++++++ .../model/TreatmentProductImpl.java | 255 ++++ .../studydesign/model/TreatmentVisitMap.java | 27 + .../model/TreatmentVisitMapImpl.java | 121 ++ .../view/AssayScheduleWebpartFactory.java | 72 ++ .../ImmunizationScheduleWebpartFactory.java | 73 ++ .../view/StudyDesignConfigureMenuItem.java | 37 + .../view/StudyDesignWebpartFactory.java | 24 + .../view/VaccineDesignWebpartFactory.java | 48 + .../studydesign/view/assayScheduleWebpart.jsp | 88 ++ .../view/immunizationScheduleWebpart.jsp | 174 +++ .../studydesign/view/manageAssaySchedule.jsp | 128 ++ .../studydesign/view/manageStudyProducts.jsp | 135 ++ .../studydesign/view/manageTreatments.jsp | 169 +++ .../studydesign/view/vaccineDesignWebpart.jsp | 89 ++ .../study/vaccineDesign/AssaySchedule.js | 659 ++++++++++ .../study/vaccineDesign/BaseDataView.js | 678 ++++++++++ .../vaccineDesign/BaseDataViewAddVisit.js | 99 ++ .../webapp/study/vaccineDesign/Models.js | 82 ++ .../study/vaccineDesign/StudyProducts.js | 508 ++++++++ .../study/vaccineDesign/TreatmentDialog.js | 189 +++ .../study/vaccineDesign/TreatmentSchedule.js | 465 +++++++ .../vaccineDesign/TreatmentScheduleBase.js | 437 +++++++ .../TreatmentScheduleSingleTablePanel.js | 224 ++++ .../webapp/study/vaccineDesign/Utils.js | 212 +++ .../study/vaccineDesign/VaccineDesign.css | 82 ++ .../webapp/study/vaccineDesign/VisitWindow.js | 319 +++++ .../study/vaccineDesign/vaccineDesign.lib.xml | 18 + 42 files changed, 9209 insertions(+) create mode 100644 studydesign/build.gradle create mode 100644 studydesign/module.properties create mode 100644 studydesign/src/org/labkey/studydesign/StudyDesignController.java create mode 100644 studydesign/src/org/labkey/studydesign/StudyDesignModule.java create mode 100644 studydesign/src/org/labkey/studydesign/StudyDesignServiceImpl.java create mode 100644 studydesign/src/org/labkey/studydesign/model/AssaySpecimenConfigImpl.java create mode 100644 studydesign/src/org/labkey/studydesign/model/AssaySpecimenVisitImpl.java create mode 100644 studydesign/src/org/labkey/studydesign/model/DoseAndRoute.java create mode 100644 studydesign/src/org/labkey/studydesign/model/ProductAntigenImpl.java create mode 100644 studydesign/src/org/labkey/studydesign/model/ProductImpl.java create mode 100644 studydesign/src/org/labkey/studydesign/model/StudyAssaySchedule.java create mode 100644 studydesign/src/org/labkey/studydesign/model/StudyDesignCohort.java create mode 100644 studydesign/src/org/labkey/studydesign/model/StudyTreatmentSchedule.java create mode 100644 studydesign/src/org/labkey/studydesign/model/TreatmentImpl.java create mode 100644 studydesign/src/org/labkey/studydesign/model/TreatmentManager.java create mode 100644 studydesign/src/org/labkey/studydesign/model/TreatmentProductImpl.java create mode 100644 studydesign/src/org/labkey/studydesign/model/TreatmentVisitMap.java create mode 100644 studydesign/src/org/labkey/studydesign/model/TreatmentVisitMapImpl.java create mode 100644 studydesign/src/org/labkey/studydesign/view/AssayScheduleWebpartFactory.java create mode 100644 studydesign/src/org/labkey/studydesign/view/ImmunizationScheduleWebpartFactory.java create mode 100644 studydesign/src/org/labkey/studydesign/view/StudyDesignConfigureMenuItem.java create mode 100644 studydesign/src/org/labkey/studydesign/view/StudyDesignWebpartFactory.java create mode 100644 studydesign/src/org/labkey/studydesign/view/VaccineDesignWebpartFactory.java create mode 100644 studydesign/src/org/labkey/studydesign/view/assayScheduleWebpart.jsp create mode 100644 studydesign/src/org/labkey/studydesign/view/immunizationScheduleWebpart.jsp create mode 100644 studydesign/src/org/labkey/studydesign/view/manageAssaySchedule.jsp create mode 100644 studydesign/src/org/labkey/studydesign/view/manageStudyProducts.jsp create mode 100644 studydesign/src/org/labkey/studydesign/view/manageTreatments.jsp create mode 100644 studydesign/src/org/labkey/studydesign/view/vaccineDesignWebpart.jsp create mode 100644 studydesign/webapp/study/vaccineDesign/AssaySchedule.js create mode 100644 studydesign/webapp/study/vaccineDesign/BaseDataView.js create mode 100644 studydesign/webapp/study/vaccineDesign/BaseDataViewAddVisit.js create mode 100644 studydesign/webapp/study/vaccineDesign/Models.js create mode 100644 studydesign/webapp/study/vaccineDesign/StudyProducts.js create mode 100644 studydesign/webapp/study/vaccineDesign/TreatmentDialog.js create mode 100644 studydesign/webapp/study/vaccineDesign/TreatmentSchedule.js create mode 100644 studydesign/webapp/study/vaccineDesign/TreatmentScheduleBase.js create mode 100644 studydesign/webapp/study/vaccineDesign/TreatmentScheduleSingleTablePanel.js create mode 100644 studydesign/webapp/study/vaccineDesign/Utils.js create mode 100644 studydesign/webapp/study/vaccineDesign/VaccineDesign.css create mode 100644 studydesign/webapp/study/vaccineDesign/VisitWindow.js create mode 100644 studydesign/webapp/study/vaccineDesign/vaccineDesign.lib.xml diff --git a/studydesign/build.gradle b/studydesign/build.gradle new file mode 100644 index 00000000..8188c8b0 --- /dev/null +++ b/studydesign/build.gradle @@ -0,0 +1,11 @@ +import org.labkey.gradle.util.BuildUtils + +plugins { + id 'org.labkey.build.module' +} + +dependencies { + BuildUtils.addLabKeyDependency(project: project, config: "implementation", depProjectPath: BuildUtils.getPlatformModuleProjectPath(project.gradle, "study"), depProjectConfig: "apiJarFile") + BuildUtils.addLabKeyDependency(project: project, config: "jspImplementation", depProjectPath: BuildUtils.getPlatformModuleProjectPath(project.gradle, "study"), depProjectConfig: "apiJarFile") + BuildUtils.addLabKeyDependency(project: project, config: "modules", depProjectPath: BuildUtils.getPlatformModuleProjectPath(project.gradle, "study"), depProjectConfig: "published", depExtension: "module") +} diff --git a/studydesign/module.properties b/studydesign/module.properties new file mode 100644 index 00000000..d600824f --- /dev/null +++ b/studydesign/module.properties @@ -0,0 +1,5 @@ +ModuleClass: org.labkey.studydesign.StudyDesignModule +License: Apache 2.0 +LicenseURL: http://www.apache.org/licenses/LICENSE-2.0 +SupportedDatabases: pgsql +ManageVersion: true diff --git a/studydesign/src/org/labkey/studydesign/StudyDesignController.java b/studydesign/src/org/labkey/studydesign/StudyDesignController.java new file mode 100644 index 00000000..ce02bff5 --- /dev/null +++ b/studydesign/src/org/labkey/studydesign/StudyDesignController.java @@ -0,0 +1,949 @@ +/* + * Copyright (c) 2014-2019 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.labkey.studydesign; + +import org.apache.commons.lang3.StringUtils; +import org.jetbrains.annotations.Nullable; +import org.json.JSONArray; +import org.json.JSONObject; +import org.labkey.api.action.ApiJsonForm; +import org.labkey.api.action.ApiResponse; +import org.labkey.api.action.ApiSimpleResponse; +import org.labkey.api.action.MutatingApiAction; +import org.labkey.api.action.ReadOnlyApiAction; +import org.labkey.api.action.ReturnUrlForm; +import org.labkey.api.action.SimpleViewAction; +import org.labkey.api.action.SpringActionController; +import org.labkey.api.data.Container; +import org.labkey.api.data.DbScope; +import org.labkey.api.data.SimpleFilter; +import org.labkey.api.data.Table; +import org.labkey.api.query.FieldKey; +import org.labkey.api.query.ValidationException; +import org.labkey.api.security.ActionNames; +import org.labkey.api.security.RequiresPermission; +import org.labkey.api.security.User; +import org.labkey.api.security.permissions.ReadPermission; +import org.labkey.api.security.permissions.UpdatePermission; +import org.labkey.api.study.Cohort; +import org.labkey.api.study.Study; +import org.labkey.api.study.StudyService; +import org.labkey.api.study.StudyUrls; +import org.labkey.api.study.Visit; +import org.labkey.api.study.model.CohortService; +import org.labkey.api.study.security.permissions.ManageStudyPermission; +import org.labkey.api.studydesign.StudyDesignUrls; +import org.labkey.api.studydesign.query.StudyDesignSchema; +import org.labkey.api.util.JsonUtil; +import org.labkey.api.util.PageFlowUtil; +import org.labkey.api.view.ActionURL; +import org.labkey.api.view.HttpView; +import org.labkey.api.view.JspView; +import org.labkey.api.view.NavTree; +import org.labkey.studydesign.model.AssaySpecimenConfigImpl; +import org.labkey.studydesign.model.AssaySpecimenVisitImpl; +import org.labkey.studydesign.model.DoseAndRoute; +import org.labkey.studydesign.model.ProductAntigenImpl; +import org.labkey.studydesign.model.ProductImpl; +import org.labkey.studydesign.model.StudyAssaySchedule; +import org.labkey.studydesign.model.StudyDesignCohort; +import org.labkey.studydesign.model.StudyTreatmentSchedule; +import org.labkey.studydesign.model.TreatmentImpl; +import org.labkey.studydesign.model.TreatmentManager; +import org.labkey.studydesign.model.TreatmentProductImpl; +import org.labkey.studydesign.model.TreatmentVisitMapImpl; +import org.springframework.validation.BindException; +import org.springframework.validation.Errors; +import org.springframework.web.servlet.ModelAndView; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +public class StudyDesignController extends SpringActionController +{ + private static final SpringActionController.ActionResolver ACTION_RESOLVER = new SpringActionController.DefaultActionResolver(StudyDesignController.class); + + public StudyDesignController() + { + setActionResolver(ACTION_RESOLVER); + } + + public static class StudyDesignUrlsImpl implements StudyDesignUrls + { + @Override + public ActionURL getManageAssayScheduleURL(Container container, boolean useAlternateLookupFields) + { + ActionURL url = new ActionURL(ManageAssayScheduleAction.class, container); + url.addParameter("useAlternateLookupFields", useAlternateLookupFields); + return url; + } + + @Override + public ActionURL getManageStudyProductsURL(Container container) + { + return new ActionURL(ManageStudyProductsAction.class, container); + } + + @Override + public ActionURL getManageTreatmentsURL(Container container, boolean useSingleTableEditor) + { + ActionURL url = new ActionURL(ManageTreatmentsAction.class, container); + url.addParameter("singleTable", useSingleTableEditor); + return url; + } + } + + @Nullable + private Study getStudy(Container container) + { + return StudyService.get().getStudy(container); + } + + @ActionNames("manageAssaySchedule, manageAssaySpecimen") + @RequiresPermission(UpdatePermission.class) + public class ManageAssayScheduleAction extends SimpleViewAction + { + @Override + public ModelAndView getView(AssayScheduleForm form, BindException errors) + { + return new JspView<>("/org/labkey/studydesign/view/manageAssaySchedule.jsp", form); + } + + @Override + public void addNavTrail(NavTree root) + { + setHelpTopic("manageAssaySchedule"); + if (getContainer().hasPermission(getUser(), ManageStudyPermission.class)) + root.addChild("Manage Study", PageFlowUtil.urlProvider(StudyUrls.class).getManageStudyURL(getContainer())); + root.addChild("Manage Assay Schedule"); + } + } + + public static class AssayScheduleForm extends ReturnUrlForm + { + private boolean useAlternateLookupFields; + + public boolean isUseAlternateLookupFields() + { + return useAlternateLookupFields; + } + + public void setUseAlternateLookupFields(boolean useAlternateLookupFields) + { + this.useAlternateLookupFields = useAlternateLookupFields; + } + } + + @RequiresPermission(UpdatePermission.class) + public class ManageStudyProductsAction extends SimpleViewAction + { + @Override + public ModelAndView getView(ReturnUrlForm form, BindException errors) + { + return new JspView<>("/org/labkey/studydesign/view/manageStudyProducts.jsp", form); + } + + @Override + public void addNavTrail(NavTree root) + { + setHelpTopic("studyProducts"); + if (getContainer().hasPermission(getUser(), ManageStudyPermission.class)) + root.addChild("Manage Study", PageFlowUtil.urlProvider(StudyUrls.class).getManageStudyURL(getContainer())); + root.addChild("Manage Study Products"); + } + } + + public static class ManageTreatmentsBean extends ReturnUrlForm + { + private boolean _singleTable; + + public boolean isSingleTable() + { + return _singleTable; + } + + public void setSingleTable(boolean singleTable) + { + _singleTable = singleTable; + } + } + + @RequiresPermission(UpdatePermission.class) + public class ManageTreatmentsAction extends SimpleViewAction + { + @Override + public ModelAndView getView(ManageTreatmentsBean form, BindException errors) + { + // if the singleTable param is not explicitly set, do a container check + if (getViewContext().getRequest().getParameter("singleTable") == null) + form.setSingleTable(getContainer().hasActiveModuleByName("viscstudies")); + + return new JspView<>("/org/labkey/studydesign/view/manageTreatments.jsp", form); + } + + @Override + public void addNavTrail(NavTree root) + { + setHelpTopic("manageTreatments"); + if (getContainer().hasPermission(getUser(), ManageStudyPermission.class)) + root.addChild("Manage Study", PageFlowUtil.urlProvider(StudyUrls.class).getManageStudyURL(getContainer())); + root.addChild("Manage Treatments"); + } + } + + @RequiresPermission(ReadPermission.class) + public class GetStudyProducts extends ReadOnlyApiAction + { + private Study _study; + + @Override + public void validateForm(GetStudyProductsForm form, Errors errors) + { + _study = getStudy(getContainer()); + if (_study == null) + errors.reject(SpringActionController.ERROR_MSG, "A study does not exist in this folder"); + } + + @Override + public ApiResponse execute(GetStudyProductsForm form, BindException errors) + { + ApiSimpleResponse resp = new ApiSimpleResponse(); + + List> productList = new ArrayList<>(); + List studyProducts = TreatmentManager.getInstance().getStudyProducts(getContainer(), getUser(), form.getRole(), form.getRowId()); + for (ProductImpl product : studyProducts) + { + // note: we are currently only including the base fields for this extensible table + Map productProperties = product.serialize(); + + List> productAntigenList = new ArrayList<>(); + List studyProductAntigens = TreatmentManager.getInstance().getStudyProductAntigens(getContainer(), getUser(), product.getRowId()); + for (ProductAntigenImpl antigen : studyProductAntigens) + { + // note: we are currently only including the base fields for this extensible table + productAntigenList.add(antigen.serialize()); + } + productProperties.put("Antigens", productAntigenList); + + // get dose and route information associated with this product + List> doseAndRoutes = TreatmentManager.getInstance().getStudyProductsDoseAndRoute(getContainer(), getUser(), product.getRowId()) + .stream() + .map(DoseAndRoute::serialize) + .collect(Collectors.toList()); + productProperties.put("DoseAndRoute", doseAndRoutes); + productList.add(productProperties); + } + + resp.put("success", true); + resp.put("products", productList); + + return resp; + } + } + + public static class GetStudyProductsForm + { + private Integer _rowId; + private String _role; + + public Integer getRowId() + { + return _rowId; + } + + public void setRowId(Integer rowId) + { + _rowId = rowId; + } + + public String getRole() + { + return _role; + } + + public void setRole(String role) + { + _role = role; + } + } + + @RequiresPermission(ReadPermission.class) + public class GetStudyTreatments extends ReadOnlyApiAction + { + private Study _study; + + @Override + public void validateForm(GetStudyTreatmentsForm form, Errors errors) + { + _study = getStudy(getContainer()); + if (_study == null) + errors.reject(SpringActionController.ERROR_MSG, "A study does not exist in this folder"); + } + + @Override + public ApiResponse execute(GetStudyTreatmentsForm form, BindException errors) + { + ApiSimpleResponse resp = new ApiSimpleResponse(); + + List> treatmentList = new ArrayList<>(); + List studyTreatments = TreatmentManager.getInstance().getStudyTreatments(getContainer(), getUser()); + for (TreatmentImpl treatment : studyTreatments) + { + if (form.getTreatmentId() > 0 && form.getTreatmentId() != treatment.getRowId()) + continue; + + Map treatmentProperties = treatment.serialize(); + + List> treatmentProductList = new ArrayList<>(); + List studyTreatmentProducts = TreatmentManager.getInstance().getStudyTreatmentProducts(getContainer(), getUser(), treatment.getRowId(), treatment.getProductSort()); + for (TreatmentProductImpl treatmentProduct : studyTreatmentProducts) + { + // note: we are currently only including the base fields for this extensible table + Map treatmentProductProperties = treatmentProduct.serialize(); + + // add the product label and role for convenience, to prevent the need for another round trip to the server + List products = TreatmentManager.getInstance().getStudyProducts(getContainer(), getUser(), null, treatmentProduct.getProductId()); + if (products.size() == 1) + { + treatmentProductProperties.put("ProductId/Label", products.get(0).getLabel()); + treatmentProductProperties.put("ProductId/Role", products.get(0).getRole()); + } + + treatmentProductList.add(treatmentProductProperties); + } + + if (!form.isSplitByRole()) + { + treatmentProperties.put("Products", treatmentProductList); + } + else + { + Map>> treatmentProductsListByRole = new HashMap<>(); + for (Map productProperties : treatmentProductList) + { + String role = productProperties.get("ProductId/Role").toString(); + if (!treatmentProductsListByRole.containsKey(role)) + treatmentProductsListByRole.put(role, new ArrayList<>()); + + treatmentProductsListByRole.get(role).add(productProperties); + } + + for (Map.Entry>> entry : treatmentProductsListByRole.entrySet()) + treatmentProperties.put(entry.getKey(), entry.getValue()); + } + + treatmentList.add(treatmentProperties); + } + + resp.put("success", true); + resp.put("treatments", treatmentList); + + return resp; + } + } + + private static class GetStudyTreatmentsForm + { + private boolean _splitByRole; + + private int treatmentId; + + public boolean isSplitByRole() + { + return _splitByRole; + } + + public void setSplitByRole(boolean splitByRole) + { + _splitByRole = splitByRole; + } + + public int getTreatmentId() + { + return treatmentId; + } + + public void setTreatmentId(int treatmentId) + { + this.treatmentId = treatmentId; + } + } + + @RequiresPermission(ReadPermission.class) + public class GetStudyTreatmentSchedule extends ReadOnlyApiAction + { + private Study _study; + + @Override + public void validateForm(Object form, Errors errors) + { + _study = getStudy(getContainer()); + if (_study == null) + errors.reject(SpringActionController.ERROR_MSG, "A study does not exist in this folder"); + } + + @Override + public ApiResponse execute(Object form, BindException errors) + { + ApiSimpleResponse resp = new ApiSimpleResponse(); + StudyTreatmentSchedule treatmentSchedule = new StudyTreatmentSchedule(getContainer()); + + // include all cohorts for the study, regardless of it they have associated visits or not + resp.put("cohorts", treatmentSchedule.serializeCohortMapping(_study.getCohorts(getUser()))); + + // include all visits from the study, ordered by visit display order + treatmentSchedule.setVisits(_study.getVisits(Visit.Order.DISPLAY)); + resp.put("visits", treatmentSchedule.serializeVisits()); + + resp.put("success", true); + return resp; + } + } + + @RequiresPermission(UpdatePermission.class) + public class UpdateStudyProductsAction extends MutatingApiAction + { + @Override + public void validateForm(StudyProductsForm form, Errors errors) + { + if (form.getProducts() == null) + errors.reject(SpringActionController.ERROR_MSG, "No study products provided."); + + // label field is required + for (ProductImpl product : form.getProducts()) + { + if (product.getLabel() == null || StringUtils.isEmpty(product.getLabel().trim())) + { + errors.reject(SpringActionController.ERROR_MSG, "Label is a required field for all study products."); + break; + } + } + } + + @Override + public ApiResponse execute(StudyProductsForm form, BindException errors) throws Exception + { + ApiSimpleResponse response = new ApiSimpleResponse(); + Study study = getStudy(getContainer()); + + if (study != null) + { + StudyDesignSchema schema = StudyDesignSchema.getInstance(); + + try (DbScope.Transaction transaction = schema.getSchema().getScope().ensureTransaction()) + { + updateProducts(form.getProducts()); + transaction.commit(); + } + + response.put("success", true); + return response; + } + else + throw new IllegalStateException("A study does not exist in this folder"); + } + + private void updateProducts(List products) throws Exception + { + // insert new study products and update any existing ones + List productRowIds = new ArrayList<>(); + for (ProductImpl product : products) + { + Integer updatedRowId = TreatmentManager.getInstance().saveStudyProduct(getContainer(), getUser(), product); + if (updatedRowId != null) + { + productRowIds.add(updatedRowId); + + updateProductAntigens(updatedRowId, product.getAntigens()); + updateProductDoseAndRoutes(updatedRowId, product.getDoseAndRoutes()); + } + } + + // delete any other study products, not included in the insert/update list, by RowId for this container + for (ProductImpl product : TreatmentManager.getInstance().getFilteredStudyProducts(getContainer(), getUser(), productRowIds)) + TreatmentManager.getInstance().deleteStudyProduct(getContainer(), getUser(), product.getRowId()); + } + + private void updateProductAntigens(int productId, List antigens) throws Exception + { + // insert new study products antigens and update any existing ones + List antigenRowIds = new ArrayList<>(); + for (ProductAntigenImpl antigen : antigens) + { + // make sure the productId is set based on the product rowId + antigen.setProductId(productId); + + Integer updatedRowId = TreatmentManager.getInstance().saveStudyProductAntigen(getContainer(), getUser(), antigen); + if (updatedRowId != null) + antigenRowIds.add(updatedRowId); + } + + // delete any other study products antigens, not included in the insert/update list, for the given productId + for (ProductAntigenImpl antigen : TreatmentManager.getInstance().getFilteredStudyProductAntigens(getContainer(), getUser(), productId, antigenRowIds)) + TreatmentManager.getInstance().deleteStudyProductAntigen(getContainer(), getUser(), antigen.getRowId()); + } + + private void updateProductDoseAndRoutes(int productId, List doseAndRoutes) + { + // get existing dose and routes + Set existingDoseAndRoutes = TreatmentManager.getInstance().getStudyProductsDoseAndRoute(getContainer(), getUser(), productId) + .stream() + .map(DoseAndRoute::getRowId) + .collect(Collectors.toSet()); + + try (DbScope.Transaction transaction = StudyDesignSchema.getInstance().getScope().ensureTransaction()) + { + for (DoseAndRoute doseAndRoute : doseAndRoutes) + { + // dose and route both can't be blank + if (doseAndRoute.getDose() != null || doseAndRoute.getRoute() != null) + { + doseAndRoute.setProductId(productId); + existingDoseAndRoutes.remove(doseAndRoute.getRowId()); + TreatmentManager.getInstance().saveStudyProductDoseAndRoute(getContainer(), getUser(), doseAndRoute); + } + } + + // remove deleted dose and routes + if (!existingDoseAndRoutes.isEmpty()) + { + SimpleFilter filter = new SimpleFilter(); + filter.addInClause(FieldKey.fromParts("RowId"), existingDoseAndRoutes); + Table.delete(StudyDesignSchema.getInstance().getTableInfoDoseAndRoute(), filter); + } + transaction.commit(); + } + } + } + + public static class StudyProductsForm implements ApiJsonForm + { + private List _products; + + public List getProducts() + { + return _products; + } + + public void setProducts(List products) + { + _products = products; + } + + @Override + public void bindJson(JSONObject json) + { + Container container = HttpView.currentContext().getContainer(); + + JSONArray productsJSON = json.optJSONArray("products"); + if (productsJSON != null) + { + _products = new ArrayList<>(); + for (JSONObject product : JsonUtil.toJSONObjectList(productsJSON)) + _products.add(ProductImpl.fromJSON(product, container)); + } + } + } + + @RequiresPermission(UpdatePermission.class) + public class UpdateTreatmentsAction extends MutatingApiAction + { + @Override + public ApiResponse execute(StudyTreatmentSchedule form, BindException errors) throws Exception + { + ApiSimpleResponse response = new ApiSimpleResponse(); + Study study = getStudy(getContainer()); + + if (study != null) + { + StudyDesignSchema schema = StudyDesignSchema.getInstance(); + List treatmentIds; + try (DbScope.Transaction transaction = schema.getSchema().getScope().ensureTransaction()) + { + treatmentIds = updateTreatments(form.getTreatments()); + transaction.commit(); + } + + response.put("success", true); + response.put("treatmentIds", treatmentIds); + return response; + } + else + throw new IllegalStateException("A study does not exist in this folder"); + } + + private Integer getExistingTreatmentId(TreatmentImpl treatment) + { + if (treatment == null) + return -1; + List studyTreatments = TreatmentManager.getInstance().getStudyTreatments(getContainer(), getUser()); + for (TreatmentImpl existingTreatment : studyTreatments) + { + List studyTreatmentProducts = TreatmentManager.getInstance().getStudyTreatmentProducts(getContainer(), getUser(), existingTreatment.getRowId(), existingTreatment.getProductSort()); + for (TreatmentProductImpl product : studyTreatmentProducts) + { + product.serialize(); + } + existingTreatment.setTreatmentProducts(studyTreatmentProducts); + if (treatment.isSameTreatmentProductsWith(existingTreatment)) + return existingTreatment.getRowId(); + } + return -1; + } + + private List updateTreatments(List treatments) throws Exception + { + List updatedRowIds = new ArrayList<>(); + for (TreatmentImpl treatment : treatments) + { + Integer updatedRowId = getExistingTreatmentId(treatment); + if (updatedRowId == null || updatedRowId <= 0) + { + updatedRowId = TreatmentManager.getInstance().saveTreatment(getContainer(), getUser(), treatment); + if (updatedRowId != null) + { + TreatmentManager.getInstance().updateTreatmentProducts(updatedRowId, treatment.getTreatmentProducts(), getContainer(), getUser()); + } + } + updatedRowIds.add(updatedRowId); + } + return updatedRowIds; + } + } + + @RequiresPermission(UpdatePermission.class) + public class UpdateTreatmentScheduleAction extends MutatingApiAction + { + private Map _tempTreatmentIdMap = new HashMap<>(); + private Set usedTreatmentIds = new HashSet<>(); // treatmentIds referenced in single table Treatment Schedule UI + private List treatmentRowIds = new ArrayList<>(); // treatmentIds defined in 2 table UI's Treatment section + private List cohortRowIds = new ArrayList<>(); + + @Override + public void validateForm(StudyTreatmentSchedule form, Errors errors) + { + // validate that each treatment has a label + for (TreatmentImpl treatment : form.getTreatments()) + { + if (treatment.getLabel() == null || StringUtils.isEmpty(treatment.getLabel().trim())) + errors.reject(SpringActionController.ERROR_MSG, "Label is a required field for all treatments."); + + // validate that each treatment product mapping has a selected product + for (TreatmentProductImpl treatmentProduct : treatment.getTreatmentProducts()) + { + if (treatmentProduct.getProductId() <= 0) + errors.reject(SpringActionController.ERROR_MSG, "Each treatment product must have a selected study product."); + } + } + + // validate that each cohort has a label, is unique, and has a valid subject count value + for (StudyDesignCohort cohort : form.getCohorts()) + { + if (cohort.getLabel() == null || StringUtils.isEmpty(cohort.getLabel().trim())) + errors.reject(SpringActionController.ERROR_MSG, "Label is a required field for all cohorts."); + + Cohort cohortByLabel = CohortService.get().getCohortByLabel(getContainer(), getUser(), cohort.getLabel()); + if (cohort.getRowId() > 0) + { + Cohort cohortByRowId = CohortService.get().getCohortForRowId(getContainer(), getUser(), cohort.getRowId()); + if (cohortByRowId != null && cohortByLabel != null && cohortByRowId.getRowId() != cohortByLabel.getRowId()) + errors.reject(SpringActionController.ERROR_MSG, "A cohort with the label '" + cohort.getLabel() + "' already exists in this study."); + } + else if (cohortByLabel != null) + { + errors.reject(SpringActionController.ERROR_MSG, "A cohort with the label '" + cohort.getLabel() + "' already exists in this study."); + } + + if (cohort.getSubjectCount() != null) + { + if (cohort.getSubjectCount() < 0) + errors.reject(SpringActionController.ERROR_MSG, "Cohort subject count values must be a positive integer."); + if (cohort.getSubjectCount() == Integer.MAX_VALUE) + errors.reject(SpringActionController.ERROR_MSG, "Cohort subject count value larger than the max value allowed."); + } + } + } + + @Override + public ApiResponse execute(StudyTreatmentSchedule form, BindException errors) throws Exception + { + ApiSimpleResponse response = new ApiSimpleResponse(); + Study study = getStudy(getContainer()); + + if (study != null) + { + StudyDesignSchema schema = StudyDesignSchema.getInstance(); + + try (DbScope.Transaction transaction = schema.getSchema().getScope().ensureTransaction()) + { + updateTreatments(form.getTreatments()); + updateCohorts(form.getCohorts(), study); + cleanTreatments(); + cleanCohorts(); + transaction.commit(); + } + + response.put("success", true); + return response; + } + else + throw new IllegalStateException("A study does not exist in this folder"); + } + + private void cleanCohorts() throws ValidationException + { + // delete any other study cohorts, not included in the insert/update list, by RowId for this container + for (Cohort existingCohort : CohortService.get().getCohorts(getContainer(), getUser())) + { + if (!cohortRowIds.contains(existingCohort.getRowId())) + { + if (!existingCohort.isInUse()) + CohortService.get().deleteCohort(existingCohort); + else + throw new ValidationException("Unable to delete in-use cohort: " + existingCohort.getLabel()); + } + } + + } + + private void cleanTreatments() + { + // delete any other study treatments, not included in the insert/update list, by RowId for this container + for (TreatmentImpl treatment : TreatmentManager.getInstance().getFilteredTreatments(getContainer(), getUser(), treatmentRowIds, usedTreatmentIds)) + TreatmentManager.getInstance().deleteTreatment(getContainer(), getUser(), treatment.getRowId()); + + } + + private void updateTreatments(List treatments) throws Exception + { + // insert new study treatments and update any existing ones + + for (TreatmentImpl treatment : treatments) + { + Integer updatedRowId = TreatmentManager.getInstance().saveTreatment(getContainer(), getUser(), treatment); + if (updatedRowId != null) + { + treatmentRowIds.add(updatedRowId); + + if (treatment.getTempRowId() != null) + _tempTreatmentIdMap.put(treatment.getTempRowId(), updatedRowId); + + TreatmentManager.getInstance().updateTreatmentProducts(updatedRowId, treatment.getTreatmentProducts(), getContainer(), getUser()); + } + } + } + + private void updateCohorts(Collection cohorts, Study study) throws ValidationException + { + // insert new cohorts and update any existing ones + for (StudyDesignCohort cohort : cohorts) + { + int rowId = cohort.getRowId(); + if (rowId > 0) + { + CohortService.get().updateCohort(getContainer(), getUser(), rowId, cohort.getLabel(), cohort.getSubjectCount()); + cohortRowIds.add(rowId); + } + else + { + Cohort newCohort = CohortService.get().createCohort(study, getUser(), cohort.getLabel(), true, cohort.getSubjectCount(), null); + cohortRowIds.add(newCohort.getRowId()); + rowId = newCohort.getRowId(); + } + + updateTreatmentVisitMap(rowId, cohort.getTreatmentVisitMap()); + } + } + + private void updateTreatmentVisitMap(int cohortId, List treatmentVisitMaps) + { + for (TreatmentVisitMapImpl visitMap : treatmentVisitMaps) + { + usedTreatmentIds.add(visitMap.getTreatmentId()); + } + + // the mapping that is passed in will have all of the current treatment/visit maps, so we will compare + // this set with the set from the DB and if they are different, replace all + List existingVisitMaps = TreatmentManager.getInstance().getStudyTreatmentVisitMap(getContainer(), cohortId); + boolean visitMapsDiffer = existingVisitMaps.size() != treatmentVisitMaps.size(); + if (!visitMapsDiffer) + { + for (TreatmentVisitMapImpl newVisitMap : treatmentVisitMaps) + { + newVisitMap.setContainer(getContainer()); + newVisitMap.setCohortId(cohortId); + + if (!existingVisitMaps.contains(newVisitMap)) + { + visitMapsDiffer = true; + break; + } + } + } + + // if we have differences, replace all at this point + if (visitMapsDiffer) + { + TreatmentManager.getInstance().deleteTreatmentVisitMapForCohort(getContainer(), cohortId); + for (TreatmentVisitMapImpl newVisitMap : treatmentVisitMaps) + { + // if the treatmentId used here was from a treatment that was created as part of this transaction, + // lookup the new treatment record RowId from the tempRowId + if (newVisitMap.getTempTreatmentId() != null && _tempTreatmentIdMap.containsKey(newVisitMap.getTempTreatmentId())) + newVisitMap.setTreatmentId(_tempTreatmentIdMap.get(newVisitMap.getTempTreatmentId())); + + if (cohortId > 0 && newVisitMap.getVisitId() > 0 && newVisitMap.getTreatmentId() > 0) + TreatmentManager.getInstance().insertTreatmentVisitMap(getUser(), getContainer(), cohortId, newVisitMap.getVisitId(), newVisitMap.getTreatmentId()); + } + } + } + } + + @RequiresPermission(UpdatePermission.class) + public class UpdateAssayPlanAction extends MutatingApiAction + { + @Override + public ApiResponse execute(AssayPlanForm form, BindException errors) + { + ApiSimpleResponse response = new ApiSimpleResponse(); + Study study = StudyService.get().getStudy(getContainer()); + if (study != null) + { + updateAssayPlan(getUser(), study, form.getAssayPlan()); + response.put("success", true); + } + else + { + response.put("success", false); + } + + return response; + } + } + + private static class AssayPlanForm + { + private String _assayPlan; + + public AssayPlanForm() + {} + + public String getAssayPlan() + { + return _assayPlan; + } + + public void setAssayPlan(String assayPlan) + { + _assayPlan = assayPlan; + } + } + + @RequiresPermission(UpdatePermission.class) + public class UpdateAssayScheduleAction extends MutatingApiAction + { + @Override + public void validateForm(StudyAssaySchedule form, Errors errors) + { + // validate that each assay configuration has an AssayName + for (AssaySpecimenConfigImpl assay : form.getAssays()) + { + if (assay.getAssayName() == null || StringUtils.isEmpty(assay.getAssayName().trim())) + errors.reject(SpringActionController.ERROR_MSG, "Assay Name is a required field for all assay configurations."); + + if (assay.getSampleQuantity() != null && assay.getSampleQuantity() < 0) + errors.reject(SpringActionController.ERROR_MSG, "Assay sample quantity value must be a positive number."); + } + } + + @Override + public ApiResponse execute(StudyAssaySchedule form, BindException errors) throws Exception + { + ApiSimpleResponse response = new ApiSimpleResponse(); + Study study = getStudy(getContainer()); + + if (study != null) + { + StudyDesignSchema schema = StudyDesignSchema.getInstance(); + + try (DbScope.Transaction transaction = schema.getSchema().getScope().ensureTransaction()) + { + updateAssays(form.getAssays()); + updateAssayPlan(getUser(), study, form.getAssayPlan()); + transaction.commit(); + } + + response.put("success", true); + return response; + } + else + throw new IllegalStateException("A study does not exist in this folder"); + } + + private void updateAssays(List assays) throws Exception + { + // insert new assaySpecimens and update any existing ones + List assaySpecimenRowIds = new ArrayList<>(); + for (AssaySpecimenConfigImpl assay : assays) + { + Integer updatedRowId = TreatmentManager.getInstance().saveAssaySpecimen(getContainer(), getUser(), assay); + if (updatedRowId != null) + { + assaySpecimenRowIds.add(updatedRowId); + + updateAssayVisitMap(updatedRowId, assay.getAssayVisitMap()); + } + } + + // delete any other assaySpecimens, not included in the insert/update list, by RowId for this container + for (AssaySpecimenConfigImpl assaySpecimen : TreatmentManager.getInstance().getFilteredAssaySpecimens(getContainer(), assaySpecimenRowIds)) + TreatmentManager.getInstance().deleteAssaySpecimen(getContainer(), getUser(), assaySpecimen.getRowId()); + } + + private void updateAssayVisitMap(int assaySpecimenId, List assayVisitMaps) throws Exception + { + List assaySpecimenVisitIds = new ArrayList<>(); + if (assayVisitMaps != null && !assayVisitMaps.isEmpty()) + { + for (AssaySpecimenVisitImpl assaySpecimenVisit : assayVisitMaps) + { + assaySpecimenVisit.setAssaySpecimenId(assaySpecimenId); + + Integer updatedRowId = TreatmentManager.getInstance().saveAssaySpecimenVisit(getContainer(), getUser(), assaySpecimenVisit); + assaySpecimenVisitIds.add(updatedRowId); + } + } + + // delete any other assaySpecimenVisits, not included in the insert/update list, by RowId for this container and assaySpecimenId + for (AssaySpecimenVisitImpl assaySpecimenVisit : TreatmentManager.getInstance().getFilteredAssaySpecimenVisits(getContainer(), assaySpecimenId, assaySpecimenVisitIds)) + TreatmentManager.getInstance().deleteAssaySpecimenVisit(getContainer(), getUser(), assaySpecimenVisit.getRowId()); + } + } + + private void updateAssayPlan(User user, Study study, String assayPlan) + { + if (study != null) + { + StudyService.get().updateAssayPlan(user, study, assayPlan); + } + } +} \ No newline at end of file diff --git a/studydesign/src/org/labkey/studydesign/StudyDesignModule.java b/studydesign/src/org/labkey/studydesign/StudyDesignModule.java new file mode 100644 index 00000000..eff82328 --- /dev/null +++ b/studydesign/src/org/labkey/studydesign/StudyDesignModule.java @@ -0,0 +1,83 @@ +package org.labkey.studydesign; + +import org.apache.logging.log4j.Logger; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.labkey.api.module.ModuleContext; +import org.labkey.api.module.SpringModule; +import org.labkey.api.services.ServiceRegistry; +import org.labkey.api.studydesign.StudyDesignService; +import org.labkey.api.util.logging.LogHelper; +import org.labkey.api.view.WebPartFactory; +import org.labkey.studydesign.model.TreatmentManager; +import org.labkey.studydesign.view.AssayScheduleWebpartFactory; +import org.labkey.studydesign.view.ImmunizationScheduleWebpartFactory; +import org.labkey.studydesign.view.VaccineDesignWebpartFactory; + +import java.util.Collection; +import java.util.List; +import java.util.Set; + +public class StudyDesignModule extends SpringModule +{ + private static final Logger LOG = LogHelper.getLogger(StudyDesignModule.class, "Study Design"); + public static final String NAME = "StudyDesign"; + + @Override + public String getName() + { + return NAME; + } + + @Override + @NotNull + protected Collection createWebPartFactories() + { + return List.of( + new AssayScheduleWebpartFactory(), + new ImmunizationScheduleWebpartFactory(), + new VaccineDesignWebpartFactory() + ); + } + + @Override + public @Nullable Double getSchemaVersion() + { + return null; + } + + @Override + public boolean hasScripts() + { + return false; + } + + @Override + protected void init() + { + addController("study-design", StudyDesignController.class); + + ServiceRegistry.get().registerService(StudyDesignService.class, new StudyDesignServiceImpl()); + } + + @Override + protected void startupAfterSpringConfig(ModuleContext moduleContext) + { + } + + @Override + public @NotNull Collection getSchemaNames() + { + return Set.of(); + } + + @Override + @NotNull + public Set getIntegrationTests() + { + return Set.of( + TreatmentManager.TreatmentDataTestCase.class, + TreatmentManager.AssayScheduleTestCase.class + ); + } +} \ No newline at end of file diff --git a/studydesign/src/org/labkey/studydesign/StudyDesignServiceImpl.java b/studydesign/src/org/labkey/studydesign/StudyDesignServiceImpl.java new file mode 100644 index 00000000..151a0d71 --- /dev/null +++ b/studydesign/src/org/labkey/studydesign/StudyDesignServiceImpl.java @@ -0,0 +1,68 @@ +package org.labkey.studydesign; + +import org.labkey.api.data.Container; +import org.labkey.api.data.SimpleFilter; +import org.labkey.api.data.Table; +import org.labkey.api.data.TableSelector; +import org.labkey.api.query.FieldKey; +import org.labkey.api.security.User; +import org.labkey.api.study.AssaySpecimenConfig; +import org.labkey.api.study.Product; +import org.labkey.api.study.Treatment; +import org.labkey.api.study.Visit; +import org.labkey.api.studydesign.StudyDesignService; +import org.labkey.api.studydesign.query.StudyDesignSchema; +import org.labkey.studydesign.model.AssaySpecimenConfigImpl; +import org.labkey.studydesign.model.TreatmentManager; + +import java.util.Collection; +import java.util.List; + +public class StudyDesignServiceImpl implements StudyDesignService +{ + @Override + public List getStudyProducts(Container c, User user, String role) + { + return TreatmentManager.getInstance().getStudyProducts(c, user, role, null); + } + + @Override + public List getStudyTreatments(Container c, User user) + { + return TreatmentManager.getInstance().getStudyTreatments(c, user); + } + + @Override + public List getVisitsForTreatmentSchedule(Container c) + { + return TreatmentManager.getInstance().getVisitsForTreatmentSchedule(c); + } + + @Override + public Collection getAssaySpecimenConfigs(Container c) + { + return new TableSelector( + StudyDesignSchema.getInstance().getTableInfoAssaySpecimen(), + SimpleFilter.createContainerFilter(c), null).getCollection(AssaySpecimenConfigImpl.class); + } + + @Override + public void deleteTreatmentVisitMapForCohort(Container container, Integer cohortId) + { + TreatmentManager.getInstance().deleteTreatmentVisitMapForCohort(container, cohortId); + } + + @Override + public void deleteTreatmentVisitMapForVisit(Container container, Integer visitId) + { + TreatmentManager.getInstance().deleteTreatmentVisitMapForVisit(container, visitId); + } + + @Override + public void deleteAssaySpecimenVisits(Container container, int visitId) + { + SimpleFilter filter = SimpleFilter.createContainerFilter(container); + filter.addCondition(FieldKey.fromParts("VisitId"), visitId); + Table.delete(StudyDesignSchema.getInstance().getTableInfoAssaySpecimenVisit(), filter); + } +} diff --git a/studydesign/src/org/labkey/studydesign/model/AssaySpecimenConfigImpl.java b/studydesign/src/org/labkey/studydesign/model/AssaySpecimenConfigImpl.java new file mode 100644 index 00000000..448352a0 --- /dev/null +++ b/studydesign/src/org/labkey/studydesign/model/AssaySpecimenConfigImpl.java @@ -0,0 +1,302 @@ +/* + * Copyright (c) 2013-2017 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.labkey.studydesign.model; + +import org.apache.commons.lang3.StringUtils; +import org.jetbrains.annotations.NotNull; +import org.json.JSONArray; +import org.json.JSONObject; +import org.labkey.api.data.Container; +import org.labkey.api.study.AssaySpecimenConfig; +import org.labkey.api.util.JsonUtil; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/** + * Represents an assay/specimen configuration for a study + */ +public class AssaySpecimenConfigImpl implements AssaySpecimenConfig +{ + private Container _container; + private int _rowId; + private String _assayName; + private Integer _dataset; + private String _description; + private String _source; + private Integer _locationId; + private Integer _primaryTypeId; + private Integer _derivativeTypeId; + private String _tubeType; + private String _lab; + private String _sampleType; + private Double _sampleQuantity; + private String _sampleUnits; + private List _assayVisitMap; + + public AssaySpecimenConfigImpl() + { + } + + public AssaySpecimenConfigImpl(Container container, String assayName, String description) + { + _container = container; + _assayName = assayName; + _description = description; + } + + public boolean isNew() + { + return _rowId == 0; + } + + public Container getContainer() + { + return _container; + } + + public void setContainer(Container container) + { + _container = container; + } + + @Override + public int getRowId() + { + return _rowId; + } + + public void setRowId(int rowId) + { + _rowId = rowId; + } + + @Override + public String getAssayName() + { + return _assayName; + } + + public void setAssayName(String assayName) + { + _assayName = assayName; + } + + @Override + public Integer getDataset() + { + return _dataset; + } + + public void setDataset(Integer dataset) + { + _dataset = dataset; + } + + @Override + public String getDescription() + { + return _description; + } + + public void setDescription(String description) + { + _description = description; + } + + @Override + public String getSource() + { + return _source; + } + + public void setSource(String source) + { + _source = source; + } + + @Override + public Integer getLocationId() + { + return _locationId; + } + + public void setLocationId(Integer locationId) + { + _locationId = locationId; + } + + @Override + public Integer getPrimaryTypeId() + { + return _primaryTypeId; + } + + public void setPrimaryTypeId(Integer primaryTypeId) + { + _primaryTypeId = primaryTypeId; + } + + @Override + public Integer getDerivativeTypeId() + { + return _derivativeTypeId; + } + + public void setDerivativeTypeId(Integer derivativeTypeId) + { + _derivativeTypeId = derivativeTypeId; + } + + @Override + public String getTubeType() + { + return _tubeType; + } + + public void setTubeType(String tubeType) + { + _tubeType = tubeType; + } + + public String getLab() + { + return _lab; + } + + public void setLab(String lab) + { + _lab = lab; + } + + public String getSampleType() + { + return _sampleType; + } + + public void setSampleType(String sampleType) + { + _sampleType = sampleType; + } + + public Double getSampleQuantity() + { + return _sampleQuantity; + } + + public void setSampleQuantity(Double sampleQuantity) + { + _sampleQuantity = sampleQuantity; + } + + public String getSampleUnits() + { + return _sampleUnits; + } + + public void setSampleUnits(String sampleUnits) + { + _sampleUnits = sampleUnits; + } + + public void setAssayVisitMap(List assayVisitMap) + { + _assayVisitMap = assayVisitMap; + } + + public List getAssayVisitMap() + { + return _assayVisitMap; + } + + public Map serialize() + { + Map props = new HashMap<>(); + props.put("RowId", getRowId()); + props.put("AssayName", getAssayName()); + props.put("DataSet", getDataset()); + props.put("Description", getDescription()); + props.put("LocationId", getLocationId()); + props.put("Source", getSource()); + props.put("TubeType", getTubeType()); + props.put("PrimaryTypeId", getPrimaryTypeId()); + props.put("DerivativeTypeId", getDerivativeTypeId()); + props.put("Lab", getLab()); + props.put("SampleType", getSampleType()); + props.put("SampleQuantity", getSampleQuantity()); + props.put("SampleUnits", getSampleUnits()); + props.put("Container", getContainer().getId()); + return props; + } + + public static AssaySpecimenConfigImpl fromJSON(@NotNull JSONObject o, Container container) + { + AssaySpecimenConfigImpl assay = new AssaySpecimenConfigImpl(container, o.getString("AssayName"), o.getString("Description")); + + if (o.has("RowId")) + assay.setRowId(o.getInt("RowId")); + if (o.has("DataSet") && o.get("DataSet") instanceof Integer && o.getInt("DataSet") > 0) + assay.setDataset(o.getInt("DataSet")); + if (o.has("Source") && !StringUtils.isEmpty(o.getString("Source"))) + assay.setSource(o.getString("Source")); + if (o.has("LocationId") && o.get("LocationId") instanceof Integer && o.getInt("LocationId") > 0) + assay.setLocationId(o.getInt("LocationId")); + if (o.has("TubeType") && !StringUtils.isEmpty(o.getString("TubeType"))) + assay.setTubeType(o.getString("TubeType")); + if (o.has("Lab") && !StringUtils.isEmpty(o.getString("Lab"))) + assay.setLab(o.getString("Lab")); + if (o.has("SampleType") && !StringUtils.isEmpty(o.getString("SampleType"))) + assay.setSampleType(o.getString("SampleType")); + if (o.has("SampleQuantity") && (o.get("SampleQuantity") instanceof Integer || o.get("SampleQuantity") instanceof Double) && o.getDouble("SampleQuantity") > 0) + assay.setSampleQuantity(o.getDouble("SampleQuantity")); + if (o.has("SampleUnits") && !StringUtils.isEmpty(o.getString("SampleUnits"))) + assay.setSampleUnits(o.getString("SampleUnits")); + if (o.has("PrimaryTypeId") && o.get("PrimaryTypeId") instanceof Integer) + assay.setPrimaryTypeId(o.getInt("PrimaryTypeId")); + if (o.has("DerivativeTypeId") && o.get("DerivativeTypeId") instanceof Integer) + assay.setDerivativeTypeId(o.getInt("DerivativeTypeId")); + + JSONArray visitMapJSON = o.optJSONArray("VisitMap"); + if (visitMapJSON != null) + { + List assayVisitMap = new ArrayList<>(); + for (JSONObject assaySpecimen : JsonUtil.toJSONObjectList(visitMapJSON)) + assayVisitMap.add(AssaySpecimenVisitImpl.fromJSON(assaySpecimen, container)); + + assay.setAssayVisitMap(assayVisitMap); + } + + return assay; + } + + @Override + public boolean equals(Object o) + { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + AssaySpecimenConfigImpl that = (AssaySpecimenConfigImpl) o; + return _rowId == that._rowId && Objects.equals(_assayName, that._assayName) && Objects.equals(_dataset, that._dataset) && Objects.equals(_description, that._description) && Objects.equals(_source, that._source) && Objects.equals(_locationId, that._locationId) && Objects.equals(_primaryTypeId, that._primaryTypeId) && Objects.equals(_derivativeTypeId, that._derivativeTypeId) && Objects.equals(_tubeType, that._tubeType) && Objects.equals(_lab, that._lab) && Objects.equals(_sampleType, that._sampleType) && Objects.equals(_sampleQuantity, that._sampleQuantity) && Objects.equals(_sampleUnits, that._sampleUnits); + } + + @Override + public int hashCode() + { + return Objects.hash(_rowId, _assayName, _dataset, _description, _source, _locationId, _primaryTypeId, _derivativeTypeId, _tubeType, _lab, _sampleType, _sampleQuantity, _sampleUnits); + } +} diff --git a/studydesign/src/org/labkey/studydesign/model/AssaySpecimenVisitImpl.java b/studydesign/src/org/labkey/studydesign/model/AssaySpecimenVisitImpl.java new file mode 100644 index 00000000..4c8eee28 --- /dev/null +++ b/studydesign/src/org/labkey/studydesign/model/AssaySpecimenVisitImpl.java @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2014-2016 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.labkey.studydesign.model; + +import org.jetbrains.annotations.NotNull; +import org.json.JSONObject; +import org.labkey.api.data.Container; +import org.labkey.api.study.AssaySpecimenVisit; + +import java.util.HashMap; +import java.util.Map; + +public class AssaySpecimenVisitImpl implements AssaySpecimenVisit +{ + private int _assaySpecimenId; + private int _visitId; + private int _rowId; + private Container _container; + + public AssaySpecimenVisitImpl() + { + } + + public AssaySpecimenVisitImpl(Container container, int assaySpecimenId, int visitId) + { + _container = container; + _assaySpecimenId = assaySpecimenId; + _visitId = visitId; + } + + public boolean isNew() + { + return _rowId == 0; + } + + @Override + public int getAssaySpecimenId() + { + return _assaySpecimenId; + } + + public void setAssaySpecimenId(int assaySpecimenId) + { + _assaySpecimenId = assaySpecimenId; + } + + @Override + public int getVisitId() + { + return _visitId; + } + + public void setVisitId(int visitId) + { + _visitId = visitId; + } + + public Container getContainer() + { + return _container; + } + + public void setContainer(Container container) + { + _container = container; + } + + public int getRowId() + { + return _rowId; + } + + public void setRowId(int rowId) + { + _rowId = rowId; + } + + public Map serialize() + { + Map props = new HashMap<>(); + props.put("RowId", getRowId()); + props.put("VisitId", getVisitId()); + props.put("AssaySpecimenId", getAssaySpecimenId()); + props.put("Container", getContainer().getId()); + return props; + } + + public static AssaySpecimenVisitImpl fromJSON(@NotNull JSONObject o, Container container) + { + // AssaySpecimenId may not be specified in JSON + int assaySpecimenId = o.has("AssaySpecimenId") ? o.getInt("AssaySpecimenId") : 0; + AssaySpecimenVisitImpl assaySpecimenVisit = new AssaySpecimenVisitImpl(container, assaySpecimenId, o.getInt("VisitId")); + + if (o.has("RowId")) + assaySpecimenVisit.setRowId(o.getInt("RowId")); + + return assaySpecimenVisit; + } +} diff --git a/studydesign/src/org/labkey/studydesign/model/DoseAndRoute.java b/studydesign/src/org/labkey/studydesign/model/DoseAndRoute.java new file mode 100644 index 00000000..838d6007 --- /dev/null +++ b/studydesign/src/org/labkey/studydesign/model/DoseAndRoute.java @@ -0,0 +1,169 @@ +/* + * Copyright (c) 2016 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.labkey.studydesign.model; + +import org.apache.commons.lang3.StringUtils; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.json.JSONObject; +import org.labkey.api.data.Container; +import org.labkey.api.util.Pair; + +import java.util.HashMap; +import java.util.Map; + +/** + * Created by klum on 9/23/2016. + */ +public class DoseAndRoute +{ + private Integer _rowId; + private String _dose; + private String _route; + private int _productId; + private Container _container; + + public enum keys + { + RowId, + Dose, + Route, + ProductId + } + + public boolean isNew() + { + return _rowId == null; + } + + public Integer getRowId() + { + return _rowId; + } + + public void setRowId(Integer rowId) + { + _rowId = rowId; + } + + public DoseAndRoute(){} + + public DoseAndRoute(String dose, String route, int productId, Container container) + { + _dose = dose; + _route = route; + _productId = productId; + _container = container; + } + + public @Nullable String getLabel() + { + if (_dose != null || _route != null) + return String.format("%s : %s", StringUtils.trimToEmpty(_dose), StringUtils.trimToEmpty(_route)); + else + return null; + } + + public String getDose() + { + return StringUtils.trimToNull(_dose); + } + + public void setDose(String dose) + { + _dose = dose; + } + + public String getRoute() + { + return StringUtils.trimToNull(_route); + } + + public void setRoute(String route) + { + _route = route; + } + + public int getProductId() + { + return _productId; + } + + public void setProductId(int productId) + { + _productId = productId; + } + + public Container getContainer() + { + return _container; + } + + public void setContainer(Container container) + { + _container = container; + } + + public static DoseAndRoute fromJSON(@NotNull JSONObject o, Container container, int productId) + { + String dose = null; + String route = null; + if (o.has(keys.Dose.name())) + dose = String.valueOf(o.get(keys.Dose.name())); + if (o.has(keys.Route.name())) + route = String.valueOf(o.get(keys.Route.name())); + + DoseAndRoute doseAndRoute = new DoseAndRoute(dose, route, productId, container); + if (o.has(keys.RowId.name())) + doseAndRoute.setRowId(o.getInt(keys.RowId.name())); + return doseAndRoute; + } + + public Map serialize() + { + Map props = new HashMap<>(); + props.put(keys.RowId.name(), getRowId()); + props.put(keys.ProductId.name(), getProductId()); + props.put(keys.Dose.name(), getDose()); + props.put(keys.Route.name(), getRoute()); + + return props; + } + + /** + * Helper to convert the concatenated label into a dose and/or route portion + * @return Pair object where the key is the dose and the value is the route + */ + public static @Nullable + Pair parseFromLabel(String label) + { + // need to keep the label generation in sync with code in DoseAndRouteTable label expr column + if (label != null) + { + if (label.contains(":")) + { + String[] parts = label.split(":"); + if (parts.length == 2) + { + return new Pair<>( + StringUtils.trimToNull(parts[0]), + StringUtils.trimToNull(parts[1])); + } + } + } + return null; + } +} diff --git a/studydesign/src/org/labkey/studydesign/model/ProductAntigenImpl.java b/studydesign/src/org/labkey/studydesign/model/ProductAntigenImpl.java new file mode 100644 index 00000000..9c10e2a7 --- /dev/null +++ b/studydesign/src/org/labkey/studydesign/model/ProductAntigenImpl.java @@ -0,0 +1,171 @@ +/* + * Copyright (c) 2013-2016 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.labkey.studydesign.model; + +import org.jetbrains.annotations.NotNull; +import org.json.JSONObject; +import org.labkey.api.data.Container; +import org.labkey.api.study.ProductAntigen; + +import java.util.HashMap; +import java.util.Map; + +/** + * User: cnathe + * Date: 12/26/13 + */ +public class ProductAntigenImpl implements ProductAntigen +{ + private Container _container; + private int _rowId; + private int _productId; + private String _gene; + private String _subType; + private String _genBankId; + private String _sequence; + + public ProductAntigenImpl() + {} + + public ProductAntigenImpl(Container container, int productId, String gene, String subType) + { + _container = container; + _productId = productId; + _gene = gene; + _subType = subType; + } + + public ProductAntigenImpl(Container container, String gene, String subType, String genBankId, String sequence) + { + this(container, 0, gene, subType); + _genBankId = genBankId; + _sequence = sequence; + } + + public boolean isNew() + { + return _rowId == 0; + } + + public Object getPrimaryKey() + { + return getRowId(); + } + + @Override + public int getRowId() + { + return _rowId; + } + + public void setRowId(int rowId) + { + _rowId = rowId; + } + + @Override + public int getProductId() + { + return _productId; + } + + public void setProductId(int productId) + { + _productId = productId; + } + + @Override + public String getGene() + { + return _gene; + } + + public void setGene(String gene) + { + _gene = gene; + } + + @Override + public String getSubType() + { + return _subType; + } + + public void setSubType(String subType) + { + _subType = subType; + } + + @Override + public String getGenBankId() + { + return _genBankId; + } + + public void setGenBankId(String genBankId) + { + _genBankId = genBankId; + } + + @Override + public String getSequence() + { + return _sequence; + } + + public void setSequence(String sequence) + { + _sequence = sequence; + } + + public Container getContainer() + { + return _container; + } + + public void setContainer(Container container) + { + _container = container; + } + + public Map serialize() + { + Map props = new HashMap<>(); + props.put("RowId", getRowId()); + props.put("ProductId", getProductId()); + props.put("Gene", getGene()); + props.put("SubType", getSubType()); + props.put("GenBankId", getGenBankId()); + props.put("Sequence", getSequence()); + return props; + } + + public static ProductAntigenImpl fromJSON(@NotNull JSONObject o, Container container) + { + ProductAntigenImpl antigen = new ProductAntigenImpl( + container, o.getString("Gene"), o.getString("SubType"), + o.getString("GenBankId"), o.getString("Sequence") + ); + + if (o.has("RowId")) + antigen.setRowId(o.getInt("RowId")); + + if (o.has("ProductId")) + antigen.setProductId(o.getInt("ProductId")); + + return antigen; + } +} diff --git a/studydesign/src/org/labkey/studydesign/model/ProductImpl.java b/studydesign/src/org/labkey/studydesign/model/ProductImpl.java new file mode 100644 index 00000000..e81aec33 --- /dev/null +++ b/studydesign/src/org/labkey/studydesign/model/ProductImpl.java @@ -0,0 +1,205 @@ +/* + * Copyright (c) 2013-2016 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.labkey.studydesign.model; + +import org.jetbrains.annotations.NotNull; +import org.json.JSONArray; +import org.json.JSONObject; +import org.labkey.api.data.Container; +import org.labkey.api.study.Product; +import org.labkey.api.util.JsonUtil; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * User: cnathe + * Date: 12/26/13 + */ +public class ProductImpl implements Product +{ + private Container _container; + private int _rowId; + private String _label; + private String _role; + private String _type; + private List _antigens; + private List _doseAndRoutes; + + // from TreatmentProductMap (not serialized with product) + private String _dose; + private String _route; + + public ProductImpl() + {} + + public ProductImpl(Container container, String label, String role) + { + _container = container; + _label = label; + _role = role; + } + + public ProductImpl(Container container, String label, String role, String type) + { + this(container, label, role); + _type = type; + } + + public boolean isNew() + { + return _rowId == 0; + } + + public Object getPrimaryKey() + { + return getRowId(); + } + + @Override + public int getRowId() + { + return _rowId; + } + + public void setRowId(int rowId) + { + _rowId = rowId; + } + + @Override + public String getLabel() + { + return _label; + } + + public void setLabel(String label) + { + _label = label; + } + + @Override + public String getRole() + { + return _role; + } + + public void setRole(String role) + { + _role = role; + } + + @Override + public String getType() + { + return _type; + } + + public void setType(String type) + { + _type = type; + } + + public List getAntigens() + { + return _antigens; + } + + public void setAntigens(List antigens) + { + _antigens = antigens; + } + + public String getDose() + { + return _dose; + } + + public void setDose(String dose) + { + _dose = dose; + } + + public String getRoute() + { + return _route; + } + + public void setRoute(String route) + { + _route = route; + } + + public Container getContainer() + { + return _container; + } + + public void setContainer(Container container) + { + _container = container; + } + + public List getDoseAndRoutes() + { + return _doseAndRoutes; + } + + public void setDoseAndRoutes(List doseAndRoutes) + { + _doseAndRoutes = doseAndRoutes; + } + + public Map serialize() + { + Map props = new HashMap<>(); + props.put("RowId", getRowId()); + props.put("Label", getLabel()); + props.put("Role", getRole()); + props.put("Type", getType()); + return props; + } + + public static ProductImpl fromJSON(@NotNull JSONObject o, Container container) + { + ProductImpl product = new ProductImpl(container, o.getString("Label"), o.getString("Role"), o.getString("Type")); + + if (o.has("RowId")) + product.setRowId(o.getInt("RowId")); + + if (o.has("Antigens") && o.get("Antigens") instanceof JSONArray antigensJSON) + { + List antigens = new ArrayList<>(); + for (JSONObject productAntigen : JsonUtil.toJSONObjectList(antigensJSON)) + antigens.add(ProductAntigenImpl.fromJSON(productAntigen, container)); + + product.setAntigens(antigens); + } + + if (o.has("DoseAndRoute") && o.get("DoseAndRoute") instanceof JSONArray doseJSON) + { + List doseAndRoutes = new ArrayList<>(); + for (JSONObject doseAndRoute : JsonUtil.toJSONObjectList(doseJSON)) + doseAndRoutes.add(DoseAndRoute.fromJSON(doseAndRoute, container, product.getRowId())); + + product.setDoseAndRoutes(doseAndRoutes); + } + + return product; + } +} diff --git a/studydesign/src/org/labkey/studydesign/model/StudyAssaySchedule.java b/studydesign/src/org/labkey/studydesign/model/StudyAssaySchedule.java new file mode 100644 index 00000000..9128f5d1 --- /dev/null +++ b/studydesign/src/org/labkey/studydesign/model/StudyAssaySchedule.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2016 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.labkey.studydesign.model; + +import org.json.JSONArray; +import org.json.JSONObject; +import org.labkey.api.action.ApiJsonForm; +import org.labkey.api.data.Container; +import org.labkey.api.util.JsonUtil; +import org.labkey.api.view.HttpView; + +import java.util.ArrayList; +import java.util.List; + +public class StudyAssaySchedule implements ApiJsonForm +{ + Container _container; + List _assays; + String _assayPlan; + + public StudyAssaySchedule() + {} + + public StudyAssaySchedule(Container container) + { + _container = container; + } + + public void setAssays(List assays) + { + _assays = assays; + } + + public List getAssays() + { + return _assays; + } + + public void setAssayPlan(String assayPlan) + { + _assayPlan = assayPlan; + } + + public String getAssayPlan() + { + return _assayPlan; + } + + @Override + public void bindJson(JSONObject json) + { + _container = HttpView.currentContext().getContainer(); + + JSONArray assaysJSON = json.optJSONArray("assays"); + if (assaysJSON != null) + { + _assays = new ArrayList<>(); + for (JSONObject assayJSON : JsonUtil.toJSONObjectList(assaysJSON)) + _assays.add(AssaySpecimenConfigImpl.fromJSON(assayJSON, _container)); + } + + _assayPlan = json.optString("assayPlan", null); + } +} diff --git a/studydesign/src/org/labkey/studydesign/model/StudyDesignCohort.java b/studydesign/src/org/labkey/studydesign/model/StudyDesignCohort.java new file mode 100644 index 00000000..d040fa74 --- /dev/null +++ b/studydesign/src/org/labkey/studydesign/model/StudyDesignCohort.java @@ -0,0 +1,94 @@ +package org.labkey.studydesign.model; + +import org.jetbrains.annotations.NotNull; +import org.json.JSONArray; +import org.json.JSONObject; +import org.labkey.api.study.Cohort; +import org.labkey.api.util.JsonUtil; + +import java.util.ArrayList; +import java.util.List; + +/** + * Used as a bean in the treatment schedule to map to actual study cohorts + */ +public class StudyDesignCohort +{ + private int _rowId; + private String _label; + private Integer _subjectCount; + List _treatmentVisitMap = new ArrayList<>(); + + public StudyDesignCohort() + { + } + + public StudyDesignCohort(Cohort cohort) + { + _rowId = cohort.getRowId(); + _label = cohort.getLabel(); + _subjectCount = cohort.getSubjectCount(); + } + + public int getRowId() + { + return _rowId; + } + + public void setRowId(int rowId) + { + _rowId = rowId; + } + + public String getLabel() + { + return _label; + } + + public void setLabel(String label) + { + _label = label; + } + + public Integer getSubjectCount() + { + return _subjectCount; + } + + public void setSubjectCount(Integer subjectCount) + { + _subjectCount = subjectCount; + } + + public List getTreatmentVisitMap() + { + return _treatmentVisitMap; + } + + public void setTreatmentVisitMap(List treatmentVisitMap) + { + _treatmentVisitMap = treatmentVisitMap; + } + + public static StudyDesignCohort fromJSON(@NotNull JSONObject o) + { + StudyDesignCohort cohort = new StudyDesignCohort(); + cohort.setLabel(o.getString("Label")); + if (o.has("SubjectCount") && !"".equals(o.getString("SubjectCount"))) + cohort.setSubjectCount(o.getInt("SubjectCount")); + if (o.has("RowId")) + cohort.setRowId(o.getInt("RowId")); + + JSONArray visitMapJSON = o.optJSONArray("VisitMap"); + if (visitMapJSON != null) + { + List treatmentVisitMap = new ArrayList<>(); + for (JSONObject json : JsonUtil.toJSONObjectList(visitMapJSON)) + treatmentVisitMap.add(TreatmentVisitMapImpl.fromJSON(json)); + + cohort.setTreatmentVisitMap(treatmentVisitMap); + } + + return cohort; + } +} diff --git a/studydesign/src/org/labkey/studydesign/model/StudyTreatmentSchedule.java b/studydesign/src/org/labkey/studydesign/model/StudyTreatmentSchedule.java new file mode 100644 index 00000000..f9afc6e7 --- /dev/null +++ b/studydesign/src/org/labkey/studydesign/model/StudyTreatmentSchedule.java @@ -0,0 +1,174 @@ +/* + * Copyright (c) 2014-2018 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.labkey.studydesign.model; + +import org.json.JSONArray; +import org.json.JSONObject; +import org.labkey.api.action.ApiJsonForm; +import org.labkey.api.data.Container; +import org.labkey.api.study.Cohort; +import org.labkey.api.study.Visit; +import org.labkey.api.util.JsonUtil; +import org.labkey.api.view.HttpView; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Represents a study's cohort/treatment/visit mapping information. Used to serialize JSON to the treatment schedule. + */ +public class StudyTreatmentSchedule implements ApiJsonForm +{ + Container _container; + + // treatment schedule properties + List _treatments; + Collection _visits; + Collection _cohorts; + + public StudyTreatmentSchedule() + {} + + public StudyTreatmentSchedule(Container container) + { + _container = container; + } + + public void setTreatments(List treatments) + { + _treatments = treatments; + } + + public List getTreatments() + { + return _treatments; + } + + public List> serializeTreatments() + { + List> treatmentList = new ArrayList<>(); + for (TreatmentImpl treatment : _treatments) + { + treatmentList.add(treatment.serialize()); + } + return treatmentList; + } + + public void setVisits(Collection visits) + { + _visits = visits; + } + + public Collection getVisits() + { + return _visits; + } + + public List> serializeVisits() + { + List> visitList = new ArrayList<>(); + List includedIds = getIncludedVisitIds(); + for (Visit v : _visits) + { + Map visitProperties = new HashMap<>(); + visitProperties.put("RowId", v.getId()); + visitProperties.put("Label", v.getDisplayString()); + visitProperties.put("DisplayOrder", v.getDisplayOrder()); + visitProperties.put("SequenceNumMin", v.getSequenceNumMin()); + + // tag those visits that are used in the treatment schedule + visitProperties.put("Included", includedIds.contains(v.getId())); + + visitList.add(visitProperties); + } + return visitList; + } + + private List getIncludedVisitIds() + { + List ids = new ArrayList<>(); + for (StudyDesignCohort cohort : _cohorts) + { + for (TreatmentVisitMapImpl tvm : TreatmentManager.getInstance().getStudyTreatmentVisitMap(_container, cohort.getRowId())) + { + if (!ids.contains(tvm.getVisitId())) + ids.add(tvm.getVisitId()); + } + } + + return ids; + } + + public Collection getCohorts() + { + return _cohorts; + } + + public List> serializeCohortMapping(Collection cohorts) + { + List> cohortMappingList = new ArrayList<>(); + _cohorts = new ArrayList<>(); + for (Cohort cohort : cohorts) + { + _cohorts.add(new StudyDesignCohort(cohort)); + + Map mapProperties = new HashMap<>(); + mapProperties.put("RowId", cohort.getRowId()); + mapProperties.put("Label", cohort.getLabel()); + mapProperties.put("SubjectCount", cohort.getSubjectCount()); + mapProperties.put("CanDelete", !cohort.isInUse()); + + List> treatmentVisitMap = new ArrayList<>(); + for (TreatmentVisitMapImpl mapping : TreatmentManager.getInstance().getStudyTreatmentVisitMap(_container, cohort.getRowId())) + { + Map visitMapping = new HashMap<>(); + visitMapping.put("CohortId", mapping.getCohortId()); + visitMapping.put("TreatmentId", mapping.getTreatmentId()); + visitMapping.put("VisitId", mapping.getVisitId()); + treatmentVisitMap.add(visitMapping); + } + mapProperties.put("VisitMap", treatmentVisitMap); + + cohortMappingList.add(mapProperties); + } + return cohortMappingList; + } + + @Override + public void bindJson(JSONObject json) + { + _container = HttpView.currentContext().getContainer(); + + JSONArray treatmentsJSON = json.optJSONArray("treatments"); + if (treatmentsJSON != null) + { + _treatments = new ArrayList<>(); + for (JSONObject treatment : JsonUtil.toJSONObjectList(treatmentsJSON)) + _treatments.add(TreatmentImpl.fromJSON(treatment, _container)); + } + + JSONArray cohortsJSON = json.optJSONArray("cohorts"); + if (cohortsJSON != null) + { + _cohorts = new ArrayList<>(); + for (JSONObject cohortJSON : JsonUtil.toJSONObjectList(cohortsJSON)) + _cohorts.add(StudyDesignCohort.fromJSON(cohortJSON)); + } + } +} diff --git a/studydesign/src/org/labkey/studydesign/model/TreatmentImpl.java b/studydesign/src/org/labkey/studydesign/model/TreatmentImpl.java new file mode 100644 index 00000000..8558fafc --- /dev/null +++ b/studydesign/src/org/labkey/studydesign/model/TreatmentImpl.java @@ -0,0 +1,226 @@ +/* + * Copyright (c) 2013-2016 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.labkey.studydesign.model; + +import org.jetbrains.annotations.NotNull; +import org.json.JSONArray; +import org.json.JSONObject; +import org.labkey.api.data.Container; +import org.labkey.api.data.Sort; +import org.labkey.api.query.FieldKey; +import org.labkey.api.study.Treatment; +import org.labkey.api.util.JsonUtil; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * User: cnathe + * Date: 12/27/13 + */ +public class TreatmentImpl implements Treatment +{ + private Container _container; + private int _rowId; + private String _tempRowId; // used to map new treatment records being inserted to their usage in TreatmentVisitMapImpl + private String _label; + private String _description; + private List _treatmentProducts; + private List _products; + + public TreatmentImpl() + {} + + public TreatmentImpl(Container container, String label, String description) + { + _container = container; + _label = label; + _description = description; + } + + public boolean isNew() + { + return _rowId == 0; + } + + public Object getPrimaryKey() + { + return getRowId(); + } + + @Override + public int getRowId() + { + return _rowId; + } + + public void setRowId(int rowId) + { + _rowId = rowId; + } + + private void setTempRowId(String tempRowId) + { + _tempRowId = tempRowId; + } + + public String getTempRowId() + { + return _tempRowId; + } + + @Override + public String getLabel() + { + return _label; + } + + public void setLabel(String label) + { + _label = label; + } + + @Override + public String getDescription() + { + return _description; + } + + public void setDescription(String description) + { + _description = description; + } + + public List getTreatmentProducts() + { + return _treatmentProducts; + } + + public void setTreatmentProducts(List treatmentProducts) + { + _treatmentProducts = treatmentProducts; + } + + public List getProducts() + { + return _products; + } + + public void setProducts(List products) + { + _products = products; + } + + public void addProduct(ProductImpl product) + { + if (_products == null) + _products = new ArrayList<>(); + + _products.add(product); + } + + public Container getContainer() + { + return _container; + } + + public void setContainer(Container container) + { + _container = container; + } + + public Map serialize() + { + Map props = new HashMap<>(); + props.put("RowId", getRowId()); + props.put("Label", getLabel()); + props.put("Description", getDescription()); + return props; + } + + public Sort getProductSort() + { + // sort the product list to match the manage study products page (i.e. Immunogens before Adjuvants) + Sort sort = new Sort(); + sort.appendSortColumn(FieldKey.fromParts("ProductId", "Role"), Sort.SortDirection.DESC, false); + sort.appendSortColumn(FieldKey.fromParts("ProductId", "RowId"), Sort.SortDirection.ASC, false); + return sort; + } + + public static TreatmentImpl fromJSON(@NotNull JSONObject o, Container container) + { + TreatmentImpl treatment = new TreatmentImpl(container, o.getString("Label"), o.optString("Description", null)); + + if (o.has("RowId")) + { + if (o.get("RowId") instanceof Integer) + treatment.setRowId(o.getInt("RowId")); + else + treatment.setTempRowId(o.getString("RowId")); + } + + if (o.has("Products") && o.get("Products") instanceof JSONArray productsJSON) + { + List treatmentProducts = new ArrayList<>(); + for (JSONObject productJson : JsonUtil.toJSONObjectList(productsJSON)) + treatmentProducts.add(TreatmentProductImpl.fromJSON(productJson, container)); + + treatment.setTreatmentProducts(treatmentProducts); + } + + return treatment; + } + + public boolean isSameTreatmentProductsWith(TreatmentImpl other) + { + if (other == null) + return false; + if (this.getTreatmentProducts() == null) + { + return other.getTreatmentProducts() == null; + } + else + { + if (other.getTreatmentProducts() == null) + return false; + + if (this.getTreatmentProducts().size() != other.getTreatmentProducts().size()) + return false; + + boolean hasMismatch = false; + for (TreatmentProductImpl product : this.getTreatmentProducts()) + { + boolean foundMatch =false; + for (TreatmentProductImpl otherProduct : other.getTreatmentProducts()) + { + if (product.isSameTreatmentProductWith(otherProduct)) + { + foundMatch = true; + break; + } + } + if (!foundMatch) + { + hasMismatch = true; + break; + } + } + return !hasMismatch; + } + } +} diff --git a/studydesign/src/org/labkey/studydesign/model/TreatmentManager.java b/studydesign/src/org/labkey/studydesign/model/TreatmentManager.java new file mode 100644 index 00000000..187d2ac4 --- /dev/null +++ b/studydesign/src/org/labkey/studydesign/model/TreatmentManager.java @@ -0,0 +1,1151 @@ +/* + * Copyright (c) 2014-2018 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.labkey.studydesign.model; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.junit.Assert; +import org.junit.Test; +import org.labkey.api.data.ColumnInfo; +import org.labkey.api.data.CompareType; +import org.labkey.api.data.Container; +import org.labkey.api.data.ContainerManager; +import org.labkey.api.data.DbScope; +import org.labkey.api.data.SimpleFilter; +import org.labkey.api.data.Sort; +import org.labkey.api.data.Table; +import org.labkey.api.data.TableInfo; +import org.labkey.api.data.TableSelector; +import org.labkey.api.module.Module; +import org.labkey.api.module.ModuleLoader; +import org.labkey.api.query.BatchValidationException; +import org.labkey.api.query.FieldKey; +import org.labkey.api.query.FilteredTable; +import org.labkey.api.query.QueryService; +import org.labkey.api.query.QueryUpdateService; +import org.labkey.api.query.UserSchema; +import org.labkey.api.query.ValidationException; +import org.labkey.api.security.User; +import org.labkey.api.study.AssaySpecimenConfig; +import org.labkey.api.study.Cohort; +import org.labkey.api.study.Study; +import org.labkey.api.study.StudyService; +import org.labkey.api.study.TimepointType; +import org.labkey.api.study.Visit; +import org.labkey.api.study.model.CohortService; +import org.labkey.api.study.model.VisitService; +import org.labkey.api.studydesign.StudyDesignService; +import org.labkey.api.studydesign.query.StudyDesignQuerySchema; +import org.labkey.api.studydesign.query.StudyDesignSchema; +import org.labkey.api.test.TestWhen; +import org.labkey.api.util.GUID; +import org.labkey.api.util.JunitUtil; +import org.labkey.api.util.TestContext; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Created by cnathe on 1/24/14. + */ +public class TreatmentManager +{ + private static final TreatmentManager _instance = new TreatmentManager(); + + public static final String ASSAY_SPECIMEN_TABLE_NAME = "AssaySpecimen"; + public static final String ASSAY_SPECIMEN_VISIT_TABLE_NAME = "AssaySpecimenVisit"; + + private TreatmentManager() + { + } + + public static TreatmentManager getInstance() + { + return _instance; + } + + public List getStudyProducts(Container container, User user) + { + return getStudyProducts(container, user, null, null); + } + + public List getStudyProducts(Container container, User user, @Nullable String role, @Nullable Integer rowId) + { + //Using a user schema so containerFilter will be created for us later (so don't need SimpleFilter.createContainerFilter) + SimpleFilter filter = new SimpleFilter(); + if (role != null) + filter.addCondition(FieldKey.fromParts("Role"), role); + if (rowId != null) + filter.addCondition(FieldKey.fromParts("RowId"), rowId); + + TableInfo ti = QueryService.get().getUserSchema(user, container, StudyDesignQuerySchema.STUDY_SCHEMA_NAME).getTable(StudyDesignQuerySchema.PRODUCT_TABLE_NAME); + return new TableSelector(ti, filter, new Sort("RowId")).getArrayList(ProductImpl.class); + } + + public List getFilteredStudyProducts(Container container, User user, List filterRowIds) + { + TableInfo ti = QueryService.get().getUserSchema(user, container, StudyDesignQuerySchema.STUDY_SCHEMA_NAME).getTable(StudyDesignQuerySchema.PRODUCT_TABLE_NAME); + + //Using a user schema so containerFilter will be created for us later (so don't need SimpleFilter.createContainerFilter) + SimpleFilter filter = new SimpleFilter(); + if (filterRowIds != null && !filterRowIds.isEmpty()) + filter.addCondition(FieldKey.fromParts("RowId"), filterRowIds, CompareType.NOT_IN); + + return new TableSelector(ti, filter, new Sort("RowId")).getArrayList(ProductImpl.class); + } + + public List getStudyProductAntigens(Container container, User user, int productId) + { + SimpleFilter filter = new SimpleFilter(); + filter.addCondition(FieldKey.fromParts("ProductId"), productId); + + TableInfo ti = QueryService.get().getUserSchema(user, container, StudyDesignQuerySchema.STUDY_SCHEMA_NAME).getTable(StudyDesignQuerySchema.PRODUCT_ANTIGEN_TABLE_NAME); + return new TableSelector(ti, filter, new Sort("RowId")).getArrayList(ProductAntigenImpl.class); + } + + public List getFilteredStudyProductAntigens(Container container, User user, @NotNull Integer productId, List filterRowIds) + { + TableInfo ti = QueryService.get().getUserSchema(user, container, StudyDesignQuerySchema.STUDY_SCHEMA_NAME).getTable(StudyDesignQuerySchema.PRODUCT_ANTIGEN_TABLE_NAME); + + //Using a user schema so containerFilter will be created for us later (so don't need SimpleFilter.createContainerFilter) + SimpleFilter filter = new SimpleFilter(); + filter.addCondition(FieldKey.fromParts("ProductId"), productId); + if (filterRowIds != null && !filterRowIds.isEmpty()) + filter.addCondition(FieldKey.fromParts("RowId"), filterRowIds, CompareType.NOT_IN); + + return new TableSelector(ti, filter, new Sort("RowId")).getArrayList(ProductAntigenImpl.class); + } + + public Integer saveTreatment(Container container, User user, TreatmentImpl treatment) throws Exception + { + TableInfo treatmentTable = QueryService.get().getUserSchema(user, container, StudyDesignQuerySchema.STUDY_SCHEMA_NAME).getTable(StudyDesignQuerySchema.TREATMENT_TABLE_NAME); + return saveStudyDesignRow(container, user, treatmentTable, treatment.serialize(), treatment.isNew() ? null : treatment.getRowId(), "RowId"); + } + + public List getStudyTreatments(Container container, User user) + { + SimpleFilter filter = new SimpleFilter(); + TableInfo ti = QueryService.get().getUserSchema(user, container, StudyDesignQuerySchema.STUDY_SCHEMA_NAME).getTable(StudyDesignQuerySchema.TREATMENT_TABLE_NAME); + return new TableSelector(ti, filter, new Sort("RowId")).getArrayList(TreatmentImpl.class); + } + + public TreatmentImpl getStudyTreatmentByRowId(Container container, User user, int rowId) + { + SimpleFilter filter = new SimpleFilter(FieldKey.fromParts("RowId"), rowId); + TableInfo ti = QueryService.get().getUserSchema(user, container, StudyDesignQuerySchema.STUDY_SCHEMA_NAME).getTable(StudyDesignQuerySchema.TREATMENT_TABLE_NAME); + TreatmentImpl treatment = new TableSelector(ti, filter, null).getObject(TreatmentImpl.class); + + // attach the associated study products to the treatment object + if (treatment != null) + { + List treatmentProducts = getStudyTreatmentProducts(container, user, treatment.getRowId(), treatment.getProductSort()); + for (TreatmentProductImpl treatmentProduct : treatmentProducts) + { + List products = getStudyProducts(container, user, null, treatmentProduct.getProductId()); + for (ProductImpl product : products) + { + product.setDose(treatmentProduct.getDose()); + product.setRoute(treatmentProduct.getRoute()); + treatment.addProduct(product); + } + } + } + + return treatment; + } + + public List getFilteredTreatments(Container container, User user, List definedTreatmentIds, Set usedTreatmentIds) + { + List filterRowIds = new ArrayList<>(); + filterRowIds.addAll(definedTreatmentIds); + filterRowIds.addAll(usedTreatmentIds); + TableInfo ti = QueryService.get().getUserSchema(user, container, StudyDesignQuerySchema.STUDY_SCHEMA_NAME).getTable(StudyDesignQuerySchema.TREATMENT_TABLE_NAME); + + //Using a user schema so containerFilter will be created for us later (so don't need SimpleFilter.createContainerFilter) + SimpleFilter filter = new SimpleFilter().addCondition(FieldKey.fromParts("RowId"), filterRowIds, CompareType.NOT_IN); + + return new TableSelector(ti, filter, new Sort("RowId")).getArrayList(TreatmentImpl.class); + } + + public Integer saveTreatmentProductMapping(Container container, User user, TreatmentProductImpl treatmentProduct) throws Exception + { + TableInfo treatmentProductTable = QueryService.get().getUserSchema(user, container, StudyDesignQuerySchema.STUDY_SCHEMA_NAME).getTable(StudyDesignQuerySchema.TREATMENT_PRODUCT_MAP_TABLE_NAME); + return saveStudyDesignRow(container, user, treatmentProductTable, treatmentProduct.serialize(), treatmentProduct.isNew() ? null : treatmentProduct.getRowId(), "RowId"); + } + + public List getStudyTreatmentProducts(Container container, User user, int treatmentId) + { + return getStudyTreatmentProducts(container, user, treatmentId, new Sort("RowId")); + } + + public List getStudyTreatmentProducts(Container container, User user, int treatmentId, Sort sort) + { + SimpleFilter filter = new SimpleFilter(FieldKey.fromParts("TreatmentId"), treatmentId); + + TableInfo ti = QueryService.get().getUserSchema(user, container, StudyDesignQuerySchema.STUDY_SCHEMA_NAME).getTable(StudyDesignQuerySchema.TREATMENT_PRODUCT_MAP_TABLE_NAME); + return new TableSelector(ti, filter, sort).getArrayList(TreatmentProductImpl.class); + } + + public List getFilteredTreatmentProductMappings(Container container, User user, @NotNull Integer treatmentId, List filterRowIds) + { + TableInfo ti = QueryService.get().getUserSchema(user, container, StudyDesignQuerySchema.STUDY_SCHEMA_NAME).getTable(StudyDesignQuerySchema.TREATMENT_PRODUCT_MAP_TABLE_NAME); + + //Using a user schema so containerFilter will be created for us later (so don't need SimpleFilter.createContainerFilter) + SimpleFilter filter = new SimpleFilter(); + filter.addCondition(FieldKey.fromParts("TreatmentId"), treatmentId); + if (filterRowIds != null && !filterRowIds.isEmpty()) + filter.addCondition(FieldKey.fromParts("RowId"), filterRowIds, CompareType.NOT_IN); + + return new TableSelector(ti, filter, new Sort("RowId")).getArrayList(TreatmentProductImpl.class); + } + + public List getStudyTreatmentVisitMap(Container container, @Nullable Integer cohortId) + { + SimpleFilter filter = SimpleFilter.createContainerFilter(container); + if (cohortId != null) + filter.addCondition(FieldKey.fromParts("CohortId"), cohortId); + + TableInfo ti = StudyDesignSchema.getInstance().getTableInfoTreatmentVisitMap(); + return new TableSelector(ti, filter, new Sort("CohortId")).getArrayList(TreatmentVisitMapImpl.class); + } + + public List getVisitsForTreatmentSchedule(Container container) + { + SimpleFilter filter = SimpleFilter.createContainerFilter(container); + List visitRowIds = new TableSelector(StudyDesignSchema.getInstance().getTableInfoTreatmentVisitMap(), + Collections.singleton("VisitId"), filter, new Sort("VisitId")).getArrayList(Integer.class); + + Study study = StudyService.get().getStudy(container); + List visits = new ArrayList<>(); + if (study != null) + { + for (Visit visit : study.getVisits(Visit.Order.DISPLAY)) + { + if (visitRowIds.contains(visit.getId())) + visits.add(visit); + } + } + return visits; + } + + public TreatmentVisitMapImpl insertTreatmentVisitMap(User user, Container container, int cohortId, int visitId, int treatmentId) + { + TreatmentVisitMapImpl newMapping = new TreatmentVisitMapImpl(); + newMapping.setContainer(container); + newMapping.setCohortId(cohortId); + newMapping.setVisitId(visitId); + newMapping.setTreatmentId(treatmentId); + + return Table.insert(user, StudyDesignSchema.getInstance().getTableInfoTreatmentVisitMap(), newMapping); + } + + public void deleteTreatmentVisitMapForCohort(Container container, int rowId) + { + SimpleFilter filter = SimpleFilter.createContainerFilter(container); + filter.addCondition(FieldKey.fromParts("CohortId"), rowId); + Table.delete(StudyDesignSchema.getInstance().getTableInfoTreatmentVisitMap(), filter); + } + + public void deleteTreatmentVisitMapForVisit(Container container, int rowId) + { + SimpleFilter filter = SimpleFilter.createContainerFilter(container); + filter.addCondition(FieldKey.fromParts("VisitId"), rowId); + Table.delete(StudyDesignSchema.getInstance().getTableInfoTreatmentVisitMap(), filter); + } + + public void deleteTreatment(Container container, User user, int rowId) + { + StudyDesignSchema schema = StudyDesignSchema.getInstance(); + + try (DbScope.Transaction transaction = schema.getSchema().getScope().ensureTransaction()) + { + // delete the usages of this treatment in the TreatmentVisitMap + SimpleFilter filter = SimpleFilter.createContainerFilter(container); + filter.addCondition(FieldKey.fromParts("TreatmentId"), rowId); + Table.delete(StudyDesignSchema.getInstance().getTableInfoTreatmentVisitMap(), filter); + + // delete the associated treatment study product mappings (provision table) + filter = SimpleFilter.createContainerFilter(container); + filter.addCondition(FieldKey.fromParts("TreatmentId"), rowId); + deleteTreatmentProductMap(container, user, filter); + + // finally delete the record from the Treatment (provision table) + TableInfo treatmentTable = QueryService.get().getUserSchema(user, container, StudyDesignQuerySchema.STUDY_SCHEMA_NAME).getTable(StudyDesignQuerySchema.TREATMENT_TABLE_NAME); + if (treatmentTable != null) + { + QueryUpdateService qus = treatmentTable.getUpdateService(); + List> keys = new ArrayList<>(); + ColumnInfo treatmentPk = treatmentTable.getColumn(FieldKey.fromParts("RowId")); + keys.add(Collections.singletonMap(treatmentPk.getName(), rowId)); + qus.deleteRows(user, container, keys, null, null); + } + + transaction.commit(); + } + catch (Exception e) + { + throw new RuntimeException(e); + } + } + + public void deleteStudyProduct(Container container, User user, int rowId) + { + StudyDesignSchema schema = StudyDesignSchema.getInstance(); + + try (DbScope.Transaction transaction = schema.getSchema().getScope().ensureTransaction()) + { + // delete the usages of this study product in the ProductAntigen table (provision table) + deleteProductAntigens(container, user, rowId); + + // delete the associated doses and routes for this product + Table.delete(StudyDesignSchema.getInstance().getTableInfoDoseAndRoute(), new SimpleFilter(FieldKey.fromParts("ProductId"), rowId)); + + // delete the associated treatment study product mappings (provision table) + SimpleFilter filter = SimpleFilter.createContainerFilter(container); + filter.addCondition(FieldKey.fromParts("ProductId"), rowId); + deleteTreatmentProductMap(container, user, filter); + + // finally delete the record from the Products (provision table) + TableInfo productTable = QueryService.get().getUserSchema(user, container, StudyDesignQuerySchema.STUDY_SCHEMA_NAME).getTable(StudyDesignQuerySchema.PRODUCT_TABLE_NAME); + if (productTable != null) + { + QueryUpdateService qus = productTable.getUpdateService(); + List> keys = new ArrayList<>(); + ColumnInfo productPk = productTable.getColumn(FieldKey.fromParts("RowId")); + keys.add(Collections.singletonMap(productPk.getName(), rowId)); + qus.deleteRows(user, container, keys, null, null); + } + else + throw new IllegalStateException("Could not find table: " + StudyDesignQuerySchema.PRODUCT_TABLE_NAME); + + transaction.commit(); + } + catch (Exception e) + { + throw new RuntimeException(e); + } + } + + public Integer saveStudyProduct(Container container, User user, ProductImpl product) throws Exception + { + TableInfo productTable = QueryService.get().getUserSchema(user, container, StudyDesignQuerySchema.STUDY_SCHEMA_NAME).getTable(StudyDesignQuerySchema.PRODUCT_TABLE_NAME); + return saveStudyDesignRow(container, user, productTable, product.serialize(), product.isNew() ? null : product.getRowId(), "RowId"); + } + + public Integer saveStudyProductAntigen(Container container, User user, ProductAntigenImpl antigen) throws Exception + { + TableInfo productAntigenTable = QueryService.get().getUserSchema(user, container, StudyDesignQuerySchema.STUDY_SCHEMA_NAME).getTable(StudyDesignQuerySchema.PRODUCT_ANTIGEN_TABLE_NAME); + return saveStudyDesignRow(container, user, productAntigenTable, antigen.serialize(), antigen.isNew() ? null : antigen.getRowId(), "RowId"); + } + + public DoseAndRoute saveStudyProductDoseAndRoute(Container container, User user, DoseAndRoute doseAndRoute) + { + if (doseAndRoute.isNew()) + return Table.insert(user, StudyDesignSchema.getInstance().getTableInfoDoseAndRoute(), doseAndRoute); + else + return Table.update(user, StudyDesignSchema.getInstance().getTableInfoDoseAndRoute(), doseAndRoute, doseAndRoute.getRowId()); + } + + public Collection getStudyProductsDoseAndRoute(Container container, User user, int productId) + { + SimpleFilter filter = new SimpleFilter(FieldKey.fromParts("ProductId"), productId); + return new TableSelector(StudyDesignSchema.getInstance().getTableInfoDoseAndRoute(), filter, null).getCollection(DoseAndRoute.class); + } + + @Nullable + public DoseAndRoute getDoseAndRoute(Container container, String dose, String route, int productId) + { + SimpleFilter filter = new SimpleFilter(FieldKey.fromParts("ProductId"), productId); + if (dose != null) + filter.addCondition(FieldKey.fromParts("Dose"), dose); + else + filter.addCondition(FieldKey.fromParts("Dose"), null, CompareType.ISBLANK); + if (route != null) + filter.addCondition(FieldKey.fromParts("Route"), route); + else + filter.addCondition(FieldKey.fromParts("Route"), null, CompareType.ISBLANK); + Collection doseAndRoutes = new TableSelector(StudyDesignSchema.getInstance().getTableInfoDoseAndRoute(), filter, null).getCollection(DoseAndRoute.class); + + if (!doseAndRoutes.isEmpty()) + { + return doseAndRoutes.iterator().next(); + } + return null; + } + + public Integer saveAssaySpecimen(Container container, User user, AssaySpecimenConfigImpl assaySpecimen) throws Exception + { + TableInfo assaySpecimenTable = QueryService.get().getUserSchema(user, container, StudyDesignQuerySchema.STUDY_SCHEMA_NAME).getTable(ASSAY_SPECIMEN_TABLE_NAME); + Integer ret = saveStudyDesignRow(container, user, assaySpecimenTable, assaySpecimen.serialize(), assaySpecimen.isNew() ? null : assaySpecimen.getRowId(), "RowId", true); + return ret; + } + + public Integer saveAssaySpecimenVisit(Container container, User user, AssaySpecimenVisitImpl assaySpecimenVisit) throws Exception + { + TableInfo assaySpecimenVIsitTable = QueryService.get().getUserSchema(user, container, StudyDesignQuerySchema.STUDY_SCHEMA_NAME).getTable(ASSAY_SPECIMEN_VISIT_TABLE_NAME); + return saveStudyDesignRow(container, user, assaySpecimenVIsitTable, assaySpecimenVisit.serialize(), assaySpecimenVisit.isNew() ? null : assaySpecimenVisit.getRowId(), "RowId", true); + } + + public List getFilteredAssaySpecimens(Container container, List filterRowIds) + { + SimpleFilter filter = SimpleFilter.createContainerFilter(container); + if (filterRowIds != null && !filterRowIds.isEmpty()) + filter.addCondition(FieldKey.fromParts("RowId"), filterRowIds, CompareType.NOT_IN); + + return new TableSelector(StudyDesignSchema.getInstance().getTableInfoAssaySpecimen(), filter, new Sort("RowId")).getArrayList(AssaySpecimenConfigImpl.class); + } + + public List getFilteredAssaySpecimenVisits(Container container, int assaySpecimenId, List filterRowIds) + { + SimpleFilter filter = SimpleFilter.createContainerFilter(container); + filter.addCondition(FieldKey.fromParts("AssaySpecimenId"), assaySpecimenId); + if (filterRowIds != null && !filterRowIds.isEmpty()) + filter.addCondition(FieldKey.fromParts("RowId"), filterRowIds, CompareType.NOT_IN); + + return new TableSelector(StudyDesignSchema.getInstance().getTableInfoAssaySpecimenVisit(), filter, new Sort("RowId")).getArrayList(AssaySpecimenVisitImpl.class); + } + + public Integer saveStudyDesignRow(Container container, User user, TableInfo tableInfo, Map row, Integer key, String pkColName) throws Exception + { + return saveStudyDesignRow(container, user, tableInfo, row, key, pkColName, false); + } + + public Integer saveStudyDesignRow(Container container, User user, TableInfo tableInfo, Map row, Integer key, String pkColName, boolean includeContainerKey) throws Exception + { + QueryUpdateService qus = tableInfo != null ? tableInfo.getUpdateService() : null; + if (qus != null) + { + BatchValidationException errors = new BatchValidationException(); + List> updatedRows; + + if (key == null) + { + updatedRows = qus.insertRows(user, container, Collections.singletonList(row), errors, null, null); + } + else + { + Map oldKey = new HashMap<>(); + oldKey.put(pkColName, key); + if (includeContainerKey) + oldKey.put("Container", container.getId()); + + updatedRows = qus.updateRows(user, container, Collections.singletonList(row), Collections.singletonList(oldKey), errors, null, null); + } + + if (errors.hasErrors()) + throw errors.getLastRowError(); + + if (updatedRows.size() == 1) + return (Integer) updatedRows.get(0).get(pkColName); + } + + return null; + } + + public void deleteStudyProductAntigen(Container container, User user, int rowId) throws Exception + { + TableInfo productAntigenTable = QueryService.get().getUserSchema(user, container, StudyDesignQuerySchema.STUDY_SCHEMA_NAME).getTable(StudyDesignQuerySchema.PRODUCT_ANTIGEN_TABLE_NAME); + if (productAntigenTable != null) + { + QueryUpdateService qus = productAntigenTable.getUpdateService(); + if (qus != null) + { + List> keys = Collections.singletonList(Collections.singletonMap("RowId", rowId)); + qus.deleteRows(user, container, keys, null, null); + } + else + throw new IllegalStateException("Could not find query update service for table: " + StudyDesignQuerySchema.PRODUCT_ANTIGEN_TABLE_NAME); + } + else + throw new IllegalStateException("Could not find table: " + StudyDesignQuerySchema.PRODUCT_ANTIGEN_TABLE_NAME); + } + + public void deleteProductAntigens(Container container, User user, int productId) throws Exception + { + TableInfo productAntigenTable = QueryService.get().getUserSchema(user, container, StudyDesignQuerySchema.STUDY_SCHEMA_NAME).getTable(StudyDesignQuerySchema.PRODUCT_ANTIGEN_TABLE_NAME); + if (productAntigenTable != null) + { + SimpleFilter filter = SimpleFilter.createContainerFilter(container); + filter.addCondition(FieldKey.fromParts("ProductId"), productId); + TableSelector selector = new TableSelector(productAntigenTable, Collections.singleton("RowId"), filter, null); + Integer[] productAntigenIds = selector.getArray(Integer.class); + + QueryUpdateService qus = productAntigenTable.getUpdateService(); + if (qus != null) + { + List> keys = new ArrayList<>(); + ColumnInfo productAntigenPk = productAntigenTable.getColumn(FieldKey.fromParts("RowId")); + for (Integer productAntigenId : productAntigenIds) + { + keys.add(Collections.singletonMap(productAntigenPk.getName(), productAntigenId)); + } + + qus.deleteRows(user, container, keys, null, null); + } + else + throw new IllegalStateException("Could not find query update service for table: " + StudyDesignQuerySchema.PRODUCT_ANTIGEN_TABLE_NAME); + } + else + throw new IllegalStateException("Could not find table: " + StudyDesignQuerySchema.PRODUCT_ANTIGEN_TABLE_NAME); + } + + public void deleteTreatmentProductMap(Container container, User user, SimpleFilter filter) throws Exception + { + TableInfo productMapTable = QueryService.get().getUserSchema(user, container, StudyDesignQuerySchema.STUDY_SCHEMA_NAME).getTable(StudyDesignQuerySchema.TREATMENT_PRODUCT_MAP_TABLE_NAME); + if (productMapTable != null) + { + TableSelector selector = new TableSelector(productMapTable, Collections.singleton("RowId"), filter, null); + deleteTreatmentProductMap(container, user, selector.getArrayList(Integer.class)); + } + else + throw new IllegalStateException("Could not find table: " + StudyDesignQuerySchema.TREATMENT_PRODUCT_MAP_TABLE_NAME); + } + + public void deleteTreatmentProductMap(Container container, User user, List rowIds) throws Exception + { + TableInfo productMapTable = QueryService.get().getUserSchema(user, container, StudyDesignQuerySchema.STUDY_SCHEMA_NAME).getTable(StudyDesignQuerySchema.TREATMENT_PRODUCT_MAP_TABLE_NAME); + if (productMapTable != null) + { + QueryUpdateService qus = productMapTable.getUpdateService(); + if (qus != null) + { + List> keys = new ArrayList<>(); + ColumnInfo productMapPk = productMapTable.getColumn(FieldKey.fromParts("RowId")); + for (Integer rowId : rowIds) + keys.add(Collections.singletonMap(productMapPk.getName(), rowId)); + + qus.deleteRows(user, container, keys, null, null); + } + else + throw new IllegalStateException("Could not find query update service for table: " + StudyDesignQuerySchema.TREATMENT_PRODUCT_MAP_TABLE_NAME); + } + else + throw new IllegalStateException("Could not find table: " + StudyDesignQuerySchema.TREATMENT_PRODUCT_MAP_TABLE_NAME); + } + + public void deleteAssaySpecimen(Container container, User user, int rowId) + { + // first delete any usages of the AssaySpecimenId in the AssaySpecimenVisit table + SimpleFilter filter = SimpleFilter.createContainerFilter(container); + filter.addCondition(FieldKey.fromParts("AssaySpecimenId"), rowId); + Table.delete(StudyDesignSchema.getInstance().getTableInfoAssaySpecimenVisit(), filter); + + // delete the AssaySpecimen record by RowId + filter = SimpleFilter.createContainerFilter(container); + filter.addCondition(FieldKey.fromParts("RowId"), rowId); + Table.delete(StudyDesignSchema.getInstance().getTableInfoAssaySpecimen(), filter); + } + + public void deleteAssaySpecimenVisit(Container container, User user, int rowId) + { + SimpleFilter filter = SimpleFilter.createContainerFilter(container); + filter.addCondition(FieldKey.fromParts("RowId"), rowId); + Table.delete(StudyDesignSchema.getInstance().getTableInfoAssaySpecimenVisit(), filter); + } + + public String getStudyDesignRouteLabelByName(Container container, String name) + { + return getStudyDesignLabelByName(container, StudyDesignSchema.getInstance().getTableInfoStudyDesignRoutes(), name); + } + + public String getStudyDesignImmunogenTypeLabelByName(Container container, String name) + { + return getStudyDesignLabelByName(container, StudyDesignSchema.getInstance().getTableInfoStudyDesignImmunogenTypes(), name); + } + + public String getStudyDesignGeneLabelByName(Container container, String name) + { + return getStudyDesignLabelByName(container, StudyDesignSchema.getInstance().getTableInfoStudyDesignGenes(), name); + } + + public String getStudyDesignSubTypeLabelByName(Container container, String name) + { + return getStudyDesignLabelByName(container, StudyDesignSchema.getInstance().getTableInfoStudyDesignSubTypes(), name); + } + + public String getStudyDesignLabLabelByName(Container container, String name) + { + return getStudyDesignLabelByName(container, StudyDesignSchema.getInstance().getTableInfoStudyDesignLabs(), name); + } + + public String getStudyDesignLabelByName(Container container, TableInfo tableInfo, String name) + { + // first look in the current container for the StudyDesign record, then look for it at the project level + SimpleFilter filter = SimpleFilter.createContainerFilter(container); + filter.addCondition(FieldKey.fromParts("Name"), name); + String label = new TableSelector(tableInfo, Collections.singleton("Label"), filter, null).getObject(String.class); + if (label == null && !container.isProject()) + { + filter = SimpleFilter.createContainerFilter(container.getProject()); + filter.addCondition(FieldKey.fromParts("Name"), name); + label = new TableSelector(tableInfo, Collections.singleton("Label"), filter, null).getObject(String.class); + } + + return label; + } + + public void updateTreatmentProducts(int treatmentId, List treatmentProducts, Container container, User user) throws Exception + { + // insert new study treatment product mappings and update any existing ones + List treatmentProductRowIds = new ArrayList<>(); + for (TreatmentProductImpl treatmentProduct : treatmentProducts) + { + // make sure the treatmentId is set based on the treatment rowId + treatmentProduct.setTreatmentId(treatmentId); + + Integer updatedRowId = TreatmentManager.getInstance().saveTreatmentProductMapping(container, user, treatmentProduct); + if (updatedRowId != null) + treatmentProductRowIds.add(updatedRowId); + } + + // delete any other treatment product mappings, not included in the insert/update list, for the given treatmentId + for (TreatmentProductImpl treatmentProduct : TreatmentManager.getInstance().getFilteredTreatmentProductMappings(container, user, treatmentId, treatmentProductRowIds)) + TreatmentManager.getInstance().deleteTreatmentProductMap(container, user, Collections.singletonList(treatmentProduct.getRowId())); + } + + public List getAssaySpecimenVisitIds(Container container, AssaySpecimenConfig assaySpecimenConfig) + { + SimpleFilter filter = SimpleFilter.createContainerFilter(container); + filter.addCondition(FieldKey.fromParts("AssaySpecimenId"), assaySpecimenConfig.getRowId()); + + return new TableSelector(StudyDesignSchema.getInstance().getTableInfoAssaySpecimenVisit(), + Collections.singleton("VisitId"), filter, new Sort("VisitId")).getArrayList(Integer.class); + } + + public List getVisitsForAssaySchedule(Container container) + { + SimpleFilter filter = SimpleFilter.createContainerFilter(container); + List visitRowIds = new TableSelector(StudyDesignSchema.getInstance().getTableInfoAssaySpecimenVisit(), + Collections.singleton("VisitId"), filter, new Sort("VisitId")).getArrayList(Integer.class); + + return getSortedVisitsByRowIds(container, visitRowIds); + } + + public List getSortedVisitsByRowIds(Container container, List visitRowIds) + { + List visits = new ArrayList<>(); + Study study = StudyService.get().getStudy(container); + if (study != null) + { + for (Visit v : study.getVisits(Visit.Order.DISPLAY)) + { + if (visitRowIds.contains(v.getId())) + visits.add(v); + } + } + return visits; + } + + public AssaySpecimenConfigImpl addAssaySpecimenConfig(User user, AssaySpecimenConfigImpl config) + { + return Table.insert(user, StudyDesignSchema.getInstance().getTableInfoAssaySpecimen(), config); + } + + /**** + * + * + * + * TESTING + * + * + */ + + @TestWhen(TestWhen.When.BVT) + public static class TreatmentDataTestCase extends Assert + { + TestContext _context = null; + User _user = null; + Container _container = null; + Study _junitStudy = null; + TreatmentManager _manager = TreatmentManager.getInstance(); + UserSchema _schema = null; + + Map _lookups = new HashMap<>(); + List _products = new ArrayList<>(); + List _treatments = new ArrayList<>(); + List _cohorts = new ArrayList<>(); + List _visits = new ArrayList<>(); + + @Test + public void test() throws Throwable + { + try + { + createStudy(); + _user = _context.getUser(); + _container = _junitStudy.getContainer(); + _schema = QueryService.get().getUserSchema(_user, _container, StudyDesignQuerySchema.STUDY_SCHEMA_NAME); + + populateLookupTables(); + populateStudyProducts(); + populateTreatments(); + populateTreatmentSchedule(); + + verifyStudyProducts(); + verifyTreatments(); + verifyTreatmentSchedule(); + verifyCleanUpTreatmentData(); + } + finally + { + tearDown(); + } + } + + private void verifyCleanUpTreatmentData() throws Exception + { + // remove cohort and verify delete of TreatmentVisitMap + CohortService.get().deleteCohort(_cohorts.get(0)); + verifyTreatmentVisitMapRecords(4); + + // remove visit and verify delete of TreatmentVisitMap + VisitService.get().deleteVisit(_junitStudy, _user, _visits.get(0)); + verifyTreatmentVisitMapRecords(2); + + // we should still have all of our treatments and study products + verifyTreatments(); + verifyStudyProducts(); + + // remove treatment visit map records via visit + _manager.deleteTreatmentVisitMapForVisit(_container, _visits.get(1).getId()); + verifyTreatmentVisitMapRecords(0); + + // remove treatment and verify delete of TreatmentProductMap + _manager.deleteTreatment(_container, _user, _treatments.get(0).getRowId()); + verifyTreatmentProductMapRecords(_treatments.get(0).getRowId(), 0); + verifyTreatmentProductMapRecords(_treatments.get(1).getRowId(), 4); + + // remove product and verify delete of TreatmentProductMap and ProductAntigen + _manager.deleteStudyProduct(_container, _user, _products.get(0).getRowId()); + verifyTreatmentProductMapRecords(_treatments.get(1).getRowId(), 3); + verifyStudyProductAntigens(_products.get(0).getRowId(), 0); + verifyStudyProductAntigens(_products.get(1).getRowId(), 1); + + // directly delete product antigen + _manager.deleteProductAntigens(_container, _user, _products.get(1).getRowId()); + verifyStudyProductAntigens(_products.get(1).getRowId(), 0); + + // delete treatment product map by productId and then treatmentId + SimpleFilter filter = SimpleFilter.createContainerFilter(_container); + filter.addCondition(FieldKey.fromParts("ProductId"), _products.get(1).getRowId()); + _manager.deleteTreatmentProductMap(_container, _user, filter); + verifyTreatmentProductMapRecords(_treatments.get(1).getRowId(), 2); + filter = SimpleFilter.createContainerFilter(_container); + filter.addCondition(FieldKey.fromParts("TreatmentId"), _treatments.get(1).getRowId()); + _manager.deleteTreatmentProductMap(_container, _user, filter); + verifyTreatmentProductMapRecords(_treatments.get(1).getRowId(), 0); + } + + private void verifyTreatmentSchedule() + { + verifyTreatmentVisitMapRecords(8); + + _visits.add(VisitService.get().createVisit(_junitStudy, _user, BigDecimal.valueOf(3.0), "Visit 3", Visit.Type.FINAL_VISIT)); + assertEquals("Unexpected number of treatment schedule visits", 2, _manager.getVisitsForTreatmentSchedule(_container).size()); + } + + private void verifyTreatments() + { + List treatments = _manager.getStudyTreatments(_container, _user); + assertEquals("Unexpected study treatment count", 2, treatments.size()); + + for (TreatmentImpl treatment : treatments) + { + verifyTreatmentProductMapRecords(treatment.getRowId(), 4); + + treatment = _manager.getStudyTreatmentByRowId(_container, _user, treatment.getRowId()); + assertEquals("Unexpected number of treatment products", 4, treatment.getProducts().size()); + + for (ProductImpl product : treatment.getProducts()) + { + assertEquals("Unexpected product dose value", "Test Dose", product.getDose()); + assertEquals("Unexpected product route value", _lookups.get("Route"), product.getRoute()); + } + } + + } + + private void verifyStudyProducts() + { + List products = _manager.getStudyProducts(_container, _user); + assertEquals("Unexpected study product count", 4, products.size()); + + for (ProductImpl product : products) + verifyStudyProductAntigens(product.getRowId(), 1); + + assertEquals("Unexpected study product count by role", 2, _manager.getStudyProducts(_container, _user, "Immunogen", null).size()); + assertEquals("Unexpected study product count by role", 2, _manager.getStudyProducts(_container, _user, "Adjuvant", null).size()); + assertEquals("Unexpected study product count by role", 0, _manager.getStudyProducts(_container, _user, "UNK", null).size()); + + for (ProductImpl immunogen : _manager.getStudyProducts(_container, _user, "Immunogen", null)) + assertEquals("Unexpected product lookup value", _lookups.get("ImmunogenType"), immunogen.getType()); + } + + private void populateTreatmentSchedule() throws ValidationException + { + _cohorts.add(CohortService.get().createCohort(_junitStudy, _user, "Cohort1", true, 10, null)); + _cohorts.add(CohortService.get().createCohort(_junitStudy, _user, "Cohort2", true, 20, null)); + assertEquals(_cohorts.size(), 2); + + _visits.add(VisitService.get().createVisit(_junitStudy, _user, BigDecimal.valueOf(1.0), "Visit 1", Visit.Type.BASELINE)); + _visits.add(VisitService.get().createVisit(_junitStudy, _user, BigDecimal.valueOf(2.0), "Visit 2", Visit.Type.SCHEDULED_FOLLOWUP)); + assertEquals(_visits.size(), 2); + + for (Cohort cohort : _cohorts) + { + for (Visit visit : _visits) + { + for (TreatmentImpl treatment : _treatments) + { + _manager.insertTreatmentVisitMap(_user, _container, cohort.getRowId(), visit.getId(), treatment.getRowId()); + } + } + } + + verifyTreatmentVisitMapRecords(_cohorts.size() * _visits.size() * _treatments.size()); + } + + private void populateTreatments() + { + TableInfo treatmentTable = _schema.getTable(StudyDesignQuerySchema.TREATMENT_TABLE_NAME); + if (treatmentTable != null) + { + TableInfo ti = ((FilteredTable)treatmentTable).getRealTable(); + + TreatmentImpl treatment1 = new TreatmentImpl(_container, "Treatment1", "Treatment1 description"); + treatment1 = Table.insert(_user, ti, treatment1); + addProductsForTreatment(treatment1.getRowId()); + _treatments.add(treatment1); + + TreatmentImpl treatment2 = new TreatmentImpl(_container, "Treatment2", "Treatment2 description"); + treatment2 = Table.insert(_user, ti, treatment2); + addProductsForTreatment(treatment2.getRowId()); + _treatments.add(treatment2); + } + + assertEquals(_treatments.size(), 2); + } + + private void addProductsForTreatment(int treatmentId) + { + TableInfo treatmentProductTable = _schema.getTable(StudyDesignQuerySchema.TREATMENT_PRODUCT_MAP_TABLE_NAME); + if (treatmentProductTable != null) + { + TableInfo ti = ((FilteredTable)treatmentProductTable).getRealTable(); + + for (ProductImpl product : _products) + { + TreatmentProductImpl tp = new TreatmentProductImpl(_container, treatmentId, product.getRowId()); + tp.setDose("Test Dose"); + tp.setRoute(_lookups.get("Route")); + Table.insert(_user, ti, tp); + } + } + + verifyTreatmentProductMapRecords(treatmentId, _products.size()); + } + + private void populateStudyProducts() + { + TableInfo productTable = _schema.getTable(StudyDesignQuerySchema.PRODUCT_TABLE_NAME); + if (productTable != null) + { + TableInfo ti = ((FilteredTable)productTable).getRealTable(); + + ProductImpl product1 = new ProductImpl(_container, "Immunogen1", "Immunogen"); + product1.setType(_lookups.get("ImmunogenType")); + _products.add(Table.insert(_user, ti, product1)); + + ProductImpl product2 = new ProductImpl(_container, "Immunogen2", "Immunogen"); + product2.setType(_lookups.get("ImmunogenType")); + _products.add(Table.insert(_user, ti, product2)); + + ProductImpl product3 = new ProductImpl(_container, "Adjuvant1", "Adjuvant"); + _products.add(Table.insert(_user, ti, product3)); + + ProductImpl product4 = new ProductImpl(_container, "Adjuvant2", "Adjuvant"); + _products.add(Table.insert(_user, ti, product4)); + } + + assertEquals(_products.size(), 4); + + for (ProductImpl product : _products) + addAntigenToProduct(product.getRowId()); + } + + private void addAntigenToProduct(int productId) + { + TableInfo productAntigenTable = _schema.getTable(StudyDesignQuerySchema.PRODUCT_ANTIGEN_TABLE_NAME); + if (productAntigenTable != null) + { + TableInfo ti = ((FilteredTable)productAntigenTable).getRealTable(); + + ProductAntigenImpl productAntigen = new ProductAntigenImpl(_container, productId, _lookups.get("Gene"), _lookups.get("SubType")); + Table.insert(_user, ti, productAntigen); + } + + verifyStudyProductAntigens(productId, 1); + } + + private void populateLookupTables() + { + String name, label; + + Map data = new HashMap<>(); + data.put("Container", _container.getId()); + + data.put("Name", name = "Test Immunogen Type"); + data.put("Label", label = "Test Immunogen Type Label"); + Table.insert(_user, StudyDesignSchema.getInstance().getTableInfoStudyDesignImmunogenTypes(), data); + assertEquals("Unexpected study design lookup label", label, _manager.getStudyDesignImmunogenTypeLabelByName(_container, name)); + assertNull("Unexpected study design lookup label", _manager.getStudyDesignImmunogenTypeLabelByName(_container, "UNK")); + _lookups.put("ImmunogenType", name); + + data.put("Name", name = "Test Gene"); + data.put("Label", label = "Test Gene Label"); + Table.insert(_user, StudyDesignSchema.getInstance().getTableInfoStudyDesignGenes(), data); + assertEquals("Unexpected study design lookup label", label, _manager.getStudyDesignGeneLabelByName(_container, name)); + assertNull("Unexpected study design lookup label", _manager.getStudyDesignGeneLabelByName(_container, "UNK")); + _lookups.put("Gene", name); + + data.put("Name", name = "Test SubType"); + data.put("Label", label = "Test SubType Label"); + Table.insert(_user, StudyDesignSchema.getInstance().getTableInfoStudyDesignSubTypes(), data); + assertEquals("Unexpected study design lookup label", label, _manager.getStudyDesignSubTypeLabelByName(_container, name)); + assertNull("Unexpected study design lookup label", _manager.getStudyDesignSubTypeLabelByName(_container, "UNK")); + _lookups.put("SubType", name); + + data.put("Name", name = "Test Route"); + data.put("Label", label = "Test Route Label"); + Table.insert(_user, StudyDesignSchema.getInstance().getTableInfoStudyDesignRoutes(), data); + assertEquals("Unexpected study design lookup label", label, _manager.getStudyDesignRouteLabelByName(_container, name)); + assertNull("Unexpected study design lookup label", _manager.getStudyDesignRouteLabelByName(_container, "UNK")); + _lookups.put("Route", name); + + assertEquals(_lookups.keySet().size(), 4); + } + + private void verifyTreatmentVisitMapRecords(int expectedCount) + { + List rows = _manager.getStudyTreatmentVisitMap(_container, null); + assertEquals("Unexpected number of study.TreatmentVisitMap rows", expectedCount, rows.size()); + } + + private void verifyTreatmentProductMapRecords(int treatmentId, int expectedCount) + { + List rows = _manager.getStudyTreatmentProducts(_container, _user, treatmentId); + assertEquals("Unexpected number of study.TreatmentProductMap rows", expectedCount, rows.size()); + } + + private void verifyStudyProductAntigens(int productId, int expectedCount) + { + List rows = _manager.getStudyProductAntigens(_container, _user, productId); + assertEquals("Unexpected number of study.ProductAntigen rows", expectedCount, rows.size()); + + for (ProductAntigenImpl row : rows) + { + assertEquals("Unexpected antigen lookup value", _lookups.get("Gene"), row.getGene()); + assertEquals("Unexpected antigen lookup value", _lookups.get("SubType"), row.getSubType()); + } + } + + private void createStudy() + { + _context = TestContext.get(); + Container junit = JunitUtil.getTestContainer(); + + String name = GUID.makeHash(); + Container c = ContainerManager.createContainer(junit, name, _context.getUser()); + Set modules = new HashSet<>(c.getActiveModules()); + modules.add(ModuleLoader.getInstance().getModule("studydesign")); + c.setActiveModules(modules); + _junitStudy = StudyService.get().createStudy(c, _context.getUser(), "Junit Study", TimepointType.VISIT, true); + } + + private void tearDown() + { + if (null != _junitStudy) + { + assertTrue(ContainerManager.delete(_junitStudy.getContainer(), _context.getUser())); + } + } + } + + @TestWhen(TestWhen.When.BVT) + public static class AssayScheduleTestCase extends Assert + { + TestContext _context = null; + User _user = null; + Container _container = null; + Study _junitStudy = null; + + Map _lookups = new HashMap<>(); + List _assays = new ArrayList<>(); + List _visits = new ArrayList<>(); + + @Test + public void test() + { + try + { + createStudy(); + _user = _context.getUser(); + _container = _junitStudy.getContainer(); + + populateLookupTables(); + populateAssayConfigurations(); + populateAssaySchedule(); + + verifyAssayConfigurations(); + verifyAssaySchedule(); + verifyCleanUpAssayConfigurations(); + } + finally + { + tearDown(); + } + } + + private void verifyCleanUpAssayConfigurations() + { + StudyDesignService.get().deleteAssaySpecimenVisits(_container, _visits.get(0).getId()); + verifyAssayScheduleRowCount(2); + assertEquals(1, TreatmentManager.getInstance().getAssaySpecimenVisitIds(_container, _assays.get(0)).size()); + assertEquals(1, TreatmentManager.getInstance().getVisitsForAssaySchedule(_container).size()); + + StudyDesignService.get().deleteAssaySpecimenVisits(_container, _visits.get(1).getId()); + verifyAssayScheduleRowCount(0); + assertEquals(0, TreatmentManager.getInstance().getAssaySpecimenVisitIds(_container, _assays.get(0)).size()); + assertEquals(0, TreatmentManager.getInstance().getVisitsForAssaySchedule(_container).size()); + } + + private void verifyAssaySchedule() + { + verifyAssayScheduleRowCount(4); + + List visits = TreatmentManager.getInstance().getVisitsForAssaySchedule(_container); + assertEquals("Unexpected assay schedule visit count", 2, visits.size()); + + for (AssaySpecimenConfig assay : StudyDesignService.get().getAssaySpecimenConfigs(_container)) + { + List visitIds = TreatmentManager.getInstance().getAssaySpecimenVisitIds(_container, assay); + for (Visit visit : _visits) + assertTrue("Assay schedule does not contain expected visitId", visitIds.contains(visit.getId())); + } + } + + private void verifyAssayScheduleRowCount(int expectedCount) + { + TableSelector selector = new TableSelector(StudyDesignSchema.getInstance().getTableInfoAssaySpecimenVisit(), SimpleFilter.createContainerFilter(_container), null); + assertEquals("Unexpected number of assay schedule visit records", expectedCount, selector.getRowCount()); + } + + private void verifyAssayConfigurations() + { + Collection assays = StudyDesignService.get().getAssaySpecimenConfigs(_container); + assertEquals("Unexpected assay configuration count", 2, assays.size()); + + for (AssaySpecimenConfig assay : assays) + { + if (assay instanceof AssaySpecimenConfigImpl assayConfig) + { + assertEquals("Unexpected assay configuration lookup value", _lookups.get("Lab"), assayConfig.getLab()); + assertEquals("Unexpected assay configuration lookup value", _lookups.get("SampleType"), assayConfig.getSampleType()); + } + } + } + + private void populateAssaySchedule() + { + _visits.add(VisitService.get().createVisit(_junitStudy, _user, BigDecimal.valueOf(1.0), "Visit 1", Visit.Type.BASELINE)); + _visits.add(VisitService.get().createVisit(_junitStudy, _user, BigDecimal.valueOf(2.0), "Visit 2", Visit.Type.SCHEDULED_FOLLOWUP)); + assertEquals(_visits.size(), 2); + + for (AssaySpecimenConfigImpl assay : _assays) + { + for (Visit visit : _visits) + { + AssaySpecimenVisitImpl asv = new AssaySpecimenVisitImpl(_container, assay.getRowId(), visit.getId()); + Table.insert(_user, StudyDesignSchema.getInstance().getTableInfoAssaySpecimenVisit(), asv); + } + } + + verifyAssayScheduleRowCount(_assays.size() * _visits.size()); + } + + private void populateAssayConfigurations() + { + AssaySpecimenConfigImpl assay1 = new AssaySpecimenConfigImpl(_container, "Assay1", "Assay 1 description"); + assay1.setLab(_lookups.get("Lab")); + assay1.setSampleType(_lookups.get("SampleType")); + _assays.add(TreatmentManager.getInstance().addAssaySpecimenConfig(_user, assay1)); + + AssaySpecimenConfigImpl assay2 = new AssaySpecimenConfigImpl(_container, "Assay2", "Assay 2 description"); + assay2.setLab(_lookups.get("Lab")); + assay2.setSampleType(_lookups.get("SampleType")); + _assays.add(TreatmentManager.getInstance().addAssaySpecimenConfig(_user, assay2)); + + assertEquals(2, _assays.size()); + } + + private void populateLookupTables() + { + String name, label; + + Map data = new HashMap<>(); + data.put("Container", _container.getId()); + + data.put("Name", name = "Test Lab"); + data.put("Label", label = "Test Lab Label"); + Table.insert(_user, StudyDesignSchema.getInstance().getTableInfoStudyDesignLabs(), data); + assertEquals("Unexpected study design lookup label", label, TreatmentManager.getInstance().getStudyDesignLabLabelByName(_container, name)); + assertNull("Unexpected study design lookup label", TreatmentManager.getInstance().getStudyDesignLabLabelByName(_container, "UNK")); + _lookups.put("Lab", name); + + data.put("Name", name = "Test Sample Type"); + data.put("Label", label = "Test Sample Type Label"); + data.put("PrimaryType", "Test Primary Type"); + data.put("ShortSampleCode", "TP"); + Table.insert(_user, StudyDesignSchema.getInstance().getTableInfoStudyDesignSampleTypes(), data); + _lookups.put("SampleType", name); + } + + private void createStudy() + { + _context = TestContext.get(); + Container junit = JunitUtil.getTestContainer(); + + String name = GUID.makeHash(); + Container c = ContainerManager.createContainer(junit, name, _context.getUser()); + _junitStudy = StudyService.get().createStudy(c, _context.getUser(), "Junit Study", TimepointType.VISIT, true); + } + + private void tearDown() + { + if (null != _junitStudy) + { + assertTrue(ContainerManager.delete(_junitStudy.getContainer(), _context.getUser())); + } + } + } +} diff --git a/studydesign/src/org/labkey/studydesign/model/TreatmentProductImpl.java b/studydesign/src/org/labkey/studydesign/model/TreatmentProductImpl.java new file mode 100644 index 00000000..c2c5e882 --- /dev/null +++ b/studydesign/src/org/labkey/studydesign/model/TreatmentProductImpl.java @@ -0,0 +1,255 @@ +/* + * Copyright (c) 2013-2018 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.labkey.studydesign.model; + +import org.jetbrains.annotations.NotNull; +import org.json.JSONObject; +import org.labkey.api.data.Container; +import org.labkey.api.study.TreatmentProduct; +import org.labkey.api.util.Pair; + +import java.util.HashMap; +import java.util.Map; + +/** + * User: cnathe + * Date: 12/27/13 + */ +public class TreatmentProductImpl implements TreatmentProduct +{ + public static final String PRODUCT_DOSE_DELIMITER = "-#-"; + private Container _container; + private int _rowId; + private int _treatmentId; + private int _productId; + private String _dose; + private String _route; + private String _doseAndRoute; + private String _productDoseRoute; + + public TreatmentProductImpl() + {} + + public TreatmentProductImpl(Container container, int treatmentId, int productId) + { + _container = container; + _treatmentId = treatmentId; + _productId = productId; + } + + public boolean isNew() + { + return _rowId == 0; + } + + public Object getPrimaryKey() + { + return getRowId(); + } + + @Override + public int getRowId() + { + return _rowId; + } + + public void setRowId(int rowId) + { + _rowId = rowId; + } + + @Override + public int getTreatmentId() + { + return _treatmentId; + } + + public void setTreatmentId(int treatmentId) + { + _treatmentId = treatmentId; + } + + @Override + public int getProductId() + { + return _productId; + } + + public void setProductId(int productId) + { + _productId = productId; + } + + @Override + public String getDose() + { + return _dose; + } + + public void setDose(String dose) + { + _dose = dose; + } + + @Override + public String getRoute() + { + return _route; + } + + public void setRoute(String route) + { + _route = route; + } + + public Container getContainer() + { + return _container; + } + + public void setContainer(Container container) + { + _container = container; + } + + public String getDoseAndRoute() + { + return _doseAndRoute; + } + + public void setDoseAndRoute(String doseAndRoute) + { + _doseAndRoute = doseAndRoute; + } + + public Map serialize() + { + syncDoseAndRoute(); + Map props = new HashMap<>(); + props.put("RowId", getRowId()); + props.put("TreatmentId", getTreatmentId()); + props.put("ProductId", getProductId()); + props.put("Dose", getDose()); + props.put("Route", getRoute()); + props.put("DoseAndRoute", getDoseAndRoute()); + props.put("ProductDoseRoute", getProductDoseRoute()); + + return props; + } + + /** + * Keeps the dose, route, and doseAndRoute fields synchronized + */ + private void syncDoseAndRoute() + { + if (getDoseAndRoute() == null && (getDose() != null || getRoute() != null) && getProductId() > 0) + { + // get the entry from the DoseAndRoute table so we can serialize the label + DoseAndRoute doseAndRoute = TreatmentManager.getInstance().getDoseAndRoute(getContainer(), getDose(), getRoute(), getProductId()); + if (doseAndRoute != null) + { + setDoseAndRoute(doseAndRoute.getLabel()); + setProductDoseRoute(String.valueOf(getProductId() + PRODUCT_DOSE_DELIMITER + doseAndRoute.getLabel())); + } + } + else if (getDoseAndRoute() != null && getDose() == null && getRoute() == null) + { + Pair parts = DoseAndRoute.parseFromLabel(getDoseAndRoute()); + if (parts != null) + { + DoseAndRoute doseAndRoute = TreatmentManager.getInstance().getDoseAndRoute(getContainer(), parts.getKey(), parts.getValue(), getProductId()); + if (doseAndRoute != null) + { + setDose(doseAndRoute.getDose()); + setRoute(doseAndRoute.getRoute()); + } + } + } + else if (getDoseAndRoute() == null && getDose() == null && getRoute() == null && getProductId() > 0) + { + setProductDoseRoute(String.valueOf(getProductId() + PRODUCT_DOSE_DELIMITER)); + } + } + + public static TreatmentProductImpl fromJSON(@NotNull JSONObject o, Container container) + { + TreatmentProductImpl treatmentProduct = new TreatmentProductImpl(); + //treatmentProduct.setDose(o.getString("Dose")); + //treatmentProduct.setRoute(o.getString("Route")); + treatmentProduct.setContainer(container); + if (o.has("ProductId") && o.get("ProductId") instanceof Integer productId) + treatmentProduct.setProductId(productId); + if (o.has("TreatmentId") && o.get("TreatmentId") instanceof Integer treatmentId) + treatmentProduct.setTreatmentId(treatmentId); + if (o.has("RowId")) + treatmentProduct.setRowId(o.getInt("RowId")); + if (o.has("DoseAndRoute")) + treatmentProduct.setDoseAndRoute(o.getString("DoseAndRoute")); + if (o.has("ProductDoseRoute")) + treatmentProduct.populateProductDoseRoute(o.getString("ProductDoseRoute")); + + return treatmentProduct; + } + + public void setProductDoseRoute(String productDoseRoute) + { + _productDoseRoute = productDoseRoute; + } + + public String getProductDoseRoute() + { + return _productDoseRoute; + } + + private void populateProductDoseRoute(String productDoseRoute) + { + if (productDoseRoute == null) + return; + setProductDoseRoute(productDoseRoute); + String[] parts = productDoseRoute.split(PRODUCT_DOSE_DELIMITER); + setProductId(Integer.parseInt(parts[0])); + if (parts.length > 1) + setDoseAndRoute(parts[1]); + } + + public boolean isSameTreatmentProductWith(TreatmentProductImpl other) + { + if (other == null) + return false; + if (this.getProductId() != other.getProductId()) + return false; + if (this.getProductDoseRoute() != null && other.getProductDoseRoute() != null) + return this.getProductDoseRoute().equals(other.getProductDoseRoute()); + + if (this.getDose() != null) + { + if (other.getDose() == null || !this.getDose().equals(other.getDose())) + return false; + } + else if (other.getDose() != null) + return false; + + if (this.getRoute() != null) + { + if (other.getRoute() == null || !this.getRoute().equals(other.getRoute())) + return false; + } + else if (other.getRoute() != null) + return false; + + return true; + } +} diff --git a/studydesign/src/org/labkey/studydesign/model/TreatmentVisitMap.java b/studydesign/src/org/labkey/studydesign/model/TreatmentVisitMap.java new file mode 100644 index 00000000..3c5107c5 --- /dev/null +++ b/studydesign/src/org/labkey/studydesign/model/TreatmentVisitMap.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2014 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.labkey.studydesign.model; + +/** + * User: cnathe + * Date: 12/30/13 + */ +public interface TreatmentVisitMap +{ + int getCohortId(); + int getTreatmentId(); + int getVisitId(); +} diff --git a/studydesign/src/org/labkey/studydesign/model/TreatmentVisitMapImpl.java b/studydesign/src/org/labkey/studydesign/model/TreatmentVisitMapImpl.java new file mode 100644 index 00000000..78b51362 --- /dev/null +++ b/studydesign/src/org/labkey/studydesign/model/TreatmentVisitMapImpl.java @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2014-2016 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.labkey.studydesign.model; + +import org.jetbrains.annotations.NotNull; +import org.json.JSONObject; +import org.labkey.api.data.Container; + +/** + * User: cnathe + * Date: 12/30/13 + */ +public class TreatmentVisitMapImpl implements TreatmentVisitMap +{ + private int _cohortId; + private int _treatmentId; + private String _tempTreatmentId; // used to map new treatment records used in mappings to the tempRowId in TreatmentImpl + private int _visitId; + private Container _container; + + public TreatmentVisitMapImpl() + { + } + + @Override + public int getCohortId() + { + return _cohortId; + } + + public void setCohortId(int cohortId) + { + _cohortId = cohortId; + } + + @Override + public int getTreatmentId() + { + return _treatmentId; + } + + public void setTreatmentId(int treatmentId) + { + _treatmentId = treatmentId; + } + + public String getTempTreatmentId() + { + return _tempTreatmentId; + } + + private void setTempTreatmentId(String tempTreatmentId) + { + _tempTreatmentId = tempTreatmentId; + } + + @Override + public int getVisitId() + { + return _visitId; + } + + public void setVisitId(int visitId) + { + _visitId = visitId; + } + + public Container getContainer() + { + return _container; + } + + public void setContainer(Container container) + { + _container = container; + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) return true; + if (obj == null || getClass() != obj.getClass()) return false; + + final TreatmentVisitMapImpl o = (TreatmentVisitMapImpl) obj; + + return o.getCohortId() == getCohortId() + && o.getTreatmentId() == getTreatmentId() + && o.getVisitId() == getVisitId() + && ((o.getContainer() == null && getContainer() == null) || o.getContainer().equals(getContainer())); + } + + public static TreatmentVisitMapImpl fromJSON(@NotNull JSONObject o) + { + TreatmentVisitMapImpl visitMap = new TreatmentVisitMapImpl(); + visitMap.setVisitId(o.getInt("VisitId")); + if (o.has("CohortId")) + visitMap.setCohortId(o.getInt("CohortId")); + if (o.has("TreatmentId")) + { + if (o.get("TreatmentId") instanceof Integer) + visitMap.setTreatmentId(o.getInt("TreatmentId")); + else + visitMap.setTempTreatmentId(o.getString("TreatmentId")); + } + + return visitMap; + } +} diff --git a/studydesign/src/org/labkey/studydesign/view/AssayScheduleWebpartFactory.java b/studydesign/src/org/labkey/studydesign/view/AssayScheduleWebpartFactory.java new file mode 100644 index 00000000..7295b36b --- /dev/null +++ b/studydesign/src/org/labkey/studydesign/view/AssayScheduleWebpartFactory.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2013-2014 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.labkey.studydesign.view; + +import org.jetbrains.annotations.NotNull; +import org.labkey.api.data.Container; +import org.labkey.api.study.Study; +import org.labkey.api.study.StudyService; +import org.labkey.api.study.StudyUrls; +import org.labkey.api.study.TimepointType; +import org.labkey.api.study.security.permissions.ManageStudyPermission; +import org.labkey.api.util.PageFlowUtil; +import org.labkey.api.view.JspView; +import org.labkey.api.view.NavTree; +import org.labkey.api.view.Portal; +import org.labkey.api.view.ViewContext; +import org.labkey.api.view.WebPartView; + +/** + * User: cnathe + * Date: 12/16/13 + */ +public class AssayScheduleWebpartFactory extends StudyDesignWebpartFactory +{ + public static String NAME = "Assay Schedule"; + + public AssayScheduleWebpartFactory() + { + super(NAME); + } + + @Override + public WebPartView getWebPartView(@NotNull ViewContext portalCtx, @NotNull Portal.WebPart webPart) + { + if (!canShow(portalCtx.getContainer())) + return null; + + JspView view = new JspView<>("/org/labkey/studydesign/view/assayScheduleWebpart.jsp", webPart); + view.setTitle(NAME); + view.setFrame(WebPartView.FrameType.PORTAL); + + Container c = portalCtx.getContainer(); + Study study = StudyService.get().getStudy(c); + if (c.hasPermission(portalCtx.getUser(), ManageStudyPermission.class)) + { + String timepointMenuName; + if (study != null && study.getTimepointType() == TimepointType.DATE) + timepointMenuName = "Manage Timepoints"; + else + timepointMenuName = "Manage Visits"; + + NavTree menu = new NavTree(); + menu.addChild(timepointMenuName, PageFlowUtil.urlProvider(StudyUrls.class).getManageVisitsURL(c)); + view.setNavMenu(menu); + } + + return view; + } +} diff --git a/studydesign/src/org/labkey/studydesign/view/ImmunizationScheduleWebpartFactory.java b/studydesign/src/org/labkey/studydesign/view/ImmunizationScheduleWebpartFactory.java new file mode 100644 index 00000000..abfe3e25 --- /dev/null +++ b/studydesign/src/org/labkey/studydesign/view/ImmunizationScheduleWebpartFactory.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2013-2014 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.labkey.studydesign.view; + +import org.jetbrains.annotations.NotNull; +import org.labkey.api.data.Container; +import org.labkey.api.study.Study; +import org.labkey.api.study.StudyService; +import org.labkey.api.study.StudyUrls; +import org.labkey.api.study.TimepointType; +import org.labkey.api.study.security.permissions.ManageStudyPermission; +import org.labkey.api.util.PageFlowUtil; +import org.labkey.api.view.JspView; +import org.labkey.api.view.NavTree; +import org.labkey.api.view.Portal; +import org.labkey.api.view.ViewContext; +import org.labkey.api.view.WebPartView; + +/** + * User: cnathe + * Date: 12/30/13 + */ +public class ImmunizationScheduleWebpartFactory extends StudyDesignWebpartFactory +{ + public static String NAME = "Immunization Schedule"; + + public ImmunizationScheduleWebpartFactory() + { + super(NAME); + } + + @Override + public WebPartView getWebPartView(@NotNull ViewContext portalCtx, @NotNull Portal.WebPart webPart) + { + if (!canShow(portalCtx.getContainer())) + return null; + + JspView view = new JspView<>("/org/labkey/studydesign/view/immunizationScheduleWebpart.jsp", webPart); + view.setTitle(NAME); + view.setFrame(WebPartView.FrameType.PORTAL); + + Container c = portalCtx.getContainer(); + Study study = StudyService.get().getStudy(c); + if (c.hasPermission(portalCtx.getUser(), ManageStudyPermission.class)) + { + String timepointMenuName; + if (study != null && study.getTimepointType() == TimepointType.DATE) + timepointMenuName = "Manage Timepoints"; + else + timepointMenuName = "Manage Visits"; + + NavTree menu = new NavTree(); + menu.addChild("Manage Cohorts", PageFlowUtil.urlProvider(StudyUrls.class).getManageCohortsURL(c)); + menu.addChild(timepointMenuName, PageFlowUtil.urlProvider(StudyUrls.class).getManageVisitsURL(c)); + view.setNavMenu(menu); + } + + return view; + } +} diff --git a/studydesign/src/org/labkey/studydesign/view/StudyDesignConfigureMenuItem.java b/studydesign/src/org/labkey/studydesign/view/StudyDesignConfigureMenuItem.java new file mode 100644 index 00000000..b0b96d8e --- /dev/null +++ b/studydesign/src/org/labkey/studydesign/view/StudyDesignConfigureMenuItem.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2017 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.labkey.studydesign.view; + +import org.labkey.api.data.Container; +import org.labkey.api.query.QueryUrls; +import org.labkey.api.util.PageFlowUtil; +import org.labkey.api.view.ActionURL; +import org.labkey.api.view.NavTree; + +public class StudyDesignConfigureMenuItem extends NavTree +{ + public StudyDesignConfigureMenuItem(String text, String schemaName, String queryName, Container container) + { + super(text); + + ActionURL url = new ActionURL(); + url.setContainer(container); + url.addParameter("schemaName", schemaName); + url.addParameter("query.queryName", queryName); + setHref(PageFlowUtil.urlProvider(QueryUrls.class).urlExecuteQuery(url).toString()); + setTarget("_blank"); // issue 19493 + } +} diff --git a/studydesign/src/org/labkey/studydesign/view/StudyDesignWebpartFactory.java b/studydesign/src/org/labkey/studydesign/view/StudyDesignWebpartFactory.java new file mode 100644 index 00000000..b4cfa04c --- /dev/null +++ b/studydesign/src/org/labkey/studydesign/view/StudyDesignWebpartFactory.java @@ -0,0 +1,24 @@ +package org.labkey.studydesign.view; + +import org.labkey.api.data.Container; +import org.labkey.api.studydesign.StudyDesignManager; +import org.labkey.api.view.BaseWebPartFactory; + +public abstract class StudyDesignWebpartFactory extends BaseWebPartFactory +{ + public StudyDesignWebpartFactory(String name) + { + super(name); + } + + protected boolean canShow(Container c) + { + return StudyDesignManager.get().isModuleActive(c); + } + + @Override + public boolean isAvailable(Container c, String scope, String location) + { + return canShow(c) ? super.isAvailable(c, scope, location) : false; + } +} \ No newline at end of file diff --git a/studydesign/src/org/labkey/studydesign/view/VaccineDesignWebpartFactory.java b/studydesign/src/org/labkey/studydesign/view/VaccineDesignWebpartFactory.java new file mode 100644 index 00000000..0522c2ee --- /dev/null +++ b/studydesign/src/org/labkey/studydesign/view/VaccineDesignWebpartFactory.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2013-2014 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.labkey.studydesign.view; + +import org.jetbrains.annotations.NotNull; +import org.labkey.api.view.JspView; +import org.labkey.api.view.Portal; +import org.labkey.api.view.ViewContext; +import org.labkey.api.view.WebPartView; + +/** + * User: cnathe + * Date: 12/27/13 + */ +public class VaccineDesignWebpartFactory extends StudyDesignWebpartFactory +{ + public static String NAME = "Vaccine Design"; + + public VaccineDesignWebpartFactory() + { + super(NAME); + } + + @Override + public WebPartView getWebPartView(@NotNull ViewContext portalCtx, @NotNull Portal.WebPart webPart) + { + if (!canShow(portalCtx.getContainer())) + return null; + + JspView view = new JspView<>("/org/labkey/studydesign/view/vaccineDesignWebpart.jsp", webPart); + view.setTitle(NAME); + view.setFrame(WebPartView.FrameType.PORTAL); + return view; + } +} diff --git a/studydesign/src/org/labkey/studydesign/view/assayScheduleWebpart.jsp b/studydesign/src/org/labkey/studydesign/view/assayScheduleWebpart.jsp new file mode 100644 index 00000000..8cb641cc --- /dev/null +++ b/studydesign/src/org/labkey/studydesign/view/assayScheduleWebpart.jsp @@ -0,0 +1,88 @@ +<% +/* + * Copyright (c) 2013-2019 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +%> +<%@ 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.security.permissions.UpdatePermission" %> +<%@ page import="org.labkey.api.study.Study" %> +<%@ page import="org.labkey.api.study.StudyService" %> +<%@ page import="org.labkey.api.util.HtmlString" %> +<%@ page import="org.labkey.api.view.ActionURL" %> +<%@ page import="org.labkey.api.view.template.ClientDependencies" %> +<%@ page import="org.labkey.studydesign.StudyDesignController" %> +<%@ page extends="org.labkey.api.jsp.JspBase" %> +<%! + @Override + public void addClientDependencies(ClientDependencies dependencies) + { + dependencies.add("study/vaccineDesign/vaccineDesign.lib.xml"); + dependencies.add("study/vaccineDesign/VaccineDesign.css"); + } +%> +<% + Container c = getContainer(); + boolean useAlternateLookupFields = getContainer().getActiveModules().contains(ModuleLoader.getInstance().getModule("rho")); + + Study study = StudyService.get().getStudy(c); + + User user = getUser(); + boolean canEdit = c.hasPermission(user, UpdatePermission.class); + + String assayPlan = ""; + if (study != null && study.getAssayPlan() != null) + assayPlan = study.getAssayPlan(); +%> +<% + if (study != null) + { + %>This section shows the assay schedule for this study.
<% + + if (canEdit) + { + ActionURL editUrl = new ActionURL(StudyDesignController.ManageAssayScheduleAction.class, getContainer()); + if (useAlternateLookupFields) + editUrl.addParameter("useAlternateLookupFields", true); + editUrl.addReturnUrl(getActionURL()); +%> + <%=link("Manage Assay Schedule", editUrl)%>
+<% + } + +%> +

<%=HtmlString.unsafe(h(assayPlan).toString().replaceAll("\n", "
"))%>

+
+<% + } + else + { +%> +

The folder must contain a study in order to display an assay schedule.

+<% + } +%> + + \ No newline at end of file diff --git a/studydesign/src/org/labkey/studydesign/view/immunizationScheduleWebpart.jsp b/studydesign/src/org/labkey/studydesign/view/immunizationScheduleWebpart.jsp new file mode 100644 index 00000000..8263de1a --- /dev/null +++ b/studydesign/src/org/labkey/studydesign/view/immunizationScheduleWebpart.jsp @@ -0,0 +1,174 @@ +<% +/* + * Copyright (c) 2013-2019 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +%> +<%@ page import="org.labkey.api.data.Container" %> +<%@ page import="org.labkey.api.security.User" %> +<%@ page import="org.labkey.api.security.permissions.UpdatePermission" %> +<%@ page import="org.labkey.api.study.Cohort" %> +<%@ page import="org.labkey.api.study.Study" %> +<%@ page import="org.labkey.api.study.StudyService" %> +<%@ page import="org.labkey.api.study.Visit" %> +<%@ page import="org.labkey.api.studydesign.StudyDesignUrls" %> +<%@ page import="org.labkey.api.util.HtmlString" %> +<%@ page import="org.labkey.api.view.ActionURL" %> +<%@ page import="org.labkey.api.view.template.ClientDependencies" %> +<%@ page import="org.labkey.studydesign.model.ProductImpl" %> +<%@ page import="org.labkey.studydesign.model.TreatmentImpl" %> +<%@ page import="org.labkey.studydesign.model.TreatmentManager" %> +<%@ page import="org.labkey.studydesign.model.TreatmentVisitMap" %> +<%@ page import="java.util.Collection" %> +<%@ page import="java.util.HashMap" %> +<%@ page import="java.util.List" %> +<%@ page import="java.util.Map" %> +<%@ page extends="org.labkey.api.jsp.JspBase" %> +<%! + @Override + public void addClientDependencies(ClientDependencies dependencies) + { + dependencies.add("study/vaccineDesign/VaccineDesign.css"); + } +%> +<% + Container c = getContainer(); + Study study = StudyService.get().getStudy(c); + + User user = getUser(); + boolean canEdit = c.hasPermission(user, UpdatePermission.class); + + String subjectNoun = "Subject"; + if (study != null) + subjectNoun = study.getSubjectNounSingular(); +%> + + + +<% + if (study != null) + { + if (!StudyService.get().showCohorts(c, user)) + { + %>

You do not have permissions to see this data.

<% + } + else + { + Collection cohorts = study.getCohorts(user); + %>This section shows the immunization schedule for this study. Each treatment may consist of one or more study products.
<% + + if (canEdit) + { + ActionURL editUrl = urlProvider(StudyDesignUrls.class).getManageTreatmentsURL(c, c.hasActiveModuleByName("viscstudies")); + editUrl.addReturnUrl(getActionURL()); +%> + <%=link("Manage Treatments", editUrl)%>
+<% + } + + List visits = TreatmentManager.getInstance().getVisitsForTreatmentSchedule(getContainer()); +%> +
+
+
Immunization Schedule
+ + + + +<% + for (Visit visit : visits) + { +%> + +<% + } +%> + +<% + if (cohorts.size() == 0) + { + %><% + } + + int index = 0; + for (Cohort cohort : cohorts) + { + index++; + List mapping = TreatmentManager.getInstance().getStudyTreatmentVisitMap(c, cohort.getRowId()); + Map visitTreatments = new HashMap<>(); + for (TreatmentVisitMap treatmentVisitMap : mapping) + { + visitTreatments.put(treatmentVisitMap.getVisitId(), treatmentVisitMap.getTreatmentId()); + } +%> + " outer-index="<%=index-1%>"> + + +<% + for (Visit visit : visits) + { + Integer treatmentId = visitTreatments.get(visit.getId()); + TreatmentImpl treatment = null; + if (treatmentId != null) + treatment = TreatmentManager.getInstance().getStudyTreatmentByRowId(c, user, treatmentId); + + // show the list of study products for the treatment as a hover + String productHover = ""; + if (treatment != null && treatment.getProducts() != null) + { + productHover += "
Group / Cohort<%=h(subjectNoun)%> Count + <%=h(visit.getDisplayString())%> + <%=(visit.getDescription() != null ? helpPopup("Description", visit.getDescription()) : HtmlString.EMPTY_STRING)%> +
No data to show.
<%=h(cohort.getLabel())%><%=h(cohort.getSubjectCount())%>
" + + "" + + "" + + ""; + + for (ProductImpl product : treatment.getProducts()) + { + String routeLabel = TreatmentManager.getInstance().getStudyDesignRouteLabelByName(c, product.getRoute()); + + productHover += "" + + "" + + ""; + } + + productHover += "
LabelDose and unitsRoute
" + h(product.getLabel()) + "" + h(product.getDose()) + "" + h(routeLabel != null ? routeLabel : product.getRoute()) + "
"; + } +%> + + <%=h(treatment != null ? treatment.getLabel() : "")%> + <%=(!productHover.isEmpty() ? helpPopup("Treatment Products", HtmlString.unsafe(productHover), 500) : HtmlString.EMPTY_STRING)%> + +<% + } +%> + +<% + } +%> + + +<% + } + } + else + { + %>

The folder must contain a study in order to display an immunization schedule.

<% + } +%> diff --git a/studydesign/src/org/labkey/studydesign/view/manageAssaySchedule.jsp b/studydesign/src/org/labkey/studydesign/view/manageAssaySchedule.jsp new file mode 100644 index 00000000..3c2c1ee3 --- /dev/null +++ b/studydesign/src/org/labkey/studydesign/view/manageAssaySchedule.jsp @@ -0,0 +1,128 @@ +<% +/* + * Copyright (c) 2014-2019 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +%> +<%@ page import="org.labkey.api.data.Container" %> +<%@ page import="org.labkey.api.portal.ProjectUrls" %> +<%@ page import="org.labkey.api.study.Study" %> +<%@ page import="org.labkey.api.study.StudyService" %> +<%@ page import="org.labkey.api.study.StudyUrls" %> +<%@ page import="org.labkey.api.study.TimepointType" %> +<%@ page import="org.labkey.api.util.PageFlowUtil" %> +<%@ page import="org.labkey.api.view.HttpView" %> +<%@ page import="org.labkey.api.view.JspView" %> +<%@ page import="org.labkey.api.view.NavTree" %> +<%@ page import="org.labkey.api.view.PopupMenuView" %> +<%@ page import="org.labkey.api.view.template.ClientDependencies" %> +<%@ page import="org.labkey.studydesign.StudyDesignController" %> +<%@ page import="org.labkey.studydesign.view.StudyDesignConfigureMenuItem" %> +<%@ page import="java.lang.Override" %> +<%@ page import="java.lang.String" %> +<%@ page extends="org.labkey.api.jsp.JspBase" %> +<%! + @Override + public void addClientDependencies(ClientDependencies dependencies) + { + dependencies.add("study/vaccineDesign/vaccineDesign.lib.xml"); + dependencies.add("study/vaccineDesign/VaccineDesign.css"); + } +%> +<% + JspView me = HttpView.currentView(); + StudyDesignController.AssayScheduleForm form = me.getModelBean(); + + Container c = getContainer(); + Study study = StudyService.get().getStudy(getContainer()); + + // assay schedule is editable for the individual studies in a Dataspace project + boolean disableEdit = c.isProject() && c.isDataspace(); + + String visitNoun = "Visit"; + if (study != null && study.getTimepointType() == TimepointType.DATE) + visitNoun = "Timepoint"; + + String returnUrl = form.getReturnUrl() != null ? form.getReturnUrl() : urlProvider(ProjectUrls.class).getBeginURL(c).toString(); +%> + + + +Enter assay schedule information in the grids below. +
+
    +
  • > + Configure dropdown options for assays, labs, sample types, and units at the project + level to be shared across study designs or within this folder for + study specific properties: + +
  • +
  • + Select the <%=h(visitNoun.toLowerCase())%>s for each assay in the schedule + portion of the grid to define the expected assay schedule for the study. +
  • +
  • > + Use the manage locationss page to further configure information about the locations for this study. + <%= link("Manage Locations", PageFlowUtil.urlProvider(StudyUrls.class).getManageLocationsURL(getContainer())) %> +
  • +
  • + Use the manage <%=h(visitNoun.toLowerCase())%>s page to further configure + information about the <%=h(visitNoun.toLowerCase())%>s for this study or to change + the <%=h(visitNoun.toLowerCase())%> display order. + <%=link("Manage " + visitNoun + "s", PageFlowUtil.urlProvider(StudyUrls.class).getManageVisitsURL(getContainer()))%> +
  • +
+
+
+
diff --git a/studydesign/src/org/labkey/studydesign/view/manageStudyProducts.jsp b/studydesign/src/org/labkey/studydesign/view/manageStudyProducts.jsp new file mode 100644 index 00000000..d88d0739 --- /dev/null +++ b/studydesign/src/org/labkey/studydesign/view/manageStudyProducts.jsp @@ -0,0 +1,135 @@ +<% +/* + * Copyright (c) 2013-2019 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +%> +<%@ page import="org.labkey.api.action.ReturnUrlForm" %> +<%@ page import="org.labkey.api.data.Container" %> +<%@ page import="org.labkey.api.portal.ProjectUrls" %> +<%@ page import="org.labkey.api.studydesign.StudyDesignUrls" %> +<%@ page import="org.labkey.api.view.ActionURL" %> +<%@ page import="org.labkey.api.view.HttpView" %> +<%@ page import="org.labkey.api.view.JspView" %> +<%@ page import="org.labkey.api.view.NavTree" %> +<%@ page import="org.labkey.api.view.PopupMenuView" %> +<%@ page import="org.labkey.api.view.template.ClientDependencies" %> +<%@ page import="org.labkey.studydesign.view.StudyDesignConfigureMenuItem" %> +<%@ page import="org.labkey.studydesign.StudyDesignController" %> +<%@ page extends="org.labkey.api.jsp.JspBase" %> +<%! + @Override + public void addClientDependencies(ClientDependencies dependencies) + { + dependencies.add("study/vaccineDesign/vaccineDesign.lib.xml"); + dependencies.add("study/vaccineDesign/VaccineDesign.css"); + } +%> +<% + JspView me = HttpView.currentView(); + ReturnUrlForm bean = me.getModelBean(); + Container c = getContainer(); + + // study products are editable at the project level for Dataspace projects + boolean isDataspaceProject = c.getProject() != null && c.getProject().isDataspace() && !c.isDataspace(); + + String returnUrl = bean.getReturnUrl() != null ? bean.getReturnUrl() : urlProvider(ProjectUrls.class).getBeginURL(c).toString(); +%> + + + +<% +if (isDataspaceProject) +{ + ActionURL projectManageProductsURL = new ActionURL(StudyDesignController.ManageStudyProductsAction.class, getContainer().getProject()); + projectManageProductsURL.addReturnUrl(getActionURL()); +%> +Vaccine design information is defined at the project level for Dataspace projects. The grids below are read-only. +
+
    +
  • + Use the manage study products page at the project level to make changes to the information listed below. + <%=link("Manage Study Products", projectManageProductsURL)%> +
  • +<% +} +else +{ +%> +Enter vaccine design information in the grids below. +
    +
      +
    • Each immunogen, adjuvant and challenge in the study should be listed on one row of the grids below.
    • +
    • Immunogens, adjuvants and challenges should have unique labels.
    • +
    • If possible, the immunogen description should include specific sequences of HIV Antigens included in the immunogen.
    • +
    • + Use the manage treatments page to describe the schedule of treatments and combinations of study products administered at each timepoint. + <% + ActionURL manageTreatmentsURL = urlProvider(StudyDesignUrls.class).getManageTreatmentsURL(c, c.hasActiveModuleByName("viscstudies")); + manageTreatmentsURL.addReturnUrl(getActionURL()); + %> + <%=link("Manage Treatments", manageTreatmentsURL)%> +
    • +<% +} +%> +
    • + Configure dropdown options for challenge types, immunogen types, genes, subtypes and routes at the project level to be shared across study designs or within this folder for + study specific properties: + +
    • +
    +
    + +
    diff --git a/studydesign/src/org/labkey/studydesign/view/manageTreatments.jsp b/studydesign/src/org/labkey/studydesign/view/manageTreatments.jsp new file mode 100644 index 00000000..b92b00f9 --- /dev/null +++ b/studydesign/src/org/labkey/studydesign/view/manageTreatments.jsp @@ -0,0 +1,169 @@ +<% +/* + * Copyright (c) 2014-2019 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +%> +<%@ page import="org.labkey.api.data.Container" %> +<%@ page import="org.labkey.api.portal.ProjectUrls" %> +<%@ page import="org.labkey.api.security.User" %> +<%@ page import="org.labkey.api.study.Study" %> +<%@ page import="org.labkey.api.study.StudyService" %> +<%@ page import="org.labkey.api.study.StudyUrls" %> +<%@ page import="org.labkey.api.study.TimepointType" %> +<%@ page import="org.labkey.api.study.Visit" %> +<%@ page import="org.labkey.api.study.security.permissions.ManageStudyPermission" %> +<%@ page import="org.labkey.api.util.PageFlowUtil" %> +<%@ page import="org.labkey.api.view.ActionURL" %> +<%@ page import="org.labkey.api.view.HttpView" %> +<%@ page import="org.labkey.api.view.JspView" %> +<%@ page import="org.labkey.api.view.template.ClientDependencies" %> +<%@ page import="java.lang.Override" %> +<%@ page import="java.lang.String" %> +<%@ page import="org.labkey.studydesign.StudyDesignController" %> +<%@ page extends="org.labkey.api.jsp.JspBase" %> +<%! + @Override + public void addClientDependencies(ClientDependencies dependencies) + { + dependencies.add("study/vaccineDesign/vaccineDesign.lib.xml"); + dependencies.add("study/vaccineDesign/VaccineDesign.css"); + } +%> +<% + JspView me = HttpView.currentView(); + StudyDesignController.ManageTreatmentsBean bean = me.getModelBean(); + + Container c = getContainer(); + User user = getUser(); + + Study study = StudyService.get().getStudy(c); + boolean canManageStudy = c.hasPermission(user, ManageStudyPermission.class); + + // treatment schedule is editable for the individual studies in a Dataspace project + boolean isDataspaceProject = c.isProject() && c.isDataspace(); + + String visitNoun = "Visit"; + if (study != null && study.getTimepointType() == TimepointType.DATE) + visitNoun = "Timepoint"; + + String subjectNoun = "Subject"; + if (study != null) + subjectNoun = study.getSubjectNounSingular(); + + String returnUrl = bean.getReturnUrl() != null ? bean.getReturnUrl() : urlProvider(ProjectUrls.class).getBeginURL(c).toString(); +%> + + + +<% +if (isDataspaceProject) +{ +%> +Treatment information is defined at the individual study level for Dataspace projects. The grids below are read-only. +

    +<% +} +else +{ +%> +Enter treatment information in the grids below. +
    +
      + <% + if (bean.isSingleTable()) + { + %> +
    • + Click on time point to define its treatment products by selecting a combination of study products. +
    • + <% + } + else + { + %> +
    • + Each treatment label must be unique and must consist of at least one study products. +
    • + <% + } + %> +
    • + Use the manage study products page to change or update the set of available values. + <% + ActionURL manageStudyProductsURL = new ActionURL(StudyDesignController.ManageStudyProductsAction.class, getContainer()); + manageStudyProductsURL.addReturnUrl(getActionURL()); + %> + <%=link("Manage Study Products", manageStudyProductsURL)%> +
    • +
    • + Each cohort label must be unique. Enter the number of <%=h(study.getSubjectNounPlural().toLowerCase())%> for + the cohort in the count column.
    • +
    • + Use the manage cohorts page to further configuration information about the cohorts for this study. + <%=link("Manage Cohorts", PageFlowUtil.urlProvider(StudyUrls.class).getManageCohortsURL(getContainer()))%> +
    • +
    • + Use the manage <%=h(visitNoun.toLowerCase())%>s page to further configure + information about the <%=h(visitNoun.toLowerCase())%>s for this study or to change + the <%=h(visitNoun.toLowerCase())%> display order. + <%=link("Manage " + visitNoun + "s", PageFlowUtil.urlProvider(StudyUrls.class).getManageVisitsURL(getContainer()))%> +
    • +<% + if (canManageStudy) + { + if (study != null && study.getTimepointType() == TimepointType.VISIT && study.getVisits(Visit.Order.DISPLAY).size() > 1) + { +%> +
    • Use the change visit order page to adjust the display order of visits in the treatment schedule table. + <%= link("Change Visit Order", PageFlowUtil.urlProvider(StudyUrls.class).getVisitOrderURL(c).addReturnUrl(getActionURL())) %> +
    • +<% + } + } +%> +
    +
    +<% +} +%> + +
    \ No newline at end of file diff --git a/studydesign/src/org/labkey/studydesign/view/vaccineDesignWebpart.jsp b/studydesign/src/org/labkey/studydesign/view/vaccineDesignWebpart.jsp new file mode 100644 index 00000000..b4deb509 --- /dev/null +++ b/studydesign/src/org/labkey/studydesign/view/vaccineDesignWebpart.jsp @@ -0,0 +1,89 @@ +<% +/* + * Copyright (c) 2013-2019 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +%> +<%@ page import="org.labkey.api.data.Container" %> +<%@ page import="org.labkey.api.security.User" %> +<%@ page import="org.labkey.api.security.permissions.UpdatePermission" %> +<%@ page import="org.labkey.api.study.Study" %> +<%@ page import="org.labkey.api.study.StudyService" %> +<%@ page import="org.labkey.api.view.ActionURL" %> +<%@ page import="org.labkey.api.view.template.ClientDependencies" %> +<%@ page import="org.labkey.studydesign.StudyDesignController" %> +<%@ page extends="org.labkey.api.jsp.JspBase" %> +<%! + @Override + public void addClientDependencies(ClientDependencies dependencies) + { + dependencies.add("study/vaccineDesign/vaccineDesign.lib.xml"); + dependencies.add("study/vaccineDesign/VaccineDesign.css"); + } +%> +<% + User user = getUser(); + Container container = getContainer(); + Study study = StudyService.get().getStudy(container); + + if (study != null) + { + %>This section describes the study products evaluated in the study.
    <% + if (container.hasPermission(user, UpdatePermission.class)) + { + ActionURL editUrl = new ActionURL(StudyDesignController.ManageStudyProductsAction.class, getContainer()); + editUrl.addReturnUrl(getActionURL()); +%> + <%=link("Manage Study Products", editUrl)%>
    +<% + } +%> +
    +
    +
    +
    +
    +
    +<% + } + else + { +%> +

    The folder must contain a study in order to display a vaccine design.

    +<% + } +%> + + \ No newline at end of file diff --git a/studydesign/webapp/study/vaccineDesign/AssaySchedule.js b/studydesign/webapp/study/vaccineDesign/AssaySchedule.js new file mode 100644 index 00000000..48c9c968 --- /dev/null +++ b/studydesign/webapp/study/vaccineDesign/AssaySchedule.js @@ -0,0 +1,659 @@ +/* + * Copyright (c) 2016-2017 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 + */ +Ext4.define('LABKEY.VaccineDesign.AssaySchedulePanel', { + extend : 'Ext.panel.Panel', + + width: 750, + + border : false, + + bodyStyle : 'background-color: transparent;', + + disableEdit : true, + + dirty : false, + + returnUrl : null, + + initComponent : function() + { + this.items = [this.getAssaysGrid()]; + + this.callParent(); + + this.queryAssayPlan(); + + window.onbeforeunload = LABKEY.beforeunload(this.beforeUnload, this); + }, + + getAssaysGrid : function() + { + if (!this.assaysGrid) + { + this.assaysGrid = Ext4.create('LABKEY.VaccineDesign.AssaysGrid', { + disableEdit: this.disableEdit, + visitNoun: this.visitNoun, + useAlternateLookupFields: this.useAlternateLookupFields + }); + + this.assaysGrid.on('dirtychange', this.enableSaveButton, this); + this.assaysGrid.on('celledited', this.enableSaveButton, this); + } + + return this.assaysGrid; + }, + + queryAssayPlan : function() + { + // query the StudyProperties table for the initial assay plan value + LABKEY.Query.selectRows({ + schemaName: 'study', + queryName: 'StudyProperties', + columns: 'AssayPlan', + scope: this, + success: function(data) + { + var text = (data.rows.length == 1) ? data.rows[0]["AssayPlan"] : ''; + + this.add(this.getAssayPlanPanel(text)); + this.add(this.getButtonBar()); + } + }); + + }, + + getAssayPlanPanel : function(initValue) + { + if (!this.assayPlanPanel) + { + this.assayPlanPanel = Ext4.create('Ext.form.Panel', { + cls: 'study-vaccine-design', + padding: '20px 0', + border: false, + items: [ + Ext4.create('Ext.Component', { + html: '
    Assay Plan
    ' + }), + this.getAssayPlanTextArea(initValue) + ] + }); + } + + return this.assayPlanPanel; + }, + + getAssayPlanTextArea : function(initValue) + { + if (!this.assayPlanTextArea) + { + this.assayPlanTextArea = Ext4.create('Ext.form.field.TextArea', { + name: 'assayPlan', + readOnly: this.disableEdit, + value: initValue, + width: 500, + height: 100 + }); + + this.assayPlanTextArea.on('change', this.enableSaveButton, this, {buffer: 500}); + } + + return this.assayPlanTextArea; + }, + + getButtonBar : function() + { + if (!this.buttonBar) + { + this.buttonBar = Ext4.create('Ext.toolbar.Toolbar', { + dock: 'bottom', + ui: 'footer', + padding: 0, + style : 'background-color: transparent;', + defaults: {width: 75}, + items: [this.getSaveButton(), this.getCancelButton()] + }); + } + + return this.buttonBar; + }, + + getSaveButton : function() + { + if (!this.saveButton) + { + this.saveButton = Ext4.create('Ext.button.Button', { + text: 'Save', + disabled: true, + hidden: this.disableEdit, + handler: this.saveAssaySchedule, + scope: this + }); + } + + return this.saveButton; + }, + + enableSaveButton : function() + { + this.setDirty(true); + this.getSaveButton().enable(); + }, + + getCancelButton : function() + { + if (!this.cancelButton) + { + this.cancelButton = Ext4.create('Ext.button.Button', { + text: this.disableEdit ? 'Done' : 'Cancel', + handler: this.goToReturnURL, + scope: this + }); + } + + return this.cancelButton; + }, + + saveAssaySchedule : function() + { + this.getEl().mask('Saving...'); + + var assays = [], errorMsg = []; + Ext4.each(this.getAssaysGrid().getStore().getRange(), function(record) + { + var recData = Ext4.clone(record.data); + + // drop any empty treatment rows that were just added + var hasData = LABKEY.VaccineDesign.Utils.modelHasData(recData, LABKEY.VaccineDesign.Assay.getFields()); + if (hasData) + { + var sampleQuantity = Number(recData['SampleQuantity']); + if (isNaN(sampleQuantity) || sampleQuantity < 0) + errorMsg.push('Assay sample quantity value must be a positive number: ' + recData['SampleQuantity'] + '.'); + else + assays.push(recData); + } + }, this); + + if (errorMsg.length > 0) + { + this.onFailure(errorMsg.join('
    ')); + return; + } + + LABKEY.Ajax.request({ + url : LABKEY.ActionURL.buildURL('study-design', 'updateAssaySchedule.api'), + method : 'POST', + jsonData: { + assays: assays, + assayPlan: this.getAssayPlanTextArea().getValue() + }, + scope: this, + success: function(response) + { + var resp = Ext4.decode(response.responseText); + if (resp.success) + this.goToReturnURL(); + else + this.onFailure(); + }, + failure: function(response) + { + var resp = Ext4.decode(response.responseText); + if (resp.errors) + this.onFailure(Ext4.Array.pluck(resp.errors, 'message').join('
    ')); + else + this.onFailure(resp.exception); + } + }); + }, + + goToReturnURL : function() + { + this.setDirty(false); + window.location = this.returnUrl; + }, + + onFailure : function(text) + { + Ext4.Msg.show({ + title: 'Error', + msg: text || 'Unknown error occurred.', + icon: Ext4.Msg.ERROR, + buttons: Ext4.Msg.OK + }); + + this.getEl().unmask(); + }, + + setDirty : function(dirty) + { + this.dirty = dirty; + LABKEY.Utils.signalWebDriverTest("treatmentScheduleDirty", dirty); + }, + + isDirty : function() + { + return this.dirty; + }, + + beforeUnload : function() + { + if (!this.disableEdit && this.isDirty()) + return 'Please save your changes.'; + } +}); + +Ext4.define('LABKEY.VaccineDesign.AssaysGrid', { + extend : 'LABKEY.VaccineDesign.BaseDataViewAddVisit', + + cls : 'study-vaccine-design vaccine-design-assays', + + mainTitle : 'Assay Schedule', + + width : 620, + + studyDesignQueryNames : ['StudyDesignAssays', 'StudyDesignLabs', 'StudyDesignSampleTypes', 'StudyDesignUnits', 'Location', 'DataSets'], + + visitNoun : 'Visit', + + useAlternateLookupFields : false, + + //Override + getStore : function() + { + if (!this.store) + { + this.store = Ext4.create('Ext.data.Store', { + storeId : 'AssaysGridStore', + pageSize : 100000, // need to explicitly set otherwise it defaults to 25 + proxy: { + type: 'ajax', + url : LABKEY.ActionURL.buildURL('query', 'selectRows.api', null, { + 'schemaName' : 'study', + 'query.queryName' : 'assayspecimen' + }), + reader: { + type: 'json', + root: 'rows' + } + }, + sorters: [{ property: 'AssayName', direction: 'ASC' }], + autoLoad: true, + listeners: { + scope: this, + load: this.getVisitStore + } + }); + } + + return this.store; + }, + + getVisitStore : function() + { + if (!this.visitStore) + { + this.visitStore = Ext4.create('Ext.data.Store', { + model : 'LABKEY.VaccineDesign.Visit', + pageSize : 100000, // need to explicitly set otherwise it defaults to 25 + proxy: { + type: 'ajax', + url : LABKEY.ActionURL.buildURL('query', 'selectRows.api', null, { + 'schemaName' : 'study', + 'query.queryName' : 'visit' + }), + reader: { + type: 'json', + root: 'rows' + } + }, + sorters: [{ property: 'DisplayOrder', direction: 'ASC' }, { property: 'SequenceNumMin', direction: 'ASC' }], + autoLoad: true, + listeners: { + scope: this, + load: this.getAssaySpecimenVisitStore + } + }); + } + + return this.visitStore; + }, + + getAssaySpecimenVisitStore : function() + { + if (!this.assaySpecimenVisitStore) + { + this.assaySpecimenVisitStore = Ext4.create('Ext4.data.Store', { + storeId : 'AssaySpecimenVisitStore', + model : 'LABKEY.VaccineDesign.AssaySpecimenVisit', + pageSize : 100000, // need to explicitly set otherwise it defaults to 25 + proxy: { + type: 'ajax', + url : LABKEY.ActionURL.buildURL('query', 'selectRows.api', null, { + 'schemaName' : 'study', + 'query.queryName' : 'AssaySpecimenVisit' + }), + reader: { + type: 'json', + root: 'rows' + } + }, + autoLoad: true, + listeners: { + scope: this, + load: function (store, records) + { + var includedVisits = []; + + // stash the visit mapping information attached to each record in the assay store + Ext4.each(records, function(record) + { + var assayRecord = this.getStore().findRecord('RowId', record.get('AssaySpecimenId')); + if (assayRecord != null) + { + var visitMap = assayRecord.get('VisitMap') || []; + visitMap.push(Ext4.clone(record.data)); + assayRecord.set('VisitMap', visitMap); + + includedVisits.push(record.get('VisitId')); + } + }, this); + + var includedVisits = Ext4.Array.unique(includedVisits); + Ext4.each(this.getVisitStore().getRange(), function(visit) + { + var included = includedVisits.indexOf(visit.get('RowId')) > -1; + visit.set('Included', included); + }, this); + + this.add(this.getDataView()); + this.fireEvent('loadcomplete', this); + } + } + }); + } + + return this.assaySpecimenVisitStore; + }, + + //Override + loadDataViewStore : function() + { + // just call getStore here to initial the load, we will add the DataView + // and fire the loadcomplete event after all of the stores for this page are done loading + this.getStore(); + }, + + columnHasData : function(dataIndex) + { + var recordsDataArr = Ext4.Array.pluck(this.getStore().getRange(), 'data'), + colDataArr = Ext4.Array.pluck(recordsDataArr, dataIndex); + + for (var i = 0; i < colDataArr.length; i++) + { + if ((Ext4.isNumber(colDataArr[i]) && colDataArr[i] > 0) || (Ext4.isString(colDataArr[i]) && colDataArr[i] != '')) + return true; + } + + return false; + }, + + //Override + getColumnConfigs : function() + { + if (!this.columnConfigs) + { + var width = 0; // add to the running width as we go through which columns to show in the config + var columnConfigs = []; + + var assayNameEditorConfig = LABKEY.VaccineDesign.Utils.getStudyDesignComboConfig('AssayName', 185, 'StudyDesignAssays'); + assayNameEditorConfig.editable = true; // Rho use-case + + columnConfigs.push({ + label: 'Assay Name', + width: 200, + dataIndex: 'AssayName', + required: true, + editorType: 'LABKEY.ext4.ComboBox', + editorConfig: assayNameEditorConfig + }); + width += 200; + + columnConfigs.push({ + label: 'Dataset', + width: 200, + dataIndex: 'DataSet', + queryName: 'DataSets', + editorType: 'LABKEY.ext4.ComboBox', + editorConfig: LABKEY.VaccineDesign.Utils.getStudyDesignComboConfig('DataSet', 185, 'DataSets', undefined, 'Label', 'DataSetId') + }); + width += 200; + + var hidden = this.disableEdit && !this.columnHasData('Description'); + columnConfigs.push({ + label: 'Description', + width: 200, + hidden: hidden, + dataIndex: 'Description', + editorType: 'Ext.form.field.Text', + editorConfig: LABKEY.VaccineDesign.Utils.getStudyDesignTextConfig('Description', 185) + }); + if (!hidden) { + width += 200; + } + + if (this.useAlternateLookupFields) + { + hidden = this.disableEdit && !this.columnHasData('Source'); + columnConfigs.push({ + label: 'Source', + width: 60, + hidden: hidden, + dataIndex: 'Source', + editorType: 'Ext.form.field.Text', + editorConfig: LABKEY.VaccineDesign.Utils.getStudyDesignTextConfig('Source', 45) + }); + if (!hidden) { + width += 60; + } + + hidden = this.disableEdit && !this.columnHasData('LocationId'); + columnConfigs.push({ + label: 'Location', + width: 140, + hidden: hidden, + dataIndex: 'LocationId', + queryName: 'Location', + editorType: 'LABKEY.ext4.ComboBox', + editorConfig: LABKEY.VaccineDesign.Utils.getStudyDesignComboConfig('LocationId', 125, 'Location', undefined, 'Label', 'RowId') + }); + if (!hidden) { + width += 140; + } + + hidden = this.disableEdit && !this.columnHasData('TubeType'); + columnConfigs.push({ + label: 'TubeType', + width: 200, + hidden: hidden, + dataIndex: 'TubeType', + editorType: 'Ext.form.field.Text', + editorConfig: LABKEY.VaccineDesign.Utils.getStudyDesignTextConfig('TubeType', 185) + }); + if (!hidden) { + width += 200; + } + } + else + { + hidden = this.disableEdit && !this.columnHasData('Lab'); + columnConfigs.push({ + label: 'Lab', + width: 140, + hidden: hidden, + dataIndex: 'Lab', + queryName: 'StudyDesignLabs', + editorType: 'LABKEY.ext4.ComboBox', + editorConfig: LABKEY.VaccineDesign.Utils.getStudyDesignComboConfig('Lab', 125, 'StudyDesignLabs') + }); + if (!hidden) { + width += 140; + } + + hidden = this.disableEdit && !this.columnHasData('SampleType'); + columnConfigs.push({ + label: 'Sample Type', + width: 140, + hidden: hidden, + dataIndex: 'SampleType', + editorType: 'LABKEY.ext4.ComboBox', + editorConfig: LABKEY.VaccineDesign.Utils.getStudyDesignComboConfig('SampleType', 125, 'StudyDesignSampleTypes', undefined, 'Name') + }); + if (!hidden) { + width += 140; + } + + hidden = this.disableEdit && !this.columnHasData('SampleQuantity'); + columnConfigs.push({ + label: 'Sample Quantity', + width: 140, + hidden: hidden, + dataIndex: 'SampleQuantity', + editorType: 'Ext.form.field.Number', + editorConfig: LABKEY.VaccineDesign.Utils.getStudyDesignNumberConfig('SampleQuantity', 125, 2) + }); + if (!hidden) { + width += 140; + } + + hidden = this.disableEdit && !this.columnHasData('SampleUnits'); + columnConfigs.push({ + label: 'Sample Units', + width: 140, + hidden: hidden, + dataIndex: 'SampleUnits', + queryName: 'StudyDesignUnits', + editorType: 'LABKEY.ext4.ComboBox', + editorConfig: LABKEY.VaccineDesign.Utils.getStudyDesignComboConfig('SampleUnits', 125, 'StudyDesignUnits') + }); + if (!hidden) { + width += 140; + } + } + + var visitConfigs = this.getVisitColumnConfigs(); + + // update the width based on the number of visit columns + width += (Math.max(2, visitConfigs.length) * 75); + this.setWidth(width); + + // update the outer panel width if necessary + var outerPanel = this.up('panel'); + if (outerPanel != null) + outerPanel.setWidth(Math.max(width, 750)); + + this.columnConfigs = columnConfigs.concat(visitConfigs); + } + + return this.columnConfigs; + }, + + getVisitColumnConfigs : function() + { + var visitConfigs = []; + + Ext4.each(this.getVisitStore().getRange(), function(visit) + { + if (visit.get('Included')) + { + visitConfigs.push({ + label: visit.get('Label') || visit.get('SequenceNumMin'), + width: 75, + dataIndex: 'VisitMap', + dataIndexArrFilterProp: 'VisitId', + dataIndexArrFilterValue: visit.get('RowId'), + editorType: 'Ext.form.field.Checkbox', + editorConfig: { + hideFieldLabel: true, + name: 'VisitMap' + } + }); + } + }, this); + + if (visitConfigs.length == 0 && !this.disableEdit) + { + visitConfigs.push({ + label: 'No ' + this.visitNoun + 's Defined', + displayValue: '', + width: 160 + }); + } + + return visitConfigs; + }, + + //Override + getCurrentCellValue : function(column, record, dataIndex, outerDataIndex, subgridIndex) + { + var value = this.callParent([column, record, dataIndex, outerDataIndex, subgridIndex]); + + if (dataIndex == 'VisitMap' && Ext4.isArray(value)) + { + var matchingIndex = LABKEY.VaccineDesign.Utils.getMatchingRowIndexFromArray(value, column.dataIndexArrFilterProp, column.dataIndexArrFilterValue); + return matchingIndex > -1; + } + else if ((dataIndex == 'SampleQuantity' || dataIndex == 'LocationId') || dataIndex == 'DataSet' && value == 0) + { + return null; + } + + return value; + }, + + //Override + updateStoreRecordValue : function(record, column, newValue, field) + { + // special case for editing the value of one of the pivot visit columns + if (column.dataIndex == 'VisitMap') + { + var visitMapArr = record.get(column.dataIndex); + if (!Ext4.isArray(visitMapArr)) + { + visitMapArr = []; + record.set(column.dataIndex, visitMapArr); + } + + var matchingIndex = LABKEY.VaccineDesign.Utils.getMatchingRowIndexFromArray(visitMapArr, column.dataIndexArrFilterProp, column.dataIndexArrFilterValue); + + if (newValue) + visitMapArr.push({VisitId: column.dataIndexArrFilterValue}); + else + Ext4.Array.splice(visitMapArr, matchingIndex, 1); + + this.fireEvent('celledited', this, 'VisitMap', visitMapArr); + } + else + { + this.callParent([record, column, newValue]); + } + }, + + //Override + getNewModelInstance : function() + { + var newAssay = LABKEY.VaccineDesign.Assay.create(); + newAssay.set('VisitMap', []); + return newAssay; + }, + + //Override + getDeleteConfirmationMsg : function() + { + return 'Are you sure you want to delete the selected assay configuration? ' + + 'Note: this will also delete all related visit mapping information.'; + } +}); diff --git a/studydesign/webapp/study/vaccineDesign/BaseDataView.js b/studydesign/webapp/study/vaccineDesign/BaseDataView.js new file mode 100644 index 00000000..d01c5f92 --- /dev/null +++ b/studydesign/webapp/study/vaccineDesign/BaseDataView.js @@ -0,0 +1,678 @@ +/* + * Copyright (c) 2016-2017 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 + */ + +Ext4.define('LABKEY.VaccineDesign.BaseDataView', { + + extend : 'Ext.panel.Panel', + + cls : 'study-vaccine-design', + + border : false, + + mainTitle : null, + + studyDesignQueryNames : null, + + // for a DataSpace project, some scenarios don't make sense to allow insert/update + disableEdit : false, + + DELETE_ICON_CLS : 'fa fa-trash', + ADD_ICON_CLS : 'fa fa-plus-circle', + + constructor : function(config) + { + this.callParent([config]); + this.addEvents('dirtychange', 'loadcomplete', 'celledited', 'beforerowdeleted', 'renderviewcomplete'); + }, + + initComponent : function() + { + this.items = [ + this.getMainTitle() + // Note: this.getDataView() will be added after the store loads in this.loadDataViewStore() + ]; + + // Pre-load the study design lookup queries that will be used in dropdowns for this page. + // Note: these stores are also used for getting display values in the data view XTempalte so don't + // bind the data view store until they are all loaded. + if (Ext4.isArray(this.studyDesignQueryNames) && this.studyDesignQueryNames.length > 0) + { + var loadCounter = 0; + Ext4.each(this.studyDesignQueryNames, function(queryName) + { + var studyDesignStore = LABKEY.VaccineDesign.Utils.getStudyDesignStore(queryName); + studyDesignStore.on('load', function() + { + loadCounter++; + + if (loadCounter == this.studyDesignQueryNames.length) + this.loadDataViewStore(); + }, this); + }, this); + } + else + { + this.loadDataViewStore(); + } + + this.fireRenderCompleteTask = new Ext4.util.DelayedTask(function() { + this.fireEvent('renderviewcomplete', this); + }, this); + + // add a single event listener to focus the first input field on the initial render + this.on('renderviewcomplete', function() { + this.giveCellInputFocus('table.outer tr.data-row:first td.cell-value:first input', true); + LABKEY.Utils.signalWebDriverTest("VaccineDesign_renderviewcomplete"); + }, this, {single: true}); + + this.callParent(); + }, + + getMainTitle : function() + { + if (!this.mainTitleCmp && this.mainTitle != null) + { + this.mainTitleCmp = Ext4.create('Ext.Component', { + html: '
    ' + Ext4.util.Format.htmlEncode(this.mainTitle) + '
    ' + }); + } + + return this.mainTitleCmp; + }, + + getDataView : function() + { + if (!this.dataView) + { + this.dataView = Ext4.create('Ext.view.View', { + tpl: this.getDataViewTpl(), + cls: 'table-responsive', + store: this.getStore(), + itemSelector: 'tr.data-row', + disableSelection: true, + setTemplate: function(newTpl) + { + this.tpl = newTpl; + this.refresh(); + } + }); + + this.dataView.on('itemclick', this.onDataViewItemClick, this); + this.dataView.on('refresh', this.onDataViewRefresh, this, {buffer: 250}); + } + + return this.dataView; + }, + + getDataViewTpl : function() + { + var showEdit = !this.disableEdit, + tdCls = !showEdit ? 'cell-display' : 'cell-value', + tplArr = [], + columns = this.getColumnConfigs(); + + tplArr.push(''); + tplArr = tplArr.concat(this.getTableHeaderRowTpl(columns)); + + // data rows + tplArr.push(''); + tplArr.push(''); + if (showEdit) + tplArr.push(''); + Ext4.each(columns, function(column) + { + if (Ext4.isString(column.dataIndex) && !column.hidden) + { + var checkMissingReqTpl = column.required ? ' {[this.checkMissingRequired(values, "' + column.dataIndex + '")]}' : '', + tdTpl = ''; + + if (Ext4.isDefined(column.dataIndexArrFilterValue)) + tdTpl = tdTpl.substring(0, tdTpl.length -1) + ' data-filter-value="' + column.dataIndexArrFilterValue + '">'; + + // decide which of the td tpls to use based on the column definition + if (Ext4.isObject(column.subgridConfig) && Ext4.isArray(column.subgridConfig.columns)) + { + tplArr = tplArr.concat(this.getSubGridTpl(column.dataIndex, column.subgridConfig.columns)); + } + else if (Ext4.isString(column.queryName)) + { + tplArr.push(tdTpl + (!showEdit ? '{[this.getLabelFromStore(values["' + column.dataIndex + '"], "' + column.queryName + '")]}' : '') + tdCloseTpl); + } + else if (Ext4.isString(column.lookupStoreId) && Ext4.isDefined(column.dataIndexArrFilterValue)) + { + var valTpl = ''; + if (!showEdit) + { + valTpl = '{[this.getDisplayValue(values["' + column.dataIndex + '"], ' + + '"' + column.dataIndexArrFilterProp + '", "' + column.dataIndexArrFilterValue + '", ' + + '"' + column.dataIndexArrValue + '", "' + column.lookupStoreId + '")]}'; + } + + tplArr.push(tdTpl + valTpl + tdCloseTpl); + } + else if (column.editorType == 'Ext.form.field.Checkbox') + { + var valTpl = ''; + if (!showEdit) + { + valTpl = '{[this.getDisplayValue(values["' + column.dataIndex + '"], ' + + '"' + column.dataIndexArrFilterProp + '", ' + + '"' + column.dataIndexArrFilterValue + '", ' + + '"checkbox")]}'; + } + + tdTpl = tdTpl.substring(0, tdTpl.length -1) + ' style="text-align: center;">'; + tplArr.push(tdTpl + valTpl + tdCloseTpl); + } + else + { + tplArr.push(tdTpl + (!showEdit ? '{[this.getDisplayValue(values["' + column.dataIndex + '"])]}' : '') + tdCloseTpl); + } + } + else if (Ext4.isString(column.displayValue)) + { + tplArr.push(''); + } + }, this); + tplArr.push(''); + tplArr.push(''); + + tplArr = tplArr.concat(this.getEmptyTableTpl(columns)); + tplArr = tplArr.concat(this.getAddNewRowTpl(columns)); + tplArr.push('
    ', + tdCloseTpl = '' + column.displayValue + '
    '); + + tplArr.push({ + getDisplayValue : function(val, arrPropFilterName, arrPropFilterVal, arrPropDisplayField, lookupStoreId) + { + // allow showing a certain filtered row from an array + if (Ext4.isDefined(arrPropDisplayField)) + { + var matchingIndex = LABKEY.VaccineDesign.Utils.getMatchingRowIndexFromArray(val, arrPropFilterName, arrPropFilterVal); + if (matchingIndex > -1 && Ext4.isObject(val[matchingIndex])) + { + if (arrPropDisplayField == 'checkbox') + return '✓'; + else + val = val[matchingIndex][arrPropDisplayField]; + } + else + val = ''; + } + + // if we have a specific lookupStoreId, get the label value from the matching RowId record in that store + if (Ext4.isDefined(lookupStoreId) && val != null && val != '') + { + var store = Ext4.getStore(lookupStoreId); + if (store != null) + { + var record = store.findRecord('RowId', val); + if (record != null) + val = record.get('Label'); + } + } + + if (Ext4.isNumber(val)) + { + return val == 0 ? '' : val; + } + else + { + // need to htmlEncode and then handle newlines in multiline text fields (i.e. Treatment/Description) + val = Ext4.util.Format.htmlEncode(val); + val = val.replace(/\n/g, '
    '); + } + + return val; + }, + + getLabelFromStore : function(val, queryName) + { + if (val != null && val != '') + val = LABKEY.VaccineDesign.Utils.getLabelFromStore(queryName, val); + + if (Ext4.isNumber(val)) + return val == 0 ? '' : val; + else + return Ext4.util.Format.htmlEncode(val); + }, + + checkMissingRequired : function(values, dataIndex) + { + if (values[dataIndex] == null || values[dataIndex] == '') + return ' missing-required'; + + return ''; + } + }); + + return new Ext4.XTemplate(tplArr); + }, + + getSubGridTpl : function(dataIndex, columns) + { + var showEdit = !this.disableEdit, + tdCls = showEdit ? 'cell-value' : 'cell-display', + tplArr = []; + + tplArr.push(''); + + // only show the subgrid if we are allowing edits of if it has at least one row + tplArr.push(''); + + tplArr.push(''); + tplArr = tplArr.concat(this.getTableHeaderRowTpl(columns)); + + // data rows + tplArr.push(''); + tplArr.push(''); + if (showEdit) + { + tplArr.push(''); + } + Ext4.each(columns, function(column) + { + if (Ext4.isString(column.dataIndex)) + { + var checkMissingReqTpl = column.required ? ' {[this.checkMissingRequired(values, "' + column.dataIndex + '")]}' : '', + tdTpl = ''; + + if (Ext4.isString(column.queryName)) + tplArr.push(tdTpl + (!showEdit ? '{[this.getLabelFromStore(values["' + column.dataIndex + '"], "' + column.queryName + '")]}' : '') + tdCloseTpl); + else + tplArr.push(tdTpl + (!showEdit ? '{' + column.dataIndex + ':htmlEncode}' : '') + tdCloseTpl); + } + }, this); + tplArr.push(''); + tplArr.push(''); + + tplArr = tplArr.concat(this.getAddNewRowTpl(columns, dataIndex)); + tplArr.push('
    '); + tplArr.push(''); + tplArr.push('', + tdCloseTpl = '
    '); + tplArr.push('
    '); + tplArr.push(''); + + return tplArr; + }, + + getTableHeaderRowTpl : function(columns) + { + var tplArr = []; + + tplArr.push(''); + if (!this.disableEdit) + tplArr.push(' '); + Ext4.each(columns, function(column) + { + if (!column.hidden) + tplArr.push('' + Ext4.util.Format.htmlEncode(column.label) + ''); + }, this); + tplArr.push(''); + + return tplArr; + }, + + getEmptyTableTpl : function(columns) + { + var tplArr = []; + + if (this.disableEdit) + { + tplArr.push(''); + tplArr.push(''); + tplArr.push('No data to show.'); + tplArr.push(''); + tplArr.push(''); + } + + return tplArr; + }, + + getAddNewRowTpl : function(columns, dataIndex) + { + var tplArr = []; + + if (!this.disableEdit) + { + tplArr.push(''); + tplArr.push(' '); + tplArr.push(''); + if (Ext4.isString(dataIndex)) + tplArr.push(' Add new row'); + else + tplArr.push(' Add new row'); + tplArr.push(''); + tplArr.push(''); + } + + return tplArr; + }, + + onDataViewItemClick : function(view, record, item, index, event) + { + if (!this.disableEdit) + { + // handle click on trashcan icon to delete row + if (event.target.getAttribute('class') == this.DELETE_ICON_CLS) + { + if (event.target.hasAttribute('outer-index')) + { + this.removeOuterRecord(this.mainTitle, record); + } + // handle click on trashcan icon for outer grid + else if (event.target.hasAttribute('subgrid-data-index') && event.target.hasAttribute('subgrid-index')) + { + this.confirmRemoveSubgridRecord(event.target, record); + } + } + } + }, + + createNewCellEditField : function(target, record, index) + { + var dataIndex = target.getAttribute('data-index'), + dataFilterValue = target.getAttribute('data-filter-value'), + outerDataIndex = target.getAttribute('outer-data-index'), + subgridIndex = Number(target.getAttribute('subgrid-index')), + column = this.getColumnConfig(dataIndex, dataFilterValue, outerDataIndex), + editor = this.getColumnEditorConfig(column); + + if (editor != null) + { + var config = { + renderTo: target, + required: column.required, + storeIndex: index, + dataFilterValue: dataFilterValue, + outerDataIndex: outerDataIndex, + subgridIndex: subgridIndex + }; + + var currentValue = this.getCurrentCellValue(column, record, dataIndex, outerDataIndex, subgridIndex); + if (editor.type == 'Ext.form.field.Checkbox') + config.checked = currentValue; + else + config.value = currentValue; + + if (column.isTreatmentLookup) { + var treatmentLabel = this.getTreatmentCellDisplayValue(currentValue, column.lookupStoreId); + config.value = treatmentLabel; + config.treatmentId = currentValue; + } + + // create a new form field to place in the td cell + var field = Ext4.create(editor.type, Ext4.apply(editor.config, config)); + + // add listeners for when to apply the updated value and clear the input field + field.on('change', this.updateStoreValueForCellEdit, this, {buffer: 500}); + } + }, + + getCurrentCellValue : function(column, record, dataIndex, outerDataIndex, subgridIndex) + { + return Ext4.isString(outerDataIndex) ? record.get(outerDataIndex)[subgridIndex][dataIndex] : record.get(dataIndex); + }, + + updateStoreValueForCellEdit : function(field) + { + var fieldName = field.getName(), + newValue = field.getValue(), + index = field.storeIndex, + record = this.getStore().getAt(index), + dataFilterValue = field.dataFilterValue, + outerDataIndex = field.outerDataIndex, + subgridIndex = Number(field.subgridIndex); + + // suspend events on cell update so that we don't re-render the dataview + this.getStore().suspendEvents(); + + if (Ext4.isString(outerDataIndex)) + { + if (!isNaN(subgridIndex) && Ext4.isArray(record.get(outerDataIndex))) + this.updateSubgridRecordValue(record, outerDataIndex, subgridIndex, fieldName, newValue); + } + else + { + var column = this.getColumnConfig(fieldName, dataFilterValue, outerDataIndex); + this.updateStoreRecordValue(record, column, newValue, field); + } + + // update the missing-required cls based on the new field value + if (field.required) + { + if (newValue == null || newValue == '') + Ext4.get(field.renderTo).addCls('missing-required'); + else + Ext4.get(field.renderTo).removeCls('missing-required'); + } + + // resume store events so that adding and deleting will re-render the dataview + this.getStore().resumeEvents(); + }, + + updateSubgridRecordValue : function(record, outerDataIndex, subgridIndex, fieldName, newValue) + { + if (Ext4.isString(newValue)) + newValue.trim(); + + record.get(outerDataIndex)[subgridIndex][fieldName] = newValue; + this.fireEvent('celledited', this, fieldName, newValue); + }, + + updateStoreRecordValue : function(record, column, newValue, field) + { + if (Ext4.isString(newValue)) + newValue.trim(); + + record.set(column.dataIndex, newValue); + this.fireEvent('celledited', this, column.dataIndex, newValue); + }, + + removeOuterRecord : function(title, record) + { + var msg = this.getDeleteConfirmationMsg() != null ? this.getDeleteConfirmationMsg() : 'Are you sure you want to delete the selected row?'; + + Ext4.Msg.confirm('Confirm Delete: ' + title, msg, function(btn) + { + if (btn == 'yes') + { + this.fireEvent('beforerowdeleted', this, record); + + // suspend events on remove so that we don't re-render the dataview twice + this.getStore().suspendEvents(); + this.getStore().remove(record); + this.getStore().resumeEvents(); + this.refresh(true); + } + }, this); + }, + + confirmRemoveSubgridRecord : function(target, record) + { + var subgridDataIndex = target.getAttribute('subgrid-data-index'), + subgridArr = record.get(subgridDataIndex); + + if (Ext4.isArray(subgridArr)) + { + Ext4.Msg.confirm('Confirm Delete: ' + subgridDataIndex, 'Are you sure you want to delete the selected row?', function(btn) + { + if (btn == 'yes') + { + this.removeSubgridRecord(target, record); + this.refresh(true); + } + }, this); + } + }, + + removeSubgridRecord : function(target, record) + { + var subgridDataIndex = target.getAttribute('subgrid-data-index'), + subgridArr = record.get(subgridDataIndex); + + subgridArr.splice(target.getAttribute('subgrid-index'), 1); + }, + + onDataViewRefresh : function(view) + { + this.attachCellEditors(view); + this.attachAddRowListeners(view); + this.doLayout(); + this.fireRenderCompleteTask.delay(250); + }, + + attachCellEditors : function(view) + { + // attach cell editors for each of the store records (i.e. tr.row elements in the table) + var index = 0; + Ext4.each(this.getStore().getRange(), function(record) + { + var targetCellEls = Ext4.DomQuery.select('tr.data-row:nth(' + (index+1) + ') td.cell-value', view.getEl().dom); + Ext4.each(targetCellEls, function(targetCell) + { + this.createNewCellEditField(targetCell, record, index); + }, this); + + index++; + }, this); + }, + + attachAddRowListeners : function(view) + { + var addIconEls = Ext4.DomQuery.select('i.add-new-row', view.getEl().dom); + + Ext4.each(addIconEls, function(addIconEl) + { + if (addIconEl.hasAttribute('data-index')) + Ext4.get(addIconEl).on('click', this.addNewSubgridRow, this); + else + Ext4.get(addIconEl).on('click', this.addNewOuterRow, this); + }, this); + }, + + addNewOuterRow : function() + { + // suspend events on insert so that we don't re-render the dataview twice + this.getStore().suspendEvents(); + this.getStore().insert(this.getStore().getCount(), this.getNewModelInstance()); + this.getStore().resumeEvents(); + + // on refresh, call to give focus to the first column of the new row + this.on('renderviewcomplete', function(){ + this.giveCellInputFocus('table.outer tr.data-row:last td.cell-value:first input'); + }, this, {single: true}); + + this.refresh(); + }, + + addNewSubgridRow : function(event, target) + { + var dataIndex = target.getAttribute('data-index'), + rowIndex = Number(target.getAttribute('outer-index')); + + if (Ext4.isString(dataIndex) && Ext4.isNumber(rowIndex)) + { + var record = this.getStore().getAt(rowIndex), + dataIndexArr = record.get(dataIndex); + + if (Ext4.isArray(dataIndexArr)) + record.set(dataIndex, dataIndexArr.concat([{}])); + + // on refresh, call to give focus to the first column of the new row + this.on('renderviewcomplete', function(){ + var selector = 'table.subgrid-' + dataIndex + ':nth(' + (rowIndex+1) + ') tr.subrow:last td.cell-value:first input'; + this.giveCellInputFocus(selector); + }, this, {single: true}); + + this.refresh(); + } + }, + + giveCellInputFocus : function(selector, queryFullPage) + { + var cellInputField = Ext4.DomQuery.selectNode(selector, queryFullPage ? undefined : this.getDataView().getEl().dom); + if (cellInputField) + cellInputField.focus(); + }, + + refresh : function(hasChanges) + { + this.getDataView().refresh(); + + if (hasChanges) + this.fireEvent('dirtychange', this); + }, + + getColumnConfig : function(dataIndex, dataFilterValue, parentDataIndex) + { + var columns = this.getColumnConfigs(), matchingColumn = null; + + // if the parentDataIndex is defined, then we are looking for the subgrid column editor config + if (Ext4.isString(parentDataIndex)) + { + var colIndex = Ext4.pluck(columns, 'dataIndex').indexOf(parentDataIndex); + if (colIndex > -1 && columns[colIndex].hasOwnProperty('subgridConfig') && Ext4.isArray(columns[colIndex].subgridConfig.columns)) + columns = columns[colIndex].subgridConfig.columns; + else + return null; + } + + Ext4.each(columns, function(column) + { + if (column.dataIndex == dataIndex && (!Ext4.isDefined(dataFilterValue) || column.dataIndexArrFilterValue == dataFilterValue)) + { + matchingColumn = column; + return false; // break; + } + }, this); + + return matchingColumn; + }, + + getColumnEditorConfig : function(column) + { + if (column != null && column.hasOwnProperty('editorType') && column.hasOwnProperty('editorConfig')) + { + return { + type: column.editorType, + config: Ext4.isFunction(column.editorConfig) ? column.editorConfig.call(this) : column.editorConfig + }; + } + + return null; + }, + + loadDataViewStore : function() + { + // since some tables might need information from the store, wait to add the data view until the store loads + this.getStore().on('load', function() { + this.add(this.getDataView()); + this.fireEvent('loadcomplete', this); + }, this, {single: true}); + }, + + getStore : function() + { + throw "getStore must be overridden in subclass"; + }, + + getNewModelInstance : function() + { + throw "getNewModelInstance must be overridden in subclass"; + }, + + getColumnConfigs : function() + { + throw "getColumnConfigs must be overridden in subclass"; + }, + + getDeleteConfirmationMsg : function() + { + return null; + } +}); \ No newline at end of file diff --git a/studydesign/webapp/study/vaccineDesign/BaseDataViewAddVisit.js b/studydesign/webapp/study/vaccineDesign/BaseDataViewAddVisit.js new file mode 100644 index 00000000..dabfda02 --- /dev/null +++ b/studydesign/webapp/study/vaccineDesign/BaseDataViewAddVisit.js @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2016 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 + */ + +Ext4.define('LABKEY.VaccineDesign.BaseDataViewAddVisit', { + extend: 'LABKEY.VaccineDesign.BaseDataView', + + getVisitStore : function() + { + throw new Error("getVisitStore must be overridden in subclass"); + }, + + //Override + getAddNewRowTpl : function(columns, dataIndex) + { + var tplArr = []; + + if (!this.disableEdit) + { + tplArr.push(''); + tplArr.push(' '); + tplArr.push(''); + tplArr.push(' Add new row   '); + tplArr.push(' Add new ' + this.visitNoun.toLowerCase() + ''); + tplArr.push(''); + tplArr.push(''); + } + + return tplArr; + }, + + //Override + attachAddRowListeners : function(view) + { + this.callParent([view]); + + var addIconEls = Ext4.DomQuery.select('i.add-visit-column', view.getEl().dom); + + Ext4.each(addIconEls, function(addIconEl) + { + Ext4.get(addIconEl).on('click', this.addNewVisitColumn, this); + }, this); + }, + + addNewVisitColumn : function() + { + var win = Ext4.create('LABKEY.VaccineDesign.VisitWindow', { + title: 'Add ' + this.visitNoun, + visitNoun: this.visitNoun, + visitStore: this.getVisitStore(), + listeners: { + scope: this, + closewindow: function(){ + win.close(); + }, + selectexistingvisit: function(w, visitId){ + win.close(); + + // if the 'ALL' option was selected, show all of the visits in the table + if (visitId == 'ALL') + { + Ext4.each(this.getVisitStore().getRange(), function(record) + { + record.set('Included', true); + }, this); + } + // set the selected visit to be included + else + { + this.getVisitStore().findRecord('RowId', visitId).set('Included', true); + } + + this.updateDataViewTemplate(); + }, + newvisitcreated: function(w, newVisitData){ + win.close(); + + // add the new visit to the store + var newVisitRec = LABKEY.VaccineDesign.Visit.create(newVisitData); + newVisitRec.set('Included', true); + this.getVisitStore().add(newVisitRec); + + this.updateDataViewTemplate(); + } + } + }); + + win.show(); + }, + + updateDataViewTemplate : function() + { + // explicitly clear the column configs so the new visit column will be added + this.columnConfigs = null; + this.getDataView().setTemplate(this.getDataViewTpl()); + } +}); \ No newline at end of file diff --git a/studydesign/webapp/study/vaccineDesign/Models.js b/studydesign/webapp/study/vaccineDesign/Models.js new file mode 100644 index 00000000..209772bd --- /dev/null +++ b/studydesign/webapp/study/vaccineDesign/Models.js @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2016-2017 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 + */ +Ext4.define('LABKEY.VaccineDesign.Product', { + extend : 'Ext.data.Model', + idgen: 'sequential', + fields : [ + {name : 'RowId', defaultValue: undefined}, + {name : 'Label', type : 'string'}, + {name : 'Role', type : 'string'}, + {name : 'Type', type : 'string'}, + {name : 'Antigens', defaultValue: []}, + {name : 'DoseAndRoute', defaultValue: []} + ] +}); + +Ext4.define('LABKEY.VaccineDesign.Treatment', { + extend : 'Ext.data.Model', + idgen: 'sequential', + fields : [ + {name : 'RowId', defaultValue: undefined}, + {name : 'Label', type : 'string'}, + {name : 'Description', type : 'string'}, + {name : 'Immunogen', defaultValue: []}, + {name : 'Adjuvant', defaultValue: []}, + {name : 'Challenge', defaultValue: []} + ] +}); + +Ext4.define('LABKEY.VaccineDesign.Cohort', { + extend : 'Ext.data.Model', + idgen: 'sequential', + fields : [ + {name : 'RowId', defaultValue: undefined}, + {name : 'Label', type : 'string'}, + // the DataView XTemplate gets mad if this is defined as type 'int' + {name : 'SubjectCount', type : 'string'}, + {name : 'CanDelete', type : 'boolean'}, + {name : 'VisitMap', defaultValue: []} + ] +}); + +Ext4.define('LABKEY.VaccineDesign.Assay', { + extend : 'Ext.data.Model', + idgen: 'sequential', + fields : [ + {name : 'RowId', defaultValue: undefined}, + {name : 'AssayName', type : 'string'}, + {name : 'DataSet', type : 'int'}, + {name : 'Description', type : 'string'}, + {name : 'Lab', type : 'string'}, + {name : 'LocationId', type : 'int'}, + {name : 'SampleType', type : 'string'}, + {name : 'Source', type : 'string'}, + {name : 'TubeType', type : 'string'}, + {name : 'SampleQuantity', type : 'float'}, + {name : 'SampleUnits', type : 'string'}, + {name : 'VisitMap', defaultValue: []} + ] +}); + +Ext4.define('LABKEY.VaccineDesign.AssaySpecimenVisit', { + extend : 'Ext.data.Model', + fields : [ + {name : 'RowId', type : 'int'}, + {name : 'VisitId', type : 'int'}, + {name : 'AssaySpecimenId', type : 'int'} + ] +}); + +Ext4.define('LABKEY.VaccineDesign.Visit', { + extend : 'Ext.data.Model', + fields : [ + {name : 'RowId', type : 'int'}, + {name : 'Label', type : 'string'}, + {name : 'DisplayOrder', type : 'int'}, + {name : 'SequenceNumMin', type : 'numeric'}, + {name : 'Included', type : 'boolean'} + ] +}); \ No newline at end of file diff --git a/studydesign/webapp/study/vaccineDesign/StudyProducts.js b/studydesign/webapp/study/vaccineDesign/StudyProducts.js new file mode 100644 index 00000000..325173a9 --- /dev/null +++ b/studydesign/webapp/study/vaccineDesign/StudyProducts.js @@ -0,0 +1,508 @@ +/* + * Copyright (c) 2016-2017 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 + */ +Ext4.define('LABKEY.VaccineDesign.StudyProductsPanel', { + extend : 'Ext.panel.Panel', + + border : false, + + bodyStyle : 'background-color: transparent;', + + minWidth: 1350, + + disableEdit : true, + + dirty : false, + + returnUrl : null, + + initComponent : function() + { + this.items = [ + this.getImmunogensGrid(), + this.getAdjuvantGrid(), + this.getChallengesGrid(), + this.getButtonBar() + ]; + + this.callParent(); + + window.onbeforeunload = LABKEY.beforeunload(this.beforeUnload, this); + }, + + getImmunogensGrid : function() + { + if (!this.immunogenGrid) + { + this.immunogenGrid = Ext4.create('LABKEY.VaccineDesign.ImmunogensGrid', { + disableEdit: this.disableEdit + }); + + this.immunogenGrid.on('dirtychange', this.enableSaveButton, this); + this.immunogenGrid.on('celledited', this.enableSaveButton, this); + } + + return this.immunogenGrid; + }, + + getAdjuvantGrid : function() + { + if (!this.adjuvantGrid) + { + this.adjuvantGrid = Ext4.create('LABKEY.VaccineDesign.AdjuvantsGrid', { + padding: '20px 0', + disableEdit: this.disableEdit + }); + + this.adjuvantGrid.on('dirtychange', this.enableSaveButton, this); + this.adjuvantGrid.on('celledited', this.enableSaveButton, this); + } + + return this.adjuvantGrid; + }, + + getChallengesGrid : function() + { + if (!this.challengesGrid) + { + this.challengesGrid = Ext4.create('LABKEY.VaccineDesign.ChallengesGrid', { + padding: '20px 0', + disableEdit: this.disableEdit + }); + + this.challengesGrid.on('dirtychange', this.enableSaveButton, this); + this.challengesGrid.on('celledited', this.enableSaveButton, this); + } + + return this.challengesGrid; + }, + + getButtonBar : function() + { + if (!this.buttonBar) + { + this.buttonBar = Ext4.create('Ext.toolbar.Toolbar', { + dock: 'bottom', + ui: 'footer', + padding: 0, + style : 'background-color: transparent;', + defaults: {width: 75}, + items: [this.getSaveButton(), this.getCancelButton()] + }); + } + + return this.buttonBar; + }, + + getSaveButton : function() + { + if (!this.saveButton) + { + this.saveButton = Ext4.create('Ext.button.Button', { + text: 'Save', + disabled: true, + hidden: this.disableEdit, + handler: this.saveStudyProducts, + scope: this + }); + } + + return this.saveButton; + }, + + enableSaveButton : function() + { + this.setDirty(true); + this.getSaveButton().enable(); + }, + + getCancelButton : function() + { + if (!this.cancelButton) + { + this.cancelButton = Ext4.create('Ext.button.Button', { + text: this.disableEdit ? 'Done' : 'Cancel', + handler: this.goToReturnURL, + scope: this + }); + } + + return this.cancelButton; + }, + + saveStudyProducts : function() + { + var studyProducts = []; + + this.getEl().mask('Saving...'); + + Ext4.each(this.getImmunogensGrid().getStore().getRange(), function(record) + { + var recData = Ext4.clone(record.data); + + // drop any empty antigen rows that were just added + var antigenArr = []; + Ext4.each(recData['Antigens'], function(antigen) + { + if (Ext4.isDefined(antigen['RowId']) || LABKEY.VaccineDesign.Utils.objectHasData(antigen)) + antigenArr.push(antigen); + }, this); + recData['Antigens'] = antigenArr; + + // drop any empty rows that were just added + var hasData = recData['Label'] != '' || recData['Type'] != '' || recData['Antigens'].length > 0; + if (Ext4.isDefined(recData['RowId']) || hasData) + studyProducts.push(recData); + }, this); + + Ext4.each(this.getAdjuvantGrid().getStore().getRange(), function(record) + { + // drop any empty rows that were just added + if (Ext4.isDefined(record.get('RowId')) || record.get('Label') != '') + studyProducts.push(Ext4.clone(record.data)); + }, this); + + Ext4.each(this.getChallengesGrid().getStore().getRange(), function(record) + { + var hasData = record['Label'] != '' || record['Type'] != ''; + if (Ext4.isDefined(record.get('RowId')) || hasData) + studyProducts.push(Ext4.clone(record.data)); + }, this); + + LABKEY.Ajax.request({ + url : LABKEY.ActionURL.buildURL('study-design', 'updateStudyProducts.api'), + method : 'POST', + jsonData: { products: studyProducts }, + scope: this, + success: function(response) + { + var resp = Ext4.decode(response.responseText); + if (resp.success) + this.goToReturnURL(); + else + this.onFailure(); + }, + failure: function(response) + { + var resp = Ext4.decode(response.responseText); + if (resp.errors) + this.onFailure(Ext4.Array.pluck(resp.errors, 'message').join('
    ')); + else + this.onFailure(resp.exception); + } + }); + }, + + goToReturnURL : function() + { + this.setDirty(false); + window.location = this.returnUrl; + }, + + onFailure : function(text) + { + Ext4.Msg.show({ + title: 'Error', + msg: text || 'Unknown error occurred.', + icon: Ext4.Msg.ERROR, + buttons: Ext4.Msg.OK + }); + + this.getEl().unmask(); + }, + + setDirty : function(dirty) + { + this.dirty = dirty; + LABKEY.Utils.signalWebDriverTest("studyProductsDirty", dirty); + }, + + isDirty : function() + { + return this.dirty; + }, + + beforeUnload : function() + { + if (!this.disableEdit && this.isDirty()) + return 'Please save your changes.'; + } +}); + +Ext4.define('LABKEY.VaccineDesign.StudyProductsGrid', { + + extend : 'LABKEY.VaccineDesign.BaseDataView', + + filterRole : null, + + showDoseRoute : true, + + //Override + getStore : function() + { + if (!this.store) + { + this.store = Ext4.create('Ext.data.Store', { + model : 'LABKEY.VaccineDesign.Product', + proxy: { + type: 'ajax', + url : LABKEY.ActionURL.buildURL("study-design", "getStudyProducts", null, {role: this.filterRole}), + reader: { + type: 'json', + root: 'products' + } + }, + sorters: [{ property: 'RowId', direction: 'ASC' }], + autoLoad: true + }); + } + + return this.store; + }, + + //Override + getNewModelInstance : function() + { + return LABKEY.VaccineDesign.Product.create({Role: this.filterRole}); + }, + + //Override + getDeleteConfirmationMsg : function() + { + return 'Are you sure you want to delete the selected study product? ' + + 'Note: if this study product is being used by any treatment definitions, ' + + 'those associations will also be deleted upon save.'; + } +}); + +Ext4.define('LABKEY.VaccineDesign.ImmunogensGrid', { + extend : 'LABKEY.VaccineDesign.StudyProductsGrid', + + cls : 'study-vaccine-design vaccine-design-immunogens', + + mainTitle : 'Immunogens', + + filterRole : 'Immunogen', + + studyDesignQueryNames : ['StudyDesignImmunogenTypes', 'StudyDesignGenes', 'StudyDesignSubTypes', 'StudyDesignRoutes'], + + //Override + getColumnConfigs : function() + { + if (!this.columnConfigs) + { + var width = 0; // add to the running width as we go through which columns to show in the config + + this.columnConfigs = [{ + label: 'Label', + width: 200, + dataIndex: 'Label', + required: true, + editorType: 'Ext.form.field.Text', + editorConfig: LABKEY.VaccineDesign.Utils.getStudyDesignTextConfig('Label', 185) + }, { + label: 'Type', + width: 200, + dataIndex: 'Type', + queryName: 'StudyDesignImmunogenTypes', + editorType: 'LABKEY.ext4.ComboBox', + editorConfig: LABKEY.VaccineDesign.Utils.getStudyDesignComboConfig('Type', 185, 'StudyDesignImmunogenTypes') + },{ + label: 'HIV Antigens', + width: 600, + dataIndex: 'Antigens', + subgridConfig: { + columns: [{ + label: 'Gene', + width: 140, + dataIndex: 'Gene', + queryName: 'StudyDesignGenes', + editorType: 'LABKEY.ext4.ComboBox', + editorConfig: LABKEY.VaccineDesign.Utils.getStudyDesignComboConfig('Gene', 125, 'StudyDesignGenes') + },{ + label: 'Subtype', + width: 140, + dataIndex: 'SubType', + queryName: 'StudyDesignSubTypes', + editorType: 'LABKEY.ext4.ComboBox', + editorConfig: LABKEY.VaccineDesign.Utils.getStudyDesignComboConfig('SubType', 125, 'StudyDesignSubTypes') + },{ + label: 'GenBank Id', + width: 150, + dataIndex: 'GenBankId', + editorType: 'Ext.form.field.Text', + editorConfig: LABKEY.VaccineDesign.Utils.getStudyDesignTextConfig('GenBankId', 135) + },{ + label: 'Sequence', + width: 150, + dataIndex: 'Sequence', + editorType: 'Ext.form.field.Text', + editorConfig: LABKEY.VaccineDesign.Utils.getStudyDesignTextConfig('Sequence', 135) + }] + } + }]; + width += 1000; + + if (this.showDoseRoute) + { + this.columnConfigs.push({ + label: 'Doses and Routes', + width: 315, + dataIndex: 'DoseAndRoute', + subgridConfig: { + columns: [{ + label: 'Dose', + width: 140, + dataIndex: 'Dose', + editorType: 'Ext.form.field.Text', + editorConfig: LABKEY.VaccineDesign.Utils.getStudyDesignTextConfig('Dose', 125) + },{ + label: 'Route', + width: 140, + dataIndex: 'Route', + queryName: 'StudyDesignRoutes', + editorType: 'LABKEY.ext4.ComboBox', + editorConfig: LABKEY.VaccineDesign.Utils.getStudyDesignComboConfig('Route', 125, 'StudyDesignRoutes') + }] + } + }); + width += 315; + } + + this.setWidth(width); + } + + return this.columnConfigs; + } +}); + +Ext4.define('LABKEY.VaccineDesign.AdjuvantsGrid', { + extend : 'LABKEY.VaccineDesign.StudyProductsGrid', + + cls : 'study-vaccine-design vaccine-design-adjuvants', + + mainTitle : 'Adjuvants', + + filterRole : 'Adjuvant', + + studyDesignQueryNames : ['StudyDesignRoutes'], + + //Override + getColumnConfigs : function() + { + if (!this.columnConfigs) + { + var width = 0; // add to the running width as we go through which columns to show in the config + + this.columnConfigs = [{ + label: 'Label', + width: 200, + dataIndex: 'Label', + required: true, + editorType: 'Ext.form.field.Text', + editorConfig: LABKEY.VaccineDesign.Utils.getStudyDesignTextConfig('Label', 185) + }]; + width += 200; + + if (this.showDoseRoute) + { + this.columnConfigs.push({ + label: 'Doses and Routes', + width: 330, + dataIndex: 'DoseAndRoute', + subgridConfig: { + columns: [{ + label: 'Dose', + width: 140, + dataIndex: 'Dose', + editorType: 'Ext.form.field.Text', + editorConfig: LABKEY.VaccineDesign.Utils.getStudyDesignTextConfig('Dose', 125) + }, { + label: 'Route', + width: 140, + dataIndex: 'Route', + queryName: 'StudyDesignRoutes', + editorType: 'LABKEY.ext4.ComboBox', + editorConfig: LABKEY.VaccineDesign.Utils.getStudyDesignComboConfig('Route', 125, 'StudyDesignRoutes') + }] + } + }); + width += 330; + } + + this.setWidth(width); + } + + return this.columnConfigs; + } +}); + +Ext4.define('LABKEY.VaccineDesign.ChallengesGrid', { + extend : 'LABKEY.VaccineDesign.StudyProductsGrid', + + cls : 'study-vaccine-design vaccine-design-challenges', + + mainTitle : 'Challenges', + + filterRole : 'Challenge', + + studyDesignQueryNames : ['StudyDesignChallengeTypes', 'StudyDesignRoutes'], + + //Override + getColumnConfigs : function() + { + if (!this.columnConfigs) + { + var width = 0; // add to the running width as we go through which columns to show in the config + + this.columnConfigs = [{ + label: 'Label', + width: 200, + dataIndex: 'Label', + required: true, + editorType: 'Ext.form.field.Text', + editorConfig: LABKEY.VaccineDesign.Utils.getStudyDesignTextConfig('Label', 185) + }, { + label: 'Type', + width: 200, + dataIndex: 'Type', + queryName: 'StudyDesignChallengeTypes', + editorType: 'LABKEY.ext4.ComboBox', + editorConfig: LABKEY.VaccineDesign.Utils.getStudyDesignComboConfig('Type', 185, 'StudyDesignChallengeTypes') + }]; + width += 400; + + if (this.showDoseRoute) + { + this.columnConfigs.push({ + label: 'Doses and Routes', + width: 330, + dataIndex: 'DoseAndRoute', + subgridConfig: { + columns: [{ + label: 'Dose', + width: 140, + dataIndex: 'Dose', + editorType: 'Ext.form.field.Text', + editorConfig: LABKEY.VaccineDesign.Utils.getStudyDesignTextConfig('Dose', 125) + }, { + label: 'Route', + width: 140, + dataIndex: 'Route', + queryName: 'StudyDesignRoutes', + editorType: 'LABKEY.ext4.ComboBox', + editorConfig: LABKEY.VaccineDesign.Utils.getStudyDesignComboConfig('Route', 125, 'StudyDesignRoutes') + }] + } + }); + width += 330; + } + + this.setWidth(width); + } + + return this.columnConfigs; + } +}); \ No newline at end of file diff --git a/studydesign/webapp/study/vaccineDesign/TreatmentDialog.js b/studydesign/webapp/study/vaccineDesign/TreatmentDialog.js new file mode 100644 index 00000000..0b4f63df --- /dev/null +++ b/studydesign/webapp/study/vaccineDesign/TreatmentDialog.js @@ -0,0 +1,189 @@ +/* + * Copyright (c) 2016-2017 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 + */ +Ext4.define('LABKEY.VaccineDesign.TreatmentDialog', { + + extend: 'Ext.window.Window', + + bodyStyle: 'overflow-y: auto; padding: 10px 0;', + + listeners: { + resize: function (cmp) + { + cmp.doLayout(); + } + }, + + initComponent : function() + { + this.items = this.getForm(); + this.callParent(); + }, + + getForm : function() + { + if (!this.formPanel) { + this.formPanel = Ext4.create('Ext.form.Panel',{ + border : false, + layout : { + type : 'vbox', + align: 'stretch' + }, + fieldDefaults :{ + labelAlign : 'top', + labelWidth : 130 + }, + items : this.getFormItems(), + scope : this + }); + } + return this.formPanel; + }, + + getFormItems : function() { + var productRolesItems = {}, columns = []; + Ext4.each(this.productRoles, function(role){ + var checkedProducts = this.treatmentDetails ? this.treatmentDetails[role] : []; + productRolesItems[role] = []; + var values = this.getProductAndDoseRouteValues(role, checkedProducts); + Ext4.each(values, function(val){ + var checked = false; + Ext4.each(checkedProducts, function(product){ + if (product.ProductDoseRoute == val.ProductDoseRoute) { + checked = true; + return false; + } + }); + productRolesItems[role].push({ + boxLabel: Ext4.util.Format.htmlEncode(val.Label), + name: role, + inputValue: val.ProductDoseRoute, + productLabel: val.ProductLabel, + checked: checked, + cls: 'dialog-product-label', + listeners: { + render: function (cmp) + { + //tooltip + cmp.getEl().dom.title = val.Label; + } + } + }); + }, this); + }, this); + + Ext4.iterate(productRolesItems, function(key, val){ + var column = { + xtype: 'checkboxgroup', + fieldLabel: key, + labelStyle: 'font-weight: bold;', + colspan: 1, + columns: 1, + flex: 0.75, + height: 22 * val.length + 22, + style: 'margin-left: 20px;', + items: val + + }; + columns.push(column); + }); + return columns; + }, + + getProductAndDoseRouteValues : function(productRole, checkedProducts) + { + var data = []; + + Ext4.each(Ext4.getStore('Product').getRange(), function(product) + { + if (!product.get('RowId') || product.get('Role') != productRole) + return; + var hasDoseRoute = false; + Ext4.each(Ext4.getStore('DoseAndRoute').getRange(), function(dose) + { + if (dose.get('RowId') != null && dose.get('ProductId') == product.get('RowId') && product.get('Role') == productRole) { + var productDose = Ext4.clone(dose.data); + productDose.ProductLabel = product.get('Label'); + productDose.Label = product.get('Label') + ' - ' + dose.get('Label'); + productDose.ProductDoseRoute = dose.get('ProductId') + '-#-' + dose.get('Label'); + productDose.SortKey = product.get('RowId'); + data.push(productDose); + hasDoseRoute = true; + } + }, this); + if (!hasDoseRoute) + { + var productDose = Ext4.clone(product.data); + productDose.ProductLabel = product.get('Label'); + productDose.Label = product.get('Label'); + productDose.ProductDoseRoute = product.get('RowId') + '-#-'; + productDose.SortKey = product.get('RowId'); + data.push(productDose); + } + else { + //Issue 28273: No products selected in Treatment dialog for single table manage treatment page if product does not have dose/route selected + Ext4.each(checkedProducts, function(checked){ + if (checked.ProductId == product.get('RowId') && !checked.DoseAndRoute && !checked.Dose && !checked.Route) + { + var productDose = Ext4.clone(checked); + productDose.ProductLabel = checked['ProductId/Label']; + productDose.Label = checked['ProductId/Label']; + productDose.SortKey = product.get('RowId'); + data.push(productDose); + } + }); + } + }, this); + + data.sort(function(a, b){ + // if different product, sort by product RowId + // otherwise use a natural sort on the generated product + dose/route label + if (a.SortKey > b.SortKey) { + return 1; + } + else if (a.SortKey < b.SortKey) { + return -1; + } + else { + return LABKEY.internal.SortUtil.naturalSort(a.Label, b.Label); + } + }); + + return data; + }, + + getTreatmentFormValues: function() { + var fields = this.getForm().getForm().getFields().items, treatmentLabel = ''; + for (var f = 0; f < fields.length; f++) { + var field = fields[f]; + var data = field.getSubmitData(); + if (Ext4.isObject(data) && field.productLabel) { + if (treatmentLabel != '') { + treatmentLabel += '|'; + } + treatmentLabel += field.productLabel; + } + } + + var productValues = this.getForm().getValues(), treatment = {Label: treatmentLabel, Products: []}; + Ext4.iterate(productValues, function(key, val){ + treatment[key] = []; + if (val) { + if (Ext4.isArray(val)){ + Ext4.each(val, function(v){ + treatment[key].push({ProductDoseRoute: v}); + treatment.Products.push({ProductDoseRoute: v}); + }) + } + else { + treatment[key].push({ProductDoseRoute: val}); + treatment.Products.push({ProductDoseRoute: val}); + } + } + }); + return treatment; + } +}); + diff --git a/studydesign/webapp/study/vaccineDesign/TreatmentSchedule.js b/studydesign/webapp/study/vaccineDesign/TreatmentSchedule.js new file mode 100644 index 00000000..de25f2f1 --- /dev/null +++ b/studydesign/webapp/study/vaccineDesign/TreatmentSchedule.js @@ -0,0 +1,465 @@ +/* + * Copyright (c) 2016-2017 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 + */ +Ext4.define('LABKEY.VaccineDesign.TreatmentSchedulePanel', { + extend : 'LABKEY.VaccineDesign.TreatmentSchedulePanelBase', + + width: 1400, + + initComponent : function() + { + this.items = [this.getTreatmentsGrid()]; + + this.callParent(); + + window.onbeforeunload = LABKEY.beforeunload(this.beforeUnload, this); + }, + + getTreatmentsGrid : function() + { + if (!this.treatmentsGrid) + { + this.treatmentsGrid = Ext4.create('LABKEY.VaccineDesign.TreatmentsGrid', { + disableEdit: this.disableEdit, + productRoles: this.productRoles + }); + + this.treatmentsGrid.on('dirtychange', this.enableSaveButton, this); + this.treatmentsGrid.on('celledited', this.enableSaveButton, this); + + // Note: since we need the data from the treatment grid, don't add this.getTreatmentScheduleGrid() until the treatment grid store has loaded + this.treatmentsGrid.on('loadcomplete', this.onTreatmentGridLoadComplete, this, {single: true}); + } + + return this.treatmentsGrid; + }, + + onTreatmentGridLoadComplete : function() + { + this.add(this.getTreatmentScheduleGrid()); + this.add(this.getButtonBar()); + + // since a treatment label change needs to be reflected in the treatment schedule grid, force a refresh there + this.getTreatmentsGrid().on('celledited', function(view, fieldName, value){ + if (fieldName == 'Label') + this.getTreatmentScheduleGrid().refresh(); + }, this); + + // removing a treatment row needs to also remove any visit mappings for that treatment + this.getTreatmentsGrid().on('beforerowdeleted', function(grid, record){ + this.getTreatmentScheduleGrid().removeTreatmentUsages(record.get('RowId')); + }, this); + }, + + getTreatmentScheduleGrid : function() + { + if (!this.treatmentScheduleGrid) + { + this.treatmentScheduleGrid = Ext4.create('LABKEY.VaccineDesign.TreatmentScheduleGrid', { + padding: '20px 0', + disableEdit: this.disableEdit, + subjectNoun: this.subjectNoun, + visitNoun: this.visitNoun + }); + + this.treatmentScheduleGrid.on('dirtychange', this.enableSaveButton, this); + this.treatmentScheduleGrid.on('celledited', this.enableSaveButton, this); + } + + return this.treatmentScheduleGrid; + }, + + getTreatments: function() + { + var treatments = [], index = 0, errorMsg = []; + + Ext4.each(this.getTreatmentsGrid().getStore().getRange(), function(record) + { + var recData = Ext4.clone(record.data); + index++; + + // drop any empty immunogen or adjuvant or challenge rows that were just added + recData['Products'] = []; + Ext4.each(this.productRoles, function(role) + { + Ext4.each(recData[role], function(product) + { + if (Ext4.isDefined(product['RowId']) || LABKEY.VaccineDesign.Utils.objectHasData(product)) + recData['Products'].push(product); + }, this); + }, this); + + // drop any empty treatment rows that were just added + var hasData = recData['Label'] != '' || recData['Description'] != '' || recData['Products'].length > 0; + if (Ext4.isDefined(recData['RowId']) || hasData) + { + var treatmentLabel = recData['Label'] != '' ? '\'' + recData['Label'] + '\'' : index; + + // validation: treatment must have at least one immunogen or adjuvant, no duplicate immunogens/adjuvants for a treatment + var treatmentProductIds = Ext4.Array.clean(Ext4.Array.pluck(recData['Products'], 'ProductId')); + if (recData['Products'].length == 0) + errorMsg.push('Treatment ' + treatmentLabel + ' must have at least one immunogen, adjuvant or challenge defined.'); + else if (treatmentProductIds.length != Ext4.Array.unique(treatmentProductIds).length) + errorMsg.push('Treatment ' + treatmentLabel + ' contains a duplicate immunogen, adjuvant or challenge.'); + else + treatments.push(recData); + } + }, this); + + if (errorMsg.length > 0) + { + this.onFailure(errorMsg.join('
    ')); + return false; + } + return treatments; + } +}); + +Ext4.define('LABKEY.VaccineDesign.TreatmentsGrid', { + extend : 'LABKEY.VaccineDesign.BaseDataView', + + cls : 'study-vaccine-design vaccine-design-treatments', + + mainTitle : 'Treatments', + + width: 1400, + + studyDesignQueryNames : ['StudyDesignRoutes', 'Product', 'DoseAndRoute'], + + //Override + getStore : function() + { + if (!this.store) + { + this.store = Ext4.create('Ext.data.Store', { + storeId : 'TreatmentsGridStore', + model : 'LABKEY.VaccineDesign.Treatment', + proxy: { + type: 'ajax', + url : LABKEY.ActionURL.buildURL("study-design", "getStudyTreatments", null, {splitByRole: true}), + reader: { + type: 'json', + root: 'treatments' + } + }, + sorters: [{ property: 'RowId', direction: 'ASC' }], + autoLoad: true + }); + } + + return this.store; + }, + + //Override + getColumnConfigs : function() + { + if (!this.columnConfigs) + { + this.columnConfigs = [{ + label: 'Label', + width: 200, + dataIndex: 'Label', + required: true, + editorType: 'Ext.form.field.Text', + editorConfig: LABKEY.VaccineDesign.Utils.getStudyDesignTextConfig('Label', 185) + },{ + label: 'Description', + width: 200, + dataIndex: 'Description', + editorType: 'Ext.form.field.TextArea', + editorConfig: LABKEY.VaccineDesign.Utils.getStudyDesignTextConfig('Description', 185, '95%') + }]; + + if (Ext4.isArray(this.productRoles)) { + Ext4.each(this.productRoles, function(role){ + var roleColumn = this.getProductRoleColumn(role); + this.columnConfigs.push(roleColumn); + }, this); + } + } + + return this.columnConfigs; + }, + + getProductRoleColumn: function(roleName) { + var column = { + label: roleName + 's', + width: 310, + dataIndex: roleName, + subgridConfig: { + columns: [{ + label: roleName, + width: 140, + dataIndex: 'ProductId', + required: true, + queryName: 'Product', + editorType: 'LABKEY.ext4.ComboBox', + editorConfig: this.getProductEditor(roleName) + },{ + label: 'Dose and Route', + width: 140, + dataIndex: 'DoseAndRoute', + queryName: 'DoseAndRoute', + editorType: 'LABKEY.ext4.ComboBox', + editorConfig: this.getDoseAndRouteEditorConfig() + }] + } + }; + return column; + }, + + getProductEditor : function(roleName){ + + var filter = LABKEY.Filter.create('Role', roleName), + cfg = LABKEY.VaccineDesign.Utils.getStudyDesignComboConfig('ProductId', 125, 'Product', filter, 'Label', 'RowId'); + + cfg.listeners = { + scope: this, + change : function(cmp, productId) { + // clear out (if any) value for the dose and route field + var record = this.getStore().getAt(cmp.storeIndex), + outerDataIndex = cmp.outerDataIndex, + subgridIndex = Number(cmp.subgridIndex), + selector = 'tr.data-row:nth(' + (this.getStore().indexOf(record)+1) + ') table.subgrid-' + outerDataIndex + + ' tr.subrow:nth(' + (subgridIndex+1) + ') td[data-index=DoseAndRoute] input'; + + var inputField = this.getInputFieldFromSelector(selector); + if (inputField != null) + { + inputField.setValue(''); + inputField.bindStore(this.getNewDoseAndRouteComboStore(productId)); + } + } + }; + return cfg; + }, + + getDoseAndRouteEditorConfig : function() + { + return { + hideFieldLabel: true, + name: 'DoseAndRoute', + width: 125, + forceSelection : false, // allow usage of inactive types + editable : false, + queryMode : 'local', + displayField : 'Label', + valueField : 'Label', + store : null, // the store will be created and bound to this combo after render + listeners : { + scope: this, + render : function(cmp) { + var record = this.getStore().getAt(cmp.storeIndex), + outerDataIndex = cmp.outerDataIndex, + subgridIndex = Number(cmp.subgridIndex), + productId = record.get(outerDataIndex)[subgridIndex]['ProductId']; + + cmp.bindStore(this.getNewDoseAndRouteComboStore(productId)); + }, + change : function(cmp, newValue, oldValue) { + var record = this.getStore().getAt(cmp.storeIndex), + outerDataIndex = cmp.outerDataIndex, + subgridIndex = Number(cmp.subgridIndex), + subRecord = record.get(outerDataIndex)[subgridIndex]; + + // if the ProductDoseRoute is set, we need to update it + if (Ext4.isDefined(subRecord['ProductDoseRoute']) && Ext4.isDefined(subRecord['ProductId'])) + subRecord['ProductDoseRoute'] = subRecord['ProductId'] + '-#-' + newValue; + } + } + }; + }, + + getNewDoseAndRouteComboStore : function(productId) + { + // need to create a new store each time since we need to add a [none] option and include any new treatment records + var data = []; + Ext4.each(Ext4.getStore('DoseAndRoute').getRange(), function(record) + { + if (record.get('ProductId') == null || record.get('ProductId') == productId) + data.push(Ext4.clone(record.data)); + }, this); + + return Ext4.create('Ext.data.Store', { + fields: ['RowId', 'Label'], + data: data + }); + }, + + //Override + getNewModelInstance : function() + { + return LABKEY.VaccineDesign.Treatment.create({ + RowId: Ext4.id() // need to generate an id so that the treatment schedule grid can use it + }); + }, + + //Override + getDeleteConfirmationMsg : function() + { + return 'Are you sure you want to delete the selected treatment? ' + + 'Note: this will also delete any usages of this treatment record in the Treatment Schedule grid below.'; + }, + + //Override + updateSubgridRecordValue : function(record, outerDataIndex, subgridIndex, fieldName, newValue) + { + var preProductIds = []; + Ext4.each(this.productRoles, function(role){ + var productRoleIds = Ext4.Array.pluck(record.get(role), 'ProductId'); + if (preProductIds.length == 0) + preProductIds = productRoleIds; + else + preProductIds = preProductIds.concat(productRoleIds); + }); + + this.callParent([record, outerDataIndex, subgridIndex, fieldName, newValue]); + + // auto populate the treatment label if the user has not already entered a value + if (fieldName == 'ProductId') + this.populateTreatmentLabel(record, preProductIds); + }, + + //Override + removeSubgridRecord : function(target, record) + { + var preProductIds = []; + Ext4.each(this.productRoles, function(role){ + var productRoleIds = Ext4.Array.pluck(record.get(role), 'ProductId'); + if (preProductIds.length == 0) + preProductIds = productRoleIds; + else + preProductIds = preProductIds.concat(productRoleIds); + }); + this.callParent([target, record]); + this.populateTreatmentLabel(record, preProductIds); + this.refresh(true); + }, + + populateTreatmentLabel : function(record, preProductIds) + { + var currentLabel = record.get('Label'); + if (currentLabel == '' || currentLabel == this.getLabelFromProductIds(preProductIds)) + { + var postProductIds = []; + Ext4.each(this.productRoles, function(role){ + var productRoleIds = Ext4.Array.pluck(record.get(role), 'ProductId'); + if (postProductIds.length == 0) + postProductIds = productRoleIds; + else + postProductIds = postProductIds.concat(productRoleIds); + }); + + var updatedTreatmentLabel = this.getLabelFromProductIds(postProductIds); + + // need to update the input field value, which will intern update the record and fire teh celledited event + var inputField = this.getInputFieldFromSelector('tr.data-row:nth(' + (this.getStore().indexOf(record)+1) + ') td.cell-value input'); + if (inputField != null) + { + inputField.setValue(updatedTreatmentLabel); + record.set('Label', updatedTreatmentLabel); + } + } + }, + + getInputFieldFromSelector : function(selector) + { + var inputFieldEl = Ext4.DomQuery.selectNode(selector, this.getEl().dom); + if (inputFieldEl != null) + return Ext4.ComponentManager.get(inputFieldEl.id.replace('-inputEl', '')); + + return null; + }, + + getLabelFromProductIds : function(productIdsArr) + { + var labelArr = []; + + if (Ext4.isArray(productIdsArr)) + { + Ext4.each(productIdsArr, function(productId){ + if (productId != undefined || productId != null) + labelArr.push(LABKEY.VaccineDesign.Utils.getLabelFromStore('Product', productId)); + }); + } + + return labelArr.join(' | '); + } +}); + +Ext4.define('LABKEY.VaccineDesign.TreatmentScheduleGrid', { + extend : 'LABKEY.VaccineDesign.TreatmentScheduleGridBase', + + //Override + onStudyTreatmentScheduleStoreLoad : function() + { + this.getStore().fireEvent('load', this.getStore()); + }, + + getTreatmentsStore : function() + { + if (!this.treatmentsStore) + { + this.treatmentsStore = Ext4.getStore('TreatmentsGridStore'); + } + + return this.treatmentsStore; + }, + + //Override + getTreatmentFieldEditorType: function() + { + return 'LABKEY.ext4.ComboBox'; + }, + + //Override + isFieldTreatmentLookup: function() + { + return false; + }, + + getTreatmentFieldConfig : function() + { + return { + hideFieldLabel: true, + name: 'VisitMap', + width: 135, + forceSelection : false, // allow usage of inactive types + editable : false, + queryMode : 'local', + displayField : 'Label', + valueField : 'RowId', + store : this.getNewTreatmentComboStore() + }; + }, + + getNewTreatmentComboStore : function() + { + // need to create a new store each time since we need to add a [none] option and include any new treatment records + var data = [{RowId: null, Label: '[none]'}]; + Ext4.each(this.getTreatmentsStore().getRange(), function(record) + { + data.push(Ext4.clone(record.data)); + }, this); + + return Ext4.create('Ext.data.Store', { + fields: ['RowId', 'Label'], + data: data + }); + }, + + removeTreatmentUsages : function(treatmentId) + { + this.getStore().suspendEvents(); + Ext4.each(this.getStore().getRange(), function(record) + { + var newVisitMapArr = Ext4.Array.filter(record.get('VisitMap'), function(item){ return item.TreatmentId != treatmentId; }); + record.set('VisitMap', newVisitMapArr); + }, this); + this.getStore().resumeEvents(); + + this.refresh(true); + } +}); \ No newline at end of file diff --git a/studydesign/webapp/study/vaccineDesign/TreatmentScheduleBase.js b/studydesign/webapp/study/vaccineDesign/TreatmentScheduleBase.js new file mode 100644 index 00000000..73aece24 --- /dev/null +++ b/studydesign/webapp/study/vaccineDesign/TreatmentScheduleBase.js @@ -0,0 +1,437 @@ +/* + * Copyright (c) 2016-2017 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 + */ +Ext4.define('LABKEY.VaccineDesign.TreatmentSchedulePanelBase', { + extend : 'Ext.panel.Panel', + + border : false, + + bodyStyle : 'background-color: transparent;', + + disableEdit : true, + + dirty : false, + + returnUrl : null, + + getButtonBar : function() + { + if (!this.buttonBar) + { + this.buttonBar = Ext4.create('Ext.toolbar.Toolbar', { + dock: 'bottom', + ui: 'footer', + padding: 0, + style : 'background-color: transparent;', + defaults: {width: 75}, + items: [this.getSaveButton(), this.getCancelButton()] + }); + } + + return this.buttonBar; + }, + + getSaveButton : function() + { + if (!this.saveButton) + { + this.saveButton = Ext4.create('Ext.button.Button', { + text: 'Save', + disabled: true, + hidden: this.disableEdit, + handler: this.saveTreatmentSchedule, + scope: this + }); + } + + return this.saveButton; + }, + + enableSaveButton : function() + { + this.setDirty(true); + this.getSaveButton().enable(); + }, + + getCancelButton : function() + { + if (!this.cancelButton) + { + this.cancelButton = Ext4.create('Ext.button.Button', { + text: this.disableEdit ? 'Done' : 'Cancel', + handler: this.goToReturnURL, + scope: this + }); + } + + return this.cancelButton; + }, + + getTreatments: function() + { + return []; + }, + + saveTreatmentSchedule : function() + { + this.getEl().mask('Saving...'); + + var treatments = this.getTreatments(), cohorts = [], errorMsg = []; + + if (!Ext4.isArray(treatments)) // treatments invalid + return; + + Ext4.each(this.getTreatmentScheduleGrid().getStore().getRange(), function(record) + { + var recData = Ext4.clone(record.data); + + // drop any empty cohort rows that were just added + var hasData = recData['Label'] != '' || recData['SubjectCount'] != '' || recData['VisitMap'].length > 0; + if (Ext4.isDefined(recData['RowId']) || hasData) + { + var countVal = Number(recData['SubjectCount']); + if (isNaN(countVal) || countVal < 0) + errorMsg.push('Cohort ' + this.subjectNoun.toLowerCase() + ' count values must be a positive integer: ' + recData['SubjectCount'] + '.'); + else + cohorts.push(recData); + } + }, this); + + if (errorMsg.length > 0) + { + this.onFailure(errorMsg.join('
    ')); + return; + } + + LABKEY.Ajax.request({ + url : LABKEY.ActionURL.buildURL('study-design', 'updateTreatmentSchedule.api'), + method : 'POST', + jsonData: { + treatments: treatments, + cohorts: cohorts + }, + scope: this, + success: function(response) + { + var resp = Ext4.decode(response.responseText); + if (resp.success) + this.goToReturnURL(); + else + this.onFailure(); + }, + failure: function(response) + { + var resp = Ext4.decode(response.responseText); + if (resp.errors) + this.onFailure(Ext4.Array.pluck(resp.errors, 'message').join('
    ')); + else + this.onFailure(resp.exception); + } + }); + }, + + goToReturnURL : function() + { + this.setDirty(false); + window.location = this.returnUrl; + }, + + onFailure : function(text) + { + Ext4.Msg.show({ + title: 'Error', + msg: text || 'Unknown error occurred.', + icon: Ext4.Msg.ERROR, + buttons: Ext4.Msg.OK + }); + + this.getEl().unmask(); + }, + + setDirty : function(dirty) + { + this.dirty = dirty; + LABKEY.Utils.signalWebDriverTest("treatmentScheduleDirty", dirty); + }, + + isDirty : function() + { + return this.dirty; + }, + + beforeUnload : function() + { + if (!this.disableEdit && this.isDirty()) + return 'Please save your changes.'; + } +}); + +Ext4.define('LABKEY.VaccineDesign.TreatmentScheduleGridBase', { + extend : 'LABKEY.VaccineDesign.BaseDataViewAddVisit', + + cls : 'study-vaccine-design vaccine-design-cohorts', + + mainTitle : 'Treatment Schedule', + + width: 350, + + subjectNoun : 'Subject', + + visitNoun : 'Visit', + + //studyDesignQueryNames : ['StudyDesignRoutes', 'Product', 'DoseAndRoute'], + + getStore : function() + { + if (!this.store) + { + this.store = Ext4.create('Ext.data.Store', { + model : 'LABKEY.VaccineDesign.Cohort', + sorters: [{ property: 'RowId', direction: 'ASC' }] + }); + + this.queryStudyTreatmentSchedule(); + } + + return this.store; + }, + + queryStudyTreatmentSchedule : function() + { + LABKEY.Ajax.request({ + url: LABKEY.ActionURL.buildURL('study-design', 'getStudyTreatmentSchedule', null, {splitByRole: true}), + method: 'GET', + scope: this, + success: function (response) + { + var o = Ext4.decode(response.responseText); + if (o.success) + { + this.getVisitStore(o['visits']); + this.getStore().loadData(o['cohorts']); + this.onStudyTreatmentScheduleStoreLoad(); + } + } + }); + }, + + getVisitStore : function(data) + { + if (!this.visitStore) + { + this.visitStore = Ext4.create('Ext.data.Store', { + model : 'LABKEY.VaccineDesign.Visit', + data : data, + sorters : [{property: 'DisplayOrder', direction: 'ASC'},{property: 'SequenceNumMin', direction: 'ASC'}] + }); + } + + return this.visitStore; + }, + + getTreatmentsStore : function() + { + if (!this.treatmentsStore) + { + var me = this; + this.treatmentsStore = Ext4.create('Ext.data.Store', { + storeId : 'TreatmentsGridStore', + model : 'LABKEY.VaccineDesign.Treatment', + proxy: { + type: 'ajax', + url : LABKEY.ActionURL.buildURL("study-design", "getStudyTreatments", null, {splitByRole: true}), + reader: { + type: 'json', + root: 'treatments' + } + }, + sorters: [{ property: 'RowId', direction: 'ASC' }], + autoLoad: true, + listeners: { + scope: this, + load: function (store) + { + me.getStore().fireEvent('load', me.getStore()); + } + } + }); + } + + return this.treatmentsStore; + }, + + getVisitColumnConfigs : function() + { + var visitConfigs = []; + + Ext4.each(this.getVisitStore().getRange(), function(visit) + { + if (visit.get('Included')) + { + visitConfigs.push({ + label: visit.get('Label') || (this.visitNoun + visit.get('RowId')), + width: 150, + dataIndex: 'VisitMap', + dataIndexArrFilterProp: 'VisitId', + dataIndexArrFilterValue: visit.get('RowId'), + dataIndexArrValue: 'TreatmentId', + lookupStoreId: 'TreatmentsGridStore', + editorType: this.getTreatmentFieldEditorType(), + editorConfig: this.getTreatmentFieldConfig, + isTreatmentLookup: this.isFieldTreatmentLookup() + }); + } + }, this); + + if (visitConfigs.length == 0 && !this.disableEdit) + { + visitConfigs.push({ + label: 'No ' + this.visitNoun + 's Defined', + displayValue: '', + width: 160 + }); + } + + return visitConfigs; + }, + + //Override + getColumnConfigs : function() + { + if (!this.columnConfigs) + { + var columnConfigs = [{ + label: 'Group / Cohort', + width: 200, + dataIndex: 'Label', + required: true, + editorType: 'Ext.form.field.Text', + editorConfig: LABKEY.VaccineDesign.Utils.getStudyDesignTextConfig('Label', 185) + },{ + label: this.subjectNoun + ' Count', + width: 130, + dataIndex: 'SubjectCount', + editorType: 'Ext.form.field.Number', + editorConfig: LABKEY.VaccineDesign.Utils.getStudyDesignNumberConfig('SubjectCount', 115) + }]; + + var visitConfigs = this.getVisitColumnConfigs(); + + // update the width based on the number of visit columns + var width = 400 + (Math.max(2, visitConfigs.length) * 150); + this.setWidth(width); + + // update the outer panel width if necessary + var outerPanel = this.up('panel'); + if (outerPanel != null) + outerPanel.setWidth(Math.max(width, 1400)); + + this.columnConfigs = columnConfigs.concat(visitConfigs); + } + + return this.columnConfigs; + }, + + //Override + getNewModelInstance : function() + { + var newCohort = LABKEY.VaccineDesign.Cohort.create(); + newCohort.set('VisitMap', []); + return newCohort; + }, + + //Override + removeOuterRecord : function(title, record) { + if (!record.get('CanDelete')) { + Ext4.Msg.show({ + title: 'Unable to Remove Cohort', + msg: 'The selected cohort can not be removed because it is in-use in the study.
    ' + + Ext4.String.htmlEncode(record.get('Label')) + '', + buttons: Ext4.Msg.OK, + icon: Ext4.Msg.INFO + }); + } + else { + this.callParent([title, record]); + } + }, + + //Override + getDeleteConfirmationMsg : function() + { + return 'Are you sure you want to delete the selected group / cohort and its associated treatment / visit mapping records?'; + }, + + //Override + getCurrentCellValue : function(column, record, dataIndex, outerDataIndex, subgridIndex) + { + var value = this.callParent([column, record, dataIndex, outerDataIndex, subgridIndex]); + + if (Ext4.isArray(value)) + { + var matchingIndex = LABKEY.VaccineDesign.Utils.getMatchingRowIndexFromArray(value, column.dataIndexArrFilterProp, column.dataIndexArrFilterValue); + if (matchingIndex > -1) + return value[matchingIndex][column.dataIndexArrValue]; + else + return null; + } + + return value; + }, + + getTreatmentCellDisplayValue : function(val, lookupStore) + { + var displayVal = val; + if (Ext4.isDefined(lookupStore) && val != null && val != '') + { + var store = Ext4.getStore(lookupStore); + if (store != null) + { + var record = store.findRecord('RowId', val); + if (record != null) + displayVal = record.get('Label'); + } + } + return displayVal; + }, + + //Override + updateStoreRecordValue : function(record, column, newValue, field) + { + var value = this.getTreatmentValue(column, newValue, field); + // special case for editing the value of one of the pivot visit columns + if (column.dataIndex == 'VisitMap') + { + var visitMapArr = record.get(column.dataIndex), + matchingIndex = LABKEY.VaccineDesign.Utils.getMatchingRowIndexFromArray(visitMapArr, column.dataIndexArrFilterProp, column.dataIndexArrFilterValue); + + if (matchingIndex > -1) + { + if (value != null) + visitMapArr[matchingIndex][column.dataIndexArrValue] = value; + else + Ext4.Array.splice(visitMapArr, matchingIndex, 1); + } + else if (value != null) + { + visitMapArr.push({ + CohortId: record.get('RowId'), + VisitId: column.dataIndexArrFilterValue, + TreatmentId: value + }); + } + + this.fireEvent('celledited', this, 'VisitMap', visitMapArr); + } + else + { + this.callParent([record, column, value]); + } + }, + + getTreatmentValue : function(column, newValue, field) { + return newValue; + } +}); \ No newline at end of file diff --git a/studydesign/webapp/study/vaccineDesign/TreatmentScheduleSingleTablePanel.js b/studydesign/webapp/study/vaccineDesign/TreatmentScheduleSingleTablePanel.js new file mode 100644 index 00000000..f71af73e --- /dev/null +++ b/studydesign/webapp/study/vaccineDesign/TreatmentScheduleSingleTablePanel.js @@ -0,0 +1,224 @@ +/* + * Copyright (c) 2016-2019 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 + */ +Ext4.define('LABKEY.VaccineDesign.TreatmentScheduleSingleTablePanel', { + extend : 'LABKEY.VaccineDesign.TreatmentSchedulePanelBase', + + width: 1400, + + initComponent : function() + { + this.items = [this.getTreatmentScheduleGrid(), this.getButtonBar()]; + + this.callParent(); + + window.onbeforeunload = LABKEY.beforeunload(this.beforeUnload, this); + }, + + getTreatmentScheduleGrid : function() + { + if (!this.treatmentScheduleGrid) + { + this.treatmentScheduleGrid = Ext4.create('LABKEY.VaccineDesign.TreatmentScheduleSingleTableGrid', { + padding: '20px 0', + disableEdit: this.disableEdit, + subjectNoun: this.subjectNoun, + visitNoun: this.visitNoun, + productRoles: this.productRoles + }); + + this.treatmentScheduleGrid.on('dirtychange', this.enableSaveButton, this); + this.treatmentScheduleGrid.on('celledited', this.enableSaveButton, this); + } + + return this.treatmentScheduleGrid; + } + +}); + +Ext4.define('LABKEY.VaccineDesign.TreatmentScheduleSingleTableGrid', { + extend : 'LABKEY.VaccineDesign.TreatmentScheduleGridBase', + + studyDesignQueryNames : ['StudyDesignRoutes', 'Product', 'DoseAndRoute'], + + //Override + onStudyTreatmentScheduleStoreLoad : function() + { + this.getTreatmentsStore(); + }, + + getTreatmentsStore : function() + { + if (!this.treatmentsStore) + { + var me = this; + this.treatmentsStore = Ext4.create('Ext.data.Store', { + storeId : 'TreatmentsGridStore', + model : 'LABKEY.VaccineDesign.Treatment', + proxy: { + type: 'ajax', + url : LABKEY.ActionURL.buildURL("study-design", "getStudyTreatments", null, {splitByRole: true}), + reader: { + type: 'json', + root: 'treatments' + } + }, + sorters: [{ property: 'RowId', direction: 'ASC' }], + autoLoad: true, + listeners: { + scope: this, + load: function (store) + { + me.getStore().fireEvent('load', me.getStore()); + } + } + }); + } + + return this.treatmentsStore; + }, + + getTreatmentFieldConfig : function() + { + var me = this; + return { + hideFieldLabel: true, + name: 'VisitMap', + width: 135, + readOnly: true, + enableKeyEvents: false, + cls: 'treatment-input-cell', + listeners: { + render: function(cmp) { + if (cmp.value) + cmp.getEl().dom.title = cmp.value; //tooltip + + cmp.getEl().on('click', function(){ + if (me.productRoles == null || me.productRoles.length == 0) { + Ext4.Msg.show({ + title: 'Error', + msg: 'No study products have been defined.', + icon: Ext4.Msg.ERROR, + buttons: Ext4.Msg.OK + }); + return; + } + + var win; + var popupConfig = { + productRoles: me.productRoles, + autoScroll : true, + buttonAlign : 'right', + modal: true, + width: 400, + height: 500, + border: false, + closable: false, + title: 'Treatment', + draggable: false, + buttons: [{ + text: 'Cancel', + onClick : function () { + win.close(); + } + },{ + text: 'OK', + cls: 'commentSubmit', + onClick : function () { + var isFormDirty = win.getForm().getForm().isDirty(); + if (!isFormDirty) { + win.close(); + return; + } + + var treatment = win.getTreatmentFormValues(); + if (treatment && treatment.Products.length == 0) { + win.close(); + cmp.treatmentId = null; + cmp.setValue(null); + cmp.getEl().dom.title = ''; + return; + } + var treatments = [treatment]; + LABKEY.Ajax.request({ + url : LABKEY.ActionURL.buildURL('study-design', 'updateTreatments.api'), + method : 'POST', + jsonData: { + treatments: treatments + }, + scope: this, + success: function(response) + { + var resp = Ext4.decode(response.responseText); + if (resp.success) { + win.close(); + me.fireEvent('celledited'); + var changed = cmp.treatmentId != resp.treatmentIds[0]; + if (changed) + cmp.setValue(''); // force a change event, in case label is same, but id changed + cmp.treatmentId = resp.treatmentIds[0]; + cmp.setValue(treatments[0].Label); + cmp.getEl().dom.title = treatments[0].Label; + me.getTreatmentsStore().load(); + } + else + this.onFailure(); + }, + failure: function(response) + { + var resp = Ext4.decode(response.responseText); + if (resp.errors) + this.onFailure(Ext4.Array.pluck(resp.errors, 'message').join('
    ')); + else + this.onFailure(resp.exception); + } + }); + + } + }] + + }; + if (cmp.treatmentId) { + Ext4.Ajax.request({ + url : LABKEY.ActionURL.buildURL("study-design", "getStudyTreatments", null, {splitByRole: true, treatmentId: cmp.treatmentId}), + method : 'POST', + success: LABKEY.Utils.getCallbackWrapper(function(response){ + popupConfig.treatmentDetails = response.treatments.length > 0 ? response.treatments[0] : null; + win = new LABKEY.VaccineDesign.TreatmentDialog(popupConfig); + win.show(); + + }, me) + }); + } + else { + win = new LABKEY.VaccineDesign.TreatmentDialog(popupConfig); + win.show(); + } + }); + } + } + }; + }, + + //Override + getTreatmentFieldEditorType: function() + { + return 'Ext.form.field.Text'; + }, + + //Override + isFieldTreatmentLookup: function() + { + return true; + }, + + //Override + getTreatmentValue : function(column, newValue, field) { + var value = newValue; + if (column.lookupStoreId && field && field.treatmentId) + value = field.treatmentId; + return value; + } +}); \ No newline at end of file diff --git a/studydesign/webapp/study/vaccineDesign/Utils.js b/studydesign/webapp/study/vaccineDesign/Utils.js new file mode 100644 index 00000000..ce684cd9 --- /dev/null +++ b/studydesign/webapp/study/vaccineDesign/Utils.js @@ -0,0 +1,212 @@ +/* + * Copyright (c) 2016-2017 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 + */ + +Ext4.define('LABKEY.VaccineDesign.Utils', { + + singleton: true, + + /** + * Helper function to get field editor config object for a study design lookup combo. + * @param name Field name + * @param width Field width + * @param queryName If combo, the queryName for the store + * @param filter LABKEY.Filter.create() object + * @param displayField The field name of the combo store displayField + * @param valueField The field name of the combo store valueField + * @returns {Object} Field config + */ + getStudyDesignComboConfig : function(name, width, queryName, filter, displayField, valueField) + { + return { + hideFieldLabel: true, + name: name, + width: width || 150, + forceSelection : false, // allow usage of inactive types + editable : false, + queryMode : 'local', + // TODO: this does not htmlEncode the display value in expanded options list + displayField : displayField || 'Label', + valueField : valueField || 'Name', + store : LABKEY.VaccineDesign.Utils.getStudyDesignStore(queryName, filter) + }; + }, + + /** + * Helper function to get field editor config object for a study design text or text area field. + * @param name Field name + * @param width Field width + * @param height Field height + * @returns {Object} Field config + */ + getStudyDesignTextConfig : function(name, width, height) + { + return { + hideFieldLabel: true, + name: name, + width: width, + height: height, + selectOnFocus: true + } + }, + + /** + * Helper function to get field editor config object for a study design number field. + * @param name Field name + * @param width Field width + * @param decimalPrecision Field maximum precision to display after decimal + * @returns {Object} Field config + */ + getStudyDesignNumberConfig : function(name, width, decimalPrecision) + { + return { + hideFieldLabel: true, + name: name, + width: width, + minValue: 0, + allowDecimals: Ext4.isNumber(decimalPrecision), + decimalPrecision: decimalPrecision + } + }, + + /** + * Create a new LABKEY.ext4.Store for the given queryName from the study schema. + * @param queryName + * @param filter LABKEY.Filter.create() object + * @returns {LABKEY.ext4.Store} + */ + getStudyDesignStore : function(queryName, filter) + { + var key = Ext4.isDefined(filter) ? queryName + '|' + filter.getColumnName() + '|' + filter.getValue() : queryName, + store = Ext4.getStore(key), + hasStudyDesignPrefix = queryName.indexOf('StudyDesign') == 0, + columns = 'RowId,Name,Label'; + + if (Ext4.isDefined(store)) + return store; + + // special case to query DisplayOrder and SequenceNumMin for Visit table + if (queryName == 'Visit') + columns += ',DisplayOrder,SequenceNumMin'; + // special case to query ProductId column for DoseAndRoute table + else if (queryName == 'DoseAndRoute') + columns += ',ProductId'; + else if (queryName == 'Product') + columns += ',Role'; + else if (queryName == 'DataSets') + columns += ',DataSetId'; + + return Ext4.create('LABKEY.ext4.Store', { + storeId: key, + schemaName: 'study', + queryName: queryName, + columns: columns, + filterArray: Ext4.isDefined(filter) ? [filter] : (hasStudyDesignPrefix ? [LABKEY.Filter.create('Inactive', false)] : []), + containerFilter: LABKEY.container.type == 'project' || Ext4.isDefined(filter) || !hasStudyDesignPrefix ? undefined : 'CurrentPlusProject', + sort: '-Container/Path,Label', + autoLoad: true, + listeners: { + load: function(store) + { + store.insert(0, {Name: null}); + } + } + }); + }, + + /** + * Lookup a label for a given value using a store generated from the queryName. + * @param queryName + * @param value + * @returns {String} + */ + getLabelFromStore : function(queryName, value) + { + var store = LABKEY.VaccineDesign.Utils.getStudyDesignStore(queryName), + storeCols = Ext4.Array.pluck(store.getColumns(), 'dataIndex'), + keys = ['RowId', 'Name', 'DataSetId'], + record = null; + + Ext4.each(keys, function(key) { + if (record == null && storeCols.indexOf(key) > -1) + record = store.findRecord(key, value, 0, false, true, true); + }); + + return record != null ? record.get("Label") : value; + }, + + /** + * Check if the given object has any properties which have data (i.e. non null, not an empty string, or is an array) + * @param obj The object to test + * @returns {boolean} + */ + objectHasData : function(obj) + { + var hasNonNull = false; + + if (Ext4.isObject(obj)) + { + Ext4.Object.each(obj, function (key, value) + { + if ((Ext4.isArray(value) && value.length > 0) || (value != null && value != '')) + { + hasNonNull = true; + return false; // break + } + }); + } + + return hasNonNull; + }, + + /** + * Check if the given model object has any properties which have data (i.e. non null, not an empty string, or is an array) + * @param obj The object to test + * @returns {boolean} + */ + modelHasData : function(obj, fields) + { + var hasNonNull = false; + + if (Ext4.isObject(obj) && Ext4.isArray(fields)) + { + Ext4.each(fields, function(field) + { + if (Ext4.isArray(obj[field.name]) && obj[field.name].length > 0) + hasNonNull = true; + else if ((field.type.type == 'int' || field.type.type == 'float') && obj[field.name] != null && obj[field.name] != 0) + hasNonNull = true; + else if (field.type.type == 'string' && obj[field.name] != null && obj[field.name] != '') + hasNonNull = true; + + if (hasNonNull) + return false; // break; + }); + } + + return hasNonNull; + }, + + /** + * Get the matching row index from an array based on a given row's object property name and value. + * @param arr The array to traverse + * @param filterPropName The name of the row's object property to compare + * @param filterPropValue The value of the row's object property that indicates a match + * @returns {Object} + */ + getMatchingRowIndexFromArray : function(arr, filterPropName, filterPropValue) + { + if (Ext4.isString(filterPropName) && Ext4.isDefined(filterPropValue) && Ext4.isArray(arr)) + { + for (var i = 0; i < arr.length; i++) + { + if (arr[i].hasOwnProperty(filterPropName) && arr[i][filterPropName] == filterPropValue) + return i; + } + } + + return -1; + } +}); \ No newline at end of file diff --git a/studydesign/webapp/study/vaccineDesign/VaccineDesign.css b/studydesign/webapp/study/vaccineDesign/VaccineDesign.css new file mode 100644 index 00000000..a44ba06e --- /dev/null +++ b/studydesign/webapp/study/vaccineDesign/VaccineDesign.css @@ -0,0 +1,82 @@ +.study-vaccine-design .x4-panel-body { + background-color: transparent; +} + +.study-vaccine-design .main-title { + font-weight: bold; + font-size: 16px; + padding-bottom: 5px; +} + +.study-vaccine-design table.outer, +.study-vaccine-design table.subgrid { + border-collapse: collapse; +} + +.study-vaccine-design td.cell-display, +.study-vaccine-design td.cell-value { + padding: 5px; + border: solid 1px #DDDDDD; + vertical-align: top; +} + +.study-vaccine-design td.cell-display { + height: 30px; +} +.study-vaccine-design td.cell-value { + height: 33px; +} + +.study-vaccine-design td.cell-value .x4-form-cb-wrap { + height: 18px; +} + +.study-vaccine-design table.subgrid td { + background-color: #FFFFFF !important; +} + +.study-vaccine-design tr.header-row td { + font-weight: bold; + padding: 5px; + background-color: #EEEEEE !important; + border-bottom-color: #C0C0C0; +} + +.study-vaccine-design table.outer tr.data-row td { + background-color: #FFFFFF; +} +.study-vaccine-design table.outer tr.alternate-row td { + background-color: #F4F4F4; +} + +.study-vaccine-design td.cell-value.missing-required { + background-color: #ffe5e5 !important; +} + +.study-vaccine-design td.empty { + font-style: italic; +} + +.study-vaccine-design td.action i { + color: #777777; +} +.study-vaccine-design td.action i:hover { + cursor: pointer; + color: #000000; +} + +.dialog-product-label .x4-form-cb-label-after { + /* Firefox */ + width: -moz-calc(100% - 20px); + /* WebKit */ + width: -webkit-calc(100% - 20px); + /* Standard */ + width: calc(100% - 20px); + white-space: nowrap; +} + +.treatment-input-cell .x4-form-field { + cursor: pointer; + opacity: 0.8; +} + diff --git a/studydesign/webapp/study/vaccineDesign/VisitWindow.js b/studydesign/webapp/study/vaccineDesign/VisitWindow.js new file mode 100644 index 00000000..f8c1917b --- /dev/null +++ b/studydesign/webapp/study/vaccineDesign/VisitWindow.js @@ -0,0 +1,319 @@ +/* + * Copyright (c) 2016-2017 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 + */ + +Ext4.define('LABKEY.VaccineDesign.VisitWindow', { + extend: 'Ext.window.Window', + + modal: true, + + visitStore: null, + + visitNoun: 'Visit', + + constructor: function(config) + { + this.callParent([config]); + this.addEvents('closewindow', 'selectexistingvisit', 'newvisitcreated'); + }, + + initComponent: function() + { + this.isTimepoint = this.visitNoun.toLowerCase() == 'timepoint'; + this.items = [this.getFormPanel()]; + this.callParent(); + }, + + getFormPanel : function() + { + if (!this.formPanel) + { + this.formPanel = Ext4.create('Ext.form.Panel',{ + border: false, + padding: 10, + items: [ + this.getExistingVisitRadio(), + this.getExistingVisitCombo(), + this.getNewVisitRadio(), + this.getNewVisitLabelField(), + this.getNewVisitMinMaxContainer() + ], + buttons: [ + this.getSelectBtn(), + this.getCancelBtn() + ] + }); + } + + return this.formPanel; + }, + + getExistingVisitRadio : function() + { + if (!this.existingVisitRadio) + { + this.existingVisitRadio = Ext4.create('Ext.form.field.Radio', { + name: 'visitType', + disabled: this.getFilteredVisitStore().getCount() == 0, + inputValue: 'existing', + boxLabel: 'Select an existing study ' + this.visitNoun.toLowerCase() + ':', + checked: this.getFilteredVisitStore().getCount() > 0, + hideFieldLabel: true, + width: 300 + }); + } + + return this.existingVisitRadio; + }, + + getNewVisitRadio : function() + { + if (!this.newVisitRadio) + { + this.newVisitRadio = Ext4.create('Ext.form.field.Radio', { + name: 'visitType', + inputValue: 'new', + boxLabel: 'Create a new study ' + this.visitNoun.toLowerCase() + ':', + checked: this.getFilteredVisitStore().getCount() == 0, + hideFieldLabel: true, + width: 300 + }); + + this.newVisitRadio.on('change', function(radio, newValue){ + this.getExistingVisitCombo().setDisabled(newValue); + this.getNewVisitLabelField().setDisabled(!newValue); + this.getNewVisitMinMaxContainer().setDisabled(!newValue); + this.getSelectBtn().setText(newValue ? 'Submit' : 'Select'); + this.updateSelectBtnState(); + + if (newValue) + this.getNewVisitLabelField().focus(); + else + this.getExistingVisitCombo().focus(); + }, this); + } + + return this.newVisitRadio; + }, + + getExistingVisitCombo : function() + { + if (!this.existingVisitCombo) + { + this.existingVisitCombo = Ext4.create('Ext.form.field.ComboBox', { + name: 'existingVisit', + disabled: this.getFilteredVisitStore().getCount() == 0, + hideFieldLabel: true, + style: 'margin-left: 15px;', + width: 300, + store: this.getFilteredVisitStore(), + editable: false, + queryMode: 'local', + displayField: 'Label', + valueField: 'RowId' + }); + + this.existingVisitCombo.on('change', this.updateSelectBtnState, this); + } + + return this.existingVisitCombo; + }, + + getFilteredVisitStore : function() + { + if (!this.filteredVisitStore) + { + var data = []; + if (this.visitStore != null) + { + Ext4.each(this.visitStore.query('Included', false).items, function(record) + { + var recData = Ext4.clone(record.data); + recData['Label'] = recData['Label'] || recData['SequenceNumMin']; + data.push(recData); + }, this); + } + + // add an option to select all existing visits for display + if (data.length > 1) + data.push({Label: '[Show All]', RowId: -1, DisplayOrder: -999999}); + + this.filteredVisitStore = Ext4.create('Ext.data.Store', { + model : 'LABKEY.VaccineDesign.Visit', + data : data, + sorters : [{property: 'DisplayOrder', direction: 'ASC'},{property: 'SequenceNumMin', direction: 'ASC'}] + }); + } + + return this.filteredVisitStore; + }, + + getNewVisitLabelField : function() + { + if (!this.newVisitLabelField) + { + this.newVisitLabelField = Ext4.create('Ext.form.field.Text', { + name: 'newVisitLabel', + disabled: this.getFilteredVisitStore().getCount() > 0, + fieldLabel: 'Label', + labelWidth: 50, + width: 300, + style: 'margin-left: 15px;' + }); + + this.newVisitLabelField.on('change', this.updateSelectBtnState, this); + } + + return this.newVisitLabelField; + }, + + getNewVisitMinField : function() + { + if (!this.newVisitMinField) + { + this.newVisitMinField = Ext4.create('Ext.form.field.Number', { + name: 'newVisitRangeMin', + hideLabel: true, + width: this.isTimepoint ? 100 : 80, + emptyText: 'min', + hideTrigger: true, + decimalPrecision: 4 + }); + + this.newVisitMinField.on('change', this.updateSelectBtnState, this); + } + + return this.newVisitMinField; + }, + + getNewVisitMaxField : function() + { + if (!this.newVisitMaxField) + { + this.newVisitMaxField = Ext4.create('Ext.form.field.Number', { + name: 'newVisitRangeMax', + hideLabel: true, + width: this.isTimepoint ? 100 : 80, + emptyText: 'max', + hideTrigger: true, + decimalPrecision: 4 + }); + + this.newVisitMinField.on('change', this.updateSelectBtnState, this); + } + + return this.newVisitMaxField; + }, + + getNewVisitMinMaxContainer : function() + { + if (!this.newVisitMinMaxContainer) + { + this.newVisitMinMaxContainer = Ext4.create('Ext.form.FieldContainer', { + layout: 'hbox', + style: 'margin-left: 15px; margin-bottom: 15px;', + fieldLabel: (this.isTimepoint ? 'Day' : 'Sequence') + ' Range', + labelWidth: this.isTimepoint ? 85 : 125, + disabled: this.getFilteredVisitStore().getCount() > 0, + items: [ + this.getNewVisitMinField(), + {xtype: 'label', width: 10}, // spacer + this.getNewVisitMaxField() + ] + }); + } + + return this.newVisitMinMaxContainer; + }, + + getSelectBtn : function() + { + if (!this.selectBtn) + { + this.selectBtn = Ext4.create('Ext.button.Button', { + text: this.getFilteredVisitStore().getCount() == 0 ? 'Submit' : 'Select', + disabled: true, + scope: this, + handler: function() { + var values = this.getFormPanel().getValues(); + + if (values['visitType'] == 'existing') + this.fireEvent('selectexistingvisit', this, values['existingVisit'] == -1 ? 'ALL' : values['existingVisit']); + else + this.createNewVisit(); + } + }); + } + + return this.selectBtn; + }, + + updateSelectBtnState : function() + { + var values = this.getFormPanel().getValues(); + + if (values['visitType'] == 'existing') + this.getSelectBtn().setDisabled(values['existingVisit'] == ''); + else + this.getSelectBtn().setDisabled(values['newVisitLabel'] == '' || values['newVisitRangeMin'] == ''); + }, + + getCancelBtn : function() + { + if (!this.cancelBtn) + { + this.cancelBtn = Ext4.create('Ext.button.Button', { + text: 'Cancel', + scope: this, + handler: function() { + this.fireEvent('closewindow', this); + } + }); + } + + return this.cancelBtn; + }, + + createNewVisit : function() + { + this.getEl().mask('Creating new visit...'); + var values = this.getFormPanel().getValues(); + + LABKEY.Ajax.request({ + url : LABKEY.ActionURL.buildURL('study', 'createVisitForVaccineDesign.api'), + method : 'POST', + jsonData: { + label: values['newVisitLabel'], + sequenceNumMin: values['newVisitRangeMin'], + sequenceNumMax: values['newVisitRangeMax'], + showByDefault: true + }, + success: function(response) { + var resp = Ext4.decode(response.responseText); + if (resp.success) + this.fireEvent('newvisitcreated', this, resp); + else + this.onFailure(); + }, + failure: function(response) { + var resp = Ext4.decode(response.responseText); + this.onFailure(resp.exception); + }, + scope : this + }); + }, + + onFailure : function(text) + { + Ext4.Msg.show({ + title: 'Error', + msg: text || 'Unknown error occurred.', + icon: Ext4.Msg.ERROR, + buttons: Ext4.Msg.OK + }); + + this.getEl().unmask(); + } +}); \ No newline at end of file diff --git a/studydesign/webapp/study/vaccineDesign/vaccineDesign.lib.xml b/studydesign/webapp/study/vaccineDesign/vaccineDesign.lib.xml new file mode 100644 index 00000000..0cdb9ab0 --- /dev/null +++ b/studydesign/webapp/study/vaccineDesign/vaccineDesign.lib.xml @@ -0,0 +1,18 @@ + + +