Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
92dba17
feat: data visibility and review feature
chasetmartin Dec 11, 2025
c26faee
fix: change file name
chasetmartin Dec 11, 2025
3f6e94f
feat: update data visibility and review scenarios for clarity and con…
jirhiker Dec 12, 2025
a267088
fix: update validation error status code for new record creation
jirhiker Dec 13, 2025
6494f4d
Merge pull request #298 from DataIntegrationGroup/jir-data-visibility…
jirhiker Dec 13, 2025
19c8bc9
Add legacy business logic to reconcile
kbighorse Dec 16, 2025
e5074d4
Refactor public_release.feature to use business language
kbighorse Dec 17, 2025
6f4f781
Rename deprecated `public_release` feature in favor of `release_statu…
kbighorse Dec 17, 2025
6cbce07
Implement feature with integration tests
kbighorse Dec 17, 2025
b006a78
Implement non-passing integration tests for release_status
kbighorse Dec 17, 2025
7e8dfb7
Implement release_status filtering for data visibility control
kbighorse Dec 17, 2025
98847d1
Add unit tests for release_status filtering logic
kbighorse Dec 17, 2025
5d0d31f
Fix test infrastructure for scenario isolation
kbighorse Dec 17, 2025
dc80fa2
Formatting changes
kbighorse Dec 17, 2025
ef3a484
feat: align data visibility/review feature with release_status legacy…
chasetmartin Dec 17, 2025
103ee6e
feat: update functioning api and model given to be more explicit abou…
chasetmartin Dec 18, 2025
b94a42f
fix: update default authentication and cascading language per comments
chasetmartin Dec 18, 2025
650b3e7
fix: continued rework of scenario wording for authenticated staff and…
chasetmartin Dec 18, 2025
d295b8d
fix: minor typo in given statement
chasetmartin Dec 19, 2025
c6dfcee
fix: staff permission in review management
chasetmartin Dec 19, 2025
e9bb418
Merge pull request #312 from DataIntegrationGroup/cm-data-visibility-…
chasetmartin Dec 19, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 24 additions & 3 deletions api/location.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
admin_dependency,
editor_dependency,
viewer_dependency,
optional_viewer_dependency,
)
from db.location import Location
from schemas.location import CreateLocation, LocationResponse, UpdateLocation
Expand Down Expand Up @@ -133,7 +134,7 @@ async def update_location(
)
async def get_location(
session: session_dependency,
user: viewer_dependency,
user: optional_viewer_dependency,
nearby_point: str = None,
nearby_distance_km: float = 1,
within: str = None,
Expand All @@ -143,10 +144,18 @@ async def get_location(
filter_: str = Query(alias="filter", default=None),
) -> CustomPage[LocationResponse]:
"""
Retrieve all wells from the database.
Retrieve all locations from the database.

Public users (unauthenticated) see only locations with release_status = "public".
Authenticated users see all locations regardless of release_status.
"""
sql = select(Location)

# Apply visibility filtering based on authentication
if user is None:
# Public/unauthenticated user - only show public data
sql = sql.where(Location.release_status == "public")

if query:
sql = sql.where(make_query(Location, query))
elif nearby_point:
Expand All @@ -169,12 +178,24 @@ async def get_location(
summary="Get location by ID",
)
async def get_location_by_id(
location_id: int, session: session_dependency, user: viewer_dependency
location_id: int, session: session_dependency, user: optional_viewer_dependency
) -> LocationResponse:
"""
Retrieve a sample location by ID from the database.

Public users (unauthenticated) can only access locations with release_status = "public".
Authenticated users can access all locations regardless of release_status.
"""
location = simple_get_by_id(session, Location, location_id)

# Check visibility for public users
if user is None and location.release_status != "public":
from fastapi import HTTPException

raise HTTPException(
status_code=404, detail="Location not found or not publicly accessible"
)

return location


Expand Down
22 changes: 22 additions & 0 deletions core/dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,4 +75,26 @@
amp_viewer_dependency: type[dict] = Annotated[dict, Depends(amp_viewer_function)]

no_permission_dependency: type[dict] = Annotated[dict, Depends(no_permission_function)]


# Optional Authentication for Public Endpoints --------------------------------
from typing import Optional


def optional_viewer_function():
"""
Optional authentication for public endpoints.
Returns None if not authenticated, user dict if authenticated.
"""
try:
return viewer_function()
except Exception:
# Not authenticated - this is a public user
return None


optional_viewer_dependency: type[Optional[dict]] = Annotated[
Optional[dict], Depends(optional_viewer_function)
]

# ============= EOF =============================================
90 changes: 57 additions & 33 deletions tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,41 +38,65 @@
from fastapi_pagination import add_pagination
from starlette.middleware.cors import CORSMiddleware

from core.initializers import (
register_routes,
erase_and_rebuild_db,
)
from db import Base, Parameter
from db.engine import session_ctx
from core.app import app

erase_and_rebuild_db()
register_routes(app)

app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # Allows all origins, adjust as needed for security
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)

add_pagination(app)

client = TestClient(app)

# map (name, type) to id for easy lookup in tests
parameter_map = {}
with session_ctx() as session:
for param in session.query(Parameter).all():
if (
param.parameter_name in ["groundwater level", "pH"]
and param.parameter_type == "Field Parameter"
):
parameter_map[(param.parameter_name, param.parameter_type)] = param.id

groundwater_level_parameter_id = parameter_map[("groundwater level", "Field Parameter")]
pH_parameter_id = parameter_map[("pH", "Field Parameter")]
# Initialize integration test dependencies (database, etc.)
# These are only needed for integration tests, not unit tests
_INTEGRATION_TEST_DEPS_AVAILABLE = False
try:
from core.initializers import (
register_routes,
erase_and_rebuild_db,
)
from db import Base, Parameter
from db.engine import session_ctx

erase_and_rebuild_db()
register_routes(app)

app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # Allows all origins, adjust as needed for security
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)

add_pagination(app)

client = TestClient(app)

# map (name, type) to id for easy lookup in tests
parameter_map = {}
with session_ctx() as session:
for param in session.query(Parameter).all():
if (
param.parameter_name in ["groundwater level", "pH"]
and param.parameter_type == "Field Parameter"
):
parameter_map[(param.parameter_name, param.parameter_type)] = param.id

groundwater_level_parameter_id = parameter_map[
("groundwater level", "Field Parameter")
]
pH_parameter_id = parameter_map[("pH", "Field Parameter")]

_INTEGRATION_TEST_DEPS_AVAILABLE = True
except Exception as e:
# Database not available - this is okay for unit tests
# Unit tests will use mocks instead
import warnings

warnings.warn(
f"Integration test dependencies not available: {e}. Unit tests will run with mocks."
)

# Set dummy values for unit tests
client = None
parameter_map = {}
groundwater_level_parameter_id = None
pH_parameter_id = None
Base = None


def override_authentication(default=True):
Expand Down
98 changes: 98 additions & 0 deletions tests/features/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# Data Visibility Feature Files

This directory contains feature files documenting data visibility and access control requirements during the ongoing migration from AMPAPI's legacy model to NMSampleLocations' new design.

## Active Migration: Three Approaches

### 1. AMPAPI Legacy (`public_release.feature`)
**Model:** Single `PublicRelease` Boolean field
- `True` = public, `False`/`NULL` = private
- 21 scenarios, fully implemented with unit tests
- Binary decision: data is either public or not

### 2. NMSampleLocations Current (in codebase)
**Model:** Single `release_status` enum field (default: "draft")
- Values: draft, provisional, final, published, public, private, archived
- ⚠️ Field exists but filtering NOT implemented
- ⚠️ All data currently visible to all users
- Workflow stages implied but not enforced

### 3. NMSampleLocations Proposed (`data-visibility-and-review.feature`)
**Model:** Two separate fields for independent control
- `visibility`: "internal" | "public" (who can see it)
- `review_status`: "provisional" | "approved" (quality status)
- Both fields REQUIRED, no defaults
- Supports four combinations (e.g., public+provisional, internal+approved)

## Migration Path: Two-Field Design (Recommended)

**Adopt the two-field approach** - separates "who can see" from "data quality"

### Current Implementation Mapping

**AMPAPI → NMSampleLocations (implemented in transfers/):**
```
PublicRelease Boolean → release_status
--------------------|------------------
True → "public"
False/NULL → "private"
(new records) → "draft" (default)
```

**Proposed Two-Field Design:**
```
Current release_status → (visibility, review_status)
----------------------------------------------------
draft → (internal, provisional)
provisional → (internal, provisional)
final → (internal, approved)
published → (public, approved)
public → (public, approved)
private → (internal, approved)
archived → (internal, approved)
```

**Business Concepts (from `public_release.feature`):**
- "public data" = data visible to unauthenticated users
- "private data" = data visible only to authenticated staff
- "draft data" = work in progress, staff only

### Key Business Rules to Implement

From refactored scenarios in `public_release.feature`:
- Public users see only public data
- Staff see ALL data (public, private, draft)
- New data defaults to safe visibility (private or draft)
- Data can be changed from private to public (and vice versa)
- Visibility filtering is consistent across all endpoints (API, GeoJSON, maps, reports)
- Associated data inherits visibility from parent location
- Bulk visibility changes supported for projects

### Implementation Status
- [x] Schema design documented
- [x] Legacy scenarios documented (`public_release.feature`)
- [x] New design scenarios documented (`data-visibility-and-review.feature`)
- [ ] Add `visibility` and `review_status` columns to models
- [ ] Migrate existing `release_status` data to new fields
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wrapping my head around this file/workflow. I feel like this migration step assumes the existing 'release_status' field properly migrates the legacy scenarios, so is that the first step in this process? Like ensuring testing and documenting:

  1. legacy -> existing (release_status)
    and then moving on to:
  2. existing -> new proposal (data-visibility-review)
    ?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, step 1. is now theoretically accomplished by this commit: e5074d4?new_files_changed=true
step 2 will follow.

- [ ] Implement filtering in routers (public vs. internal users)
- [ ] Add unit tests for all scenarios
- [ ] Update API schemas
- [ ] Deprecate `release_status` field

## File Status

- **`release_status.feature`** - Refactored from AMPAPI, 16 scenarios adapted to NMSampleLocations
- Uses business language (public/private) instead of technical fields
- Maps AMPAPI `PublicRelease` Boolean → NMSampleLocations `release_status` values
- Updated terminology: AMPAPI concepts → NMSampleLocations concepts
- Implemented as non-passing integration tests (requires filtering implementation)
- **`data-visibility-and-review.feature`** - Proposed two-field design, 3 active scenarios
- Other .feature files - Existing NMSampleLocations integration tests (unrelated to visibility)

## Next Steps

1. Add new columns to ReleaseMixin
2. Create Alembic migration with data transformation
3. Implement router filtering based on `visibility`
4. Port AMPAPI unit test approach to validate scenarios
5. Update client apps (Ocotillo, Weaver) to use new fields
Loading
Loading