Skip to content

Commit 9587a4f

Browse files
authored
feat(annotator): add nexuslims_annotate app for dataset annotation (#16)
feat(annotator): add full NexusLIMS annotator app with branding updates ## nexuslims_annotate app Adds a new `nexuslims_annotate` Django app that allows authorized users to annotate and manage experiment records directly from the CDCS interface. Key capabilities: - Full-page annotator view: card-based layout of all datasets within a record, with drag-and-drop reordering via SortableJS - Shift-click range selection for checkboxes, with multi-card drag (dragging a selected card moves all selected cards together, with stacked shadow and "N selected" badge on the dragged element) - Inline editing of record fields via Monaco editor - Dataset move/reassignment between records and download dropdown - Annotator side panel (offcanvas) accessible from detail pages - Guided tour using Shepherd.js + Floating UI - Permission-gated: requires write permission; toggled via NX_ENABLE_ANNOTATOR setting; 404s if app not in INSTALLED_APPS - Robust input validation: rejects negative datasetIndex, guards against non-list moves JSON, XSS-safe, proper 404 handling - Full unit test suite with GitHub Actions CI workflow ## Branding - Replace favicon.png and detail-page loading spinner with the new NexusLIMS diffraction-pattern icon (icon.png / icon.svg) - Loading spinner is an inline SVG with two counter-rotating hexagonal rings of circles, animated via CSS keyframes (nx-cw / nx-ccw) - XSLT uses xsl:attribute for viewBox to preserve case under HTML serialization ## Terms of Use - Add setup_terms_of_use() to init_environment.py (Step 6): inserts a default Terms of Use page into the WebPage model on first run, idempotent on subsequent runs - Template override for the CDCS terms-of-use page with scoped CSS: centered 720px column, compact heading/paragraph spacing, lead style for first paragraph - Document customization path in CUSTOMIZATION.md
1 parent 690ab0d commit 9587a4f

47 files changed

Lines changed: 4233 additions & 122 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/tests.yml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
name: Tests
2+
3+
on:
4+
push:
5+
branches: ["main", "feat/**"]
6+
pull_request:
7+
branches: ["main"]
8+
9+
jobs:
10+
test:
11+
runs-on: ubuntu-latest
12+
13+
steps:
14+
- uses: actions/checkout@v4
15+
16+
- name: Install uv
17+
uses: astral-sh/setup-uv@v5
18+
with:
19+
enable-cache: true
20+
21+
- name: Set up Python
22+
run: uv python install
23+
24+
- name: Install dependencies
25+
run: uv sync --dev
26+
27+
- name: Run tests
28+
run: uv run python runtests.py

CLAUDE.md

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -154,8 +154,7 @@ dev-up # Start services
154154
**For local development (outside Docker):**
155155
```bash
156156
# Install/sync dependencies locally
157-
# IMPORTANT: Use --no-install-project flag (this is a Django app, not a package)
158-
uv sync --no-install-project --extra core --extra server
157+
uv sync
159158

160159
# Or use the convenience alias from deployment directory
161160
cd deployment
@@ -241,13 +240,11 @@ The Dockerfile uses native UV commands for optimal performance:
241240
COPY pyproject.toml uv.lock ./
242241

243242
# Install from lockfile (no dependency resolution needed - fast!)
244-
RUN uv sync --frozen --no-dev --extra core --extra server
243+
RUN uv sync --frozen
245244
```
246245

247246
**Flags explained:**
248247
- `--frozen`: Use exact versions from lockfile (fail if lockfile is outdated)
249-
- `--no-dev`: Skip development dependencies (not needed in Docker)
250-
- `--extra core --extra server`: Install optional dependency groups
251248

252249
### Why UV?
253250

@@ -257,6 +254,34 @@ RUN uv sync --frozen --no-dev --extra core --extra server
257254
- **Reliable**: Better conflict resolution than pip
258255
- **Efficient**: Shared cache across projects, parallel downloads
259256

257+
## Running Tests
258+
259+
Tests use `runtests.py` at the repo root, which runs Django tests with an in-memory SQLite database (no Docker or external services required).
260+
261+
### Running Tests Locally
262+
263+
```bash
264+
# From the repo root, with uv:
265+
uv run python runtests.py
266+
267+
# Or with a local venv:
268+
python runtests.py
269+
```
270+
271+
### How It Works
272+
273+
- Uses `tests/test_settings.py` as the Django settings module
274+
- Runs `migrate` automatically before tests (in-memory SQLite)
275+
- Discovers and runs all tests in the `tests/` directory
276+
- Exits with a non-zero code if any tests fail
277+
278+
### Test Settings (`tests/test_settings.py`)
279+
280+
- Uses an in-memory SQLite database (`:memory:`) - no external DB needed
281+
- Loads `.env` via `python-dotenv` if present
282+
- `INSTALLED_APPS` includes `nexuslims_annotate` and `tests`
283+
- `ROOT_URLCONF` points to `tests.urls`
284+
260285
## Planning Documents
261286

262287
All planning and analysis documents should be stored in this repository, not in the user's home directory:

deployment/Dockerfile

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,7 @@ COPY pyproject.toml uv.lock README.md ./
1616
# Install all dependencies using UV sync
1717
# UV_PROJECT_ENVIRONMENT: Install to system Python instead of creating venv
1818
# --frozen: Use exact versions from uv.lock (reproducible builds)
19-
# --no-dev: Skip dev dependencies (not needed in Docker)
20-
# --no-install-project: Skip installing the project itself (just dependencies)
21-
# --extra: Install optional dependency groups
22-
RUN UV_PROJECT_ENVIRONMENT=/usr/local uv sync --frozen --no-dev --no-install-project --extra core --extra server
19+
RUN UV_PROJECT_ENVIRONMENT=/usr/local uv sync --frozen
2320

2421
# Copy application source code and configuration
2522
COPY manage.py ./

deployment/dev-commands.ps1

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ function dev-uv-upgrade {
138138

139139
function dev-uv-sync {
140140
Push-Location ..
141-
uv sync --no-install-project --extra core --extra server
141+
uv sync
142142
Pop-Location
143143
}
144144

deployment/dev-commands.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ alias dev-update-xslt-list='bash scripts/update-xslt.sh list'
5757
# Note: --no-install-project skips building the project itself (Django app, not a package)
5858
alias dev-uv-lock='cd .. && uv lock && cd deployment'
5959
alias dev-uv-upgrade='cd .. && uv lock --upgrade && cd deployment'
60-
alias dev-uv-sync='cd .. && uv sync --no-install-project --extra core --extra server && cd deployment'
60+
alias dev-uv-sync='cd .. && uv sync && cd deployment'
6161
alias dev-uv-add='echo "Usage: cd .. && uv add package-name && cd deployment && dev-build-clean"'
6262

6363
echo "NexusLIMS-CDCS Development aliases loaded! Available commands:"

deployment/scripts/init_environment.py

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -645,6 +645,105 @@ def setup_api_tokens(superuser):
645645
log_success(f"Created {token_name} API token for user '{superuser.username}'")
646646

647647

648+
def setup_terms_of_use():
649+
"""Insert a default Terms of Use page if none exists.
650+
651+
The content is stored as a WebPage record in the database and can be
652+
updated at any time through the admin UI at:
653+
/admin/core-admin/core_website_app_terms
654+
"""
655+
log_info("Checking Terms of Use page...")
656+
657+
try:
658+
from core_main_app.components.web_page.models import WebPage
659+
from core_website_app.commons.enums import WEB_PAGE_TYPES
660+
661+
t = WEB_PAGE_TYPES["terms_of_use"]
662+
663+
# If any non-empty record exists, leave it alone so admins can customise freely
664+
pages = WebPage.objects.filter(type=t)
665+
if pages.filter(content__regex=r'\S').exists():
666+
log_success("Terms of Use page already exists")
667+
return
668+
669+
# Clean up any empty/blank records before inserting
670+
pages.delete()
671+
672+
except Exception:
673+
pass # No record yet -- fall through to create one
674+
675+
default_content = """
676+
<p>
677+
These Terms of Use govern your access to and use of this NexusLIMS instance.
678+
By accessing this site you agree to the terms below. The system administrator
679+
may update these terms at any time; continued use of the system after changes
680+
are posted constitutes acceptance of the revised terms.
681+
</p>
682+
683+
<h5>1. Authorised Use</h5>
684+
<p>Access to this system is granted solely for legitimate scientific research,
685+
data management, and analysis activities consistent with the mission of the
686+
operating institution. You must not use the system for any unlawful purpose
687+
or in any way that disrupts the availability of the service for other users.</p>
688+
689+
<h5>2. User Accounts</h5>
690+
<p>You are responsible for maintaining the confidentiality of your login
691+
credentials and for all activity that occurs under your account. Notify the
692+
system administrator immediately if you suspect unauthorised access.
693+
Accounts that have been inactive for an extended period may be suspended
694+
at the discretion of the administrator.</p>
695+
696+
<h5>3. Data and Intellectual Property</h5>
697+
<p>You retain ownership of any data you upload. By uploading data you grant
698+
the system the right to store, process, and display that data as necessary
699+
to provide the service. You are responsible for ensuring that you have the
700+
right to upload and share any data you submit, and that doing so does not
701+
violate applicable laws or third-party agreements.</p>
702+
703+
<h5>4. Acceptable Data Standards</h5>
704+
<p>Uploaded records should conform to the NexusLIMS schema and represent
705+
genuine experimental data. Falsification, fabrication, or misrepresentation
706+
of research data is a serious violation and may result in account suspension
707+
and referral to institutional authorities.</p>
708+
709+
<h5>5. Privacy</h5>
710+
<p>This system may collect information about your usage (such as login times
711+
and records accessed) for administrative and audit purposes. This information
712+
is not shared with third parties except as required by law or institutional
713+
policy.</p>
714+
715+
<h5>6. Disclaimer of Warranties</h5>
716+
<p>This system is provided &ldquo;as is&rdquo; without warranty of any kind. The operating
717+
institution makes no guarantees regarding uptime, data integrity, or fitness
718+
for a particular purpose. Users are encouraged to maintain independent
719+
backups of critical data.</p>
720+
721+
<h5>7. Limitation of Liability</h5>
722+
<p>To the fullest extent permitted by applicable law, the operating institution
723+
shall not be liable for any indirect, incidental, or consequential damages
724+
arising from your use of or inability to use this system.</p>
725+
726+
<h5>8. Contact</h5>
727+
<p>Questions about these terms or your account should be directed to the
728+
system administrator via the <a href="/contact/">contact page</a>.</p>
729+
"""
730+
731+
try:
732+
from core_main_app.components.web_page.models import WebPage
733+
from core_website_app.commons.enums import WEB_PAGE_TYPES
734+
from core_website_app.components.terms_of_use import api as terms_api
735+
736+
page = WebPage()
737+
page.type = WEB_PAGE_TYPES["terms_of_use"]
738+
page.content = default_content.strip()
739+
terms_api.upsert(page)
740+
log_success("Default Terms of Use page created")
741+
log_info(" Update it any time at: /admin/core-admin/core_website_app_terms")
742+
743+
except Exception as e:
744+
log_error(f"Failed to create Terms of Use page: {e}")
745+
746+
648747
def load_exporters():
649748
"""Load the exporters into the database."""
650749
log_info("Loading exporters...")
@@ -756,6 +855,9 @@ def main():
756855
# Step 5: Load exporters
757856
load_exporters()
758857

858+
# Step 6: Set up default Terms of Use page
859+
setup_terms_of_use()
860+
759861
print("=" * 70)
760862
log_success("Initialization complete!")
761863

-358 Bytes
Binary file not shown.

mdcs/settings.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@
105105

106106
# NexusLIMS customizations (must come before core apps)
107107
'nexuslims_overrides',
108+
'nexuslims_annotate',
108109

109110
# Core apps
110111
"core_main_app",

mdcs/urls.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
1. Add a URL to urlpatterns: re_path(r'^blog/', include('blog.urls'))
1414
"""
1515

16+
from django.conf import settings
1617
from django.conf.urls import include
1718
from django.contrib import admin
1819
from django.urls import re_path
@@ -30,6 +31,12 @@
3031
),
3132
# NexusLIMS overrides MUST come before core_main_app to override its URLs
3233
re_path(r"^", include("nexuslims_overrides.urls")),
34+
*(
35+
[re_path(r"^annotate/", include("nexuslims_annotate.urls"))]
36+
if "nexuslims_annotate" in settings.INSTALLED_APPS
37+
and getattr(settings, "NX_ENABLE_ANNOTATOR", True)
38+
else []
39+
),
3340
re_path(r"^", include("core_main_app.urls")),
3441
re_path(r"^home/", include("mdcs_home.urls")),
3542
re_path(r"^", include("core_website_app.urls")),

nexuslims_annotate/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)