feat(annotator): add nexuslims_annotate app for dataset annotation#16
Merged
feat(annotator): add nexuslims_annotate app for dataset annotation#16
Conversation
Adds a new Django app that allows authenticated users to add textual descriptions to individual datasets within a NexusLIMS experiment record. An offcanvas panel is accessible from the record detail page via an "Annotate Record" button. Descriptions are saved directly into the XML record's existing <description> elements and persist across page loads. Controlled by the NX_ENABLE_ANNOTATOR feature flag (default True in dev).
- Move button into #top-button-div action bar via JS (window load, prepend so it appears rightmost in the flex-reversed container) - Add Bootstrap tooltip "Annotate this record with dataset descriptions" initialized via JS to avoid data-bs-toggle conflict with offcanvas - Add annotate.css in the app's static directory with responsive rules: font-size scaling matching other btn-top-group buttons, icon-only at ≤696px, and full-width offcanvas at narrow viewports - Blur button after offcanvas closes (setTimeout to yield after Bootstrap's focus-return) to prevent the gray focus background
- Redesign dataset cards with color-coded left-border accents: green for saved annotations, gray for empty, yellow/amber for unsaved changes - Add activity dividers with 1-based numbering and horizontal rule separator - Track dirty state via data-original attribute on each textarea; update card colors live on input - Warn before closing panel when unsaved changes exist (confirm dialog), with forceClose flag to bypass check after save or confirmed discard - Fix sticky Save/Cancel footer by moving it outside the form element, using HTML5 form= attribute on the submit button, and applying flex column layout to the offcanvas body so the form scrolls independently - Use monospace font for dataset filenames; reduce thumbnail size to 56px
…ssions Full-width annotator page (annotate.html): - Fix broken template: extend theme.html (not the non-existent core_main_app/user/main.html) - Extract record title from XML via _get_title() helper rather than data.title - Match panel UI: color-coded cards, activity dividers with 1-based numbering, data-original dirty tracking, monospace filenames, align-self-stretch images - Style Back to Record button as btn-top-group with menu-fa icon spacing - Save redirects to record detail; errors redirect back with ?error=1 Offcanvas panel improvements: - Offset offcanvas top by 60px so header clears the fixed navbar - Add expand button (fa-expand) in offcanvas header linking to full-width page; initialize its tooltip via JS with placement:left - Reduce first-activity top margin to mt-1; subsequent activities keep mt-3 - Images use align-self-stretch + width:auto;max-width:80px;height:100% so they fill card height proportionally instead of being clipped at a fixed size Permission fix: - Gate Annotate button on data.can_write (workspace write access) rather than request.user.is_authenticated, matching the Edit/Open in text editor buttons Save view: - Detect AJAX via X-Requested-With header; return JSON for panel, redirect for full-width form POST - Add X-Requested-With: XMLHttpRequest to panel's fetch call
Annotator features: - Add inline pencil icon on dataset rows (hover-reveal) that opens a floating edit popup — saves via new annotate_save_one endpoint - Add annotate_save_one view and URL for single-dataset saves - Add help modal (? button) to offcanvas panel and full-page annotator explaining color coding, keyboard shortcuts, and inline editing - Add Ctrl/Cmd+Enter keyboard shortcut to save in both offcanvas and inline popup; Escape to close inline popup Full-page annotator UI: - Modernize dataset cards: rounded corners, hover shadow, borderless textarea with subtle fill and green focus ring - DRY CSS: extract shared .annotate-card and .annotate-textarea rules into annotate.css, removing duplicate inline styles from templates - Fix CDCS theme interference (padding-top: 40px / margin-top: -40px on form children) that caused misaligned activity headers; use row-gap for vertical card spacing instead of Bootstrap gutters Detail page / XSLT: - Fix first gallery image description not shown on page load (setTimeout) - Fix off-page DataTables rows not updating after save by storing descriptions in window.__nxAnnotateDescriptions and applying on draw.dt - Fix dataset metadata modal description overflow (white-space, max-width) - Add .nx-desc-row flex layout for pencil icon alignment in tables - Refresh descriptions on save via refreshDisplayedDescriptions (exposed on window for cross-IIFE access) CSS / styles: - Add .nx-modal-desc-wrapper line-height and .dataset-meta-modal max-width/white-space fixes in modals.css - Add .nx-desc-edit hover styles and #nx-inline-edit-popup in modals.css - Fix Monaco editor find widget broken by MDCS global .button rule (scoped reset in main.css)
…INSTALLED_APPS Setting NX_ENABLE_ANNOTATOR=False now removes both the annotator UI and the /annotate/ URL routes, so the full-page editor is not directly accessible via URL. Routes are also suppressed if nexuslims_annotate is removed from INSTALLED_APPS.
Add check_can_write checks to annotate_record and annotate_panel so unauthorized users receive a 403 instead of seeing the form. Save views now also catch AccessControlError explicitly and return 403 rather than 500 for permission failures.
Merge core and server optional extras into main dependencies so plain `uv sync` installs everything without --extra flags. Add python-dotenv as a dev dependency group. Update Dockerfile and dev-commands aliases accordingly. Also add nexuslims_annotate to test INSTALLED_APPS and document runtests.py usage in CLAUDE.md.
Add ability to reassign datasets between acquisition activities in the annotator panel. New XML helpers handle activity parsing, setup param injection/reconciliation, creation-time sorting, and applying moves. The save endpoint now accepts a moves JSON payload alongside descriptions. Also consolidate the XML and JSON download buttons in the detail view into a single Download Record dropdown (XSLT, buttons.css, tour.js). Includes 936-line test suite covering all new pure XML-processing helpers.
…elpers Move StageX into activity 0 setup (matching real record structure), add image_004.dm3 to activity 1, and add Unique_to_N meta fields to each dataset to make the fixture more realistic. Update all affected tests to match the new fixture, using orig_0/orig_1 class attributes for relative dataset-count assertions so tests stay correct regardless of fixture size. Remove test_conflicting_dataset_value_prevents_ setup_param_promotion (tests an impossible state) and test_existing_meta_ not_overwritten (no longer applicable after StageX moved to setup level). Add _dump_activity_el, _dump_activities, _dump_el, and _dump_xml helpers for use when debugging tests. Also update nexuslims-test-data.tar.gz with the latest example_record.xml.
…oating UI - Add tut-aa-inline-edit tour step that highlights the pencil icon for inline dataset description editing (only shown when annotator is enabled and user has write access) - Pin Shepherd.js to 11.2.0 (supports floatingUIOptions) and add Floating UI (utils 0.2.11, core 1.7.5, dom 1.7.6) as static assets - Fix modal overlay leak on tour cancel by explicitly removing .shepherd-modal-overlay-container in cleanupOnExit - Fix modal visibility check to also require display !== 'none' - Add scrollDisabled body class during tour for consistent scroll management - Refactor annotator inline styles to .nx-help-text and .nx-dataset-name CSS classes; fix offcanvas-backdrop top offset
- Add tut-annotate-panel step that programmatically opens the Bootstrap offcanvas via beforeShowPromise (waits for shown.bs.offcanvas), then attaches the tooltip to the panel header and closes the panel on hide - Change tut-annotate-record buttons from end to next so the tour continues into the new panel step - Close the offcanvas in clean_up_on_exit as a safety net for cancel/complete events that race with the step's when.hide hook
- Add createAnnotatorTour() with steps covering the welcome overview, dataset card color coding, checkbox/batch-move (with forced opacity to reveal the hidden checkbox), and the save button with keyboard shortcut - Add NexusLIMSTours.startAnnotatorTour() public method and wire it into startTour() page detection via #annotate-page-form - Remove stale detail/tour.js and detail/downloads.js script tags from theme_base.html override (files no longer exist; correct scripts are loaded by theme.html) - Fix indentation regression in annotate.html Help button
…rror handling
- Replace all hardcoded `/annotate/...` paths with `reverse()` calls in views.py
and `{% url %}` tags in detail.html; pass URLs via `data-url-*` attributes so
JS never constructs paths itself
- Fix TypeError in `_sort_datasets_by_creation_time` when multiple datasets lack
timestamps (use sentinel datetime.min instead of None comparison)
- Add proper 404/403 responses: `annotate_record` raises Http404 on missing
records; `annotate_panel` returns JSON 404; `annotate_descriptions` checks
write permission and returns 403
- Change `NX_ENABLE_ANNOTATOR` default from False to True in context processor
- Add SortableJS 1.15.6 vendored library
- Remove unused date-formatting code from detail/main.js
- Add tests for the sort fix and view-level integration tests
…t handling, popup layout - Replace mark_safe(json.dumps(activities)) with json_script filter to prevent XSS via </script> injection in annotate.html - Add DoesNotExist handler to annotate_descriptions view (was the only view missing it, causing 500 instead of 404 for deleted records) - Validate dataset_index in annotate_save_one: return 400 if missing or negative instead of silently succeeding as a no-op - Wrap popup offsetWidth read in requestAnimationFrame so position clamp uses actual rendered width rather than 0 before first layout pass
- Catch ModelError (raised for malformed/non-integer record IDs) alongside DoesNotExist in all five views, returning 404 instead of 500 - Add @require_GET to annotate_panel to reject POST with 405 - Add ET.ParseError handling in read-path views (annotate_record, annotate_panel, annotate_descriptions) returning 500 JSON instead of unhandled exception - Fix form action attribute missing from _panel.html offcanvas form - Fix XSS in offcanvas error handler: replace innerHTML string concat with createElement/textContent - Add tests: DoesNotExist/ModelError in save views, malformed XML in read views, POST to panel returns 405, annotate_save 404 and success
Views: - Move _get_title inside ET.ParseError guard in annotate_record (issue #1) - annotate_record XML parse error now renders template with xml_error context instead of returning raw JSON in an HTML view (issue #10) - annotate_panel returns JsonResponse 403 instead of HttpResponseForbidden to stay consistent with other AJAX endpoints (issue #4) - annotate_save_one: catch ValueError separately to return 400 for non-integer dataset_index (issue #12); add idx bounds check -> 400 (issue #11); log raw_idx instead of None on int() failure (issue #5) - Both write-path views use @require_POST instead of manual method check to get correct Allow header on 405 responses (issue #15) - Log warning when moves JSON is malformed and silently dropped (issue #3) - 500 responses return generic 'Internal server error' instead of leaking str(e) (issue #14) - _apply_moves validates move dict keys before access (issue #16) - Add comments documenting write-path authorization via upsert Tests: - Add AnnotateRecordViewTest with 6 cases covering unauthenticated, 404, 403, malformed XML (500), success render, and ?error=1 banner (issue #2) - Add bounds-check test and 403 test for annotate_save_one (issues #11, #13) - Fix test_non_integer_dataset_index to expect 400, not 500 (issue #12) - Fix image_004.dm3 fixture location from image_003.dm3 (issue #8) - Add CUSTOM_NAME to test_settings.py and core_main_app_data_detail stub URL so annotate.html renders cleanly in tests Frontend: - updateActivityCounts() now syncs window.__nxActivities and rebuilds the move dropdown so counts are not stale after moves (issue #7) - Replace fragile CSRF cookie regex with {{ csrf_token }} in detail.html (issue #6) - Remove dead .sortable-drag-over CSS rule (SortableJS never applies it; ghostClass is used instead) (issue #9) - Move inline <style> blocks from annotate.html and detail.html into annotate.css - Reset saving flag on pageshow to handle failed mid-flight POSTs - Remove unused default_auto_field from apps.py (no models in this app)
Python negative indexing caused values like -1 to bypass the upper-bound check and silently move the wrong dataset. Now validates that datasetIndex is a non-negative integer before processing.
If the moves POST param decoded to a non-list JSON value (e.g. an integer or object), _apply_moves would raise TypeError, triggering the outer exception handler and returning a 500 while discarding the user's description edits. Add an isinstance check to reset moves to [] and log a warning instead.
- Shift-clicking a checkbox selects all cards between the last clicked and the current one, matching the clicked card's checked state - Dragging a selected card now brings all other selected cards along to the same destination activity, then clears the selection - Update help text and modal to document both new interactions
…g ghost - Add mousedown handler that calls preventDefault when shift is held over a checkbox label, suppressing browser text selection without affecting the checkbox toggle - Add onStart callback to SortableJS that applies nx-multi-drag class and data-multi-count attribute when dragging a selected card with others also selected; CSS renders stacked indigo box-shadows and a count badge - Clean up nx-multi-drag class/attribute in onEnd regardless of drop target
…laceholder Defer badge DOM injection to setTimeout(0) so it runs after SortableJS clones the element for the ghost placeholder. The nx-multi-drag class is still added synchronously so both the placeholder and dragged element get the stacked box-shadow, but only the dragged element receives the "N selected" badge span.
- Replace favicon.png with the updated diffraction-pattern icon - Add icon.png and icon.svg as source assets - Point admin base.html favicon at nexuslims/img/favicon.png to match the main theme (was referencing the old img/favicon.png path) - Update XSL loading spinner from /static/img/logo_bare.png to /static/nexuslims/img/favicon.png
…erms of Use page - Replace static PNG loading spinner in detail_stylesheet.xsl with an inline SVG using the NexusLIMS icon geometry (two counter-rotating hexagonal rings of circles). Use xsl:attribute for viewBox to avoid HTML serializer lowercasing. - Update loading.css to animate the two SVG groups (#nx-spin-inner / #nx-spin-outer) with counter-rotating keyframe animations. - Add setup_terms_of_use() to init_environment.py (Step 6) that inserts a default Terms of Use page into the WebPage model on first run, skipping if non-empty content already exists. - Add template override at nexuslims_overrides/templates/core_website_app/ user/terms-of-use.html with scoped CSS: centered 720px column, compact heading/paragraph spacing, lead style for first paragraph, and a proper page title h2. - Document Terms of Use customization in CUSTOMIZATION.md.
jat255
added a commit
that referenced
this pull request
Mar 29, 2026
The Dockerfile was missing a COPY directive for the nexuslims_annotate app added in #16, causing Django to fail on startup with ModuleNotFoundError: No module named 'nexuslims_annotate'.
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.
Summary
Adds the
nexuslims_annotateDjango app, which lets authenticated users with write access annotate NexusLIMS experiment records in two ways:<dataset>/<description>and rendered in the gallery, dataset activity tables, and metadata modals.Entry points
/annotate/<record_id>/as a full-page form with all dataset cards visible at once.A
?help button is available in the full-page editor.Full-page editor features
Descriptions
Type into any card's text field and save with Save Annotations or Ctrl+Enter. Cards are color-coded:
Dataset reassignment
Datasets can be moved between activities three ways:
Shift-click on checkboxes selects all cards between the last clicked and the current one, making it easy to select large ranges without clicking every card individually.
Moves are staged (shown in indigo with a "Moved" badge) and saved atomically with descriptions when you click Save Annotations. Individual moves can be undone via the "Moved" badge or the Undo all moves button in the toolbar.
Changes
nexuslims_annotate/-- new Django app with views, templates, CSS, and URL configxslt/detail_stylesheet.xsl-- renders<description>elements in gallery cards, activity tables, and metadata modals; color-codes datasets (green = annotated, orange = has file but no description, gray = no file)nexuslims_overrides/-- detail template wired up with offcanvas panel, inline edit JS, and help modal; datatables JS extended with pencil-icon inline edit; detail CSS extended for gallery/table/tooltip/modal/button styles; Monaco editor find-widget styling fixedmdcs/urls.py-- annotate routes gated on bothNX_ENABLE_ANNOTATORandnexuslims_annotateinINSTALLED_APPSnexuslims_overrides/settings.py--NX_ENABLE_ANNOTATOR = Truedefault addednexuslims_overrides/CUSTOMIZATION.md-- full documentation for the feature and its feature flagFeature flag
Requires a container restart to take effect.
Permissions
All annotator views enforce write access via
check_can_write. Users without write permission receive a 403 on all endpoints (GET and POST). The Annotate Record button is only rendered for users withcan_writeon the record.Test plan
/annotate/<id>/returns 403NX_ENABLE_ANNOTATOR = False, restart container -- button gone,/annotate/<id>/returns 404nexuslims_annotatefromINSTALLED_APPS--/annotate/<id>/returns 404