Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migrate Sample/Container Reference Fields to new Widget #37

Merged
merged 8 commits into from
Dec 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Changelog
2.5.0 (unreleased)
------------------

- no changes yet
- #37 Migrate Sample/Container Reference Fields to new Widget


2.4.1 (2023-03-11)
Expand Down
70 changes: 69 additions & 1 deletion src/senaite/storage/browser/container/store_container.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@
from senaite.storage import senaiteMessageFactory as _
from senaite.storage.browser import BaseView
from senaite.storage.interfaces import IStorageSamplesContainer
from senaite.core.catalog import SAMPLE_CATALOG

DISPLAY_TEMPLATE = "<a href='${url}' _target='blank'>${getId}</a>"


class StoreContainerView(BaseView):
Expand Down Expand Up @@ -183,7 +186,7 @@ def __call__(self):
# Handle store
if form_submitted and form_store:
alpha_position = form.get("position")
sample_uid = form.get("sample_uid")
sample_uid = form.get("sample")
if not alpha_position or not api.is_uid(sample_uid):
message = _("No position or not valid sample selected")
return self.redirect(message=message)
Expand All @@ -207,3 +210,68 @@ def __call__(self):
return self.redirect(message=_("Sample storing canceled"))

return self.template()

def get_reference_widget_attributes(self, name, obj=None):
"""Return input widget attributes for the ReactJS component
"""
if obj is None:
obj = self.context
url = api.get_url(obj)

attributes = {
"data-name": name,
"data-values": [],
"data-records": {},
"data-value_key": "uid",
"data-value_query_index": "UID",
"data-api_url": "%s/referencewidget_search" % url,
"data-query": {
"portal_type": ["AnalysisRequest"],
"review_state": [
"received",
"to_be_verified",
"verified",
"published",
],
"sort_on": "sortable_title",
"sort_order": "ascending",
},
"data-catalog": SAMPLE_CATALOG,
"data-search_index": "listing_searchable_text",
"data-search_wildcard": True,
"data-allow_user_value": False,
"data-columns": [{
"name": "getId",
"label": _("Sample ID"),
"width": "15",
}, {
"name": "getClientSampleID",
"label": _("CSID"),
"width": "15",
}, {
"name": "getClientID",
"label": _("Client ID"),
"width": "35",
}, {
"name": "getSampleTypeTitle",
"label": _("Sample Type"),
"width": "25",
}, {
"name": "review_state",
"label": _("State"),
"width": "10",
}],
"data-display_template": DISPLAY_TEMPLATE,
"data-limit": 5,
"data-multi_valued": False,
"data-disabled": False,
"data-readonly": False,
"data-required": False,
"data-clear_results_after_select": False,
}

for key, value in attributes.items():
# convert all attributes to JSON
attributes[key] = json.dumps(value)

return attributes
86 changes: 73 additions & 13 deletions src/senaite/storage/browser/container/store_samples.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,17 @@
# Copyright 2019-2023 by it's authors.
# Some rights reserved, see README and LICENSE.

import json

from bika.lims import api
from bika.lims import bikaMessageFactory as _
from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile
from senaite.storage import logger
from senaite.storage import senaiteMessageFactory as _s
from senaite.storage.browser import BaseView
from senaite.storage.catalog import STORAGE_CATALOG

DISPLAY_TEMPLATE = "<a href='${url}' _target='blank'>${get_full_title}</a>"


class StoreSamplesView(BaseView):
Expand All @@ -49,37 +54,43 @@ def __call__(self):
form_store = form.get("button_store", False)
form_cancel = form.get("button_cancel", False)

objs = self.get_objects_from_request()
samples = self.get_objects_from_request()
stored_samples = []

# No items selected
if not objs:
if not samples:
return self.redirect(message=_("No items selected"),
level="warning")

# Handle store
if form_submitted and form_store:
samples = []
for sample in form.get("samples", []):
sample_uid = sample.get("uid")
container_uid = sample.get("container_uid")
alpha_position = sample.get("container_position")
if not sample_uid or not container_uid or not alpha_position:
continue

# extract relevant data
container_mapping = form.get("sample_container", {})
container_position_mapping = form.get("sample_container_position", {})

for sample in samples:
sample_uid = api.get_uid(sample)
container_uid = container_mapping.get(sample_uid)
alpha_position = container_position_mapping.get(sample_uid)
if not all([container_uid, alpha_position]):
continue
sample_obj = self.get_object_by_uid(sample_uid)
container = self.get_object_by_uid(container_uid)
logger.info("Storing sample {} in {}".format(sample_obj.getId(),
container.getId()))
logger.info("Storing sample {} in {}"
.format(sample.getId(), container.getId()))
# Store
position = container.alpha_to_position(alpha_position)
stored = container.add_object_at(sample_obj, position[0],
position[1])
if stored:
stored = container.get_object_at(position[0], position[1])
samples.append(stored)
stored_samples.append(stored)

message = _s("Stored {} samples: {}".format(
len(samples), ", ".join(map(api.get_title, samples))))
len(stored_samples), ", ".join(
map(api.get_title, stored_samples))))

return self.redirect(message=message)

# Handle cancel
Expand All @@ -102,3 +113,52 @@ def get_samples_data(self):
"url": api.get_url(obj),
"sample_type": api.get_title(obj.getSampleType())
}

def get_reference_widget_attributes(self, name, obj=None):
"""Return input widget attributes for the ReactJS component
"""
if obj is None:
obj = self.context
url = api.get_url(obj)

attributes = {
"data-name": name,
"data-values": [],
"data-records": {},
"data-value_key": "uid",
"data-value_query_index": "UID",
"data-api_url": "%s/referencewidget_search" % url,
"data-query": {
"portal_type": ["StorageSamplesContainer"],
"is_full": False,
"review_state": "active",
"sort_on": "getId",
"sort_order": "ascending",
},
"data-catalog": STORAGE_CATALOG,
"data-search_index": "listing_searchable_text",
"data-search_wildcard": True,
"data-allow_user_value": False,
"data-columns": [{
"name": "id",
"label": _("Id"),
"width": 10,
}, {
"name": "get_full_title",
"label": _("Container path"),
"width": 90,
}],
"data-display_template": DISPLAY_TEMPLATE,
"data-limit": 5,
"data-multi_valued": False,
"data-disabled": False,
"data-readonly": False,
"data-required": False,
"data-clear_results_after_select": False,
}

for key, value in attributes.items():
# convert all attributes to JSON
attributes[key] = json.dumps(value)

return attributes
78 changes: 19 additions & 59 deletions src/senaite/storage/browser/container/templates/store_container.pt
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,6 @@
xmlns:metal="http://xml.zope.org/namespaces/metal"
metal:use-macro="here/main_template/macros/master"
i18n:domain="senaite.storage">
<head>
<metal:block fill-slot="senaite_legacy_resources"
tal:define="portal context/@@plone_portal_state/portal;">
<script type="text/javascript"
tal:attributes="src string:${portal/absolute_url}/senaite_widgets/referencewidget.js"></script>
<style type="text/css" media="screen">
.cg-resetButton span { margin-left: -35px; }
</style>
</metal:block>
</head>
<body>

<!-- Title -->
Expand Down Expand Up @@ -43,7 +33,8 @@
<div id="viewlet-senaite-storage-js" tal:content="structure provider:senaite.storage.js" />
<div id="store-samples-view"
tal:define="portal context/@@plone_portal_state/portal;
container python:view.get_container()">
container python:view.get_container();
container_uid container/UID;">

<form class="form"
id="store_container_form"
Expand All @@ -54,54 +45,21 @@
<input type="hidden" name="submitted" value="1"/>
<input tal:replace="structure context/@@authenticator/authenticator"/>

<div class="form-row">
<!-- Sample selection area -->
<div class="col-sm-4">
<div class="input-group field ArchetypesReferenceWidget">
<div class="input-group-prepend">
<div i18n:translate="" class="input-group-text">Sample</div>
</div>
<input tal:attributes="name string:sample; base_query view/get_base_query"
type="text"
ui_item="getId"
autocomplete="false"
class="custom-select blurrable firstToFocus referencewidget"
base_query='{"sort_on": "getId", "sort_order": "ascending", "limit": "30"}'
search_query='{}'
catalog_name="senaite_catalog_sample"
combogrid_options='{"colModel": [
{"columnName": "getId", "align": "left", "label": "Id", "width": "15"},
{"columnName": "getClientSampleID", "align": "left", "label": "CSID", "width": "15"},
{"columnName": "getSampleTypeTitle", "align": "left", "label": "Sample Type", "width": "70"},
{"columnName": "UID", "hidden": true}
],
"search_fields": ["listing_searchable_text"],
"catalog_name": "senaite_catalog_sample",
"url": "referencewidget_search",
"discard_empty": [],
"showOn": true,
"searchIcon": true,
"minLength": "0",
"resetButton": true,
"sord": "asc",
"sidx": "getId",
"width": "700px",
"force_all": true,
"portal_types": {}}'/>
<input type="hidden"
tal:attributes="id string:sample_uid"
name="sample_uid"
value="" />
</div>
<div class="d-flex flex-row">
<!-- Sample select -->
<div class="senaite-uidreference-widget-input"
tal:attributes="python:view.get_reference_widget_attributes('sample', container);
id string:sample_container_${container_uid};">
<!-- ReactJS controlled widget -->
</div>

<!-- Sample Position -->
<div class="col-sm-2">
<div class="input-group">
<div class="mr-2">
<div class="input-group input-group-sm">
<div class="input-group-prepend">
<div i18n:translate="" class="input-group-text">Position</div>
</div>
<select name="position" id="position" class="custom-select">
<select name="position" id="position" class="custom-select" style="max-width:fit-content;">
<tal:options repeat="position python: container.get_available_positions()">
<option tal:define="alpha_value python: container.position_to_alpha(position[0], position[1]);"
tal:content="alpha_value"
Expand All @@ -110,16 +68,18 @@
</select>
</div>
</div>
<div class="col-sm-4">

<!-- Buttons -->
<div class="mr-2">
<!-- Store samples -->
<input class="btn btn-success"
<input class="btn btn-success btn-sm"
type="submit"
id="button_store"
name="button_store"
i18n:attributes="value"
value="Add Sample"/>
<!-- Cancel -->
<input class="btn btn-secondary"
<input class="btn btn-secondary btn-sm"
type="submit"
name="button_cancel"
i18n:attributes="value"
Expand All @@ -145,7 +105,7 @@
<div class="col-sm-12">
<table class="table table-bordered container-layout table-responsive"
tal:define="rows python: container.getRows();
columns python: container.getColumns();">
columns python: container.getColumns();">

<!-- Columns header -->
<tr>
Expand All @@ -169,8 +129,8 @@
class="empty-slot">
<a class="position_slot_selector" href="#"
tal:attributes="data-row row;
data-column col;
id alpha;" >
data-column col;
id alpha;" >
<div class="col-sm-12">
<br/><br/>
</div>
Expand Down
Loading