Add hazard categories to SampleType, Sample, ReferenceDefinition and ReferenceSample (GHS + ISO 7010)#2890
Open
ramonski wants to merge 71 commits into
Open
Add hazard categories to SampleType, Sample, ReferenceDefinition and ReferenceSample (GHS + ISO 7010)#2890ramonski wants to merge 71 commits into
ramonski wants to merge 71 commits into
Conversation
Introduces a new optional ``hazard_categories`` field on SampleType (DX) and ``HazardCategories`` field on AnalysisRequest (AT) that captures the GHS pictogram categories for a sample. Backwards compatible with the existing ``hazardous`` boolean — that field is the authoritative "treat as hazardous" gate and is left untouched. - Vocabulary at ``senaite.core.vocabularies.hazard_categories`` exposing the 9 official GHS codes (GHS01..GHS09) with translatable formal name and common synonym. Tokens are the GHS codes so translations and labels do not affect persistence. - Sample-level field overrides the SampleType default. ``Sample. getHazardCategories()`` returns the override if set, else falls back to the SampleType. ``Sample.getHazardous()`` is unchanged. - Official UN/ECE GHS pictogram SVGs (public domain, sourced from Wikimedia Commons) shipped under ``browser/static/images/ghs/GHSxx.svg``. - ``hazard-categories.css`` styles the multi-checkbox widget so each category renders its pictogram next to the label in the edit form. Loaded as a separate stylesheet via the new ``HazardCategoriesStylesheet`` viewlet so the rules can evolve without a webpack rebuild. - German translations included for all new strings. - Upgrade step 2726 -> 2727 reapplies the viewlets profile so the stylesheet viewlet becomes active on existing instances.
Replace the standalone JS toggle with the proper SENAITE edit form adapter pattern. The existing sampletype form adapter now hides hazard_categories on initialize when Hazardous is unchecked, and shows or hides it in real-time when the user toggles the checkbox.
The default OrderedSelectFieldWidget for List(Choice) draws two <select> listboxes which cannot host inline pictograms via CSS. Switching to CheckBoxFieldWidget produces a label-per-option layout that the hazard-categories.css rules can decorate.
Replace the CSS-only :has() approach with a dedicated z3c.form checkbox widget. Each option emits a real <img> pointing to the matching GHS pictogram, with the GHS code as alt and the formatted label as title (tooltip). Drops the brittle :has() selectors and gives screen readers proper alt text.
Without extending ISequenceWidget, @implementer_only stripped the parent's ISequenceWidget marker. The converter lookup then fell through to FieldWidgetDataConverter which requires the field to provide IFromUnicode — schema.List does not.
Used as the default hazard pictogram when a sample is marked hazardous but no specific GHS category has been selected. Mirrors the existing GHS SVG pattern so renderers (PDF, listing) no longer depend on the U+26A0 emoji font being installed.
The SampleType view page renders the field in display mode, which triggered a ComponentLookupError because only the input template was registered. The display template renders selected categories as a row of pictograms with tooltips.
The display template wraps each pictogram <img> in a 32x32 span, but the inner <img> was unconstrained and rendered at the SVG's intrinsic (huge) size. Add explicit width/height/object-fit on the <img> so it fills only the wrapper.
- Add getHazardCategories as a metadata column on SAMPLE_CATALOG so listings can render the new pictograms without waking up each sample. - New SampleType modified subscriber walks dependent samples and reindexes the hazard metadata when hazardous or hazard_categories changes. Standard catalog pattern; SampleType edits are rare while listings render constantly, so the cost lives at the right end. - Sample title viewlet renders one <img> per assigned GHS category with the category code as alt and the formatted name as tooltip. Falls back to the ISO 7010 W001 'General warning' pictogram when the sample is hazardous but no category was selected. - Upgrade step v02_07_000.add_hazard_categories now also adds the new metadata column and reindexes all existing samples once.
Both the sample title viewlet and the edit-form widget now call
into senaite.core.api.hazard_categories. The API exposes:
- get_pictogram_url(code) for a single GHS pictogram
- get_warning_pictogram_url() for the ISO 7010 W001 fallback
- get_pictogram(code) returning a {url, alt, title} view-model
- get_pictograms_for_sample(sample) wrapping the boolean fallback
Centralizing the URL building keeps the viewlet, the widget, and
any future consumer (custom reports, listing columns) in sync.
The earlier rule img.hazard-pictogram{width:100%} was more
specific than .hazard-pictogram{width:28px}, so it overrode
the edit-form size and the icon expanded to fill its container.
Restrict the 100% rule to span.hazard-pictogram > img which
only matches display mode.
- Move the pictogram helpers into senaite.core.api so callers use the flat namespace (api.get_pictogram_url, api.get_pictograms_for_sample, ...). Matches the long-term direction of folding bika.lims.api into senaite.core.api. - The AnalysisRequest LinesField was wired with vocabulary="senaite.core.vocabularies.hazard_categories", which AT could not resolve and therefore iterated the string character-by-character. Replace with a getHazardCategoriesVocabulary method that returns a DisplayList; also surfaces the registry-based label overrides at runtime.
The hazard reindex subscriber needs to find samples by SampleType UID. The column existed only as metadata; promoting it to a FieldIndex is a small change with no downside (~1 entry per sample, single-value index). Upgrade step adds the index and reindexes.
Replace the single hazardous.png glyph in samples/view.py with one pictogram per assigned GHS category, using the brain metadata via api.get_pictograms_for_codes — no sample wakeup required. Adds a .hazard-pictogram-mini CSS class sized for inline listing icons.
Existing brains do not carry the getHazardCategories metadata column until the upgrade step runs, so attribute access climbs the acquisition chain up to the RequestContainer. Use getattr with None default; api.get_pictograms_for_codes treats it as 'no specific category, fall back to W001'.
after_icons accumulates as a Py2 bytes string elsewhere in this folderitem method. The unicode template combined with translated German titles from the GHS vocabulary triggered the implicit ASCII decode. Encode the formatted markup to utf-8 to match the bytes-str expectation of the surrounding code.
Per first-iteration scope: keep hazard categories on SampleType only. Sample.getHazardCategories() now reads exclusively from the SampleType, no per-sample override field. Removes the AT LinesField, the DisplayList vocabulary helper, the unused LinesField/MultiSelectionWidget imports, and the AR-specific i18n strings. Sample-level override can be revisited in a later iteration if labs need the granularity.
Closed samples (published, invalid, cancelled, rejected) are schema-frozen, so refreshing their hazard metadata serves no purpose. Limiting the catalog query to active states cuts the reindex work proportionally on installs with many historical samples — the most common large-DB shape.
senaite.core.api should not import bika.lims.api as the migration direction is the opposite. Replace the bika.lims.api.get_portal call with native get_portal/get_portal_url helpers built on zope.component.hooks.getSite. Pictogram helpers no longer take a portal kwarg — internal lookup is cheap enough.
Adopts the reStructuredText :param/:type/:returns/:rtype style already used in bika.lims.api so the migration of helpers from bika.lims.api to senaite.core.api lands with consistent docs. Adds src/senaite/core/tests/test_api.py covering get_portal, get_portal_url, get_pictogram_url, get_warning_pictogram_url, get_pictogram, and get_pictograms_for_codes (known/unknown codes, override mapping, hazardous=False, empty fallback, None codes).
Doctests are the canonical style for senaite.core API tests (see API_geo.rst, API_datetime.rst, API_user.rst). The doctest covers the same surface as the deleted unit test: portal helpers, GHS pictogram URL lookup, view-model dicts, label overrides, and the W001 fallback path. Run: bin/test-senaite -m test_textual_doctests -t API_core
WARNING_LABEL was a plain unicode string and ended up untranslated inside <img alt> / <title> attributes. Make it a senaite MessageFactory message and resolve it via a new senaite.core.api.translate helper that uses the current request locale. Adds the German translation 'Gefährlich'.
- Drop the local zope.i18n wrapper from senaite.core.api in favor of
the existing senaite.core.i18n.translate helper. Re-exported as
api.translate so callers can keep using api.translate(...).
- The upgrade step called bika.lims.api.get_setup_tool which does
not exist; use api.get_tool('portal_setup') instead.
get_overridden_labels read a registry record that nothing ever wrote, and there was no UI to set it. Translation files cover the per-installation customization use case; if a runtime relabeling story emerges later, it can be wired in cleanly on top of this.
zope.i18n.translate can return bytes-str on Py2 when a translation message is loaded for the active locale. When format_title then concatenated those bytes into a unicode template, samples/view.py raised UnicodeDecodeError on the first non-ASCII character (e.g. 'Ätzend'). Wrap each translated value in api.safe_unicode so format_title always returns unicode.
senaite.core.i18n.translate defaults to to_utf8=True, which returned bytes for the German 'Gefährlich'. Downstream unicode string-formatting in the sample listing then failed with UnicodeDecodeError. Pass to_utf8=False so the helper returns unicode and the bytes/unicode boundary stays at the rendering layer.
Mirror the SampleType/Sample feature on the QC side: - ReferenceDefinition gains a HazardCategories LinesField + the getHazardCategoriesVocabulary helper, alongside the existing Hazardous boolean. - ReferenceSample gains the same field and helper. Its getHazardCategories() returns the sample-level value when set, otherwise inherits from the linked ReferenceDefinition. The legacy own Hazardous boolean is unchanged. - Translations added for the four new label/description strings.
…data Move the ``getHazardCategories`` metadata column off the sample catalog and onto the setup catalog (where SampleTypes already live), then resolve a sample's hazard categories at render time by: 1. consulting an optional per-sample override ``getCustomHazardCategories`` (placeholder method on AnalysisRequest, currently always empty); 2. falling back to a UID lookup of the SampleType brain in the setup catalog and reading ``getHazardCategories`` metadata from there. This drops the bulk-reindex subscriber that walked every active sample on a SampleType edit, and the matching loop in the upgrade step. SampleType edits now only need a normal reindex of the SampleType itself. Listings call ``api.get_pictograms_for_sample(brain)`` and the helper does the lookup centrally so callers don't have to thread codes through.
The helper does not depend on samples in any way: it gets a named attribute or method-call result from any content object or catalog brain. Lifted it out of the private namespace, renamed the parameter, added a ``default`` argument and covered it with doctests.
Inline the hazard-category resolution into ``get_pictograms_for_sample`` and lift the only piece of reusable logic out as ``api.get_brain_attr(uid, name, catalog, default=None)``, which looks an object up by UID in a catalog and reads a metadata attribute from its brain without waking the object up. Drop the underscore-prefixed sample-specific helpers from the API namespace and add a doctest section covering the new helper.
Drop the separate ``get_brain_attr`` helper. ``api.get_attr`` now takes an optional ``catalog`` keyword: when given, ``obj`` is treated as a UID, looked up in that catalog, and the attribute is read from the matching brain. Without ``catalog``, the function behaves exactly as before. Update the call site in ``get_pictograms_for_sample`` and the doctests accordingly.
Accept any of the three forms ``bika.lims.api.get_uid`` handles (content object, catalog brain or UID string). Validate the input once via ``is_object``/``is_uid`` so callers don't have to wrap the call in try/except, and use ``bika.lims.api.search`` for the UID lookup when a catalog is given. The hazard-resolution call site stays the same: passing the SampleType UID through ``get_attr(uid, "getHazardCategories", catalog=SETUP_CATALOG)`` reads the metadata from the SampleType brain without waking it up.
The doctests construct plain Python objects to exercise the attribute/method dispatch, which the bika-content validity check rejected, causing the function to return default for a non-Plone input. Move the check inside the catalog is not None branch where it is actually needed; in direct-attribute mode any input that has the requested attribute now works again.
Extend the hazard categories vocabulary with 6 ISO 7010 pictograms covering hazards GHS does not address: BIO01 W009 Biohazard RAD01 W003 Radioactive (ionising radiation) NIR01 W005 Non-ionising radiation ELEC01 W012 Electricity (electric shock) COLD01 W010 Low temperature HOT01 W079 Hot content Custom code prefixes (BIO, RAD, NIR, ELEC, COLD, HOT) are used as persistent identifiers so the SVG source can be re-sourced without a data migration. Resolve hazardous flag and categories via the SampleType brain so SampleType edits are reflected in listings without reindexing samples. Add getHazardous as a setup-catalog metadata column and extend the upgrade step to populate it. Add object-fit:contain to the pictogram <img> rules and background-size:contain to the AT widget pseudo-element so the non-square ISO 7010 triangles preserve their aspect ratio in the square pictogram box. Broaden the AT widget selector from input[value^="GHS"] to input[type="checkbox"] and add background-image rules for the new codes.
The Wikimedia source SVGs for W005, W009, W010 and W079 each draw their inner glyph at a different inset relative to the triangle border (varying from ~40% to ~50% of triangle height), making the pictograms render at visibly different sizes when displayed side by side at the same box dimensions. Wrap each verbose SVG's inner-symbol group with an additional scale-1.3-around-(300,320) transform so the inner glyph fills ~57% of the triangle, matching the visual weight of the W003 and W012 minimalist source SVGs. The triangle border, yellow fill and inner-glyph geometry from the official ISO 7010 Wikimedia sources are preserved.
This reverts commit 583c8d1.
W079 from Wikimedia ships with #ffe100 yellow, while the other ISO 7010 SVGs we use (W003, W005, W009, W010, W012) use #f9a800 — the specified RAL 1003 'signal yellow' color. Match the rest.
…e, ReferenceDefinition and ReferenceSample - Rename GHS_CATEGORIES -> HAZARD_CATEGORIES and GHS_PICTOGRAM_PATH -> PICTOGRAM_PATH; scrub 'GHS hazard categories' wording in docstrings, field descriptions, doctests, ZCML, CHANGES and DE translations. - Add HazardCategories field on ReferenceDefinition and ReferenceSample (AT LinesField) with inheritance: sample-level overrides win, else fall back to the reference definition. - Render hazard pictograms in the reference sample title, reference sample listing and reference definition listing (replaces the legacy hazardous_big.png / hazardous.png). - Add get_pictograms_for_reference helper for any object exposing getHazardous / getHazardCategories. - Resolve sample-level hazardous flag exclusively via the SampleType brain in setup_catalog; remove the redundant getHazardous metadata column from sample_catalog and add a del_column step. - Add 4 more ISO 7010 categories (MAG01 magnetic field, HSURF01 hot surface, ASPH01 asphyxiating atmosphere, STEAM01 hot steam) and reorder the vocabulary by hazard family (biological, radiation / fields, electrical, thermal, atmospheric). - Set title attributes on both the wrapping span and inner img in the AT and DX view templates so tooltips show on hover. - Make .hazard-pictograms title styling generic (no longer scoped to .sample-title) so reference sample titles get the same sizing.
The modal title was set via plain text, so the hazard pictograms attached to the listing row never made it into the popover header. Pick them up from the row's title cell, append after the title text and rebuild the bundle.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description of the issue/feature this PR addresses
The only hazard signal on a SampleType today is a boolean
Hazardousflag, rendered everywhere as a generic warning triangle. There is no way to record which hazard a sample carries (acid, poison, flammable, biohazard, radioactive, etc.), so reports and listings cannot show the appropriate pictogram and laboratories handling a mix of hazardous materials cannot communicate the specific risk in their internal documents.This PR introduces an optional
hazard_categoriesfield onSampleType(DX),ReferenceDefinition(AT) andReferenceSample(AT) capturing the hazard pictogram categories. The booleanHazardousflag stays untouched and authoritative — the new field is purely additive. The 9 official UN/ECE GHS pictograms plus 10 ISO 7010 hazard pictograms (biohazard, radioactive, non-ionising radiation, magnetic field, electricity, hot surface, hot content, hot steam, low temperature, asphyxiating atmosphere) are shipped undersenaite/core/browser/static/images/. All SVGs are public domain, sourced from Wikimedia Commons. Custom widgets render the field with the matching pictogram next to each option in edit mode and as inline icons with tooltips in view mode.The existing form-adapter pattern is extended on all three content types so the categories field is hidden until
Hazardousis checked, and the toggle survives a re-render after validation errors. The sample title viewlet, sample listing, reference sample title, reference sample listing and reference definition listing all render the pictograms (with tooltips) instead of the legacy single warning glyph, falling back to the W001 SVG when the boolean is set without specific categories.A new
senaite.core.apinamespace exposes the helpers callers need (get_portal,get_portal_url,translate,get_pictogram_url,get_warning_pictogram_url,get_pictogram,get_pictograms_for_codes,get_pictograms_for_sample,get_pictograms_for_reference,get_attr). It is the gradual replacement forbika.lims.api.Hazard codes
Codes are grouped by hazard family. The first 9 are the UN/ECE GHS pictograms, the rest are ISO 7010 warning signs covering hazards GHS does not address.
Codes are persistent identifiers (used as token and stored value) and are decoupled from the SVG filename so a pictogram can be re-sourced without a data migration. Custom prefixes are used for the ISO 7010 entries to avoid clashing with GHS or ISO W-codes.
Current behavior before PR
Hazardousfield.Sample.getHazardous()returns the SampleType boolean.Desired behavior after PR is merged
hazard_categories(orHazardCategoriesin AT). Sample (AnalysisRequest) inherits its effective categories from the SampleType. ReferenceSample inherits from itsReferenceDefinitionif no own value is set.[checkbox] [pictogram] <code> — Name (synonym). The DX widget is a customHazardCategoriesWidget(z3c.formCheckBoxWidgetsubclass). The AT widget is a customHazardCategoriesWidget(subclass ofMultiSelectionWidget) that ships its own view macro and reuses the standard checkbox edit macro plus styling from the bundle.HazardCategoriesfield stays hidden unlessHazardousis checked. Implemented via the senaite edit-form adapter onSampleType,ReferenceSample,ReferenceDefinition. Toggle reads the live form payload (suffix-tolerant for ZPublisher type markers like:list/:boolean), so it survives re-renders after validation errors.title. Falls back to W001 when only the boolean is set.samples/view.py) renders inline mini-pictograms from the SampleType brain in the setup catalog (resolved via the newgetSampleTypeUIDFieldIndex), no sample wakeup needed in the hot path.bika/lims/browser/referencesample.py) render the pictograms viaget_pictograms_for_reference(readsgetHazardousandgetHazardCategorieson the brain / object, with fallback to the reference definition forReferenceSample).controlpanel/bika_referencedefinitions.py) renders the pictograms via the same helper.<img>rules useobject-fit: containand the AT widget's::beforepseudo usesbackground-size: contain, so non-square ISO 7010 triangles are not squashed into the square pictogram box.getHazardCategoriesandgetHazardousare metadata columns onSETUP_CATALOG(read off the SampleType brain).getSampleTypeUIDis added as aFieldIndexonSAMPLE_CATALOGso listings can resolve the SampleType brain cheaply by UID.getHazardousmetadata column onSAMPLE_CATALOGis removed by the upgrade step (del_column): sample-level hazardous is resolved exclusively via the SampleType brain.senaite.core.api(re-usessenaite.core.i18n.translate; nobika.lims.apiimports). All hazard-pictogram helpers, the portal helpers and a brain/object/UID-tolerantget_attrlive here.webpack/app/scss/_hazard_categories.scssand ships in the existingsenaite.corebundle (rebuilt as part of this PR).css-loadergains aurl.filteroption for the SCSS rule so absolute Plone resource URLs pass through verbatim.getHazardous()semantics unchanged on objects. New AT fields default to[]. Existing brains pre-upgrade fall through to the W001 fallback in listings until they are reindexed (the upgrade step reindexes existing SampleTypes).Files of note
src/senaite/core/api/__init__.py— new flat senaite.core.api namespace with portal, translate, get_attr and pictogram helpers (Sphinx-style docstrings, nobika.lims.apidependency).src/senaite/core/vocabularies/hazard_categories.py—HAZARD_CATEGORIESvocabulary grouped by hazard family; tokens are short codes so persistence is decoupled from translations.src/senaite/core/z3cform/widgets/hazard_categories.py(DX) andsrc/senaite/core/browser/widgets/hazardcategorieswidget.py(AT) — custom widgets.src/senaite/core/browser/static/images/ghs/GHS01..GHS09.svg,iso/W001.svg,iso/W003.svg,iso/W005.svg,iso/W006.svg,iso/W009.svg,iso/W010.svg,iso/W012.svg,iso/W017.svg,iso/W041.svg,iso/W079.svg,iso/W080.svg— public-domain SVG pictograms.src/senaite/core/browser/form/adapters/{sampletype,referencesample,referencedefinition}.py— edit-form toggle.src/senaite/core/browser/form/helpers.py— sharedis_checked,get_form_value,has_form_fieldhelpers used by the adapters.src/senaite/core/upgrade/v02_07_000.py— upgrade stepadd_hazard_categoriesadds thegetHazardCategoriesandgetHazardousmetadata columns on the setup catalog, thegetSampleTypeUIDFieldIndex on the sample catalog, removes the now-redundantgetHazardousmetadata column from the sample catalog, and reindexes existing SampleTypes.src/senaite/core/tests/doctests/API_core.rst— doctest for the new API helpers.Verification
bin/test-senaite -m test_textual_doctests -t API_core— 1 test, 0 failures.--
I confirm I have tested this PR thoroughly and coded it according to PEP8
and Plone's Python styleguide standards.