diff --git a/.env.example b/.env.example index dfdc98844..bfa04542d 100644 --- a/.env.example +++ b/.env.example @@ -84,8 +84,6 @@ AUTHENTIK_CLIENT_ID= AUTHENTIK_AUTHORIZE_URL= AUTHENTIK_TOKEN_URL= -# middleware -SESSION_SECRET_KEY=your_secret_key_here # feedback endpoint (POST /feedback) — bug reports and feature requests JIRA_BASE_URL=https://nmbgmr.atlassian.net diff --git a/.github/app.template.yaml b/.github/app.template.yaml index 6a1a52fb4..bb44e584c 100644 --- a/.github/app.template.yaml +++ b/.github/app.template.yaml @@ -42,8 +42,6 @@ env_variables: AUTHENTIK_CLIENT_ID: "${AUTHENTIK_CLIENT_ID}" AUTHENTIK_AUTHORIZE_URL: "${AUTHENTIK_AUTHORIZE_URL}" AUTHENTIK_TOKEN_URL: "${AUTHENTIK_TOKEN_URL}" - SESSION_SECRET_KEY: |- - ${SESSION_SECRET_KEY} APITALLY_CLIENT_ID: "${APITALLY_CLIENT_ID}" JIRA_BASE_URL: "${JIRA_BASE_URL}" JIRA_EMAIL: "${JIRA_EMAIL}" diff --git a/.github/workflows/CD_production.yml b/.github/workflows/CD_production.yml index 21c6acc07..0ac251141 100644 --- a/.github/workflows/CD_production.yml +++ b/.github/workflows/CD_production.yml @@ -126,7 +126,6 @@ jobs: AUTHENTIK_CLIENT_ID: "${{ vars.AUTHENTIK_CLIENT_ID }}" AUTHENTIK_AUTHORIZE_URL: "${{ vars.AUTHENTIK_AUTHORIZE_URL }}" AUTHENTIK_TOKEN_URL: "${{ vars.AUTHENTIK_TOKEN_URL }}" - SESSION_SECRET_KEY: "${{ secrets.SESSION_SECRET_KEY }}" APITALLY_CLIENT_ID: "${{ vars.APITALLY_CLIENT_ID }}" JIRA_BASE_URL: "${{ vars.JIRA_BASE_URL || 'https://nmbgmr.atlassian.net' }}" JIRA_EMAIL: "${{ steps.feedback-secrets.outputs.jira_email }}" diff --git a/.github/workflows/CD_staging.yml b/.github/workflows/CD_staging.yml index 348f2b4b8..cc54ef0f3 100644 --- a/.github/workflows/CD_staging.yml +++ b/.github/workflows/CD_staging.yml @@ -86,7 +86,6 @@ jobs: AUTHENTIK_CLIENT_ID: "${{ vars.AUTHENTIK_CLIENT_ID }}" AUTHENTIK_AUTHORIZE_URL: "${{ vars.AUTHENTIK_AUTHORIZE_URL }}" AUTHENTIK_TOKEN_URL: "${{ vars.AUTHENTIK_TOKEN_URL }}" - SESSION_SECRET_KEY: "${{ secrets.SESSION_SECRET_KEY }}" APITALLY_CLIENT_ID: "${{ vars.APITALLY_CLIENT_ID }}" JIRA_BASE_URL: "${{ vars.JIRA_BASE_URL || 'https://nmbgmr.atlassian.net' }}" JIRA_EMAIL: "${{ steps.feedback-secrets.outputs.jira_email }}" diff --git a/.github/workflows/CD_testing.yml b/.github/workflows/CD_testing.yml index ba4d4e790..8cefcb586 100644 --- a/.github/workflows/CD_testing.yml +++ b/.github/workflows/CD_testing.yml @@ -86,7 +86,6 @@ jobs: AUTHENTIK_CLIENT_ID: "${{ vars.AUTHENTIK_CLIENT_ID }}" AUTHENTIK_AUTHORIZE_URL: "${{ vars.AUTHENTIK_AUTHORIZE_URL }}" AUTHENTIK_TOKEN_URL: "${{ vars.AUTHENTIK_TOKEN_URL }}" - SESSION_SECRET_KEY: "${{ secrets.SESSION_SECRET_KEY }}" APITALLY_CLIENT_ID: "${{ vars.APITALLY_CLIENT_ID }}" JIRA_BASE_URL: "${{ vars.JIRA_BASE_URL || 'https://nmbgmr.atlassian.net' }}" JIRA_EMAIL: "${{ steps.feedback-secrets.outputs.jira_email }}" diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 57883f562..2a2da8e3e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -28,7 +28,6 @@ jobs: PYGEOAPI_POSTGRES_DB: ocotilloapi_test DB_DRIVER: postgres BASE_URL: http://localhost:8000 - SESSION_SECRET_KEY: supersecretkeyforunittests AUTHENTIK_DISABLE_AUTHENTICATION: 1 services: @@ -119,7 +118,6 @@ jobs: PYGEOAPI_POSTGRES_DB: ocotilloapi_test DB_DRIVER: postgres BASE_URL: http://localhost:8000 - SESSION_SECRET_KEY: supersecretkeyforunittests AUTHENTIK_DISABLE_AUTHENTICATION: 1 DROP_AND_REBUILD_DB: 1 diff --git a/README.md b/README.md index 90ca4bc99..7a1248d1c 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ supports research, field operations, and public data delivery for the Bureau of ## 🗺️ OGC API - Features The API exposes OGC API - Features endpoints under `/ogcapi` using `pygeoapi`. -In App Engine deployments, `/admin` and `/ogcapi` are served from the same +In App Engine deployments, `/ogcapi` is served from the same application as the primary API. The service is intended to scale to zero outside business hours and be kept warm during the workday with Cloud Scheduler hits to `/_ah/warmup`. @@ -152,7 +152,6 @@ Minimum vars to set in `.env` for local development: * `POSTGRES_HOST` (`localhost` for local psql/pytest against mapped Docker port) * `POSTGRES_PORT` (`5432`) * `MODE` (`development` recommended locally) -* `SESSION_SECRET_KEY` (required if you want to use `/admin`) Auth-related vars (required when auth is enabled, optional when `AUTHENTIK_DISABLE_AUTHENTICATION=1`): * `AUTHENTIK_DISABLE_AUTHENTICATION` @@ -206,7 +205,7 @@ Notes: * Requires Docker Desktop. * By default, spins up two containers: * `db` for PostGIS/PostgreSQL - * `app` for the primary API, admin UI, and OGC API on `http://localhost:8000` + * `app` for the primary API and OGC API on `http://localhost:8000` * `db` initializes both application databases in the same Postgres service: * `ocotilloapi_dev` * `ocotilloapi_test` @@ -216,7 +215,6 @@ Notes: * test: `ocotilloapi_test` (created by init SQL in `docker/db/init/01-create-test-db.sql`) * The database listens on port `5432` both inside the container and on your host. Ensure `POSTGRES_PORT=5432` and `POSTGRES_DB=ocotilloapi_dev` in your `.env` to run local commands against the Docker dev DB (e.g., `uv run pytest`, `uv run python -m transfers.transfer`). * To restore a local or GCS-backed SQL dump into your local target DB, run `source .venv/bin/activate && python -m cli.cli restore-local-db path/to/dump.sql` or `source .venv/bin/activate && python -m cli.cli restore-local-db gs://ocotillo/sql-exports/latest.sql.gz`. -* `SESSION_SECRET_KEY` only needs to be set in `.env` if you plan to use `/admin`; without it, the API and `/ogcapi` still boot, but `/admin` will be unavailable. #### Staging Data diff --git a/admin/__init__.py b/admin/__init__.py deleted file mode 100644 index 2816d3891..000000000 --- a/admin/__init__.py +++ /dev/null @@ -1,24 +0,0 @@ -# =============================================================================== -# Copyright 2025 -# -# 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. -# =============================================================================== -""" -Starlette Admin package for OcotilloAPI. - -Provides web-based administrative interface for managing database records. -""" - -from admin.config import create_admin - -__all__ = ["create_admin"] diff --git a/admin/auth.py b/admin/auth.py deleted file mode 100644 index 903068ab7..000000000 --- a/admin/auth.py +++ /dev/null @@ -1,298 +0,0 @@ -# =============================================================================== -# Copyright 2025 -# -# 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. -# =============================================================================== -""" -Admin authentication provider integrating with existing Authentik OIDC auth. - -This module provides a Starlette Admin AuthProvider that integrates with the -existing Authentik-based authentication system used by the OcotilloAPI API. -""" - -import base64 -import hashlib -import os -import secrets -from core.permissions import _get_token_payload, verify_token -from dataclasses import dataclass -from starlette.requests import Request -from starlette.responses import RedirectResponse -from starlette_admin.auth import AdminUser, AuthProvider -from starlette_admin.exceptions import LoginFailed -from typing import List -from typing import Optional -from urllib.parse import urlencode - - -@dataclass -class AdminUserWithRoles(AdminUser): - """Extended AdminUser with roles for RBAC.""" - - roles: List[str] = None - - def __post_init__(self): - if self.roles is None: - self.roles = [] - - -class NMSampleLocationsAuthProvider(AuthProvider): - """ - Custom auth provider that integrates with existing Authentik OIDC authentication. - - Reuses the existing authentication infrastructure from core.permissions module. - - For MS Access users: This replaces Access file-level security with user-level - authentication. Each user logs in with their Authentik credentials and gets - assigned roles (Admin, Editor, Viewer) which control what they can do in the - admin interface. - """ - - async def is_authenticated(self, request: Request) -> bool: - """ - Check if user is authenticated by verifying their JWT token. - - This method is called on every admin page request to determine if the - user should be allowed access. - - Returns: - bool: True if user has a valid JWT token, False otherwise - """ - # Check if authentication is disabled (development mode only) - if int(os.environ.get("AUTHENTIK_DISABLE_AUTHENTICATION", 0)): - from core.settings import settings - - if settings.mode != "production": - # Allow unauthenticated access in development mode - request.state.user = AdminUserWithRoles( - username="dev_user", roles=["admin"] - ) - return True - - try: - # Try to get token from Authorization header - authorization = request.headers.get("Authorization") - if not authorization: - # Try to get token from session/cookie - token = request.session.get("token") - if not token: - return False - else: - # Extract token from "Bearer " format - token = ( - authorization.split(" ")[1] - if " " in authorization - else authorization - ) - - # Verify token using existing authentication system - is_valid = verify_token(token, scope=None, permissions=None) - - if is_valid: - # Store user in request state for later access - request.state.user = self._create_admin_user_from_token(token) - - return is_valid - except Exception: - return False - - def _create_admin_user_from_token(self, token: str) -> Optional[AdminUser]: - """ - Extract user information from JWT token and create AdminUser instance. - - Args: - token: JWT access token from Authentik - - Returns: - AdminUser instance with username and roles, or None if token invalid - """ - try: - # Decode JWT payload - payload = _get_token_payload(token) - - # Extract user information from JWT claims - username = ( - payload.get("preferred_username") - or payload.get("email") - or payload.get("sub") - ) - email = payload.get("email") - groups = payload.get("groups", []) - - # Map Authentik groups to admin roles - roles = [] - - # Standard roles - if "Admin" in groups: - roles.append("admin") - if "Editor" in groups: - roles.append("editor") - if "Viewer" in groups: - roles.append("viewer") - - # AMP-specific roles (for AMPAPI-related data) - if "AMPAdmin" in groups: - roles.append("amp_admin") - if "AMPEditor" in groups: - roles.append("amp_editor") - if "AMPViewer" in groups: - roles.append("amp_viewer") - - # Lexicon-specific roles - if "LexiconAdmin" in groups: - roles.append("lexicon_admin") - if "LexiconEditor" in groups: - roles.append("lexicon_editor") - - return AdminUserWithRoles( - username=username, - photo_url=None, # Could add user avatar URL from OIDC if available - roles=roles, - ) - except Exception: - return None - - def get_admin_user(self, request: Request) -> Optional[AdminUser]: - """ - Get the current admin user from the request. - - This method is called by Starlette Admin to get user information for - display in the UI and permission checks. - - Returns: - AdminUser instance with username and roles, or None if not authenticated - """ - # Check if user is already stored in request state - if hasattr(request.state, "user"): - return request.state.user - - try: - # Get token from request - authorization = request.headers.get("Authorization") - if not authorization: - token = request.session.get("token") - if not token: - return None - else: - token = ( - authorization.split(" ")[1] - if " " in authorization - else authorization - ) - - # Create AdminUser from token - admin_user = self._create_admin_user_from_token(token) - - # Store in request state for future calls - if admin_user: - request.state.user = admin_user - - return admin_user - except Exception: - return None - - async def login(self, *args, **kwargs) -> RedirectResponse: - """ - Redirect to Authentik OIDC login page. - - Note: Starlette Admin will show a login form, but we ignore the username/password - and redirect to Authentik OAuth flow instead. - - Args: - request: Starlette request object (extracted from args/kwargs) - *args/**kwargs: Ignored, kept for compatibility with different - Starlette Admin login call signatures - - Returns: - RedirectResponse to Authentik authorization endpoint - """ - # Starlette Admin has changed the AuthProvider.login signature across versions. - # Accept *args/**kwargs and extract the Request to stay compatible whether - # it calls login(request, data, ...) or login(username, password, remember_me, request). - request: Optional[Request] = kwargs.get("request") - if request is None: - for arg in args: - if isinstance(arg, Request): - request = arg - break - - if request is None: - raise LoginFailed("Unable to determine login request context.") - - authentik_authorize_url = os.environ.get("AUTHENTIK_AUTHORIZE_URL") - authentik_client_id = os.environ.get("AUTHENTIK_CLIENT_ID") - if not authentik_authorize_url or not authentik_client_id: - raise LoginFailed( - "Authentik authentication is not configured. Please set AUTHENTIK_AUTHORIZE_URL and AUTHENTIK_CLIENT_ID environment variables." - ) - - # Store original URL to redirect back after login - original_url = str(request.url_for("admin:index")) - request.session["auth_redirect"] = original_url - redirect_uri = str(request.url_for("admin_auth_callback")) - - # PKCE for public clients - code_verifier = secrets.token_urlsafe(64) - digest = hashlib.sha256(code_verifier.encode("ascii")).digest() - code_challenge = base64.urlsafe_b64encode(digest).rstrip(b"=").decode("ascii") - - state = secrets.token_urlsafe(32) - request.session["auth_state"] = state - request.session["auth_code_verifier"] = code_verifier - - params = { - "response_type": "code", - "client_id": authentik_client_id, - "redirect_uri": redirect_uri, - "scope": "openid profile email", - "state": state, - "code_challenge": code_challenge, - "code_challenge_method": "S256", - } - - authorize_url = f"{authentik_authorize_url}?{urlencode(params)}" - return RedirectResponse(url=authorize_url, status_code=302) - - async def logout(self, *args, **kwargs) -> RedirectResponse: - """ - Handle logout by clearing session and redirecting. - - Args: - request: Starlette request object (extracted from args/kwargs) - *args/**kwargs: Ignored, kept for compatibility with different - Starlette Admin logout call signatures - - Returns: - RedirectResponse to home page - """ - request: Optional[Request] = kwargs.get("request") - if request is None: - for arg in args: - if isinstance(arg, Request): - request = arg - break - - if request is None: - raise LoginFailed("Unable to determine logout request context.") - - # Clear session tokens - request.session.pop("token", None) - request.session.pop("auth_redirect", None) - - # Clear user from request state - if hasattr(request.state, "user"): - delattr(request.state, "user") - - # Redirect to home page - # TODO: Consider redirecting to Authentik logout endpoint to fully log out - return RedirectResponse(url="/", status_code=302) diff --git a/admin/auth_routes.py b/admin/auth_routes.py deleted file mode 100644 index 9db20669e..000000000 --- a/admin/auth_routes.py +++ /dev/null @@ -1,78 +0,0 @@ -# =============================================================================== -# Copyright 2025 -# -# 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. -# =============================================================================== -""" -Admin authentication callback routes. -""" - -import os - -import httpx -from fastapi import APIRouter, Request -from starlette.responses import RedirectResponse -from starlette_admin.exceptions import LoginFailed - -router = APIRouter() - - -@router.get("/admin/auth/callback", name="admin_auth_callback", include_in_schema=False) -async def admin_auth_callback(request: Request): - code = request.query_params.get("code") - state = request.query_params.get("state") - expected_state = request.session.get("auth_state") - - if not code or not state or state != expected_state: - raise LoginFailed("Invalid authentication response.") - - token_url = os.environ.get("AUTHENTIK_TOKEN_URL") - client_id = os.environ.get("AUTHENTIK_CLIENT_ID") - if not token_url or not client_id: - raise LoginFailed( - "Authentik authentication is not configured. Please set AUTHENTIK_TOKEN_URL and AUTHENTIK_CLIENT_ID." - ) - - redirect_uri = str(request.url_for("admin_auth_callback")) - code_verifier = request.session.get("auth_code_verifier") - - data = { - "grant_type": "authorization_code", - "client_id": client_id, - "code": code, - "redirect_uri": redirect_uri, - } - - if code_verifier: - data["code_verifier"] = code_verifier - - client_secret = os.environ.get("AUTHENTIK_CLIENT_SECRET") - if client_secret: - data["client_secret"] = client_secret - - async with httpx.AsyncClient(timeout=15.0) as client: - resp = await client.post(token_url, data=data) - if resp.status_code >= 400: - raise LoginFailed("Failed to exchange token from Authentik.") - token_payload = resp.json() - - access_token = token_payload.get("access_token") - if not access_token: - raise LoginFailed("Authentik did not return an access token.") - - request.session["token"] = access_token - request.session.pop("auth_state", None) - request.session.pop("auth_code_verifier", None) - - redirect_to = request.session.pop("auth_redirect", "/admin") - return RedirectResponse(url=redirect_to, status_code=302) diff --git a/admin/config.py b/admin/config.py deleted file mode 100644 index e559fef92..000000000 --- a/admin/config.py +++ /dev/null @@ -1,218 +0,0 @@ -# =============================================================================== -# Copyright 2025 -# -# 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. -# =============================================================================== -""" -Starlette Admin configuration and initialization. - -This module creates and configures the admin interface for OcotilloAPI. -""" - -from admin.auth import NMSampleLocationsAuthProvider -from admin.views import ( - AquiferSystemAdmin, - AquiferTypeAdmin, - AssetAdmin, - AssociatedDataAdmin, - ChemistrySampleInfoAdmin, - ContactAdmin, - DataProvenanceAdmin, - DeploymentAdmin, - FieldActivityAdmin, - FieldEventAdmin, - GeologicFormationAdmin, - GroupAdmin, - HydraulicsDataAdmin, - LexiconCategoryAdmin, - LexiconTermAdmin, - LocationAdmin, - MajorChemistryAdmin, - MinorTraceChemistryAdmin, - NotesAdmin, - ObservationAdmin, - ParameterAdmin, - RadionuclidesAdmin, - SampleAdmin, - SensorAdmin, - SoilRockResultsAdmin, - StratigraphyAdmin, - SurfaceWaterDataAdmin, - SurfaceWaterPhotosAdmin, - ThingAdmin, - TransducerObservationAdmin, - WaterLevelsContinuousPressureDailyAdmin, - WeatherPhotosAdmin, - WeatherDataAdmin, - FieldParametersAdmin, -) -from db import NMA_FieldParameters -from db.aquifer_system import AquiferSystem -from db.aquifer_type import AquiferType -from db.asset import Asset -from db.contact import Contact -from db.data_provenance import DataProvenance -from db.deployment import Deployment -from db.engine import engine -from db.field import FieldActivity, FieldEvent -from db.geologic_formation import GeologicFormation -from db.group import Group -from db.lexicon import LexiconCategory, LexiconTerm -from db.location import Location -from db.nma_legacy import ( - NMA_AssociatedData, - NMA_Chemistry_SampleInfo, - NMA_MajorChemistry, - NMA_MinorTraceChemistry, - NMA_Radionuclides, - NMA_HydraulicsData, - NMA_Soil_Rock_Results, - NMA_Stratigraphy, - NMA_SurfaceWaterData, - NMA_WaterLevelsContinuous_Pressure_Daily, - NMA_WeatherPhotos, - NMA_SurfaceWaterPhotos, - NMA_WeatherData, -) -from db.notes import Notes -from db.observation import Observation -from db.parameter import Parameter -from db.sample import Sample -from db.sensor import Sensor -from db.thing import Thing -from db.transducer import TransducerObservation -from starlette_admin.contrib.sqla import Admin - - -def create_admin(app): - """ - Create and configure Starlette Admin instance. - - This function sets up the admin interface and mounts it to the FastAPI app - at the /admin route. - - For MS Access users: This replaces the Access database file with a web-based - admin interface. Instead of opening a .accdb file, staff will navigate to - https://your-domain.com/admin in their web browser. - - Args: - app: FastAPI application instance - - Returns: - Admin: Configured Starlette Admin instance - """ - # Create admin instance - admin = Admin( - engine=engine, - title="Ocotillod Admin", - base_url="/admin", - logo_url=None, # TODO: Add NMBGMR logo - auth_provider=NMSampleLocationsAuthProvider(), - middlewares=[], # Add custom middlewares here if needed - ) - - # Register model views - # Assets - admin.add_view(AssetAdmin(Asset)) - - # Aquifer - admin.add_view(AquiferSystemAdmin(AquiferSystem)) - admin.add_view(AquiferTypeAdmin(AquiferType)) - - # Contacts - admin.add_view(ContactAdmin(Contact)) - - # Data provenance - admin.add_view(DataProvenanceAdmin(DataProvenance)) - - # Deployment / Equipment - admin.add_view(DeploymentAdmin(Deployment)) - admin.add_view(SensorAdmin(Sensor)) - - # Field - admin.add_view(FieldActivityAdmin(FieldActivity)) - admin.add_view(FieldEventAdmin(FieldEvent)) - - # Geology - admin.add_view(GeologicFormationAdmin(GeologicFormation)) - - # Geography - admin.add_view(LocationAdmin(Location)) - # Associated data - admin.add_view(AssociatedDataAdmin(NMA_AssociatedData)) - - # Aquifer - admin.add_view(AquiferSystemAdmin(AquiferSystem)) - admin.add_view(AquiferTypeAdmin(AquiferType)) - - # Groups - admin.add_view(GroupAdmin(Group)) - - # Hydraulics - admin.add_view(HydraulicsDataAdmin(NMA_HydraulicsData)) - admin.add_view(MinorTraceChemistryAdmin(NMA_MinorTraceChemistry)) - admin.add_view(RadionuclidesAdmin(NMA_Radionuclides)) - admin.add_view(MajorChemistryAdmin(NMA_MajorChemistry)) - - # Lexicon - admin.add_view(LexiconCategoryAdmin(LexiconCategory)) - admin.add_view(LexiconTermAdmin(LexiconTerm)) - - # Notes - admin.add_view(NotesAdmin(Notes)) - - # Observations - admin.add_view(ObservationAdmin(Observation)) - - # Parameters - admin.add_view(ParameterAdmin(Parameter)) - admin.add_view(FieldParametersAdmin(NMA_FieldParameters)) - - # Samples - admin.add_view(ChemistrySampleInfoAdmin(NMA_Chemistry_SampleInfo)) - admin.add_view(SampleAdmin(Sample)) - admin.add_view(SurfaceWaterDataAdmin(NMA_SurfaceWaterData)) - - # Soil & Stratigraphy - admin.add_view(SoilRockResultsAdmin(NMA_Soil_Rock_Results)) - admin.add_view(StratigraphyAdmin(NMA_Stratigraphy)) - - # Things (Wells, Springs, etc.) - admin.add_view(ThingAdmin(Thing)) - - # Transducer observations - admin.add_view(TransducerObservationAdmin(TransducerObservation)) - - # Water Levels - Continuous (legacy) - admin.add_view( - WaterLevelsContinuousPressureDailyAdmin( - NMA_WaterLevelsContinuous_Pressure_Daily - ) - ) - - # Weather - admin.add_view(WeatherPhotosAdmin(NMA_WeatherPhotos)) - - # Surface Water Photos - admin.add_view(SurfaceWaterPhotosAdmin(NMA_SurfaceWaterPhotos)) - # Weather - admin.add_view(WeatherDataAdmin(NMA_WeatherData)) - - # Future: Add more views here as they are implemented - # admin.add_view(SampleAdmin) - # admin.add_view(GroupAdmin) - - # Mount admin to app - admin.mount_to(app) - - return admin diff --git a/admin/fields.py b/admin/fields.py deleted file mode 100644 index 9da16f9e9..000000000 --- a/admin/fields.py +++ /dev/null @@ -1,141 +0,0 @@ -# =============================================================================== -# Copyright 2025 -# -# 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. -# =============================================================================== -""" -Custom fields for Starlette Admin. - -Provides field handlers for complex data types like PostGIS geometry. -""" - -from typing import Any - -from geoalchemy2 import WKTElement -from geoalchemy2.shape import to_shape -from starlette.requests import Request -from starlette_admin import StringField - -from core.constants import SRID_WGS84 - - -class WKTField(StringField): - """ - Custom field for GeoAlchemy2 Geometry columns. - - This field converts between PostGIS geometry (WKBElement) and human-readable - WKT (Well-Known Text) format for display and editing in the admin interface. - - For MS Access users: Instead of entering Easting/Northing/UTM Zone in separate - fields, you'll enter coordinates in WKT format, for example: - POINT(-106.123 35.456) - - Note: Longitude comes first, then latitude (POINT(lon lat), not POINT(lat lon)) - """ - - async def serialize_value(self, request: Request, value: Any, action: str) -> str: - """ - Convert WKBElement (PostGIS geometry) to WKT string for display in form. - - This is called when rendering the edit/create form to show the current - value in a text input. - - Args: - request: Starlette request object - value: WKBElement from database (PostGIS geometry) - action: 'list', 'detail', 'edit', or 'create' - - Returns: - WKT string representation of geometry (e.g., "POINT(-106.123 35.456)") - """ - if value is None: - return "" - - try: - # Convert WKBElement to Shapely geometry, then to WKT - shape = to_shape(value) - return shape.wkt - except Exception: - # If conversion fails, return string representation - return str(value) - - async def parse_form_data( - self, request: Request, form_data: dict, action: str - ) -> Any: - """ - Convert WKT string from form input to WKTElement for database storage. - - This is called when saving the form to convert the user's input into - a format that can be stored in the PostGIS database. - - Args: - request: Starlette request object - form_data: Dictionary of form data - action: 'edit' or 'create' - - Returns: - WKTElement with SRID for PostGIS storage, or None if empty - - Raises: - ValueError: If WKT string is invalid - """ - wkt_string = form_data.get(self.name) - - if not wkt_string or wkt_string.strip() == "": - return None - - try: - # Parse and validate WKT string - from shapely.wkt import loads as wkt_loads - - shape = wkt_loads(wkt_string.strip()) - - # Convert to WKTElement with SRID (spatial reference identifier) - return WKTElement(shape.wkt, srid=SRID_WGS84) - except Exception as e: - raise ValueError( - f"Invalid WKT geometry: {e}. " - f"Expected format: POINT(longitude latitude), e.g., POINT(-106.123 35.456). " - f"Note: Longitude comes first, then latitude." - ) - - -class CoordinateHelpField(WKTField): - """ - Extended WKT field with detailed help text for coordinate entry. - - This version includes comprehensive help text for users transitioning - from MS Access UTM coordinate entry to WKT format. - """ - - def __init__(self, *args, **kwargs): - # Add detailed help text if not provided - if "help_text" not in kwargs: - kwargs["help_text"] = ( - "Enter coordinates in WKT (Well-Known Text) format.\n\n" - "Format: POINT(longitude latitude)\n" - "Example: POINT(-106.65082 35.08352)\n\n" - "Important:\n" - " * Longitude comes FIRST (negative for western hemisphere)\n" - " * Latitude comes SECOND\n" - " * No comma between values\n" - " * Use decimal degrees (not degrees-minutes-seconds)\n" - " * Coordinate system: WGS84 (SRID 4326)\n\n" - "If you have UTM coordinates:\n" - " 1. Use an online converter (e.g., https://www.latlong.net/utm-to-lat-long)\n" - " 2. Enter your Easting, Northing, and UTM Zone\n" - " 3. Convert to WGS84 lat/lon\n" - " 4. Enter here as POINT(lon lat)" - ) - - super().__init__(*args, **kwargs) diff --git a/admin/views/__init__.py b/admin/views/__init__.py deleted file mode 100644 index c8d0f5ad2..000000000 --- a/admin/views/__init__.py +++ /dev/null @@ -1,97 +0,0 @@ -# =============================================================================== -# Copyright 2025 -# -# 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. -# =============================================================================== -""" -Admin views package for OcotilloAPI. - -Provides MS Access-like interface for CRUD operations on database models. -""" - -from admin.views.aquifer_system import AquiferSystemAdmin -from admin.views.aquifer_type import AquiferTypeAdmin -from admin.views.asset import AssetAdmin -from admin.views.associated_data import AssociatedDataAdmin -from admin.views.chemistry_sampleinfo import ChemistrySampleInfoAdmin -from admin.views.contact import ContactAdmin -from admin.views.data_provenance import DataProvenanceAdmin -from admin.views.deployment import DeploymentAdmin -from admin.views.field import ( - FieldActivityAdmin, - FieldEventAdmin, - FieldEventParticipantAdmin, -) -from admin.views.field_parameters import FieldParametersAdmin -from admin.views.geologic_formation import GeologicFormationAdmin -from admin.views.group import GroupAdmin -from admin.views.hydraulicsdata import HydraulicsDataAdmin -from admin.views.lexicon import LexiconCategoryAdmin, LexiconTermAdmin -from admin.views.location import LocationAdmin -from admin.views.major_chemistry import MajorChemistryAdmin -from admin.views.minor_trace_chemistry import MinorTraceChemistryAdmin -from admin.views.notes import NotesAdmin -from admin.views.observation import ObservationAdmin -from admin.views.parameter import ParameterAdmin -from admin.views.radionuclides import RadionuclidesAdmin -from admin.views.sample import SampleAdmin -from admin.views.sensor import SensorAdmin -from admin.views.soil_rock_results import SoilRockResultsAdmin -from admin.views.stratigraphy import StratigraphyAdmin -from admin.views.surface_water import SurfaceWaterDataAdmin -from admin.views.surface_water_photos import SurfaceWaterPhotosAdmin -from admin.views.thing import ThingAdmin -from admin.views.transducer_observation import TransducerObservationAdmin -from admin.views.waterlevelscontinuous_pressure_daily import ( - WaterLevelsContinuousPressureDailyAdmin, -) -from admin.views.weather_data import WeatherDataAdmin -from admin.views.weather_photos import WeatherPhotosAdmin - -__all__ = [ - "AssetAdmin", - "AssociatedDataAdmin", - "AquiferSystemAdmin", - "AquiferTypeAdmin", - "ChemistrySampleInfoAdmin", - "ContactAdmin", - "DataProvenanceAdmin", - "DeploymentAdmin", - "FieldActivityAdmin", - "FieldEventAdmin", - "FieldEventParticipantAdmin", - "FieldParametersAdmin", - "GeologicFormationAdmin", - "GroupAdmin", - "HydraulicsDataAdmin", - "LexiconCategoryAdmin", - "LexiconTermAdmin", - "LocationAdmin", - "MajorChemistryAdmin", - "MinorTraceChemistryAdmin", - "NotesAdmin", - "ObservationAdmin", - "ParameterAdmin", - "RadionuclidesAdmin", - "SampleAdmin", - "SensorAdmin", - "SoilRockResultsAdmin", - "StratigraphyAdmin", - "SurfaceWaterDataAdmin", - "SurfaceWaterPhotosAdmin", - "ThingAdmin", - "TransducerObservationAdmin", - "WaterLevelsContinuousPressureDailyAdmin", - "WeatherPhotosAdmin", - "WeatherDataAdmin", -] diff --git a/admin/views/aquifer_system.py b/admin/views/aquifer_system.py deleted file mode 100644 index 9b384e098..000000000 --- a/admin/views/aquifer_system.py +++ /dev/null @@ -1,94 +0,0 @@ -# =============================================================================== -# Copyright 2025 -# -# 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. -# =============================================================================== -""" -AquiferSystemAdmin view for OcotilloAPI. -""" - -from admin.fields import WKTField -from admin.views.base import OcotilloModelView - - -class AquiferSystemAdmin(OcotilloModelView): - """ - Admin view for AquiferSystem model. - """ - - # ========== Basic Configuration ========== - - name = "Aquifer Systems" - label = "Aquifer Systems" - icon = "fa fa-globe" - - # ========== List View ========== - - sortable_fields = [ - "id", - "name", - "primary_aquifer_type", - "geographic_scale", - "release_status", - "created_at", - ] - - fields_default_sort = [("name", False)] - - searchable_fields = [ - "name", - "description", - "primary_aquifer_type", - "geographic_scale", - "release_status", - "created_at", - ] - - page_size = 50 - page_size_options = [25, 50, 100, 200] - - # ========== Form View ========== - - fields = [ - "id", - "name", - "description", - "primary_aquifer_type", - "geographic_scale", - WKTField("boundary", label="Boundary (WKT)"), - "release_status", - "created_at", - "created_by_id", - "created_by_name", - "updated_by_id", - "updated_by_name", - ] - - exclude_fields_from_create = [ - "id", - "created_at", - "created_by_id", - "created_by_name", - "updated_by_id", - "updated_by_name", - ] - - exclude_fields_from_edit = [ - "id", - "created_at", - "created_by_id", - "created_by_name", - ] - - -# ============= EOF ============================================= diff --git a/admin/views/aquifer_type.py b/admin/views/aquifer_type.py deleted file mode 100644 index ad319b6d3..000000000 --- a/admin/views/aquifer_type.py +++ /dev/null @@ -1,86 +0,0 @@ -# =============================================================================== -# Copyright 2025 -# -# 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. -# =============================================================================== -""" -AquiferTypeAdmin view for OcotilloAPI. -""" - -from admin.views.base import OcotilloModelView - - -class AquiferTypeAdmin(OcotilloModelView): - """ - Admin view for AquiferType model. - """ - - # ========== Basic Configuration ========== - - name = "Aquifer Types" - label = "Aquifer Types" - icon = "fa fa-tint" - - # ========== List View ========== - - sortable_fields = [ - "id", - "thing_aquifer_association_id", - "aquifer_type", - "release_status", - "created_at", - ] - - fields_default_sort = [("created_at", True)] - - searchable_fields = [ - "aquifer_type", - "release_status", - "created_at", - ] - - page_size = 50 - page_size_options = [25, 50, 100, 200] - - # ========== Form View ========== - - fields = [ - "id", - "thing_aquifer_association_id", - "aquifer_type", - "release_status", - "created_at", - "created_by_id", - "created_by_name", - "updated_by_id", - "updated_by_name", - ] - - exclude_fields_from_create = [ - "id", - "created_at", - "created_by_id", - "created_by_name", - "updated_by_id", - "updated_by_name", - ] - - exclude_fields_from_edit = [ - "id", - "created_at", - "created_by_id", - "created_by_name", - ] - - -# ============= EOF ============================================= diff --git a/admin/views/asset.py b/admin/views/asset.py deleted file mode 100644 index acec3bb80..000000000 --- a/admin/views/asset.py +++ /dev/null @@ -1,100 +0,0 @@ -# =============================================================================== -# Copyright 2025 -# -# 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. -# =============================================================================== -""" -AssetAdmin view for OcotilloAPI. - -Provides MS Access-like interface for CRUD operations on Asset model. -""" - -from admin.views.base import OcotilloModelView - - -class AssetAdmin(OcotilloModelView): - """ - Admin view for Asset model. - """ - - # ========== Basic Configuration ========== - - name = "Assets" - label = "Assets" - icon = "fa fa-file" - - # ========== List View ========== - - sortable_fields = [ - "id", - "name", - "mime_type", - "storage_service", - "size", - "release_status", - "created_at", - ] - - fields_default_sort = [("created_at", True)] - - searchable_fields = [ - "name", - "label", - "mime_type", - "storage_service", - "storage_path", - "uri", - "release_status", - "created_at", - ] - - page_size = 50 - page_size_options = [25, 50, 100, 200] - - # ========== Form View ========== - - fields = [ - "id", - "name", - "label", - "storage_service", - "storage_path", - "mime_type", - "size", - "uri", - "release_status", - "created_at", - "created_by_id", - "created_by_name", - "updated_by_id", - "updated_by_name", - ] - - exclude_fields_from_create = [ - "id", - "created_at", - "created_by_id", - "created_by_name", - "updated_by_id", - "updated_by_name", - ] - - exclude_fields_from_edit = [ - "id", - "created_at", - "created_by_id", - "created_by_name", - ] - - -# ============= EOF ============================================= diff --git a/admin/views/associated_data.py b/admin/views/associated_data.py deleted file mode 100644 index f58dcd628..000000000 --- a/admin/views/associated_data.py +++ /dev/null @@ -1,113 +0,0 @@ -# =============================================================================== -# Copyright 2026 -# -# 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. -# =============================================================================== -""" -AssociatedDataAdmin view for legacy NMA_AssociatedData. - -Updated for Integer PK schema: -- id: Integer PK (autoincrement) -- nma_assoc_id: Legacy UUID PK (AssocID), UNIQUE for audit -- nma_location_id: Legacy LocationId UUID, UNIQUE -- nma_point_id: Legacy PointID string -- nma_object_id: Legacy OBJECTID, UNIQUE -""" - -from starlette.requests import Request - -from admin.views.base import OcotilloModelView - - -class AssociatedDataAdmin(OcotilloModelView): - """ - Admin view for legacy AssociatedData model (NMA_AssociatedData). - Read-only, MS Access-like listing/details. - """ - - # ========== Basic Configuration ========== - name = "NMA Associated Data" - label = "NMA Associated Data" - icon = "fa fa-link" - - # Integer PK - pk_attr = "id" - pk_type = int - - def can_create(self, request: Request) -> bool: - return False - - def can_edit(self, request: Request) -> bool: - return False - - def can_delete(self, request: Request) -> bool: - return False - - # ========== List View ========== - - list_fields = [ - "id", - "nma_assoc_id", - "nma_location_id", - "nma_point_id", - "nma_object_id", - "notes", - "formation", - "thing_id", - ] - - sortable_fields = [ - "id", - "nma_assoc_id", - "nma_object_id", - "nma_point_id", - ] - - fields_default_sort = [("nma_point_id", False), ("nma_object_id", False)] - - searchable_fields = [ - "nma_point_id", - "nma_assoc_id", - "notes", - "formation", - ] - - page_size = 50 - page_size_options = [25, 50, 100, 200] - - # ========== Form View ========== - - fields = [ - "id", - "nma_assoc_id", - "nma_location_id", - "nma_point_id", - "nma_object_id", - "notes", - "formation", - "thing_id", - ] - - field_labels = { - "id": "ID", - "nma_assoc_id": "NMA AssocID (Legacy)", - "nma_location_id": "NMA LocationId (Legacy)", - "nma_point_id": "NMA PointID (Legacy)", - "nma_object_id": "NMA OBJECTID (Legacy)", - "notes": "Notes", - "formation": "Formation", - "thing_id": "Thing ID", - } - - -# ============= EOF ============================================= diff --git a/admin/views/base.py b/admin/views/base.py deleted file mode 100644 index a44f51c53..000000000 --- a/admin/views/base.py +++ /dev/null @@ -1,142 +0,0 @@ -# =============================================================================== -# Copyright 2026 -# -# 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. -# =============================================================================== -from __future__ import annotations - -from typing import Any, Iterable, Sequence - -from sqlalchemy import select, update -from starlette.requests import Request -from starlette.responses import Response -from starlette_admin import ExportType, action -from starlette_admin.contrib.sqla import ModelView - -from db.engine import session_ctx - - -class OcotilloModelView(ModelView): - """ - Shared admin behaviors for Ocotillo data models. - - - RBAC: admin can create/edit/delete; editor can edit; any authenticated user can view. - - Data visibility: non-admin/editor users only see published rows when a release field exists. - - Publish/Unpublish actions: toggle release status when enabled and a release field is present. - """ - - release_field = "release_status" - draft_value = "draft" - published_value = "published" - enable_publish_actions: bool = True - export_types: Sequence[ExportType] = (ExportType.CSV, ExportType.EXCEL) - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - # ========= Permissions (RBAC) ========= - def _get_user(self, request: Request) -> Any: - return getattr(request.state, "user", None) - - def _roles(self, request: Request) -> list[str]: - user = self._get_user(request) - return getattr(user, "roles", []) if user else [] - - def _has_role(self, request: Request, roles: Iterable[str]) -> bool: - return bool(set(self._roles(request)) & set(roles)) - - def can_create(self, request: Request) -> bool: - return self._has_role(request, {"admin"}) - - def can_edit(self, request: Request) -> bool: - return self._has_role(request, {"admin", "editor"}) - - def can_delete(self, request: Request) -> bool: - return self._has_role(request, {"admin"}) - - def can_view_details(self, request: Request) -> bool: - return self._get_user(request) is not None - - # ========= Data Visibility ========= - def get_list_query(self, request: Request): - query = select(self.model) - user = self._get_user(request) - if user is None: - # Return an empty result set for anonymous users - return query.where(self.model.id == -1) - - if not hasattr(self.model, self.release_field): - return query - - if self._has_role(request, {"admin", "editor"}): - return query - return query.where( - getattr(self.model, self.release_field) == self.published_value - ) - - # ========= Actions (Publish / Unpublish) ========= - def _ensure_release_field(self) -> bool: - return self.enable_publish_actions and hasattr(self.model, self.release_field) - - @action( - name="publish_selected", - text="Publish Selected", - confirmation="Are you sure you want to publish the selected records?", - submit_btn_text="Yes, publish", - submit_btn_class="btn btn-success", - ) - async def publish_selected(self, request: Request, pks: list[int]) -> Response: - if not self._has_role(request, {"admin"}): - return Response("Only admins can publish", status_code=403) - if not self._ensure_release_field(): - return Response( - "Publish action not available for this model", status_code=400 - ) - - with session_ctx() as session: - result = session.execute( - update(self.model) - .where(self.model.id.in_(pks)) - .values({self.release_field: self.published_value}) - ) - session.commit() - updated_count = result.rowcount - return Response(f"Published {updated_count} record(s)", status_code=200) - - @action( - name="unpublish_selected", - text="Unpublish Selected (set to draft)", - confirmation="Are you sure you want to unpublish the selected records?", - submit_btn_text="Yes, unpublish", - submit_btn_class="btn btn-warning", - ) - async def unpublish_selected(self, request: Request, pks: list[int]) -> Response: - if not self._has_role(request, {"admin"}): - return Response("Only admins can unpublish", status_code=403) - if not self._ensure_release_field(): - return Response( - "Unpublish action not available for this model", status_code=400 - ) - - with session_ctx() as session: - result = session.execute( - update(self.model) - .where(self.model.id.in_(pks)) - .values({self.release_field: self.draft_value}) - ) - session.commit() - updated_count = result.rowcount - return Response(f"Unpublished {updated_count} record(s)", status_code=200) - - -# ============= EOF ============================================= diff --git a/admin/views/chemistry_sampleinfo.py b/admin/views/chemistry_sampleinfo.py deleted file mode 100644 index b588da038..000000000 --- a/admin/views/chemistry_sampleinfo.py +++ /dev/null @@ -1,175 +0,0 @@ -# =============================================================================== -# Copyright 2026 -# -# 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. -# =============================================================================== -""" -ChemistrySampleInfoAdmin view for legacy Chemistry_SampleInfo. - -Updated for Integer PK schema: -- id: Integer PK (autoincrement) -- nma_sample_pt_id: Legacy UUID PK (SamplePtID), UNIQUE for audit -- nma_wclab_id: Legacy WCLab_ID -- nma_sample_point_id: Legacy SamplePointID -- nma_object_id: Legacy OBJECTID, UNIQUE -- nma_location_id: Legacy LocationId UUID (for audit trail) - -FK Change (2026-01): -- thing_id: Integer FK to Thing.id -""" - -from starlette.requests import Request -from starlette_admin.fields import HasOne - -from admin.views.base import OcotilloModelView - - -class ChemistrySampleInfoAdmin(OcotilloModelView): - """ - Admin view for ChemistrySampleInfo model. - """ - - # ========== Basic Configuration ========== - - name = "NMA Chemistry Sample Info" - label = "NMA Chemistry Sample Info" - icon = "fa fa-flask" - - # Integer PK - pk_attr = "id" - pk_type = int - - def can_create(self, request: Request) -> bool: - return False - - def can_edit(self, request: Request) -> bool: - return False - - def can_delete(self, request: Request) -> bool: - return False - - # ========== List View ========== - - list_fields = [ - "id", - "nma_sample_pt_id", - "nma_wclab_id", - "nma_sample_point_id", - "nma_object_id", - "nma_location_id", - "thing_id", - HasOne("thing", identity="thing"), - "collection_date", - "collection_method", - "collected_by", - "analyses_agency", - "sample_type", - "sample_material_not_h2o", - "water_type", - "study_sample", - "data_source", - "data_quality", - "public_release", - "added_day_to_date", - "added_month_day_to_date", - "sample_notes", - ] - - sortable_fields = [ - "id", - "nma_sample_pt_id", - "nma_wclab_id", - "nma_sample_point_id", - "nma_object_id", - "collection_date", - "sample_type", - "data_source", - "data_quality", - "public_release", - ] - - fields_default_sort = [("collection_date", True)] - - searchable_fields = [ - "nma_sample_pt_id", - "nma_wclab_id", - "nma_sample_point_id", - "collection_date", - "collected_by", - "analyses_agency", - "sample_type", - "sample_material_not_h2o", - "water_type", - "study_sample", - "data_source", - "data_quality", - "public_release", - "sample_notes", - ] - - page_size = 50 - page_size_options = [25, 50, 100, 200] - - # ========== Form View ========== - - fields = [ - "id", - "nma_sample_pt_id", - "nma_wclab_id", - "nma_sample_point_id", - "nma_object_id", - "nma_location_id", - "thing_id", - HasOne("thing", identity="thing"), - "collection_date", - "collection_method", - "collected_by", - "analyses_agency", - "sample_type", - "sample_material_not_h2o", - "water_type", - "study_sample", - "data_source", - "data_quality", - "public_release", - "added_day_to_date", - "added_month_day_to_date", - "sample_notes", - ] - - field_labels = { - "id": "ID", - "nma_sample_pt_id": "NMA SamplePtID (Legacy)", - "nma_wclab_id": "NMA WCLab_ID (Legacy)", - "nma_sample_point_id": "NMA SamplePointID (Legacy)", - "nma_object_id": "NMA OBJECTID (Legacy)", - "nma_location_id": "NMA LocationId (Legacy)", - "thing_id": "Thing ID", - "collection_date": "Collection Date", - "collection_method": "Collection Method", - "collected_by": "Collected By", - "analyses_agency": "Analyses Agency", - "sample_type": "Sample Type", - "sample_material_not_h2o": "Sample Material Not H2O", - "water_type": "Water Type", - "study_sample": "Study Sample", - "data_source": "Data Source", - "data_quality": "Data Quality", - "public_release": "Public Release", - "added_day_to_date": "Added Day to Date", - "added_month_day_to_date": "Added Month/Day to Date", - "sample_notes": "Sample Notes", - } - - -# ============= EOF ============================================= diff --git a/admin/views/contact.py b/admin/views/contact.py deleted file mode 100644 index 36bea8ee4..000000000 --- a/admin/views/contact.py +++ /dev/null @@ -1,129 +0,0 @@ -# =============================================================================== -# Copyright 2025 -# -# 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. -# =============================================================================== -""" -ContactAdmin view for OcotilloAPI. - -Provides MS Access-like interface for CRUD operations on Contact (Owners) model. -""" - -from admin.views.base import OcotilloModelView - - -class ContactAdmin(OcotilloModelView): - """ - Admin view for Contact model (Well Owners/Managers). - - Designed to replicate MS Access "Owners Data Entry Form" and "Owners Datasheet View". - - Permission Model: - - Admin: Can create, edit, delete all contacts - - Editor: Can create and edit, cannot delete - - Viewer: Can only view published contacts (read-only) - """ - - # ========== Basic Configuration ========== - - name = "Contacts" - label = "Contacts (Owners)" - icon = "fa fa-users" - - # ========== List View (MS Access Datasheet View Equivalent) ========== - - sortable_fields = [ - "id", - "name", - "organization", - "role", - "contact_type", - "release_status", - "created_at", - ] - - fields_default_sort = [("name", False)] # Alphabetical by name - - searchable_fields = [ - "name", - "organization", - "role", - "contact_type", - "release_status", - "created_at", - ] - - page_size = 50 - page_size_options = [25, 50, 100, 200] - - # ========== Form View (MS Access Form View Equivalent) ========== - - fields = [ - "id", - # Contact Information - "name", - "organization", - "role", - "contact_type", - # Release Status - "release_status", - # Audit Fields - "created_at", - "created_by_id", - "created_by_name", - "updated_by_id", - "updated_by_name", - # Legacy Migration Fields - "nma_pk_owners", - "nma_pk_waterlevels", - ] - - exclude_fields_from_create = [ - "id", - "created_at", - "created_by_id", - "created_by_name", - "updated_by_id", - "updated_by_name", - "nma_pk_owners", - "nma_pk_waterlevels", - # Exclude complex relationships (manage separately) - "phones", - "emails", - "addresses", - "incomplete_nma_phones", - "permissions", - "author_associations", - "thing_associations", - "field_event_participants", - ] - - exclude_fields_from_edit = [ - "id", - "created_at", - "created_by_id", - "created_by_name", - "nma_pk_owners", - "nma_pk_waterlevels", - # Exclude complex relationships (manage separately) - "phones", - "emails", - "addresses", - "incomplete_nma_phones", - "permissions", - "author_associations", - "thing_associations", - "field_event_participants", - ] - - # ========== Field Labels and Help Text ========== diff --git a/admin/views/data_provenance.py b/admin/views/data_provenance.py deleted file mode 100644 index c1a91551f..000000000 --- a/admin/views/data_provenance.py +++ /dev/null @@ -1,94 +0,0 @@ -# =============================================================================== -# Copyright 2025 -# -# 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. -# =============================================================================== -""" -DataProvenanceAdmin view for OcotilloAPI. -""" - -from admin.views.base import OcotilloModelView - - -class DataProvenanceAdmin(OcotilloModelView): - """ - Admin view for DataProvenance model. - """ - - name = "Data Provenance" - label = "Data Provenance" - icon = "fa fa-history" - - sortable_fields = [ - "id", - "target_table", - "target_id", - "field_name", - "origin_type", - "collection_method", - "release_status", - "created_at", - ] - - fields_default_sort = [("created_at", True)] - - searchable_fields = [ - "target_table", - "field_name", - "origin_source", - "origin_type", - "collection_method", - "accuracy_unit", - "release_status", - "created_at", - ] - - page_size = 50 - page_size_options = [25, 50, 100, 200] - - fields = [ - "id", - "target_table", - "target_id", - "field_name", - "origin_type", - "origin_source", - "collection_method", - "accuracy_value", - "accuracy_unit", - "release_status", - "created_at", - "created_by_id", - "created_by_name", - "updated_by_id", - "updated_by_name", - ] - - exclude_fields_from_create = [ - "id", - "created_at", - "created_by_id", - "created_by_name", - "updated_by_id", - "updated_by_name", - ] - - exclude_fields_from_edit = [ - "id", - "created_at", - "created_by_id", - "created_by_name", - ] - - -# ============= EOF ============================================= diff --git a/admin/views/deployment.py b/admin/views/deployment.py deleted file mode 100644 index ccdf535da..000000000 --- a/admin/views/deployment.py +++ /dev/null @@ -1,138 +0,0 @@ -# =============================================================================== -# Copyright 2025 -# -# 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. -# =============================================================================== -""" -DeploymentAdmin view for OcotilloAPI. - -Provides MS Access-like interface for CRUD operations on Deployment model. -""" - -from admin.views.base import OcotilloModelView - - -class DeploymentAdmin(OcotilloModelView): - """ - Admin view for Deployment model (Equipment Installation Log). - - Designed to replicate MS Access "Equipment Deployment Form" and "Deployment Datasheet View". - - Permission Model: - - Admin: Can create, edit, delete all deployments - - Editor: Can create and edit, cannot delete - - Viewer: Can only view published deployments (read-only) - """ - - # ========== Basic Configuration ========== - - name = "Deployments" - label = "Deployments (Equipment Installations)" - icon = "fa fa-plug" - - # ========== List View (MS Access Datasheet View Equivalent) ========== - - sortable_fields = [ - "id", - "thing_id", - "sensor_id", - "installation_date", - "removal_date", - "recording_interval", - "release_status", - "created_at", - "nma_WI_Duration", - "nma_WI_EndFrequency", - "nma_WI_Magnitude", - "nma_WI_MicGain", - "nma_WI_MinSoundDepth", - "nma_WI_StartFrequency", - ] - - fields_default_sort = [ - ("installation_date", True) - ] # True = descending (newest first) - - searchable_fields = [ - "hanging_point_description", - "notes", - "installation_date", - "removal_date", - "recording_interval_units", - "release_status", - "created_at", - "nma_WI_Duration", - "nma_WI_EndFrequency", - "nma_WI_Magnitude", - "nma_WI_MicGain", - "nma_WI_MinSoundDepth", - "nma_WI_StartFrequency", - ] - - page_size = 50 - page_size_options = [25, 50, 100, 200] - - # ========== Form View (MS Access Form View Equivalent) ========== - - fields = [ - "id", - # Deployment Information - "thing_id", - "sensor_id", - "installation_date", - "removal_date", - "recording_interval", - "recording_interval_units", - "hanging_cable_length", - "hanging_point_height", - "hanging_point_description", - "notes", - "nma_WI_Duration", - "nma_WI_EndFrequency", - "nma_WI_Magnitude", - "nma_WI_MicGain", - "nma_WI_MinSoundDepth", - "nma_WI_StartFrequency", - # Release Status - "release_status", - # Audit Fields - "created_at", - "created_by_id", - "created_by_name", - "updated_by_id", - "updated_by_name", - ] - - exclude_fields_from_create = [ - "id", - "created_at", - "created_by_id", - "created_by_name", - "updated_by_id", - "updated_by_name", - # Exclude relationship objects (use IDs instead) - "thing", - "sensor", - ] - - exclude_fields_from_edit = [ - "id", - "created_at", - "created_by_id", - "created_by_name", - # Exclude relationship objects (use IDs instead) - "thing", - "sensor", - ] - - # ========== Field Labels and Help Text ========== diff --git a/admin/views/field.py b/admin/views/field.py deleted file mode 100644 index 43a7b2cb5..000000000 --- a/admin/views/field.py +++ /dev/null @@ -1,199 +0,0 @@ -# =============================================================================== -# Copyright 2025 -# -# 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. -# =============================================================================== -""" -Field admin views for OcotilloAPI. -""" - -from admin.views.base import OcotilloModelView - - -class FieldEventAdmin(OcotilloModelView): - """ - Admin view for FieldEvent model. - """ - - name = "Field Events" - label = "Field Events" - icon = "fa fa-calendar" - - sortable_fields = [ - "id", - "thing_id", - "event_date", - "release_status", - "created_at", - ] - - fields_default_sort = [("event_date", True)] - - searchable_fields = [ - "notes", - "release_status", - "created_at", - ] - - page_size = 50 - page_size_options = [25, 50, 100, 200] - - fields = [ - "id", - "thing_id", - "event_date", - "notes", - "release_status", - "created_at", - "created_by_id", - "created_by_name", - "updated_by_id", - "updated_by_name", - ] - - exclude_fields_from_create = [ - "id", - "created_at", - "created_by_id", - "created_by_name", - "updated_by_id", - "updated_by_name", - ] - - exclude_fields_from_edit = [ - "id", - "created_at", - "created_by_id", - "created_by_name", - ] - - -class FieldActivityAdmin(OcotilloModelView): - """ - Admin view for FieldActivity model. - """ - - name = "Field Activities" - label = "Field Activities" - icon = "fa fa-tasks" - - sortable_fields = [ - "id", - "field_event_id", - "activity_type", - "release_status", - "created_at", - ] - - fields_default_sort = [("created_at", True)] - - searchable_fields = [ - "notes", - "activity_type", - "release_status", - "created_at", - ] - - page_size = 50 - page_size_options = [25, 50, 100, 200] - - fields = [ - "id", - "field_event_id", - "activity_type", - "notes", - "release_status", - "created_at", - "created_by_id", - "created_by_name", - "updated_by_id", - "updated_by_name", - ] - - exclude_fields_from_create = [ - "id", - "created_at", - "created_by_id", - "created_by_name", - "updated_by_id", - "updated_by_name", - ] - - exclude_fields_from_edit = [ - "id", - "created_at", - "created_by_id", - "created_by_name", - ] - - -class FieldEventParticipantAdmin(OcotilloModelView): - """ - Admin view for FieldEventParticipant model. - """ - - name = "Field Event Participants" - label = "Field Event Participants" - icon = "fa fa-users" - - sortable_fields = [ - "id", - "field_event_id", - "contact_id", - "participant_role", - "release_status", - "created_at", - ] - - fields_default_sort = [("created_at", True)] - - searchable_fields = [ - "participant_role", - "release_status", - "created_at", - ] - - page_size = 50 - page_size_options = [25, 50, 100, 200] - - fields = [ - "id", - "field_event_id", - "contact_id", - "participant_role", - "release_status", - "created_at", - "created_by_id", - "created_by_name", - "updated_by_id", - "updated_by_name", - ] - - exclude_fields_from_create = [ - "id", - "created_at", - "created_by_id", - "created_by_name", - "updated_by_id", - "updated_by_name", - ] - - exclude_fields_from_edit = [ - "id", - "created_at", - "created_by_id", - "created_by_name", - ] - - -# ============= EOF ============================================= diff --git a/admin/views/field_parameters.py b/admin/views/field_parameters.py deleted file mode 100644 index 5638370cc..000000000 --- a/admin/views/field_parameters.py +++ /dev/null @@ -1,139 +0,0 @@ -# =============================================================================== -# Copyright 2026 -# -# 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. -# =============================================================================== -""" -FieldParametersAdmin view for legacy NMA_FieldParameters. - -Updated for Integer PK schema: -- id: Integer PK (autoincrement) -- nma_global_id: Legacy UUID PK (GlobalID), UNIQUE for audit -- chemistry_sample_info_id: Integer FK to NMA_Chemistry_SampleInfo.id -- nma_sample_pt_id: Legacy UUID FK (SamplePtID) for audit -- nma_sample_point_id: Legacy SamplePointID string -- nma_object_id: Legacy OBJECTID -- nma_wclab_id: Legacy WCLab_ID -""" - -from starlette.requests import Request - -from admin.views.base import OcotilloModelView - - -class FieldParametersAdmin(OcotilloModelView): - """ - Admin view for FieldParameters model. - """ - - # ========== Basic Configuration ========== - - name = "NMA Field Parameters" - label = "NMA Field Parameters" - icon = "fa fa-tachometer" - - # Integer PK - pk_attr = "id" - pk_type = int - - def can_create(self, request: Request) -> bool: - return False - - def can_edit(self, request: Request) -> bool: - return False - - def can_delete(self, request: Request) -> bool: - return False - - # ========== List View ========== - - list_fields = [ - "id", - "nma_global_id", - "chemistry_sample_info_id", - "nma_sample_pt_id", - "nma_sample_point_id", - "field_parameter", - "sample_value", - "units", - "notes", - "analyses_agency", - "nma_wclab_id", - "nma_object_id", - ] - - sortable_fields = [ - "id", - "nma_global_id", - "chemistry_sample_info_id", - "nma_sample_pt_id", - "nma_sample_point_id", - "field_parameter", - "sample_value", - "units", - "notes", - "analyses_agency", - "nma_wclab_id", - "nma_object_id", - ] - - fields_default_sort = [("nma_sample_point_id", True)] - - searchable_fields = [ - "nma_global_id", - "nma_sample_pt_id", - "nma_sample_point_id", - "field_parameter", - "units", - "notes", - "analyses_agency", - "nma_wclab_id", - ] - - page_size = 50 - page_size_options = [25, 50, 100, 200] - - # ========== Form View ========== - - fields = [ - "id", - "nma_global_id", - "chemistry_sample_info_id", - "nma_sample_pt_id", - "nma_sample_point_id", - "field_parameter", - "sample_value", - "units", - "notes", - "nma_object_id", - "analyses_agency", - "nma_wclab_id", - ] - - field_labels = { - "id": "ID", - "nma_global_id": "NMA GlobalID (Legacy)", - "chemistry_sample_info_id": "Chemistry Sample Info ID", - "nma_sample_pt_id": "NMA SamplePtID (Legacy)", - "nma_sample_point_id": "NMA SamplePointID (Legacy)", - "field_parameter": "FieldParameter", - "sample_value": "SampleValue", - "units": "Units", - "notes": "Notes", - "nma_object_id": "NMA OBJECTID (Legacy)", - "analyses_agency": "AnalysesAgency", - "nma_wclab_id": "NMA WCLab_ID (Legacy)", - } - - -# ============= EOF ============================================= diff --git a/admin/views/geologic_formation.py b/admin/views/geologic_formation.py deleted file mode 100644 index bb6212026..000000000 --- a/admin/views/geologic_formation.py +++ /dev/null @@ -1,85 +0,0 @@ -# =============================================================================== -# Copyright 2025 -# -# 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. -# =============================================================================== -""" -GeologicFormationAdmin view for OcotilloAPI. -""" - -from admin.fields import WKTField -from admin.views.base import OcotilloModelView - - -class GeologicFormationAdmin(OcotilloModelView): - """ - Admin view for GeologicFormation model. - """ - - name = "Geologic Formations" - label = "Geologic Formations" - icon = "fa fa-layer-group" - - sortable_fields = [ - "id", - "formation_code", - "lithology", - "release_status", - "created_at", - ] - - fields_default_sort = [("formation_code", False)] - - searchable_fields = [ - "formation_code", - "description", - "lithology", - "release_status", - "created_at", - ] - - page_size = 50 - page_size_options = [25, 50, 100, 200] - - fields = [ - "id", - "formation_code", - "description", - "lithology", - WKTField("boundary", label="Boundary (WKT)"), - "release_status", - "created_at", - "created_by_id", - "created_by_name", - "updated_by_id", - "updated_by_name", - ] - - exclude_fields_from_create = [ - "id", - "created_at", - "created_by_id", - "created_by_name", - "updated_by_id", - "updated_by_name", - ] - - exclude_fields_from_edit = [ - "id", - "created_at", - "created_by_id", - "created_by_name", - ] - - -# ============= EOF ============================================= diff --git a/admin/views/group.py b/admin/views/group.py deleted file mode 100644 index f06a9ab76..000000000 --- a/admin/views/group.py +++ /dev/null @@ -1,93 +0,0 @@ -# =============================================================================== -# Copyright 2025 -# -# 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. -# =============================================================================== -""" -GroupAdmin view for OcotilloAPI. -""" - -from admin.fields import WKTField -from admin.views.base import OcotilloModelView - - -class GroupAdmin(OcotilloModelView): - """ - Admin view for Group model. - """ - - # ========== Basic Configuration ========== - - name = "Groups" - label = "Groups" - icon = "fa fa-object-group" - - # ========== List View ========== - - sortable_fields = [ - "id", - "name", - "group_type", - "parent_group_id", - "release_status", - "created_at", - ] - - fields_default_sort = [("name", False)] - - searchable_fields = [ - "name", - "description", - "group_type", - "release_status", - "created_at", - ] - - page_size = 50 - page_size_options = [25, 50, 100, 200] - - # ========== Form View ========== - - fields = [ - "id", - "name", - "description", - "group_type", - "parent_group_id", - WKTField("project_area", label="Project Area (WKT)"), - "release_status", - "created_at", - "created_by_id", - "created_by_name", - "updated_by_id", - "updated_by_name", - ] - - exclude_fields_from_create = [ - "id", - "created_at", - "created_by_id", - "created_by_name", - "updated_by_id", - "updated_by_name", - ] - - exclude_fields_from_edit = [ - "id", - "created_at", - "created_by_id", - "created_by_name", - ] - - -# ============= EOF ============================================= diff --git a/admin/views/hydraulicsdata.py b/admin/views/hydraulicsdata.py deleted file mode 100644 index 9723cbb38..000000000 --- a/admin/views/hydraulicsdata.py +++ /dev/null @@ -1,149 +0,0 @@ -# =============================================================================== -# Copyright 2026 -# -# 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. -# =============================================================================== -""" -HydraulicsDataAdmin view for legacy NMA_HydraulicsData. - -Updated for Integer PK schema: -- id: Integer PK (autoincrement) -- nma_global_id: Legacy UUID PK (GlobalID), UNIQUE for audit -- nma_well_id: Legacy WellID UUID -- nma_point_id: Legacy PointID string -- nma_object_id: Legacy OBJECTID, UNIQUE -""" - -from admin.views.base import OcotilloModelView - - -class HydraulicsDataAdmin(OcotilloModelView): - """ - Admin view for NMA_HydraulicsData model. - """ - - # ========== Basic Configuration ========== - - name = "Hydraulics Data" - label = "Hydraulics Data" - icon = "fa fa-tint" - - # Integer PK - pk_attr = "id" - pk_type = int - - can_create = False - can_edit = False - can_delete = False - - # ========== List View ========== - - list_fields = [ - "id", - "nma_global_id", - "nma_well_id", - "nma_point_id", - "thing_id", - "hydraulic_unit", - "hydraulic_unit_type", - "test_top", - "test_bottom", - "t_ft2_d", - "k_darcy", - "data_source", - "nma_object_id", - ] - - sortable_fields = [ - "id", - "nma_global_id", - "nma_well_id", - "nma_point_id", - "thing_id", - "hydraulic_unit", - "hydraulic_unit_type", - "test_top", - "test_bottom", - "t_ft2_d", - "k_darcy", - "data_source", - "nma_object_id", - ] - - searchable_fields = [ - "nma_global_id", - "nma_point_id", - "hydraulic_unit", - "hydraulic_remarks", - "data_source", - ] - - page_size = 50 - page_size_options = [25, 50, 100, 200] - - # ========== Form View ========== - - fields = [ - "id", - "nma_global_id", - "nma_well_id", - "nma_point_id", - "thing_id", - "hydraulic_unit", - "hydraulic_unit_type", - "hydraulic_remarks", - "test_top", - "test_bottom", - "t_ft2_d", - "s_dimensionless", - "ss_ft_1", - "sy_decimalfractn", - "kh_ft_d", - "kv_ft_d", - "hl_day_1", - "hd_ft2_d", - "cs_gal_d_ft", - "p_decimal_fraction", - "k_darcy", - "data_source", - "nma_object_id", - ] - - field_labels = { - "id": "ID", - "nma_global_id": "NMA GlobalID (Legacy)", - "nma_well_id": "NMA WellID (Legacy)", - "nma_point_id": "NMA PointID (Legacy)", - "thing_id": "Thing ID", - "hydraulic_unit": "HydraulicUnit", - "hydraulic_unit_type": "HydraulicUnitType", - "hydraulic_remarks": "Hydraulic Remarks", - "test_top": "TestTop", - "test_bottom": "TestBottom", - "t_ft2_d": "T (ft2/d)", - "s_dimensionless": "S (dimensionless)", - "ss_ft_1": "Ss (ft-1)", - "sy_decimalfractn": "Sy (decimalfractn)", - "kh_ft_d": "KH (ft/d)", - "kv_ft_d": "KV (ft/d)", - "hl_day_1": "HL (day-1)", - "hd_ft2_d": "HD (ft2/d)", - "cs_gal_d_ft": "Cs (gal/d/ft)", - "p_decimal_fraction": "P (decimal fraction)", - "k_darcy": "k (darcy)", - "data_source": "Data Source", - "nma_object_id": "NMA OBJECTID (Legacy)", - } - - -# ============= EOF ============================================= diff --git a/admin/views/lexicon.py b/admin/views/lexicon.py deleted file mode 100644 index 57cafa6a5..000000000 --- a/admin/views/lexicon.py +++ /dev/null @@ -1,97 +0,0 @@ -# =============================================================================== -# Copyright 2025 -# -# 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. -# =============================================================================== -""" -Lexicon admin views for OcotilloAPI. -""" - -from admin.views.base import OcotilloModelView - - -class LexiconTermAdmin(OcotilloModelView): - """ - Admin view for LexiconTerm model. - """ - - name = "Lexicon Terms" - label = "Lexicon Terms" - icon = "fa fa-book" - enable_publish_actions = False - - sortable_fields = [ - "id", - "term", - ] - - fields_default_sort = [("term", False)] - - searchable_fields = [ - "term", - "definition", - ] - - fields = [ - "id", - "term", - "definition", - ] - - exclude_fields_from_create = [ - "id", - ] - - exclude_fields_from_edit = [ - "id", - ] - - -class LexiconCategoryAdmin(OcotilloModelView): - """ - Admin view for LexiconCategory model. - """ - - name = "Lexicon Categories" - label = "Lexicon Categories" - icon = "fa fa-tags" - enable_publish_actions = False - - sortable_fields = [ - "id", - "name", - ] - - fields_default_sort = [("name", False)] - - searchable_fields = [ - "name", - "description", - ] - - fields = [ - "id", - "name", - "description", - ] - - exclude_fields_from_create = [ - "id", - ] - - exclude_fields_from_edit = [ - "id", - ] - - -# ============= EOF ============================================= diff --git a/admin/views/location.py b/admin/views/location.py deleted file mode 100644 index 2ec2f2616..000000000 --- a/admin/views/location.py +++ /dev/null @@ -1,122 +0,0 @@ -# =============================================================================== -# Copyright 2025 -# -# 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. -# =============================================================================== -""" -LocationAdmin view for OcotilloAPI. - -Provides MS Access-like interface for CRUD operations on Location model. -""" - -from admin.fields import CoordinateHelpField -from admin.views.base import OcotilloModelView - - -class LocationAdmin(OcotilloModelView): - """ - Admin view for Location model. - - Designed to replicate MS Access "Location Entry Form" and "Location Datasheet View". - - Permission Model: - - Admin: Can create, edit, delete all locations - - Editor: Can create and edit, cannot delete - - Viewer: Can only view published locations (read-only) - """ - - # ========== Basic Configuration ========== - - name = "Locations" - label = "Locations" - icon = "fa fa-map-marker" - - # ========== List View (MS Access Datasheet View Equivalent) ========== - - sortable_fields = [ - "id", - "description", - "elevation", - "county", - "state", - "quad_name", - "release_status", - "created_at", - ] - - fields_default_sort = [("created_at", True)] # True = descending - - searchable_fields = [ - "description", - "county", - "state", - "quad_name", - "release_status", - "elevation", - "created_at", - ] - - page_size = 50 - page_size_options = [25, 50, 100, 200] - - # ========== Form View (MS Access Form View Equivalent) ========== - - fields = [ - "id", - "description", - CoordinateHelpField( - "point", - label="Coordinates (WKT)", - required=True, - ), - "elevation", - "county", - "state", - "quad_name", - "nma_location_notes", - "nma_coordinate_notes", - "nma_data_reliability", - "release_status", - "created_at", - "created_by_id", - "created_by_name", - "updated_by_id", - "updated_by_name", - "nma_pk_location", - "nma_date_created", - "nma_site_date", - ] - - exclude_fields_from_create = [ - "id", - "created_at", - "created_by_id", - "created_by_name", - "updated_by_id", - "updated_by_name", - "nma_pk_location", - "nma_date_created", - "nma_site_date", - ] - - exclude_fields_from_edit = [ - "id", - "created_at", - "created_by_id", - "created_by_name", - "nma_pk_location", - "nma_date_created", - "nma_site_date", - ] - - # ========== Field Labels and Help Text ========== diff --git a/admin/views/major_chemistry.py b/admin/views/major_chemistry.py deleted file mode 100644 index 9578f60d1..000000000 --- a/admin/views/major_chemistry.py +++ /dev/null @@ -1,169 +0,0 @@ -# =============================================================================== -# Copyright 2026 -# -# 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. -# =============================================================================== -""" -MajorChemistryAdmin view for legacy NMA_MajorChemistry. - -Updated for Integer PK schema: -- id: Integer PK (autoincrement) -- nma_global_id: Legacy UUID PK (GlobalID), UNIQUE for audit -- chemistry_sample_info_id: Integer FK to NMA_Chemistry_SampleInfo.id -- nma_sample_pt_id: Legacy UUID FK (SamplePtID) for audit -- nma_sample_point_id: Legacy SamplePointID string -- nma_object_id: Legacy OBJECTID -- nma_wclab_id: Legacy WCLab_ID -""" - -from starlette.requests import Request -from starlette_admin.fields import HasOne - -from admin.views.base import OcotilloModelView - - -class MajorChemistryAdmin(OcotilloModelView): - """ - Admin view for NMA_MajorChemistry model. - """ - - # ========== Basic Configuration ========== - - identity = "n-m-a_-major-chemistry" - name = "NMA Major Chemistry" - label = "NMA Major Chemistry" - icon = "fa fa-flask" - - # Integer PK - pk_attr = "id" - pk_type = int - - def can_create(self, request: Request) -> bool: - return False - - def can_edit(self, request: Request) -> bool: - return False - - def can_delete(self, request: Request) -> bool: - return False - - # ========== List View ========== - - list_fields = [ - "id", - "nma_global_id", - "chemistry_sample_info_id", - "nma_sample_pt_id", - "nma_sample_point_id", - HasOne("chemistry_sample_info", identity="n-m-a_-chemistry_-sample-info"), - "analyte", - "symbol", - "sample_value", - "units", - "uncertainty", - "analysis_method", - "analysis_date", - "notes", - "volume", - "volume_unit", - "nma_object_id", - "analyses_agency", - "nma_wclab_id", - ] - - sortable_fields = [ - "id", - "nma_global_id", - "chemistry_sample_info_id", - "nma_sample_pt_id", - "nma_sample_point_id", - "analyte", - "symbol", - "sample_value", - "units", - "uncertainty", - "analysis_method", - "analysis_date", - "notes", - "volume", - "volume_unit", - "nma_object_id", - "analyses_agency", - "nma_wclab_id", - ] - - fields_default_sort = [("analysis_date", True)] - - searchable_fields = [ - "nma_global_id", - "nma_sample_pt_id", - "nma_sample_point_id", - "analyte", - "symbol", - "analysis_method", - "notes", - "analyses_agency", - "nma_wclab_id", - ] - - page_size = 50 - page_size_options = [25, 50, 100, 200] - - # ========== Form View ========== - - fields = [ - "id", - "nma_global_id", - "chemistry_sample_info_id", - "nma_sample_pt_id", - "nma_sample_point_id", - HasOne("chemistry_sample_info", identity="n-m-a_-chemistry_-sample-info"), - "analyte", - "symbol", - "sample_value", - "units", - "uncertainty", - "analysis_method", - "analysis_date", - "notes", - "volume", - "volume_unit", - "nma_object_id", - "analyses_agency", - "nma_wclab_id", - ] - - field_labels = { - "id": "ID", - "nma_global_id": "NMA GlobalID (Legacy)", - "chemistry_sample_info_id": "Chemistry Sample Info ID", - "nma_sample_pt_id": "NMA SamplePtID (Legacy)", - "nma_sample_point_id": "NMA SamplePointID (Legacy)", - "chemistry_sample_info": "Chemistry Sample Info", - "analyte": "Analyte", - "symbol": "Symbol", - "sample_value": "Sample Value", - "units": "Units", - "uncertainty": "Uncertainty", - "analysis_method": "Analysis Method", - "analysis_date": "Analysis Date", - "notes": "Notes", - "volume": "Volume", - "volume_unit": "Volume Unit", - "nma_object_id": "NMA OBJECTID (Legacy)", - "analyses_agency": "Analyses Agency", - "nma_wclab_id": "NMA WCLab_ID (Legacy)", - } - - -# ============= EOF ============================================= diff --git a/admin/views/minor_trace_chemistry.py b/admin/views/minor_trace_chemistry.py deleted file mode 100644 index 0c51e609e..000000000 --- a/admin/views/minor_trace_chemistry.py +++ /dev/null @@ -1,138 +0,0 @@ -# =============================================================================== -# Copyright 2026 -# -# 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. -# =============================================================================== -""" -MinorTraceChemistryAdmin view for legacy NMA_MinorTraceChemistry. - -Updated for Integer PK schema: -- id: Integer PK (autoincrement) -- nma_global_id: Legacy UUID PK (GlobalID), UNIQUE for audit -- chemistry_sample_info_id: Integer FK to NMA_Chemistry_SampleInfo.id -- nma_chemistry_sample_info_uuid: Legacy UUID FK for audit -""" - -from starlette.requests import Request -from starlette_admin.fields import HasOne - -from admin.views.base import OcotilloModelView - - -class MinorTraceChemistryAdmin(OcotilloModelView): - """ - Admin view for NMA_MinorTraceChemistry model. - """ - - # ========== Basic Configuration ========== - - identity = "n-m-a_-minor-trace-chemistry" - name = "Minor Trace Chemistry" - label = "Minor Trace Chemistry" - icon = "fa fa-flask" - - # Integer PK - pk_attr = "id" - pk_type = int - - def can_create(self, request: Request) -> bool: - return False - - def can_edit(self, request: Request) -> bool: - return False - - def can_delete(self, request: Request) -> bool: - return False - - # ========== List View ========== - - list_fields = [ - "id", - "nma_global_id", - HasOne("chemistry_sample_info", identity="n-m-a_-chemistry_-sample-info"), - "nma_chemistry_sample_info_uuid", - "analyte", - "sample_value", - "units", - "symbol", - "analysis_date", - "analyses_agency", - ] - - sortable_fields = [ - "id", - "nma_global_id", - "chemistry_sample_info_id", - "analyte", - "sample_value", - "units", - "symbol", - "analysis_date", - "analyses_agency", - ] - - fields_default_sort = [("analysis_date", True)] - - searchable_fields = [ - "nma_global_id", - "analyte", - "symbol", - "analysis_method", - "notes", - "analyses_agency", - ] - - page_size = 50 - page_size_options = [25, 50, 100, 200] - - # ========== Form View ========== - - fields = [ - "id", - "nma_global_id", - HasOne("chemistry_sample_info", identity="n-m-a_-chemistry_-sample-info"), - "nma_chemistry_sample_info_uuid", - "analyte", - "symbol", - "sample_value", - "units", - "uncertainty", - "analysis_method", - "analysis_date", - "notes", - "volume", - "volume_unit", - "analyses_agency", - ] - - field_labels = { - "id": "ID", - "nma_global_id": "NMA GlobalID (Legacy)", - "chemistry_sample_info": "Chemistry Sample Info", - "chemistry_sample_info_id": "Chemistry Sample Info ID", - "nma_chemistry_sample_info_uuid": "NMA Chemistry Sample Info UUID (Legacy)", - "analyte": "Analyte", - "symbol": "Symbol", - "sample_value": "Sample Value", - "units": "Units", - "uncertainty": "Uncertainty", - "analysis_method": "Analysis Method", - "analysis_date": "Analysis Date", - "notes": "Notes", - "volume": "Volume", - "volume_unit": "Volume Unit", - "analyses_agency": "Analyses Agency", - } - - -# ============= EOF ============================================= diff --git a/admin/views/notes.py b/admin/views/notes.py deleted file mode 100644 index 6be42f912..000000000 --- a/admin/views/notes.py +++ /dev/null @@ -1,91 +0,0 @@ -# =============================================================================== -# Copyright 2025 -# -# 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. -# =============================================================================== -""" -NotesAdmin view for OcotilloAPI. -""" - -from admin.views.base import OcotilloModelView - - -class NotesAdmin(OcotilloModelView): - """ - Admin view for Notes model. - """ - - # ========== Basic Configuration ========== - - name = "Notes" - label = "Notes" - icon = "fa fa-sticky-note" - - # ========== List View ========== - - sortable_fields = [ - "id", - "target_table", - "target_id", - "note_type", - "release_status", - "created_at", - ] - - fields_default_sort = [("created_at", True)] - - searchable_fields = [ - "target_table", - "note_type", - "content", - "release_status", - "created_at", - ] - - page_size = 50 - page_size_options = [25, 50, 100, 200] - - # ========== Form View ========== - - fields = [ - "id", - "target_table", - "target_id", - "note_type", - "content", - "release_status", - "created_at", - "created_by_id", - "created_by_name", - "updated_by_id", - "updated_by_name", - ] - - exclude_fields_from_create = [ - "id", - "created_at", - "created_by_id", - "created_by_name", - "updated_by_id", - "updated_by_name", - ] - - exclude_fields_from_edit = [ - "id", - "created_at", - "created_by_id", - "created_by_name", - ] - - -# ============= EOF ============================================= diff --git a/admin/views/observation.py b/admin/views/observation.py deleted file mode 100644 index d2e206e36..000000000 --- a/admin/views/observation.py +++ /dev/null @@ -1,128 +0,0 @@ -# =============================================================================== -# Copyright 2025 -# -# 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. -# =============================================================================== -""" -ObservationAdmin view for OcotilloAPI. - -Provides MS Access-like interface for CRUD operations on Observation (Water Levels) model. -""" - -from admin.views.base import OcotilloModelView - - -class ObservationAdmin(OcotilloModelView): - """ - Admin view for Observation model (Water Levels). - - Designed to replicate MS Access "Water Level Entry Form" and "Water Level Datasheet View". - - Permission Model: - - Admin: Can create, edit, delete all observations - - Editor: Can create and edit, cannot delete - - Viewer: Can only view published observations (read-only) - """ - - # ========== Basic Configuration ========== - - name = "Observations" - label = "Observations (Water Levels)" - icon = "fa fa-line-chart" - - # ========== List View (MS Access Datasheet View Equivalent) ========== - - sortable_fields = [ - "id", - "observation_datetime", - "value", - "unit", - "measuring_point_height", - "release_status", - "created_at", - ] - - fields_default_sort = [ - ("observation_datetime", True) - ] # True = descending (newest first) - - searchable_fields = [ - "groundwater_level_reason", - "notes", - "observation_datetime", - "unit", - "groundwater_level_reason", - "release_status", - "created_at", - ] - - page_size = 50 - page_size_options = [25, 50, 100, 200, 500] - - # ========== Form View (MS Access Form View Equivalent) ========== - - fields = [ - "id", - # Core measurement data - "observation_datetime", - "value", - "unit", - "measuring_point_height", - "groundwater_level_reason", - "notes", - # Relationships (display as selects) - "sample_id", - "sensor_id", - "parameter_id", - "analysis_method_id", - # Release Status - "release_status", - # Audit Fields - "created_at", - "created_by_id", - "created_by_name", - "updated_by_id", - "updated_by_name", - # Legacy Migration Fields - "nma_pk_waterlevels", - ] - - exclude_fields_from_create = [ - "id", - "created_at", - "created_by_id", - "created_by_name", - "updated_by_id", - "updated_by_name", - "nma_pk_waterlevels", - # Exclude relationship objects (use IDs instead) - "sample", - "sensor", - "parameter", - "analysis_method", - ] - - exclude_fields_from_edit = [ - "id", - "created_at", - "created_by_id", - "created_by_name", - "nma_pk_waterlevels", - # Exclude relationship objects (use IDs instead) - "sample", - "sensor", - "parameter", - "analysis_method", - ] - - # ========== Field Labels and Help Text ========== diff --git a/admin/views/parameter.py b/admin/views/parameter.py deleted file mode 100644 index 50eb674a8..000000000 --- a/admin/views/parameter.py +++ /dev/null @@ -1,90 +0,0 @@ -# =============================================================================== -# Copyright 2025 -# -# 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. -# =============================================================================== -""" -ParameterAdmin view for OcotilloAPI. -""" - -from admin.views.base import OcotilloModelView - - -class ParameterAdmin(OcotilloModelView): - """ - Admin view for Parameter model. - """ - - name = "Parameters" - label = "Parameters" - icon = "fa fa-flask" - - sortable_fields = [ - "id", - "parameter_name", - "matrix", - "parameter_type", - "cas_number", - "default_unit", - "release_status", - "created_at", - ] - - fields_default_sort = [("parameter_name", False)] - - searchable_fields = [ - "parameter_name", - "cas_number", - "matrix", - "parameter_type", - "default_unit", - "release_status", - "created_at", - ] - - page_size = 50 - page_size_options = [25, 50, 100, 200] - - fields = [ - "id", - "parameter_name", - "matrix", - "parameter_type", - "cas_number", - "default_unit", - "release_status", - "created_at", - "created_by_id", - "created_by_name", - "updated_by_id", - "updated_by_name", - ] - - exclude_fields_from_create = [ - "id", - "created_at", - "created_by_id", - "created_by_name", - "updated_by_id", - "updated_by_name", - ] - - exclude_fields_from_edit = [ - "id", - "created_at", - "created_by_id", - "created_by_name", - ] - - -# ============= EOF ============================================= diff --git a/admin/views/radionuclides.py b/admin/views/radionuclides.py deleted file mode 100644 index 27c240aea..000000000 --- a/admin/views/radionuclides.py +++ /dev/null @@ -1,165 +0,0 @@ -# =============================================================================== -# Copyright 2026 -# -# 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. -# =============================================================================== -""" -RadionuclidesAdmin view for legacy NMA_Radionuclides. - -Updated for Integer PK schema: -- id: Integer PK (autoincrement) -- nma_global_id: Legacy UUID PK (GlobalID), UNIQUE for audit -- chemistry_sample_info_id: Integer FK to NMA_Chemistry_SampleInfo.id -- nma_sample_pt_id: Legacy UUID FK (SamplePtID) for audit -- nma_sample_point_id: Legacy SamplePointID string -- nma_object_id: Legacy OBJECTID, UNIQUE -- nma_wclab_id: Legacy WCLab_ID -""" - -from starlette.requests import Request - -from admin.views.base import OcotilloModelView - - -class RadionuclidesAdmin(OcotilloModelView): - """ - Admin view for NMA_Radionuclides model. - """ - - # ========== Basic Configuration ========== - - name = "NMA Radionuclides" - label = "NMA Radionuclides" - icon = "fa fa-radiation" - - # Integer PK - pk_attr = "id" - pk_type = int - - def can_create(self, request: Request) -> bool: - return False - - def can_edit(self, request: Request) -> bool: - return False - - def can_delete(self, request: Request) -> bool: - return False - - # ========== List View ========== - - list_fields = [ - "id", - "nma_global_id", - "chemistry_sample_info_id", - "nma_sample_pt_id", - "nma_sample_point_id", - "analyte", - "symbol", - "sample_value", - "units", - "uncertainty", - "analysis_method", - "analysis_date", - "notes", - "volume", - "volume_unit", - "nma_object_id", - "analyses_agency", - "nma_wclab_id", - ] - - sortable_fields = [ - "id", - "nma_global_id", - "chemistry_sample_info_id", - "nma_sample_pt_id", - "nma_sample_point_id", - "analyte", - "symbol", - "sample_value", - "units", - "uncertainty", - "analysis_method", - "analysis_date", - "notes", - "volume", - "volume_unit", - "nma_object_id", - "analyses_agency", - "nma_wclab_id", - ] - - fields_default_sort = [("analysis_date", True)] - - searchable_fields = [ - "nma_global_id", - "nma_sample_pt_id", - "nma_sample_point_id", - "analyte", - "symbol", - "analysis_method", - "analysis_date", - "notes", - "analyses_agency", - "nma_wclab_id", - ] - - page_size = 50 - page_size_options = [25, 50, 100, 200] - - # ========== Form View ========== - - fields = [ - "id", - "nma_global_id", - "chemistry_sample_info_id", - "nma_sample_pt_id", - "nma_sample_point_id", - "analyte", - "symbol", - "sample_value", - "units", - "uncertainty", - "analysis_method", - "analysis_date", - "notes", - "volume", - "volume_unit", - "nma_object_id", - "analyses_agency", - "nma_wclab_id", - ] - - field_labels = { - "id": "ID", - "nma_global_id": "NMA GlobalID (Legacy)", - "chemistry_sample_info_id": "Chemistry Sample Info ID", - "nma_sample_pt_id": "NMA SamplePtID (Legacy)", - "nma_sample_point_id": "NMA SamplePointID (Legacy)", - "analyte": "Analyte", - "symbol": "Symbol", - "sample_value": "Sample Value", - "units": "Units", - "uncertainty": "Uncertainty", - "analysis_method": "Analysis Method", - "analysis_date": "Analysis Date", - "notes": "Notes", - "volume": "Volume", - "volume_unit": "Volume Unit", - "nma_object_id": "NMA OBJECTID (Legacy)", - "analyses_agency": "Analyses Agency", - "nma_wclab_id": "NMA WCLab_ID (Legacy)", - } - - -# ============= EOF ============================================= diff --git a/admin/views/sample.py b/admin/views/sample.py deleted file mode 100644 index b5247a913..000000000 --- a/admin/views/sample.py +++ /dev/null @@ -1,103 +0,0 @@ -# =============================================================================== -# Copyright 2025 -# -# 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. -# =============================================================================== -""" -SampleAdmin view for OcotilloAPI. -""" - -from admin.views.base import OcotilloModelView - - -class SampleAdmin(OcotilloModelView): - """ - Admin view for Sample model. - """ - - # ========== Basic Configuration ========== - - name = "Samples" - label = "Samples" - icon = "fa fa-flask" - - # ========== List View ========== - - sortable_fields = [ - "id", - "sample_name", - "sample_date", - "sample_matrix", - "sample_method", - "qc_type", - "release_status", - "created_at", - ] - - fields_default_sort = [("sample_date", True)] - - searchable_fields = [ - "sample_name", - "notes", - "nma_pk_waterlevels", - "sample_matrix", - "sample_method", - "qc_type", - "release_status", - "created_at", - ] - - page_size = 50 - page_size_options = [25, 50, 100, 200] - - # ========== Form View ========== - - fields = [ - "id", - "field_activity_id", - "field_event_participant_id", - "sample_date", - "sample_name", - "sample_matrix", - "sample_method", - "qc_type", - "depth_top", - "depth_bottom", - "notes", - "nma_pk_waterlevels", - "release_status", - "created_at", - "created_by_id", - "created_by_name", - "updated_by_id", - "updated_by_name", - ] - - exclude_fields_from_create = [ - "id", - "created_at", - "created_by_id", - "created_by_name", - "updated_by_id", - "updated_by_name", - ] - - exclude_fields_from_edit = [ - "id", - "created_at", - "created_by_id", - "created_by_name", - ] - - -# ============= EOF ============================================= diff --git a/admin/views/sensor.py b/admin/views/sensor.py deleted file mode 100644 index 28d41e44e..000000000 --- a/admin/views/sensor.py +++ /dev/null @@ -1,123 +0,0 @@ -# =============================================================================== -# Copyright 2025 -# -# 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. -# =============================================================================== -""" -SensorAdmin view for OcotilloAPI. - -Provides MS Access-like interface for CRUD operations on Sensor (Equipment) model. -""" - -from admin.views.base import OcotilloModelView - - -class SensorAdmin(OcotilloModelView): - """ - Admin view for Sensor model (Equipment). - - Designed to replicate MS Access "Equipment Entry Form" and "Equipment Datasheet View". - - Permission Model: - - Admin: Can create, edit, delete all sensors - - Editor: Can create and edit, cannot delete - - Viewer: Can only view published sensors (read-only) - """ - - # ========== Basic Configuration ========== - - name = "Sensors" - label = "Sensors (Equipment)" - icon = "fa fa-microchip" - - # ========== List View (MS Access Datasheet View Equivalent) ========== - - sortable_fields = [ - "id", - "name", - "sensor_type", - "model", - "serial_no", - "owner_agency", - "sensor_status", - "release_status", - "created_at", - ] - - fields_default_sort = [("created_at", True)] # True = descending - - searchable_fields = [ - "name", - "serial_no", - "model", - "pcn_number", - "sensor_type", - "owner_agency", - "sensor_status", - "release_status", - "created_at", - ] - - page_size = 50 - page_size_options = [25, 50, 100, 200] - - # ========== Form View (MS Access Form View Equivalent) ========== - - fields = [ - "id", - # Equipment Information - "name", - "sensor_type", - "model", - "serial_no", - "pcn_number", - "owner_agency", - "sensor_status", - "notes", - # Release Status - "release_status", - # Audit Fields - "created_at", - "created_by_id", - "created_by_name", - "updated_by_id", - "updated_by_name", - # Legacy Migration Fields - "nma_pk_equipment", - ] - - exclude_fields_from_create = [ - "id", - "created_at", - "created_by_id", - "created_by_name", - "updated_by_id", - "updated_by_name", - "nma_pk_equipment", - # Exclude complex relationships - "observations", - "deployments", - ] - - exclude_fields_from_edit = [ - "id", - "created_at", - "created_by_id", - "created_by_name", - "nma_pk_equipment", - # Exclude complex relationships - "observations", - "deployments", - ] - - # ========== Field Labels and Help Text ========== diff --git a/admin/views/soil_rock_results.py b/admin/views/soil_rock_results.py deleted file mode 100644 index 947804980..000000000 --- a/admin/views/soil_rock_results.py +++ /dev/null @@ -1,77 +0,0 @@ -""" -SoilRockResultsAdmin view for legacy NMA_Soil_Rock_Results. - -Already has Integer PK. Updated for legacy column rename: -- point_id -> nma_point_id -""" - -from admin.views.base import OcotilloModelView - - -class SoilRockResultsAdmin(OcotilloModelView): - """ - Read-only admin view for SoilRockResults legacy model. - """ - - # ========== Basic Configuration ========== - name = "NMA Soil Rock Results" - label = "NMA Soil Rock Results" - icon = "fa fa-mountain" - - # Integer PK (already correct) - pk_attr = "id" - pk_type = int - - # Pagination - page_size = 50 - page_size_options = [25, 50, 100, 200] - - # ========== List View ========== - list_fields = [ - "id", - "nma_point_id", - "sample_type", - "date_sampled", - "d13c", - "d18o", - "sampled_by", - "thing_id", - ] - - sortable_fields = [ - "id", - "nma_point_id", - ] - - searchable_fields = [ - "nma_point_id", - "sample_type", - "date_sampled", - "sampled_by", - ] - - fields_default_sort = [("id", True)] - - # ========== Detail View ========== - fields = [ - "id", - "nma_point_id", - "sample_type", - "date_sampled", - "d13c", - "d18o", - "sampled_by", - "thing_id", - ] - - # ========== Legacy Field Labels ========== - field_labels = { - "id": "ID", - "nma_point_id": "NMA Point_ID (Legacy)", - "sample_type": "Sample Type", - "date_sampled": "Date Sampled", - "d13c": "d13C", - "d18o": "d18O", - "sampled_by": "Sampled by", - "thing_id": "ThingID", - } diff --git a/admin/views/stratigraphy.py b/admin/views/stratigraphy.py deleted file mode 100644 index 0bbd32231..000000000 --- a/admin/views/stratigraphy.py +++ /dev/null @@ -1,100 +0,0 @@ -""" -StratigraphyAdmin view for legacy stratigraphy. - -Updated for Integer PK schema: -- id: Integer PK (autoincrement) -- nma_global_id: Legacy UUID PK (GlobalID), UNIQUE for audit -- nma_well_id: Legacy WellID UUID -- nma_point_id: Legacy PointID string -- nma_object_id: Legacy OBJECTID, UNIQUE -""" - -from admin.views.base import OcotilloModelView - - -class StratigraphyAdmin(OcotilloModelView): - """ - Read-only admin view for Stratigraphy legacy model. - """ - - # ========== Basic Configuration ========== - name = "NMA Stratigraphy" - label = "NMA Stratigraphy" - icon = "fa fa-layer-group" - - # Integer PK - pk_attr = "id" - pk_type = int - - # Pagination - page_size = 50 - page_size_options = [25, 50, 100, 200] - - # ========== List View ========== - - sortable_fields = [ - "id", - "nma_global_id", - "nma_object_id", - "nma_point_id", - ] - - fields_default_sort = [("nma_point_id", False), ("strat_top", False)] - - searchable_fields = [ - "nma_point_id", - "nma_global_id", - "unit_identifier", - "lithology", - "lithologic_modifier", - "contributing_unit", - "strat_source", - "strat_notes", - ] - - # ========== Form View ========== - - fields = [ - "id", - "nma_global_id", - "nma_well_id", - "nma_point_id", - "thing_id", - "strat_top", - "strat_bottom", - "unit_identifier", - "lithology", - "lithologic_modifier", - "contributing_unit", - "strat_source", - "strat_notes", - "nma_object_id", - ] - - exclude_fields_from_create = [ - "id", - "nma_object_id", - ] - - exclude_fields_from_edit = [ - "id", - "nma_object_id", - ] - - # ========== Legacy Field Labels ========== - field_labels = { - "id": "ID", - "nma_global_id": "NMA GlobalID (Legacy)", - "nma_well_id": "NMA WellID (Legacy)", - "nma_point_id": "NMA PointID (Legacy)", - "thing_id": "ThingID", - "strat_top": "StratTop", - "strat_bottom": "StratBottom", - "unit_identifier": "UnitIdentifier", - "lithology": "Lithology", - "lithologic_modifier": "LithologicModifier", - "contributing_unit": "ContributingUnit", - "strat_source": "StratSource", - "strat_notes": "StratNotes", - "nma_object_id": "NMA OBJECTID (Legacy)", - } diff --git a/admin/views/surface_water.py b/admin/views/surface_water.py deleted file mode 100644 index be6da860d..000000000 --- a/admin/views/surface_water.py +++ /dev/null @@ -1,96 +0,0 @@ -# =============================================================================== -# Copyright 2025 -# -# 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. -# =============================================================================== -""" -SurfaceWaterDataAdmin view for OcotilloAPI. -""" - -from admin.views.base import OcotilloModelView - - -class SurfaceWaterDataAdmin(OcotilloModelView): - """ - Admin view for SurfaceWaterData legacy model. - """ - - name = "NMA Surface Water Data" - label = "NMA Surface Water Data" - icon = "fa fa-water" - enable_publish_actions = False - - sortable_fields = [ - "surface_id", - "point_id", - "date_measured", - "discharge", - "discharge_units", - "discharge_method", - "discharge_source", - "formation_zone", - "aq_class", - ] - - fields_default_sort = [("date_measured", True)] - - searchable_fields = [ - "point_id", - "discharge", - "formation_zone", - "aq_class", - "data_source", - "discharge_units", - "discharge_method", - "discharge_source", - "formation_zone", - "aq_class", - ] - - page_size = 50 - page_size_options = [25, 50, 100, 200] - - fields = [ - "surface_id", - "point_id", - "object_id", - "date_measured", - "discharge", - "discharge_rate", - "discharge_units", - "discharge_method", - "discharge_source", - "formation_zone", - "aq_class", - "site_notes", - "field_method_notes", - "source_notes", - "data_source", - ] - - # ========== READ ONLY ========== - enable_publish_actions = ( - False # hides publish/unpublish actions inherited from base - ) - - def can_create(self, request) -> bool: - return False - - def can_edit(self, request) -> bool: - return False - - def can_delete(self, request) -> bool: - return False - - -# ============= EOF ============================================= diff --git a/admin/views/surface_water_photos.py b/admin/views/surface_water_photos.py deleted file mode 100644 index 2d2b73299..000000000 --- a/admin/views/surface_water_photos.py +++ /dev/null @@ -1,71 +0,0 @@ -from admin.views.base import OcotilloModelView - - -class SurfaceWaterPhotosAdmin(OcotilloModelView): - """ - Admin view for legacy SurfaceWaterPhotos model (NMA_SurfaceWaterPhotos). - """ - - # ========== Basic Configuration ========== - name = "NMA Surface Water Photos" - label = "NMA Surface Water Photos" - icon = "fa fa-water" - - # Pagination - page_size = 50 - page_size_options = [25, 50, 100, 200] - - # ========== List View ========== - list_fields = [ - "surface_id", - "point_id", - "ole_path", - "object_id", - "global_id", - ] - - sortable_fields = [ - "global_id", - "object_id", - "point_id", - ] - - fields_default_sort = [("point_id", False), ("object_id", False)] - - searchable_fields = [ - "point_id", - "global_id", - "ole_path", - ] - - # ========== Detail View ========== - fields = [ - "surface_id", - "point_id", - "ole_path", - "object_id", - "global_id", - ] - - # ========== Legacy Field Labels ========== - field_labels = { - "surface_id": "SurfaceID", - "point_id": "PointID", - "ole_path": "OLEPath", - "object_id": "OBJECTID", - "global_id": "GlobalID", - } - - # ========== READ ONLY ========== - enable_publish_actions = ( - False # hides publish/unpublish actions inherited from base - ) - - def can_create(self, request) -> bool: - return False - - def can_edit(self, request) -> bool: - return False - - def can_delete(self, request) -> bool: - return False diff --git a/admin/views/thing.py b/admin/views/thing.py deleted file mode 100644 index da6d7acbb..000000000 --- a/admin/views/thing.py +++ /dev/null @@ -1,161 +0,0 @@ -# =============================================================================== -# Copyright 2025 -# -# 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. -# =============================================================================== -""" -ThingAdmin view for OcotilloAPI. - -Provides MS Access-like interface for CRUD operations on Thing (Wells/Springs) model. -""" - -from admin.views.base import OcotilloModelView - - -class ThingAdmin(OcotilloModelView): - """ - Admin view for Thing model (Wells, Springs, etc.). - - Designed to replicate MS Access "Well Data Entry Form" and "Well Datasheet View". - - Permission Model: - - Admin: Can create, edit, delete all things - - Editor: Can create and edit, cannot delete - - Viewer: Can only view published things (read-only) - """ - - # ========== Basic Configuration ========== - - identity = "thing" - name = "Things" - label = "Things (Wells/Springs)" - icon = "fa fa-tint" - - # ========== List View (MS Access Datasheet View Equivalent) ========== - - sortable_fields = [ - "id", - "name", - "thing_type", - "well_depth", - "hole_depth", - "first_visit_date", - "release_status", - "created_at", - ] - - fields_default_sort = [("created_at", True)] # True = descending - - searchable_fields = [ - "name", - "thing_type", - "well_driller_name", - "well_depth", - "first_visit_date", - "release_status", - "created_at", - ] - - page_size = 50 - page_size_options = [25, 50, 100, 200] - - # ========== Form View (MS Access Form View Equivalent) ========== - - fields = [ - "id", - # Basic Information - "name", - "thing_type", - "first_visit_date", - # Well Construction - "well_depth", - "hole_depth", - "well_casing_diameter", - "well_casing_depth", - "well_completion_date", - "well_driller_name", - "well_construction_method", - "well_pump_type", - "well_pump_depth", - "formation_completion_code", - # Spring-specific - "spring_type", - # Release Status - "release_status", - # Audit Fields - "created_at", - "created_by_id", - "created_by_name", - "updated_by_id", - "updated_by_name", - # Legacy Migration Fields - "nma_pk_welldata", - ] - - exclude_fields_from_create = [ - "id", - "created_at", - "created_by_id", - "created_by_name", - "updated_by_id", - "updated_by_name", - "nma_pk_welldata", - # Exclude complex relationships from create form - "location_associations", - "contact_associations", - "asset_associations", - "field_events", - "deployments", - "group_associations", - "screens", - "well_purposes", - "well_casing_materials", - "links", - "measuring_points", - "monitoring_frequencies", - "aquifer_associations", - "formation_associations", - "status_history", - "permission_history", - "data_provenance", - "notes", - ] - - exclude_fields_from_edit = [ - "id", - "created_at", - "created_by_id", - "created_by_name", - "nma_pk_welldata", - # Exclude complex relationships from edit form (manage separately) - "location_associations", - "contact_associations", - "asset_associations", - "field_events", - "deployments", - "group_associations", - "screens", - "well_purposes", - "well_casing_materials", - "links", - "measuring_points", - "monitoring_frequencies", - "aquifer_associations", - "formation_associations", - "status_history", - "permission_history", - "data_provenance", - "notes", - ] - - # ========== Field Labels and Help Text ========== diff --git a/admin/views/transducer_observation.py b/admin/views/transducer_observation.py deleted file mode 100644 index d9318d0e8..000000000 --- a/admin/views/transducer_observation.py +++ /dev/null @@ -1,205 +0,0 @@ -# =============================================================================== -# Copyright 2026 -# -# 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. -# =============================================================================== -""" -TransducerObservationAdmin view for transducer observations. -""" - -from admin.views.base import OcotilloModelView - - -class TransducerObservationAdmin(OcotilloModelView): - """ - Admin view for TransducerObservation model. - """ - - # ========== Basic Configuration ========== - - name = "Transducer Observations" - label = "Transducer Observations" - icon = "fa fa-tachometer-alt" - - # ========== List View ========== - - sortable_fields = [ - "id", - "observation_datetime", - "value", - "parameter_id", - "deployment_id", - "release_status", - ] - - fields_default_sort = [("observation_datetime", True)] - - searchable_fields = [ - "observation_datetime", - "parameter_id", - "deployment_id", - "release_status", - ] - - page_size = 50 - page_size_options = [25, 50, 100, 200] - - # ========== Form View ========== - - fields = [ - "id", - "observation_datetime", - "value", - "parameter_id", - "deployment_id", - "release_status", - "nma_waterlevelscontinuous_pressure_conddl_ms_cm", - "nma_waterlevelscontinuous_pressure_checked_by", - "nma_waterlevelscontinuous_pressure_created", - "nma_waterlevelscontinuous_pressure_data_source", - "nma_waterlevelscontinuous_pressure_global_id", - "nma_waterlevelscontinuous_pressure_measurement_method", - "nma_waterlevelscontinuous_pressure_measuring_agency", - "nma_waterlevelscontinuous_pressure_notes", - "nma_waterlevelscontinuous_pressure_processed_by", - "nma_waterlevelscontinuous_pressure_qced", - "nma_waterlevelscontinuous_pressure_temperature_water", - "nma_waterlevelscontinuous_pressure_updated", - "nma_waterlevelscontinuous_pressure_water_head", - "nma_waterlevelscontinuous_pressure_water_head_adjusted", - "nma_waterlevelscontinuous_acoustic_created", - "nma_waterlevelscontinuous_acoustic_data_source", - "nma_waterlevelscontinuous_acoustic_global_id", - "nma_waterlevelscontinuous_acoustic_measurement_method", - "nma_waterlevelscontinuous_acoustic_measuring_agency", - "nma_waterlevelscontinuous_acoustic_notes", - "nma_waterlevelscontinuous_acoustic_point_id", - "nma_waterlevelscontinuous_acoustic_pre_process_data_field", - "nma_waterlevelscontinuous_acoustic_public_release", - "nma_waterlevelscontinuous_acoustic_sensor_hgt_above_mp", - "nma_waterlevelscontinuous_acoustic_serial_no", - "nma_waterlevelscontinuous_acoustic_server_receipt_date", - "nma_waterlevelscontinuous_acoustic_speaker_to_mic_length", - "nma_waterlevelscontinuous_acoustic_temperature_air", - "created_at", - "created_by_id", - "created_by_name", - "updated_by_id", - "updated_by_name", - ] - - exclude_fields_from_create = [ - "id", - "created_at", - "created_by_id", - "created_by_name", - "updated_by_id", - "updated_by_name", - "nma_waterlevelscontinuous_pressure_conddl_ms_cm", - "nma_waterlevelscontinuous_pressure_checked_by", - "nma_waterlevelscontinuous_pressure_created", - "nma_waterlevelscontinuous_pressure_data_source", - "nma_waterlevelscontinuous_pressure_global_id", - "nma_waterlevelscontinuous_pressure_measurement_method", - "nma_waterlevelscontinuous_pressure_measuring_agency", - "nma_waterlevelscontinuous_pressure_notes", - "nma_waterlevelscontinuous_pressure_processed_by", - "nma_waterlevelscontinuous_pressure_qced", - "nma_waterlevelscontinuous_pressure_temperature_water", - "nma_waterlevelscontinuous_pressure_updated", - "nma_waterlevelscontinuous_pressure_water_head", - "nma_waterlevelscontinuous_pressure_water_head_adjusted", - "nma_waterlevelscontinuous_acoustic_created", - "nma_waterlevelscontinuous_acoustic_data_source", - "nma_waterlevelscontinuous_acoustic_global_id", - "nma_waterlevelscontinuous_acoustic_measurement_method", - "nma_waterlevelscontinuous_acoustic_measuring_agency", - "nma_waterlevelscontinuous_acoustic_notes", - "nma_waterlevelscontinuous_acoustic_point_id", - "nma_waterlevelscontinuous_acoustic_pre_process_data_field", - "nma_waterlevelscontinuous_acoustic_public_release", - "nma_waterlevelscontinuous_acoustic_sensor_hgt_above_mp", - "nma_waterlevelscontinuous_acoustic_serial_no", - "nma_waterlevelscontinuous_acoustic_server_receipt_date", - "nma_waterlevelscontinuous_acoustic_speaker_to_mic_length", - "nma_waterlevelscontinuous_acoustic_temperature_air", - ] - - exclude_fields_from_edit = [ - "id", - "created_at", - "created_by_id", - "created_by_name", - "nma_waterlevelscontinuous_pressure_conddl_ms_cm", - "nma_waterlevelscontinuous_pressure_checked_by", - "nma_waterlevelscontinuous_pressure_created", - "nma_waterlevelscontinuous_pressure_data_source", - "nma_waterlevelscontinuous_pressure_global_id", - "nma_waterlevelscontinuous_pressure_measurement_method", - "nma_waterlevelscontinuous_pressure_measuring_agency", - "nma_waterlevelscontinuous_pressure_notes", - "nma_waterlevelscontinuous_pressure_processed_by", - "nma_waterlevelscontinuous_pressure_qced", - "nma_waterlevelscontinuous_pressure_temperature_water", - "nma_waterlevelscontinuous_pressure_updated", - "nma_waterlevelscontinuous_pressure_water_head", - "nma_waterlevelscontinuous_pressure_water_head_adjusted", - "nma_waterlevelscontinuous_acoustic_created", - "nma_waterlevelscontinuous_acoustic_data_source", - "nma_waterlevelscontinuous_acoustic_global_id", - "nma_waterlevelscontinuous_acoustic_measurement_method", - "nma_waterlevelscontinuous_acoustic_measuring_agency", - "nma_waterlevelscontinuous_acoustic_notes", - "nma_waterlevelscontinuous_acoustic_point_id", - "nma_waterlevelscontinuous_acoustic_pre_process_data_field", - "nma_waterlevelscontinuous_acoustic_public_release", - "nma_waterlevelscontinuous_acoustic_sensor_hgt_above_mp", - "nma_waterlevelscontinuous_acoustic_serial_no", - "nma_waterlevelscontinuous_acoustic_server_receipt_date", - "nma_waterlevelscontinuous_acoustic_speaker_to_mic_length", - "nma_waterlevelscontinuous_acoustic_temperature_air", - ] - - readonly_fields = [ - "nma_waterlevelscontinuous_pressure_conddl_ms_cm", - "nma_waterlevelscontinuous_pressure_checked_by", - "nma_waterlevelscontinuous_pressure_created", - "nma_waterlevelscontinuous_pressure_data_source", - "nma_waterlevelscontinuous_pressure_global_id", - "nma_waterlevelscontinuous_pressure_measurement_method", - "nma_waterlevelscontinuous_pressure_measuring_agency", - "nma_waterlevelscontinuous_pressure_notes", - "nma_waterlevelscontinuous_pressure_processed_by", - "nma_waterlevelscontinuous_pressure_qced", - "nma_waterlevelscontinuous_pressure_temperature_water", - "nma_waterlevelscontinuous_pressure_updated", - "nma_waterlevelscontinuous_pressure_water_head", - "nma_waterlevelscontinuous_pressure_water_head_adjusted", - "nma_waterlevelscontinuous_acoustic_created", - "nma_waterlevelscontinuous_acoustic_data_source", - "nma_waterlevelscontinuous_acoustic_global_id", - "nma_waterlevelscontinuous_acoustic_measurement_method", - "nma_waterlevelscontinuous_acoustic_measuring_agency", - "nma_waterlevelscontinuous_acoustic_notes", - "nma_waterlevelscontinuous_acoustic_point_id", - "nma_waterlevelscontinuous_acoustic_pre_process_data_field", - "nma_waterlevelscontinuous_acoustic_public_release", - "nma_waterlevelscontinuous_acoustic_sensor_hgt_above_mp", - "nma_waterlevelscontinuous_acoustic_serial_no", - "nma_waterlevelscontinuous_acoustic_server_receipt_date", - "nma_waterlevelscontinuous_acoustic_speaker_to_mic_length", - "nma_waterlevelscontinuous_acoustic_temperature_air", - ] - - -# ============= EOF ============================================= diff --git a/admin/views/waterlevelscontinuous_pressure_daily.py b/admin/views/waterlevelscontinuous_pressure_daily.py deleted file mode 100644 index ac2afb020..000000000 --- a/admin/views/waterlevelscontinuous_pressure_daily.py +++ /dev/null @@ -1,148 +0,0 @@ -# =============================================================================== -# Copyright 2026 -# -# 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. -# =============================================================================== -""" -WaterLevelsContinuousPressureDailyAdmin view for legacy NMA_WaterLevelsContinuous_Pressure_Daily. -""" - -from starlette.requests import Request - -from admin.views.base import OcotilloModelView - - -class WaterLevelsContinuousPressureDailyAdmin(OcotilloModelView): - """ - Admin view for NMA_WaterLevelsContinuous_Pressure_Daily model. - """ - - # ========== Basic Configuration ========== - name = "NMA Water Levels Continuous Pressure Daily" - label = "NMA Water Levels Continuous Pressure Daily" - icon = "fa fa-tachometer-alt" - - def can_create(self, request: Request) -> bool: - return False - - def can_edit(self, request: Request) -> bool: - return False - - def can_delete(self, request: Request) -> bool: - return False - - # ========== List View ========== - list_fields = [ - "global_id", - "object_id", - "well_id", - "point_id", - "date_measured", - "temperature_water", - "water_head", - "water_head_adjusted", - "depth_to_water_bgs", - "measurement_method", - "data_source", - "measuring_agency", - "qced", - "notes", - "created", - "updated", - "processed_by", - "checked_by", - "cond_dl_ms_cm", - ] - - sortable_fields = [ - "global_id", - "object_id", - "well_id", - "point_id", - "date_measured", - "water_head", - "depth_to_water_bgs", - "measurement_method", - "data_source", - "measuring_agency", - "qced", - "created", - "updated", - "processed_by", - "checked_by", - "cond_dl_ms_cm", - ] - - fields_default_sort = [("date_measured", True)] - - searchable_fields = [ - "global_id", - "well_id", - "point_id", - "date_measured", - "measurement_method", - "data_source", - "measuring_agency", - "notes", - ] - - page_size = 50 - page_size_options = [25, 50, 100, 200] - - # ========== Detail View ========== - fields = [ - "global_id", - "object_id", - "well_id", - "point_id", - "date_measured", - "temperature_water", - "water_head", - "water_head_adjusted", - "depth_to_water_bgs", - "measurement_method", - "data_source", - "measuring_agency", - "qced", - "notes", - "created", - "updated", - "processed_by", - "checked_by", - "cond_dl_ms_cm", - ] - - field_labels = { - "global_id": "GlobalID", - "object_id": "OBJECTID", - "well_id": "WellID", - "point_id": "PointID", - "date_measured": "Date Measured", - "temperature_water": "Temperature Water", - "water_head": "Water Head", - "water_head_adjusted": "Water Head Adjusted", - "depth_to_water_bgs": "Depth To Water (BGS)", - "measurement_method": "Measurement Method", - "data_source": "Data Source", - "measuring_agency": "Measuring Agency", - "qced": "QCed", - "notes": "Notes", - "created": "Created", - "updated": "Updated", - "processed_by": "Processed By", - "checked_by": "Checked By", - "cond_dl_ms_cm": "CONDDL (mS/cm)", - } - - -# ============= EOF ============================================= diff --git a/admin/views/weather_data.py b/admin/views/weather_data.py deleted file mode 100644 index 662721c3a..000000000 --- a/admin/views/weather_data.py +++ /dev/null @@ -1,66 +0,0 @@ -from admin.views.base import OcotilloModelView - - -class WeatherDataAdmin(OcotilloModelView): - """ - Admin view for legacy WeatherData model (NMA_WeatherData). - """ - - # ========== Basic Configuration ========== - name = "NMA Weather Data" - label = "NMA Weather Data" - icon = "fa fa-cloud-sun" - - # Pagination - page_size = 50 - page_size_options = [25, 50, 100, 200] - - # ========== List View ========== - list_fields = [ - "location_id", - "point_id", - "weather_id", - "object_id", - ] - - sortable_fields = [ - "object_id", - "point_id", - ] - - fields_default_sort = [("point_id", False), ("object_id", False)] - - searchable_fields = [ - "point_id", - "weather_id", - ] - - # ========== Detail View ========== - fields = [ - "location_id", - "point_id", - "weather_id", - "object_id", - ] - - # ========== Legacy Field Labels ========== - field_labels = { - "location_id": "LocationId", - "point_id": "PointID", - "weather_id": "WeatherID", - "object_id": "OBJECTID", - } - - # ========== READ ONLY ========== - enable_publish_actions = ( - False # hides publish/unpublish actions inherited from base - ) - - def can_create(self, request) -> bool: - return False - - def can_edit(self, request) -> bool: - return False - - def can_delete(self, request) -> bool: - return False diff --git a/admin/views/weather_photos.py b/admin/views/weather_photos.py deleted file mode 100644 index 006d1b10a..000000000 --- a/admin/views/weather_photos.py +++ /dev/null @@ -1,70 +0,0 @@ -from admin.views.base import OcotilloModelView - - -class WeatherPhotosAdmin(OcotilloModelView): - """ - Admin view for legacy WeatherPhotos model (NMA_WeatherPhotos). - """ - - # ========== Basic Configuration ========== - name = "NMA Weather Photos" - label = "NMA Weather Photos" - icon = "fa fa-cloud" - - # Pagination - page_size = 50 - page_size_options = [25, 50, 100, 200] - - # ========== List View ========== - list_fields = [ - "weather_id", - "point_id", - "ole_path", - "object_id", - "global_id", - ] - - sortable_fields = [ - "global_id", - "object_id", - "point_id", - ] - - fields_default_sort = [("point_id", False), ("object_id", False)] - - searchable_fields = [ - "point_id", - "ole_path", - ] - - # ========== Detail View ========== - fields = [ - "weather_id", - "point_id", - "ole_path", - "object_id", - "global_id", - ] - - # ========== Legacy Field Labels ========== - field_labels = { - "weather_id": "WeatherID", - "point_id": "PointID", - "ole_path": "OLEPath", - "object_id": "OBJECTID", - "global_id": "GlobalID", - } - - # ========== READ ONLY ========== - enable_publish_actions = ( - False # hides publish/unpublish actions inherited from base - ) - - def can_create(self, request) -> bool: - return False - - def can_edit(self, request) -> bool: - return False - - def can_delete(self, request) -> bool: - return False diff --git a/core/factory.py b/core/factory.py index 69bcfba7e..3877e7bf3 100644 --- a/core/factory.py +++ b/core/factory.py @@ -6,8 +6,6 @@ from core.initializers import ( configure_apitally_middleware, configure_cors_middleware, - configure_lazy_admin, - configure_session_middleware, register_api_routes, ) @@ -47,9 +45,6 @@ def create_api_app(): from core.pygeoapi import mount_pygeoapi mount_pygeoapi(app) - if os.environ.get("SESSION_SECRET_KEY"): - configure_session_middleware(app) configure_cors_middleware(app) configure_apitally_middleware(app) - configure_lazy_admin(app) return app diff --git a/core/initializers.py b/core/initializers.py index 356005d80..845d831dc 100644 --- a/core/initializers.py +++ b/core/initializers.py @@ -13,7 +13,6 @@ # See the License for the specific language governing permissions and # limitations under the License. # =============================================================================== -import asyncio import os from pathlib import Path @@ -21,7 +20,6 @@ from sqlalchemy import text, select from sqlalchemy.dialects.postgresql import insert from sqlalchemy.exc import DatabaseError -from starlette.responses import PlainTextResponse from db import Base from db.engine import session_ctx @@ -237,17 +235,6 @@ def register_api_routes(app): app.state.api_routes_registered = True -def configure_session_middleware(app): - from starlette.middleware.sessions import SessionMiddleware - - if not getattr(app.state, "session_middleware_configured", False): - session_secret_key = os.environ.get("SESSION_SECRET_KEY") - if not session_secret_key: - raise ValueError("SESSION_SECRET_KEY environment variable is not set.") - app.add_middleware(SessionMiddleware, secret_key=session_secret_key) - app.state.session_middleware_configured = True - - def configure_cors_middleware(app): from starlette.middleware.cors import CORSMiddleware @@ -284,43 +271,8 @@ def configure_apitally_middleware(app): def configure_middleware(app): - configure_session_middleware(app) configure_cors_middleware(app) configure_apitally_middleware(app) -def configure_admin(app): - if getattr(app.state, "admin_configured", False): - return - - from admin import create_admin - from admin.auth_routes import router as admin_auth_router - - app.include_router(admin_auth_router) - create_admin(app) - app.state.admin_configured = True - - -def configure_lazy_admin(app): - if getattr(app.state, "lazy_admin_configured", False): - return - - app.state.admin_configure_lock = asyncio.Lock() - - @app.middleware("http") - async def ensure_admin_initialized(request, call_next): - if request.url.path.startswith("/admin"): - if not getattr(app.state, "session_middleware_configured", False): - return PlainTextResponse( - "Admin requires SESSION_SECRET_KEY to be configured.", - status_code=503, - ) - async with app.state.admin_configure_lock: - if not getattr(app.state, "admin_configured", False): - configure_admin(app) - return await call_next(request) - - app.state.lazy_admin_configured = True - - # ============= EOF ============================================= diff --git a/docker-compose.yml b/docker-compose.yml index 94991fb99..3cfdaffe6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -36,7 +36,6 @@ services: - POSTGRES_PORT=5432 - MODE=${MODE} - AUTHENTIK_DISABLE_AUTHENTICATION=${AUTHENTIK_DISABLE_AUTHENTICATION} - - SESSION_SECRET_KEY=${SESSION_SECRET_KEY} - PYGEOAPI_POSTGRES_HOST=db - PYGEOAPI_POSTGRES_PORT=5432 - PYGEOAPI_POSTGRES_DB=ocotilloapi_dev diff --git a/pyproject.toml b/pyproject.toml index 481a389ef..a8f7305f3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,7 +48,6 @@ dependencies = [ "httpx==0.28.1", "idna==3.18", "iniconfig==2.3.0", - "itsdangerous>=2.2.0", "jinja2==3.1.6", "mako==1.3.12", "markupsafe==3.0.3", @@ -92,7 +91,6 @@ dependencies = [ "sqlalchemy-utils==0.42.1", "sqlparse>=0.5.5", "starlette==1.3.1", - "starlette-admin[i18n]==0.17.1", "typer==0.27.0", "typing-extensions==4.16.0", "typing-inspection==0.4.2", @@ -152,12 +150,6 @@ cli = [ "google-api-python-client==2.198.0", ] -[tool.pytest.ini_options] -filterwarnings = [ - "ignore:'HTTP_422_UNPROCESSABLE_ENTITY' is deprecated. Use 'HTTP_422_UNPROCESSABLE_CONTENT' instead.:DeprecationWarning:starlette_admin.*", -] - - # timezone to use when rendering the date within the migration file # as well as the filename. # If specified, requires the python>=3.9 or backports.zoneinfo library and tzdata library. diff --git a/tests/__init__.py b/tests/__init__.py index 57fa0c351..3f5666036 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -42,8 +42,6 @@ def _normalize_test_db_host() -> None: os.environ["POSTGRES_PORT"] = "5432" # Always use test database, never dev os.environ["POSTGRES_DB"] = "ocotilloapi_test" -# Keep `main:app` importable in clean test environments without a local `.env`. -os.environ.setdefault("SESSION_SECRET_KEY", "test-session-secret-key") from fastapi.testclient import TestClient diff --git a/tests/conftest.py b/tests/conftest.py index 9eb1afd15..f77d8fa3b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -29,8 +29,6 @@ def pytest_configure(): except OSError: os.environ[env_name] = "localhost" os.environ.setdefault("POSTGRES_PORT", "54321") - # NOTE: This hardcoded secret key is for tests only and must NEVER be used in production. - os.environ.setdefault("SESSION_SECRET_KEY", "test-session-secret-key") # Always use test database, never dev os.environ["POSTGRES_DB"] = "ocotilloapi_test" diff --git a/tests/features/admin-minor-trace-chemistry.feature b/tests/features/admin-minor-trace-chemistry.feature deleted file mode 100644 index b8c035b5c..000000000 --- a/tests/features/admin-minor-trace-chemistry.feature +++ /dev/null @@ -1,45 +0,0 @@ -@backend @admin -Feature: Minor Trace Chemistry Admin View - As an administrator - I want to view Minor Trace Chemistry data in the admin interface - So that I can browse and manage legacy chemistry results - - @positive - Scenario: Minor Trace Chemistry view is registered in admin - Given a functioning api - When I check the registered admin views - Then "Minor Trace Chemistry" should be in the list of admin views - - @positive - Scenario: Minor Trace Chemistry view is read-only - Given a functioning api - Then the Minor Trace Chemistry admin view should not allow create - And the Minor Trace Chemistry admin view should not allow edit - And the Minor Trace Chemistry admin view should not allow delete - - @positive - Scenario: Minor Trace Chemistry details page loads - Given a functioning api - When I request the Minor Trace Chemistry admin list page - Then the response status should be 200 - When I request the Minor Trace Chemistry admin detail page for an existing record - Then the response status should be 200 - - @positive - Scenario: Minor Trace Chemistry detail page shows expected fields - Given a functioning api - Then the Minor Trace Chemistry admin view should have these fields configured: - | field | - | global_id | - | sample_pt_id | - | analyte | - | symbol | - | sample_value | - | units | - | uncertainty | - | analysis_method | - | analysis_date | - | notes | - | volume | - | volume_unit | - | analyses_agency | diff --git a/tests/features/steps/admin-minor-trace-chemistry.py b/tests/features/steps/admin-minor-trace-chemistry.py deleted file mode 100644 index 9b193168b..000000000 --- a/tests/features/steps/admin-minor-trace-chemistry.py +++ /dev/null @@ -1,143 +0,0 @@ -# =============================================================================== -# Copyright 2026 -# -# 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. -# =============================================================================== -""" -Step definitions for Minor Trace Chemistry admin view tests. -These are fast integration tests - no HTTP calls, direct module testing. -""" - -from admin.views.minor_trace_chemistry import MinorTraceChemistryAdmin -from behave import when, then -from behave.runner import Context - -ADMIN_IDENTITY = MinorTraceChemistryAdmin.identity -ADMIN_BASE_URL = f"/admin/{ADMIN_IDENTITY}" - - -def _ensure_admin_mounted(context): - """Ensure admin is mounted on the test app.""" - if not getattr(context, "_admin_mounted", False): - from admin import create_admin - from starlette.middleware.sessions import SessionMiddleware - - # Add session middleware required by admin - context.client.app.add_middleware( - SessionMiddleware, secret_key="test-secret-key" - ) - create_admin(context.client.app) - context._admin_mounted = True - - -@when("I check the registered admin views") -def step_when_i_check_the_registered_admin_views(context: Context): - from admin.config import create_admin - from fastapi import FastAPI - - app = FastAPI() - admin = create_admin(app) - context.admin_views = [v.name for v in admin._views] - - -@then('"{view_name}" should be in the list of admin views') -def step_then_view_name_should_be_in_the_list_of_admin_views( - context: Context, view_name: str -): - assert view_name in context.admin_views, ( - f"Expected '{view_name}' to be registered in admin views. " - f"Found: {context.admin_views}" - ) - - -@then("the Minor Trace Chemistry admin view should not allow create") -def step_then_the_minor_trace_chemistry_admin_view_should_not_allow_create( - context: Context, -): - from db.nma_legacy import NMA_MinorTraceChemistry - - view = MinorTraceChemistryAdmin(NMA_MinorTraceChemistry) - assert view.can_create(None) is False - - -@then("the Minor Trace Chemistry admin view should not allow edit") -def step_then_the_minor_trace_chemistry_admin_view_should_not_allow_edit( - context: Context, -): - from db.nma_legacy import NMA_MinorTraceChemistry - - view = MinorTraceChemistryAdmin(NMA_MinorTraceChemistry) - assert view.can_edit(None) is False - - -@then("the Minor Trace Chemistry admin view should not allow delete") -def step_then_the_minor_trace_chemistry_admin_view_should_not_allow_delete( - context: Context, -): - from db.nma_legacy import NMA_MinorTraceChemistry - - view = MinorTraceChemistryAdmin(NMA_MinorTraceChemistry) - assert view.can_delete(None) is False - - -@when("I request the Minor Trace Chemistry admin list page") -def step_when_i_request_the_minor_trace_chemistry_admin_list_page(context: Context): - _ensure_admin_mounted(context) - context.response = context.client.get(f"{ADMIN_BASE_URL}/list") - - -@when("I request the Minor Trace Chemistry admin detail page for an existing record") -def step_when_i_request_the_minor_trace_chemistry_admin_detail_page_for( - context: Context, -): - _ensure_admin_mounted(context) - from db.engine import session_ctx - from db.nma_legacy import NMA_MinorTraceChemistry - - with session_ctx() as session: - record = session.query(NMA_MinorTraceChemistry).first() - if record: - context.response = context.client.get( - f"{ADMIN_BASE_URL}/detail/{record.global_id}" - ) - else: - # No records exist, skip by setting a mock 200 response - context.response = type("Response", (), {"status_code": 200})() - - -@then("the response status should be {status_code:d}") -def step_then_the_response_status_should_be_status_code_d( - context: Context, status_code: int -): - assert ( - context.response.status_code == status_code - ), f"Expected status {status_code}, got {context.response.status_code}" - - -@then("the Minor Trace Chemistry admin view should have these fields configured:") -def step_then_the_minor_trace_chemistry_admin_view_should_have_these_fields( - context: Context, -): - from admin.views.minor_trace_chemistry import MinorTraceChemistryAdmin - - expected_fields = [row["field"] for row in context.table] - actual_fields = MinorTraceChemistryAdmin.fields - - for field in expected_fields: - assert field in actual_fields, ( - f"Expected field '{field}' not found in admin view fields. " - f"Configured fields: {actual_fields}" - ) - - -# ============= EOF ============================================= diff --git a/tests/integration/test_admin_minor_trace_chemistry.py b/tests/integration/test_admin_minor_trace_chemistry.py deleted file mode 100644 index f5cf0d0fa..000000000 --- a/tests/integration/test_admin_minor_trace_chemistry.py +++ /dev/null @@ -1,237 +0,0 @@ -# =============================================================================== -# Copyright 2025 -# -# 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. -# =============================================================================== -""" -HTTP integration tests for Minor Trace Chemistry admin view. - -These tests make real HTTP requests to verify endpoint behavior. -When these tests pass, the UI should work. -""" - -import uuid - -import pytest -from fastapi import FastAPI -from fastapi.testclient import TestClient -from starlette.middleware.sessions import SessionMiddleware - -from admin.config import create_admin -from admin.views.minor_trace_chemistry import MinorTraceChemistryAdmin -from db.engine import session_ctx -from db.location import Location, LocationThingAssociation -from db.nma_legacy import NMA_MinorTraceChemistry, NMA_Chemistry_SampleInfo -from db.thing import Thing - -ADMIN_IDENTITY = MinorTraceChemistryAdmin.identity -ADMIN_BASE_URL = f"/admin/{ADMIN_IDENTITY}" - - -@pytest.fixture(scope="module") -def admin_app(): - """Create a FastAPI app with admin interface mounted.""" - app = FastAPI() - - # Add session middleware required for admin - app.add_middleware(SessionMiddleware, secret_key="test-secret-key-for-admin") - - # Mount admin interface - create_admin(app) - - return app - - -@pytest.fixture(scope="module") -def admin_client(admin_app): - """Create a test client for the admin app.""" - return TestClient(admin_app) - - -@pytest.fixture(scope="module") -def minor_trace_chemistry_record(): - """Create a minor trace chemistry record for testing.""" - with session_ctx() as session: - # First create a Location - location = Location( - point="POINT(-107.949533 33.809665)", - elevation=2464.9, - release_status="draft", - ) - session.add(location) - session.commit() - session.refresh(location) - - # Create a Thing (required for NMA_Chemistry_SampleInfo) - thing = Thing( - name="INTTEST-WELL-01", - thing_type="monitoring well", - release_status="draft", - ) - session.add(thing) - session.commit() - session.refresh(thing) - - # Associate Location with Thing - assoc = LocationThingAssociation( - location_id=location.id, - thing_id=thing.id, - ) - session.add(assoc) - session.commit() - - # Create parent NMA_Chemistry_SampleInfo - sample_info = NMA_Chemistry_SampleInfo( - nma_sample_pt_id=uuid.uuid4(), - nma_sample_point_id="INTTEST01", - thing_id=thing.id, - ) - session.add(sample_info) - session.commit() - session.refresh(sample_info) - - # Create MinorTraceChemistry record - chemistry = NMA_MinorTraceChemistry( - nma_global_id=uuid.uuid4(), - chemistry_sample_info_id=sample_info.id, # Integer FK - nma_sample_point_id=sample_info.nma_sample_point_id, - analyte="Arsenic", - symbol="As", - sample_value=0.005, - units="mg/L", - analysis_method="EPA 200.8", - analyses_agency="NMED", - ) - session.add(chemistry) - session.commit() - session.refresh(chemistry) - - yield chemistry - - # Cleanup - session.delete(chemistry) - session.delete(sample_info) - session.delete(assoc) - session.delete(thing) - session.delete(location) - session.commit() - - -class TestMinorTraceChemistryListView: - """Tests for the list view endpoint.""" - - def test_list_view_returns_200(self, admin_client): - """List view should return 200 OK.""" - response = admin_client.get(f"{ADMIN_BASE_URL}/list") - assert response.status_code == 200, ( - f"Expected 200, got {response.status_code}. " - f"Response: {response.text[:500]}" - ) - - def test_list_view_contains_view_name(self, admin_client): - """List view should contain the view name.""" - response = admin_client.get(f"{ADMIN_BASE_URL}/list") - assert response.status_code == 200 - assert "Minor Trace Chemistry" in response.text - - def test_no_create_button_in_list_view(self, admin_client): - """List view should not have a Create button for read-only view.""" - response = admin_client.get(f"{ADMIN_BASE_URL}/list") - assert response.status_code == 200 - html = response.text.lower() - assert f'href="{ADMIN_BASE_URL}/create"' not in html - - -class TestMinorTraceChemistryDetailView: - """Tests for the detail view endpoint.""" - - def test_detail_view_returns_200(self, admin_client, minor_trace_chemistry_record): - """Detail view should return 200 OK for existing record.""" - pk = str(minor_trace_chemistry_record.id) # Integer PK - response = admin_client.get(f"{ADMIN_BASE_URL}/detail/{pk}") - assert response.status_code == 200, ( - f"Expected 200, got {response.status_code}. " - f"Response: {response.text[:500]}" - ) - - def test_detail_view_shows_analyte( - self, admin_client, minor_trace_chemistry_record - ): - """Detail view should display the analyte.""" - pk = str(minor_trace_chemistry_record.id) # Integer PK - response = admin_client.get(f"{ADMIN_BASE_URL}/detail/{pk}") - assert response.status_code == 200 - assert "Arsenic" in response.text - - def test_detail_view_shows_parent_relationship( - self, admin_client, minor_trace_chemistry_record - ): - """Detail view should display the parent NMA_Chemistry_SampleInfo.""" - pk = str(minor_trace_chemistry_record.id) # Integer PK - response = admin_client.get(f"{ADMIN_BASE_URL}/detail/{pk}") - assert response.status_code == 200 - # The parent relationship should be displayed somehow - # Check for the field label - assert "Chemistry Sample Info" in response.text - - def test_detail_view_404_for_nonexistent_record(self, admin_client): - """Detail view should return 404 for non-existent record.""" - fake_pk = "999999999" # Integer PK that doesn't exist - response = admin_client.get(f"{ADMIN_BASE_URL}/detail/{fake_pk}") - assert response.status_code == 404 - - -class TestMinorTraceChemistryReadOnlyRestrictions: - """Tests for read-only restrictions.""" - - def test_create_endpoint_forbidden(self, admin_client): - """Create endpoint should be forbidden for read-only view.""" - response = admin_client.get(f"{ADMIN_BASE_URL}/create") - # Should be 403 or redirect, not 200 - assert response.status_code in ( - 403, - 302, - 307, - ), f"Expected 403 or redirect, got {response.status_code}" - - def test_edit_endpoint_forbidden(self, admin_client, minor_trace_chemistry_record): - """Edit endpoint should be forbidden for read-only view.""" - pk = str(minor_trace_chemistry_record.id) # Integer PK - response = admin_client.get(f"{ADMIN_BASE_URL}/edit/{pk}") - # Should be 403 or redirect, not 200 - assert response.status_code in ( - 403, - 302, - 307, - ), f"Expected 403 or redirect, got {response.status_code}" - - def test_delete_endpoint_forbidden( - self, admin_client, minor_trace_chemistry_record - ): - """Delete endpoint should be forbidden for read-only view.""" - pk = str(minor_trace_chemistry_record.id) # Integer PK - response = admin_client.post( - f"{ADMIN_BASE_URL}/delete", - data={"pks": [pk]}, - ) - # Should be 403, redirect, or 404/405 (route may not exist for read-only) - assert response.status_code in ( - 403, - 302, - 307, - 404, - 405, - ), f"Expected 403/redirect/404/405, got {response.status_code}" - - -# ============= EOF ============================================= diff --git a/tests/test_admin_minor_trace_chemistry.py b/tests/test_admin_minor_trace_chemistry.py deleted file mode 100644 index 4ec1705d8..000000000 --- a/tests/test_admin_minor_trace_chemistry.py +++ /dev/null @@ -1,217 +0,0 @@ -# =============================================================================== -# Copyright 2025 -# -# 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. -# =============================================================================== -""" -Unit tests for Minor Trace Chemistry admin view configuration. - -These tests verify the admin view is properly configured without requiring -a running server or database. - -Updated for Integer PK schema: -- id: Integer PK (autoincrement) -- nma_global_id: Legacy GlobalID UUID (UNIQUE) -- chemistry_sample_info_id: Integer FK to NMA_Chemistry_SampleInfo.id -- nma_chemistry_sample_info_uuid: Legacy UUID FK (for audit) -""" - -import pytest -from fastapi import FastAPI - -from admin.config import create_admin -from admin.views.minor_trace_chemistry import MinorTraceChemistryAdmin -from db.nma_legacy import NMA_MinorTraceChemistry - - -class TestMinorTraceChemistryAdminRegistration: - """Tests for MinorTraceChemistry admin view registration.""" - - def test_minor_trace_chemistry_view_is_registered(self): - """Minor Trace Chemistry should appear in admin views.""" - app = FastAPI() - admin = create_admin(app) - view_names = [v.name for v in admin._views] - - assert "Minor Trace Chemistry" in view_names, ( - f"Expected 'Minor Trace Chemistry' to be registered in admin views. " - f"Found: {view_names}" - ) - - def test_view_has_correct_label(self): - """View should have proper label for sidebar display.""" - view = MinorTraceChemistryAdmin(NMA_MinorTraceChemistry) - assert view.label == "Minor Trace Chemistry" - - def test_class_has_flask_icon_configured(self): - """View class should have flask icon configured for chemistry data.""" - # Note: icon attribute may be processed by starlette-admin on instantiation - # so we check the class attribute directly - assert MinorTraceChemistryAdmin.icon == "fa fa-flask" - - -class TestMinorTraceChemistryAdminReadOnly: - """Tests for read-only restrictions on legacy data.""" - - @pytest.fixture - def view(self): - """Create a MinorTraceChemistryAdmin instance for testing.""" - return MinorTraceChemistryAdmin(NMA_MinorTraceChemistry) - - def test_can_create_returns_false(self, view): - """Create should be disabled for legacy data.""" - assert view.can_create(None) is False - - def test_can_edit_returns_false(self, view): - """Edit should be disabled for legacy data.""" - assert view.can_edit(None) is False - - def test_can_delete_returns_false(self, view): - """Delete should be disabled for legacy data.""" - assert view.can_delete(None) is False - - def test_read_only_methods_are_callable(self, view): - """Permission methods should be callable (not boolean attributes).""" - # This test catches the bug where can_create/can_edit/can_delete - # were set as boolean attributes instead of methods - assert callable(view.can_create) - assert callable(view.can_edit) - assert callable(view.can_delete) - - -class TestMinorTraceChemistryAdminListView: - """Tests for list view configuration.""" - - @pytest.fixture - def view(self): - """Create a MinorTraceChemistryAdmin instance for testing.""" - return MinorTraceChemistryAdmin(NMA_MinorTraceChemistry) - - def test_list_fields_include_required_columns(self, view): - """List view should show key chemistry data columns.""" - from starlette_admin.fields import HasOne - - # Get field names (handling both string fields and HasOne fields) - field_names = [] - for f in view.list_fields: - if isinstance(f, str): - field_names.append(f) - elif isinstance(f, HasOne): - field_names.append(f.name) - else: - field_names.append(getattr(f, "name", str(f))) - - required_columns = [ - "id", # Integer PK - "nma_global_id", # Legacy UUID - "chemistry_sample_info", # HasOne relationship to parent - "analyte", - "sample_value", - "units", - ] - for col in required_columns: - assert col in field_names, f"Expected '{col}' in list_fields" - - def test_default_sort_by_analysis_date(self, view): - """Default sort should be by analysis_date descending.""" - assert view.fields_default_sort == [("analysis_date", True)] - - def test_page_size_is_50(self, view): - """Default page size should be 50.""" - assert view.page_size == 50 - - def test_page_size_options_available(self, view): - """Multiple page size options should be available.""" - assert 25 in view.page_size_options - assert 50 in view.page_size_options - assert 100 in view.page_size_options - - -class TestMinorTraceChemistryAdminFormView: - """Tests for form/detail view configuration.""" - - @pytest.fixture - def view(self): - """Create a MinorTraceChemistryAdmin instance for testing.""" - return MinorTraceChemistryAdmin(NMA_MinorTraceChemistry) - - def test_form_includes_all_chemistry_fields(self): - """Form should include all relevant chemistry data fields in configuration.""" - from starlette_admin.fields import HasOne - - # Check the class-level configuration - # Note: chemistry_sample_info is a HasOne field, not a string - expected_string_fields = [ - "id", # Integer PK - "nma_global_id", # Legacy GlobalID - "nma_chemistry_sample_info_uuid", # Legacy UUID FK - "analyte", - "symbol", - "sample_value", - "units", - "uncertainty", - "analysis_method", - "analysis_date", - "notes", - "volume", - "volume_unit", - "analyses_agency", - ] - configured_fields = MinorTraceChemistryAdmin.fields - - # Check string fields - for field in expected_string_fields: - assert ( - field in configured_fields - ), f"Expected '{field}' in configured fields" - - # Check that chemistry_sample_info HasOne relationship is configured - has_one_fields = [f for f in configured_fields if isinstance(f, HasOne)] - assert ( - len(has_one_fields) == 1 - ), "Expected one HasOne field for parent relationship" - assert has_one_fields[0].name == "chemistry_sample_info" - - def test_field_labels_are_human_readable(self, view): - """Field labels should be human-readable.""" - assert view.field_labels.get("id") == "ID" - assert view.field_labels.get("nma_global_id") == "NMA GlobalID (Legacy)" - assert view.field_labels.get("sample_value") == "Sample Value" - assert view.field_labels.get("analysis_date") == "Analysis Date" - - def test_searchable_fields_include_key_fields(self, view): - """Searchable fields should include commonly searched columns.""" - assert "nma_global_id" in view.searchable_fields - assert "analyte" in view.searchable_fields - assert "symbol" in view.searchable_fields - assert "analyses_agency" in view.searchable_fields - - -class TestMinorTraceChemistryAdminIntegerPK: - """Tests for Integer PK configuration.""" - - @pytest.fixture - def view(self): - """Create a MinorTraceChemistryAdmin instance for testing.""" - return MinorTraceChemistryAdmin(NMA_MinorTraceChemistry) - - def test_pk_attr_is_id(self, view): - """Primary key attribute should be 'id'.""" - assert view.pk_attr == "id" - - def test_pk_type_is_int(self, view): - """Primary key type should be int.""" - assert view.pk_type == int - - -# ============= EOF ============================================= diff --git a/tests/test_admin_views.py b/tests/test_admin_views.py deleted file mode 100644 index 9696ed1ba..000000000 --- a/tests/test_admin_views.py +++ /dev/null @@ -1,110 +0,0 @@ -# =============================================================================== -# Copyright 2026 -# -# 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. -# =============================================================================== -""" -Tests for admin views module. - -These tests ensure admin views can be imported without errors, -catching missing imports and syntax issues early in CI. -""" - -import importlib -import pkgutil - -import pytest - - -class TestAdminViewsImport: - """Tests that verify all admin views can be imported successfully.""" - - def test_admin_package_imports(self): - """ - Admin package should import without errors. - - This catches missing imports like Request, HasOne, etc. - """ - import admin # noqa: F401 - - def test_admin_views_package_imports(self): - """Admin views subpackage should import without errors.""" - import admin.views # noqa: F401 - - def test_all_view_modules_import(self): - """ - All individual admin view modules should import successfully. - - Iterates through all modules in admin.views and verifies each can be imported. - """ - import admin.views - - failed_imports = [] - - for importer, modname, ispkg in pkgutil.iter_modules(admin.views.__path__): - if modname.startswith("_"): - continue - full_name = f"admin.views.{modname}" - try: - importlib.import_module(full_name) - except Exception as e: - failed_imports.append((full_name, str(e))) - - assert ( - not failed_imports - ), f"Failed to import admin view modules:\n" + "\n".join( - f" {name}: {err}" for name, err in failed_imports - ) - - @pytest.mark.parametrize( - "view_module", - [ - "base", - "thing", - "location", - "observation", - "sample", - "contact", - "chemistry_sampleinfo", - "major_chemistry", - "minor_trace_chemistry", - ], - ) - def test_core_view_modules_import(self, view_module: str): - """Core admin view modules should import without errors.""" - importlib.import_module(f"admin.views.{view_module}") - - -class TestAdminViewsConfiguration: - """Tests for admin view configuration validity.""" - - def test_all_exported_views_have_required_attributes(self): - """All exported admin views should have required attributes.""" - import admin.views - - for name in admin.views.__all__: - view_class = getattr(admin.views, name) - - # All views should have a name attribute - assert hasattr( - view_class, "name" - ), f"{view_class.__name__} missing 'name' attribute" - - # All views inheriting from ModelView should have pk_attr - if hasattr(view_class, "model"): - assert hasattr( - view_class, "pk_attr" - ), f"{view_class.__name__} missing 'pk_attr' attribute" - - -# ============= EOF ============================================= diff --git a/tests/test_lazy_admin.py b/tests/test_lazy_admin.py deleted file mode 100644 index ac2f22448..000000000 --- a/tests/test_lazy_admin.py +++ /dev/null @@ -1,34 +0,0 @@ -import os -from collections.abc import Iterable - -from core.factory import create_api_app -from fastapi.testclient import TestClient - - -def _iter_route_paths(routes: Iterable) -> Iterable[str]: - for route in routes: - path = getattr(route, "path", None) - if path: - yield path - nested = getattr(route, "routes", None) - if nested: - yield from _iter_route_paths(nested) - - -def _has_admin_route(routes: Iterable) -> bool: - return any(path.startswith("/admin") for path in _iter_route_paths(routes)) - - -def test_admin_is_lazy_loaded_on_first_admin_request(): - os.environ["SESSION_SECRET_KEY"] = "test-session-secret-key" - app = create_api_app() - - assert not _has_admin_route(app.routes) - assert getattr(app.state, "admin_configured", False) is False - - with TestClient(app) as client: - response = client.get("/admin", follow_redirects=False) - - assert response.status_code in {200, 302, 307} - assert app.state.admin_configured is True - assert _has_admin_route(app.routes) diff --git a/uv.lock b/uv.lock index 81c778f97..195ad31ef 100644 --- a/uv.lock +++ b/uv.lock @@ -1631,7 +1631,6 @@ dependencies = [ { name = "httpx" }, { name = "idna" }, { name = "iniconfig" }, - { name = "itsdangerous" }, { name = "jinja2" }, { name = "mako" }, { name = "markupsafe" }, @@ -1676,7 +1675,6 @@ dependencies = [ { name = "sqlalchemy-utils" }, { name = "sqlparse" }, { name = "starlette" }, - { name = "starlette-admin", extra = ["i18n"] }, { name = "typer" }, { name = "typing-extensions" }, { name = "typing-inspection" }, @@ -1750,7 +1748,6 @@ requires-dist = [ { name = "httpx", specifier = "==0.28.1" }, { name = "idna", specifier = "==3.18" }, { name = "iniconfig", specifier = "==2.3.0" }, - { name = "itsdangerous", specifier = ">=2.2.0" }, { name = "jinja2", specifier = "==3.1.6" }, { name = "mako", specifier = "==1.3.12" }, { name = "markupsafe", specifier = "==3.0.3" }, @@ -1795,7 +1792,6 @@ requires-dist = [ { name = "sqlalchemy-utils", specifier = "==0.42.1" }, { name = "sqlparse", specifier = ">=0.5.5" }, { name = "starlette", specifier = "==1.3.1" }, - { name = "starlette-admin", extras = ["i18n"], specifier = "==0.17.1" }, { name = "typer", specifier = "==0.27.0" }, { name = "typing-extensions", specifier = "==4.16.0" }, { name = "typing-inspection", specifier = "==0.4.2" }, @@ -3070,25 +3066,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ec/bb/2799cc2ede3ed41131f8975621e7213dfc7ef4acbbaadfa440f32500c370/starlette-1.3.1-py3-none-any.whl", hash = "sha256:c7372aae11c3c3f26a42df7bd626cec2f47d03483d261d369516a615a53714c6", size = 73632, upload-time = "2026-06-12T09:23:10.017Z" }, ] -[[package]] -name = "starlette-admin" -version = "0.17.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "jinja2" }, - { name = "python-multipart" }, - { name = "starlette" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/d3/1d/49347d67cf11a453d6f8379e0b87e6c1ecc671dee347f61de8240c5d2b11/starlette_admin-0.17.1.tar.gz", hash = "sha256:7bdeaf1c30fd9036ef3779fb0255002d3d18aaf6f7e674200e21c604ee563fc7", size = 2106865, upload-time = "2026-07-20T06:13:58.132Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fe/d1/5fc2df30b98b59cc81b8184b66ca5d76fe194a7a9ff5197b70a23e49fc0b/starlette_admin-0.17.1-py3-none-any.whl", hash = "sha256:685615945d55de636879e3523ec70a6419176f7cd6f04a17470e35670f96b972", size = 2183488, upload-time = "2026-07-20T06:13:56.048Z" }, -] - -[package.optional-dependencies] -i18n = [ - { name = "babel" }, -] - [[package]] name = "tinydb" version = "4.8.2"