diff --git a/src/seis_lab_data/db/queries/datasetcategories.py b/src/seis_lab_data/db/queries/datasetcategories.py index 0134f03..d6732e6 100644 --- a/src/seis_lab_data/db/queries/datasetcategories.py +++ b/src/seis_lab_data/db/queries/datasetcategories.py @@ -2,7 +2,10 @@ from typing import Literal from sqlmodel.ext.asyncio.session import AsyncSession -from sqlmodel import select +from sqlmodel import ( + func, + select, +) from ...db import models from ...schemas import identifiers @@ -22,7 +25,7 @@ async def list_dataset_categories( limit = page_size offset = page_size * (page - 1) statement = select(models.DatasetCategory).order_by( - models.DatasetCategory.name["en"].astext.desc() + func.lower(models.DatasetCategory.name["en"].astext) ) if en_name_filter is not None: statement = statement.where( @@ -46,8 +49,8 @@ async def collect_all_dataset_categories( order_by: Literal["name_en", "name_pt"] = "name_en", ) -> list[models.DatasetCategory]: order_by_clause = { - "name_pt": models.DatasetCategory.name["pt"].astext.desc(), - }.get(order_by, models.DatasetCategory.name["en"].astext.desc()) + "name_pt": func.lower(models.DatasetCategory.name["pt"].astext), + }.get(order_by, func.lower(models.DatasetCategory.name["en"].astext)) statement = select(models.DatasetCategory).order_by(order_by_clause) if en_name_filter is not None: diff --git a/src/seis_lab_data/db/queries/discovery.py b/src/seis_lab_data/db/queries/discovery.py index a38b806..16ed370 100644 --- a/src/seis_lab_data/db/queries/discovery.py +++ b/src/seis_lab_data/db/queries/discovery.py @@ -57,7 +57,7 @@ async def list_asset_discovery_configurations( statement = ( select(models.AssetDiscoveryConfiguration) .options(*_SELECT_IN_LOAD_OPTIONS) - .order_by(models.AssetDiscoveryConfiguration.name.asc()) + .order_by(models.AssetDiscoveryConfiguration.name) ) if name_filter is not None: statement = statement.where( @@ -84,7 +84,7 @@ async def collect_all_asset_discovery_configurations( statement = ( select(models.AssetDiscoveryConfiguration) .options(*_SELECT_IN_LOAD_OPTIONS) - .order_by(models.AssetDiscoveryConfiguration.name.asc()) + .order_by(models.AssetDiscoveryConfiguration.name) ) if name_filter is not None: statement = statement.where( diff --git a/src/seis_lab_data/db/queries/projects.py b/src/seis_lab_data/db/queries/projects.py index 0b8dcb9..fb1a022 100644 --- a/src/seis_lab_data/db/queries/projects.py +++ b/src/seis_lab_data/db/queries/projects.py @@ -125,11 +125,14 @@ async def list_projects( pt_name_filter: str | None = None, spatial_intersect: shapely.Polygon | None = None, temporal_extent: filter_schemas.TemporalExtentFilterValue | None = None, + only_internal: bool = False, ) -> tuple[list[models.Project], int | None]: - """Return all projects regardless of status. Intended for admin use.""" + """Return all projects. Intended for admin use.""" statement = _build_project_statement( en_name_filter, pt_name_filter, spatial_intersect, temporal_extent ) + if only_internal: + statement = statement.where(models.Project.status != ProjectStatus.PUBLISHED) limit = page_size offset = page_size * (page - 1) return await _exec_project_list(session, statement, limit, offset, include_total) diff --git a/src/seis_lab_data/db/queries/surveymissions.py b/src/seis_lab_data/db/queries/surveymissions.py index 33e2a4a..109eabd 100644 --- a/src/seis_lab_data/db/queries/surveymissions.py +++ b/src/seis_lab_data/db/queries/surveymissions.py @@ -141,11 +141,16 @@ async def list_survey_missions( pt_name_filter: str | None = None, spatial_intersect: shapely.Polygon | None = None, temporal_extent: filter_schemas.TemporalExtentFilterValue | None = None, + only_internal: bool = False, ) -> tuple[list[models.SurveyMission], int | None]: """Return all survey missions regardless of status. Intended for admin use.""" statement = _build_survey_mission_statement( project_id, en_name_filter, pt_name_filter, spatial_intersect, temporal_extent ) + if only_internal: + statement = statement.where( + models.SurveyMission.status != SurveyMissionStatus.PUBLISHED + ) limit = page_size offset = page_size * (page - 1) return await _exec_survey_mission_list( diff --git a/src/seis_lab_data/db/queries/surveyrelatedrecords.py b/src/seis_lab_data/db/queries/surveyrelatedrecords.py index 249ac0e..fa8996f 100644 --- a/src/seis_lab_data/db/queries/surveyrelatedrecords.py +++ b/src/seis_lab_data/db/queries/surveyrelatedrecords.py @@ -1,7 +1,7 @@ import logging import shapely -from sqlalchemy.orm import selectinload +from sqlalchemy.orm import aliased, selectinload from sqlmodel.ext.asyncio.session import AsyncSession from sqlmodel import ( exists, @@ -24,12 +24,15 @@ def _apply_survey_related_record_filters( statement, survey_mission_id: identifiers.SurveyMissionId | None = None, + project_id: identifiers.ProjectId | None = None, en_name_filter: str | None = None, pt_name_filter: str | None = None, spatial_intersect: shapely.Polygon | None = None, temporal_extent: filter_schemas.TemporalExtentFilterValue | None = None, asset_path_fragment_filter: str | None = None, record_ids: list[identifiers.SurveyRelatedRecordId] | None = None, + dataset_category_id: identifiers.DatasetCategoryId | None = None, + workflow_stage_id: identifiers.WorkflowStageId | None = None, ): """Apply the common survey-related record search filters to a statement. @@ -60,6 +63,13 @@ def _apply_survey_related_record_filters( statement = statement.where( models.SurveyRelatedRecord.survey_mission_id == survey_mission_id ) + if project_id is not None: + # aliased so this join doesn't collide with the unaliased SurveyMission + # join that `_restrict_to_accessible`/`_restrict_to_owned` add later + mission = aliased(models.SurveyMission) + statement = statement.join( + mission, models.SurveyRelatedRecord.survey_mission_id == mission.id + ).where(mission.project_id == project_id) if temporal_extent is not None: if temporal_extent.begin is not None: statement = statement.where( @@ -70,6 +80,14 @@ def _apply_survey_related_record_filters( statement = statement.where( models.SurveyRelatedRecord.temporal_extent_end <= temporal_extent.end ) + if dataset_category_id is not None: + statement = statement.where( + models.SurveyRelatedRecord.dataset_category_id == dataset_category_id + ) + if workflow_stage_id is not None: + statement = statement.where( + models.SurveyRelatedRecord.workflow_stage_id == workflow_stage_id + ) if asset_path_fragment_filter is not None: statement = statement.where( exists( @@ -90,12 +108,15 @@ def _apply_survey_related_record_filters( def _build_survey_related_record_statement( survey_mission_id: identifiers.SurveyMissionId | None = None, + project_id: identifiers.ProjectId | None = None, en_name_filter: str | None = None, pt_name_filter: str | None = None, spatial_intersect: shapely.Polygon | None = None, temporal_extent: filter_schemas.TemporalExtentFilterValue | None = None, asset_path_fragment_filter: str | None = None, record_ids: list[identifiers.SurveyRelatedRecordId] | None = None, + dataset_category_id: identifiers.DatasetCategoryId | None = None, + workflow_stage_id: identifiers.WorkflowStageId | None = None, ): statement = ( select(models.SurveyRelatedRecord) @@ -106,16 +127,24 @@ def _build_survey_related_record_statement( ) .options(selectinload(models.SurveyRelatedRecord.dataset_category)) .options(selectinload(models.SurveyRelatedRecord.workflow_stage)) + # adding all assets too, since they will always be a small list + .options(selectinload(models.SurveyRelatedRecord.assets)) + # also adding relationships with other records - only first order relationships are loaded, not the full tree + .options(selectinload(models.SurveyRelatedRecord.related_to_links)) + .options(selectinload(models.SurveyRelatedRecord.subject_links)) ) statement = _apply_survey_related_record_filters( - statement, - survey_mission_id, - en_name_filter, - pt_name_filter, - spatial_intersect, - temporal_extent, - asset_path_fragment_filter, - record_ids, + statement=statement, + survey_mission_id=survey_mission_id, + project_id=project_id, + en_name_filter=en_name_filter, + pt_name_filter=pt_name_filter, + spatial_intersect=spatial_intersect, + temporal_extent=temporal_extent, + asset_path_fragment_filter=asset_path_fragment_filter, + record_ids=record_ids, + dataset_category_id=dataset_category_id, + workflow_stage_id=workflow_stage_id, ) return statement.order_by( models.SurveyRelatedRecord.temporal_extent_end.desc().nullslast() @@ -130,6 +159,8 @@ def _build_survey_related_record_id_statement( temporal_extent: filter_schemas.TemporalExtentFilterValue | None = None, asset_path_fragment_filter: str | None = None, record_ids: list[identifiers.SurveyRelatedRecordId] | None = None, + dataset_category_id: identifiers.DatasetCategoryId | None = None, + workflow_stage_id: identifiers.WorkflowStageId | None = None, ): """Build a statement selecting only the ids of matching records. @@ -137,14 +168,16 @@ def _build_survey_related_record_id_statement( bulk-update commands), where loading full records would be wasteful. """ return _apply_survey_related_record_filters( - select(models.SurveyRelatedRecord.id), - survey_mission_id, - en_name_filter, - pt_name_filter, - spatial_intersect, - temporal_extent, - asset_path_fragment_filter, - record_ids, + statement=select(models.SurveyRelatedRecord.id), + survey_mission_id=survey_mission_id, + en_name_filter=en_name_filter, + pt_name_filter=pt_name_filter, + spatial_intersect=spatial_intersect, + temporal_extent=temporal_extent, + asset_path_fragment_filter=asset_path_fragment_filter, + record_ids=record_ids, + dataset_category_id=dataset_category_id, + workflow_stage_id=workflow_stage_id, ) @@ -165,6 +198,7 @@ async def _exec_survey_related_record_list( async def list_published_survey_related_records( session: AsyncSession, survey_mission_id: identifiers.SurveyMissionId | None = None, + project_id: identifiers.ProjectId | None = None, page: int = 1, page_size: int = 20, include_total: bool = False, @@ -174,15 +208,20 @@ async def list_published_survey_related_records( temporal_extent: filter_schemas.TemporalExtentFilterValue | None = None, asset_path_fragment_filter: str | None = None, record_ids: list[identifiers.SurveyRelatedRecordId] | None = None, + dataset_category_id: identifiers.DatasetCategoryId | None = None, + workflow_stage_id: identifiers.WorkflowStageId | None = None, ) -> tuple[list[models.SurveyRelatedRecord], int | None]: statement = _build_survey_related_record_statement( - survey_mission_id, - en_name_filter, - pt_name_filter, - spatial_intersect, - temporal_extent, - asset_path_fragment_filter, - record_ids, + survey_mission_id=survey_mission_id, + project_id=project_id, + en_name_filter=en_name_filter, + pt_name_filter=pt_name_filter, + spatial_intersect=spatial_intersect, + temporal_extent=temporal_extent, + asset_path_fragment_filter=asset_path_fragment_filter, + record_ids=record_ids, + dataset_category_id=dataset_category_id, + workflow_stage_id=workflow_stage_id, ).where(models.SurveyRelatedRecord.status == SurveyRelatedRecordStatus.PUBLISHED) limit = page_size offset = page_size * (page - 1) @@ -219,6 +258,7 @@ async def list_accessible_survey_related_records( session: AsyncSession, user_id: str, survey_mission_id: identifiers.SurveyMissionId | None = None, + project_id: identifiers.ProjectId | None = None, page: int = 1, page_size: int = 20, include_total: bool = False, @@ -228,16 +268,21 @@ async def list_accessible_survey_related_records( temporal_extent: filter_schemas.TemporalExtentFilterValue | None = None, asset_path_fragment_filter: str | None = None, record_ids: list[identifiers.SurveyRelatedRecordId] | None = None, + dataset_category_id: identifiers.DatasetCategoryId | None = None, + workflow_stage_id: identifiers.WorkflowStageId | None = None, ) -> tuple[list[models.SurveyRelatedRecord], int | None]: statement = _restrict_to_accessible( _build_survey_related_record_statement( - survey_mission_id, - en_name_filter, - pt_name_filter, - spatial_intersect, - temporal_extent, - asset_path_fragment_filter, - record_ids, + survey_mission_id=survey_mission_id, + project_id=project_id, + en_name_filter=en_name_filter, + pt_name_filter=pt_name_filter, + spatial_intersect=spatial_intersect, + temporal_extent=temporal_extent, + asset_path_fragment_filter=asset_path_fragment_filter, + record_ids=record_ids, + dataset_category_id=dataset_category_id, + workflow_stage_id=workflow_stage_id, ), user_id, ) @@ -351,6 +396,7 @@ async def count_survey_related_records_matching( async def list_survey_related_records( session: AsyncSession, survey_mission_id: identifiers.SurveyMissionId | None = None, + project_id: identifiers.ProjectId | None = None, page: int = 1, page_size: int = 20, include_total: bool = False, @@ -360,17 +406,27 @@ async def list_survey_related_records( temporal_extent: filter_schemas.TemporalExtentFilterValue | None = None, asset_path_fragment_filter: str | None = None, record_ids: list[identifiers.SurveyRelatedRecordId] | None = None, + only_internal: bool = False, + dataset_category_id: identifiers.DatasetCategoryId | None = None, + workflow_stage_id: identifiers.WorkflowStageId | None = None, ) -> tuple[list[models.SurveyRelatedRecord], int | None]: - """Return all records regardless of status. Intended for admin use.""" + """Return all records. Intended for admin use.""" statement = _build_survey_related_record_statement( - survey_mission_id, - en_name_filter, - pt_name_filter, - spatial_intersect, - temporal_extent, - asset_path_fragment_filter, - record_ids, + survey_mission_id=survey_mission_id, + project_id=project_id, + en_name_filter=en_name_filter, + pt_name_filter=pt_name_filter, + spatial_intersect=spatial_intersect, + temporal_extent=temporal_extent, + asset_path_fragment_filter=asset_path_fragment_filter, + record_ids=record_ids, + dataset_category_id=dataset_category_id, + workflow_stage_id=workflow_stage_id, ) + if only_internal: + statement = statement.where( + models.SurveyRelatedRecord.status != SurveyRelatedRecordStatus.PUBLISHED + ) limit = page_size offset = page_size * (page - 1) return await _exec_survey_related_record_list( diff --git a/src/seis_lab_data/db/queries/workflowstages.py b/src/seis_lab_data/db/queries/workflowstages.py index 597819e..d5e940c 100644 --- a/src/seis_lab_data/db/queries/workflowstages.py +++ b/src/seis_lab_data/db/queries/workflowstages.py @@ -3,7 +3,10 @@ from typing import Literal from sqlmodel.ext.asyncio.session import AsyncSession -from sqlmodel import select +from sqlmodel import ( + func, + select, +) from ...db import models from .common import _get_total_num_records @@ -22,7 +25,7 @@ async def list_workflow_stages( limit = page_size offset = page_size * (page - 1) statement = select(models.WorkflowStage).order_by( - models.WorkflowStage.name["en"].astext.desc() + models.WorkflowStage.name["en"].astext ) if en_name_filter is not None: statement = statement.where( @@ -46,8 +49,8 @@ async def collect_all_workflow_stages( order_by: Literal["name_en", "name_pt"] = "name_en", ) -> list[models.WorkflowStage]: order_by_clause = { - "name_pt": models.WorkflowStage.name["pt"].astext.desc(), - }.get(order_by, models.WorkflowStage.name["en"].astext.desc()) + "name_pt": func.lower(models.WorkflowStage.name["pt"].astext), + }.get(order_by, func.lower(models.WorkflowStage.name["en"].astext)) statement = select(models.WorkflowStage).order_by(order_by_clause) if en_name_filter is not None: diff --git a/src/seis_lab_data/localization.py b/src/seis_lab_data/localization.py index c983df4..213fccb 100644 --- a/src/seis_lab_data/localization.py +++ b/src/seis_lab_data/localization.py @@ -6,3 +6,7 @@ def translate_localizable( current_lang: str, ) -> str: return getattr(value, current_lang, value.en) or "" + + +def translate_localizable_dict(value: dict[str, str], current_lang: str) -> str: + return value.get(current_lang, "en") or "" diff --git a/src/seis_lab_data/operations/projects.py b/src/seis_lab_data/operations/projects.py index 815edc1..723ace9 100644 --- a/src/seis_lab_data/operations/projects.py +++ b/src/seis_lab_data/operations/projects.py @@ -405,6 +405,7 @@ async def list_projects( pt_name_filter: str | None = None, spatial_intersect: shapely.Polygon | None = None, temporal_extent: filter_schemas.TemporalExtentFilterValue | None = None, + only_internal: bool = False, ) -> tuple[list[models.Project], int | None]: kwargs = dict( page=page, @@ -420,6 +421,7 @@ async def list_projects( elif not {constants.ROLE_ADMIN, constants.ROLE_SYSTEM_ADMIN}.isdisjoint( initiator.roles ): + kwargs.update(only_internal=only_internal) return await project_queries.list_projects(session, **kwargs) else: return await project_queries.list_accessible_projects( diff --git a/src/seis_lab_data/operations/surveymissions.py b/src/seis_lab_data/operations/surveymissions.py index 2bd3c7b..e4a658b 100644 --- a/src/seis_lab_data/operations/surveymissions.py +++ b/src/seis_lab_data/operations/surveymissions.py @@ -428,6 +428,7 @@ async def list_survey_missions( pt_name_filter: str | None = None, spatial_intersect: shapely.Polygon | None = None, temporal_extent: filter_schemas.TemporalExtentFilterValue | None = None, + only_internal: bool = False, ) -> tuple[list[models.SurveyMission], int | None]: kwargs = dict( project_id=project_id, @@ -444,6 +445,7 @@ async def list_survey_missions( elif not {constants.ROLE_ADMIN, constants.ROLE_SYSTEM_ADMIN}.isdisjoint( initiator.roles ): + kwargs.update(only_internal=only_internal) return await mission_queries.list_survey_missions(session, **kwargs) else: return await mission_queries.list_accessible_survey_missions( diff --git a/src/seis_lab_data/operations/surveyrelatedrecords.py b/src/seis_lab_data/operations/surveyrelatedrecords.py index 643b365..410146c 100644 --- a/src/seis_lab_data/operations/surveyrelatedrecords.py +++ b/src/seis_lab_data/operations/surveyrelatedrecords.py @@ -337,6 +337,7 @@ async def list_survey_related_records( session: AsyncSession, initiator: user_schemas.User | None, survey_mission_id: identifiers.SurveyMissionId | None = None, + project_id: identifiers.ProjectId | None = None, page: int = 1, page_size: int = 20, include_total: bool = False, @@ -345,9 +346,13 @@ async def list_survey_related_records( spatial_intersect: shapely.Polygon | None = None, temporal_extent: filter_schemas.TemporalExtentFilterValue | None = None, asset_path_fragment_filter: str | None = None, + only_internal: bool = False, + dataset_category_id: identifiers.DatasetCategoryId | None = None, + workflow_stage_id: identifiers.WorkflowStageId | None = None, ) -> tuple[list[models.SurveyRelatedRecord], int | None]: kwargs = dict( survey_mission_id=survey_mission_id, + project_id=project_id, page=page, page_size=page_size, include_total=include_total, @@ -356,6 +361,8 @@ async def list_survey_related_records( spatial_intersect=spatial_intersect, temporal_extent=temporal_extent, asset_path_fragment_filter=asset_path_fragment_filter, + dataset_category_id=dataset_category_id, + workflow_stage_id=workflow_stage_id, ) if initiator is None: return await record_queries.list_published_survey_related_records( @@ -364,6 +371,7 @@ async def list_survey_related_records( elif not {constants.ROLE_ADMIN, constants.ROLE_SYSTEM_ADMIN}.isdisjoint( initiator.roles ): + kwargs.update(only_internal=only_internal) return await record_queries.list_survey_related_records(session, **kwargs) else: return await record_queries.list_accessible_survey_related_records( diff --git a/src/seis_lab_data/schemas/surveyrelatedrecords.py b/src/seis_lab_data/schemas/surveyrelatedrecords.py index 9fe4c24..ecbb984 100644 --- a/src/seis_lab_data/schemas/surveyrelatedrecords.py +++ b/src/seis_lab_data/schemas/surveyrelatedrecords.py @@ -195,6 +195,7 @@ class SurveyRelatedRecordReadListItem(pydantic.BaseModel): temporal_extent_end: Annotated[ dt.date | None, pydantic.PlainSerializer(serialize_possibly_empty_date) ] + record_assets: list[RecordAssetReadDetailEmbedded] @classmethod def from_db_instance( @@ -215,16 +216,18 @@ def from_db_instance( ) if instance.workflow_stage else None, + record_assets=[ + RecordAssetReadDetailEmbedded.model_validate( + db_asset, from_attributes=True + ) + for db_asset in instance.assets + ], ) class SurveyRelatedRecordReadDetail(SurveyRelatedRecordReadListItem): owner_id: UserId links: list[LinkSchema] = [] - survey_mission: SurveyMissionReadEmbedded - # dataset_category: DatasetCategoryReadListItem - # workflow_stage: WorkflowStageReadListItem - record_assets: list[RecordAssetReadDetailEmbedded] related_to_records: list[ tuple[LocalizableDraftDescription, SurveyRelatedRecordReadEmbedded] ] diff --git a/src/seis_lab_data/webapp/filters.py b/src/seis_lab_data/webapp/filters.py index caf62e3..d653575 100644 --- a/src/seis_lab_data/webapp/filters.py +++ b/src/seis_lab_data/webapp/filters.py @@ -2,6 +2,8 @@ import datetime as dt import json import logging +import uuid +import re from typing import ( Mapping, Protocol, @@ -11,6 +13,7 @@ import shapely +from ..schemas import identifiers from ..schemas.filters import TemporalExtentFilterValue logger = logging.getLogger(__name__) @@ -68,6 +71,23 @@ def serialize_to_query_string(self) -> str: return result[1:] if result.startswith("&") else result +@dataclasses.dataclass +class OnlyInternalFilter(SimpleListFilter): + value: bool + internal_name = "only_internal" + public_name: str = "filterOnlyInternal" + + @classmethod + def from_params(cls, params: Mapping[str, str]) -> Self: + return cls(value=bool(params.get(cls.public_name, False))) + + def serialize_to_query_string(self) -> str: + if self.value: + return f"{self.public_name}={self.value}" + else: + return "" + + @dataclasses.dataclass class BoundingBoxFilter(SimpleListFilter): internal_name = "spatial_intersect" @@ -153,15 +173,40 @@ class PtNameFilter(_StringFilter): @dataclasses.dataclass -class DatasetCategoryFilter(_StringFilter): - internal_name: str = "dataset_category_filter" - public_name: str = "dataset_category" +class DatasetCategoryFilter(SimpleListFilter): + value: identifiers.DatasetCategoryId | None + internal_name: str = "dataset_category_id" + public_name: str = "filterDatasetCategory" + + @classmethod + def from_params(cls, params: Mapping[str, str]) -> Self: + if (raw_value := params.get(cls.public_name)) is not None: + parsed = identifiers.DatasetCategoryId(uuid.UUID(raw_value)) + else: + parsed = None + logger.debug(f"inside DatasetCategoryFilter.from_params() - {locals()=}") + return cls(value=parsed) + + def serialize_to_query_string(self) -> str: + return f"{self.public_name}={str(self.value)}" if self.value else "" @dataclasses.dataclass -class WorkflowStageFilter(_StringFilter): - internal_name: str = "workflow_stage_filter" - public_name: str = "workflow_stage" +class WorkflowStageFilter(SimpleListFilter): + value: identifiers.WorkflowStageId | None + internal_name: str = "workflow_stage_id" + public_name: str = "filterWorkflowStage" + + @classmethod + def from_params(cls, params: Mapping[str, str]) -> Self: + if (raw_value := params.get(cls.public_name)) is not None: + parsed = identifiers.WorkflowStageId(uuid.UUID(raw_value)) + else: + parsed = None + return cls(value=parsed) + + def serialize_to_query_string(self) -> str: + return f"{self.public_name}={str(self.value)}" if self.value else "" @dataclasses.dataclass @@ -188,6 +233,41 @@ def serialize_to_query_string(self) -> str: return "" +_compound_name_id_regex = re.compile(r" (?P\w{8}-\w{4}-\w{4}-\w{4}-\w{12})$") + + +@dataclasses.dataclass +class ProjectCompoundNameFilter(_StringFilter): + internal_name: str = "project_id" + public_name: str = "filterProject" + value: identifiers.ProjectId | None + + @classmethod + def from_params(cls, params: Mapping[str, str]) -> Self: + raw_value = params.get(cls.public_name, "") + if (re_obj := _compound_name_id_regex.search(raw_value)) is not None: + return cls(value=identifiers.ProjectId(uuid.UUID(re_obj.groupdict()["id"]))) + else: + return cls(value=None) + + +@dataclasses.dataclass +class SurveyMissionCompoundNameFilter(_StringFilter): + internal_name: str = "survey_mission_id" + public_name: str = "filterMission" + value: identifiers.SurveyMissionId | None + + @classmethod + def from_params(cls, params: Mapping[str, str]) -> Self: + raw_value = params.get(cls.public_name, "") + if (re_obj := _compound_name_id_regex.search(raw_value)) is not None: + return cls( + value=identifiers.SurveyMissionId(uuid.UUID(re_obj.groupdict()["id"])) + ) + else: + return cls(value=None) + + @dataclasses.dataclass class SearchNameFilter(LanguageDependantListFilter): internal_name: str @@ -237,7 +317,9 @@ def serialize_to_query_string(self) -> str: return f"?{result}" if result != "" else "" def as_kwargs(self) -> dict: - return {f.internal_name: f.value for f in self.filters.values()} + return { + f.internal_name: f.value for f in self.filters.values() if f.value != "" + } @dataclasses.dataclass @@ -316,6 +398,7 @@ def from_params(cls, params: Mapping[str, str], current_language: str) -> Self: filters: dict[str, SimpleListFilter | LanguageDependantListFilter] = {} for simple_type in ( BoundingBoxFilter, + OnlyInternalFilter, TemporalExtentFilter, EnNameFilter, PtNameFilter, @@ -346,9 +429,8 @@ def from_params(cls, params: Mapping[str, str], current_language: str) -> Self: BoundingBoxFilter, TemporalExtentFilter, EnNameFilter, + OnlyInternalFilter, PtNameFilter, - DatasetCategoryFilter, - WorkflowStageFilter, ProjectIdFilter, ): try: @@ -378,9 +460,12 @@ def from_params(cls, params: Mapping[str, str], current_language: str) -> Self: TemporalExtentFilter, EnNameFilter, PtNameFilter, + OnlyInternalFilter, DatasetCategoryFilter, WorkflowStageFilter, SurveyMissionIdFilter, + SurveyMissionCompoundNameFilter, + ProjectCompoundNameFilter, PathFragmentFilter, ): try: diff --git a/src/seis_lab_data/webapp/routes/common.py b/src/seis_lab_data/webapp/routes/common.py index 300d861..653896c 100644 --- a/src/seis_lab_data/webapp/routes/common.py +++ b/src/seis_lab_data/webapp/routes/common.py @@ -13,7 +13,12 @@ from starlette.requests import Request from ...localization import translate_localizable -from ...schemas import identifiers, surveyrelatedrecords as record_schemas +from ...schemas import ( + identifiers, + projects as project_schemas, + surveymissions as mission_schemas, + surveyrelatedrecords as record_schemas, +) logger = logging.getLogger(__name__) @@ -130,3 +135,26 @@ def build_related_record_compound_name( leeway=0, ) return f"{current_name} ({current_mission_name} - {current_project_name}) - {survey_related_record.id}" + + +def build_mission_compound_name( + request: Request, survey_mission: mission_schemas.SurveyMissionReadListItem +) -> str: + current_language = request.state.language + current_name = translate_localizable(survey_mission.name, current_language) + current_project_name = do_truncate( + request.state.templates.env, + translate_localizable(survey_mission.project.name, current_language), + length=15, + killwords=True, + leeway=0, + ) + return f"{current_name} ({current_project_name}) - {survey_mission.id}" + + +def build_project_compound_name( + request: Request, project: project_schemas.ProjectReadListItem +) -> str: + current_language = request.state.language + current_name = translate_localizable(project.name, current_language) + return f"{current_name} - {project.id}" diff --git a/src/seis_lab_data/webapp/routes/projects.py b/src/seis_lab_data/webapp/routes/projects.py index 66bbba1..2d9e735 100644 --- a/src/seis_lab_data/webapp/routes/projects.py +++ b/src/seis_lab_data/webapp/routes/projects.py @@ -58,6 +58,7 @@ get_pagination_info, UPDATE_BASEMAP_JS_SCRIPT, ) +from .datalist import get_projects_datalist logger = logging.getLogger(__name__) @@ -1066,6 +1067,7 @@ async def event_streamer(): routes = [ Route("/", ProjectCollectionEndpoint, name="list"), + Route("/datalist", get_projects_datalist, name="get_datalist"), Route("/stream", stream_to_list_page, name="list_stream"), Route("/search", get_list_component, name="get_list_component"), Route( diff --git a/src/seis_lab_data/webapp/routes/surveymissions.py b/src/seis_lab_data/webapp/routes/surveymissions.py index a82329e..52e1c26 100644 --- a/src/seis_lab_data/webapp/routes/surveymissions.py +++ b/src/seis_lab_data/webapp/routes/surveymissions.py @@ -60,6 +60,7 @@ get_pagination_info, UPDATE_BASEMAP_JS_SCRIPT, ) +from .datalist import get_missions_datalist logger = logging.getLogger(__name__) @@ -352,6 +353,7 @@ async def get_mission_records_list_component(request: Request): else: internal_filter_kwargs = {} filter_query_string = "" + logger.debug(f"{internal_filter_kwargs=}") current_page = get_page_from_request_params(request) settings: config.SeisLabDataSettings = request.state.settings user = request.user if request.user.is_authenticated else None @@ -1442,6 +1444,7 @@ async def event_streamer(): methods=["GET"], name="list", ), + Route("/datalist", get_missions_datalist, name="get_datalist"), Route( "/search", get_list_component, diff --git a/src/seis_lab_data/webapp/routes/surveyrelatedrecords.py b/src/seis_lab_data/webapp/routes/surveyrelatedrecords.py index f2d62a4..4da1b15 100644 --- a/src/seis_lab_data/webapp/routes/surveyrelatedrecords.py +++ b/src/seis_lab_data/webapp/routes/surveyrelatedrecords.py @@ -26,10 +26,12 @@ constants, errors, geojson, + localization, subscribers, ) from ...constants import SURVEY_RELATED_RECORD_MAX_RELATED from ...operations import ( + projects as project_ops, surveymissions as survey_mission_ops, surveyrelatedrecords as survey_related_record_ops, ) @@ -43,6 +45,8 @@ from ...schemas import ( common as common_schemas, identifiers, + projects as project_schemas, + surveymissions as mission_schemas, surveyrelatedrecords as record_schemas, webui as webui_schemas, ) @@ -56,6 +60,8 @@ ) from .common import ( build_related_record_compound_name, + build_mission_compound_name, + build_project_compound_name, get_id_from_request_path, get_page_from_request_params, get_pagination_info, @@ -997,6 +1003,48 @@ async def get(self, request: Request): settings: config.SeisLabDataSettings = request.state.settings user = request.user if request.user.is_authenticated else None async with settings.get_db_session_maker()() as session: + some_db_projects = ( + await project_ops.list_projects( + session, initiator=user, include_total=False + ) + )[0] + some_projects = [ + project_schemas.ProjectReadListItem.from_db_instance(i) + for i in some_db_projects + ] + some_db_missions = ( + await survey_mission_ops.list_survey_missions( + session, initiator=user, include_total=False + ) + )[0] + some_missions = [ + mission_schemas.SurveyMissionReadListItem.from_db_instance(i) + for i in some_db_missions + ] + dataset_category_filter_options = [] + for ( + dataset_category + ) in await category_queries.collect_all_dataset_categories(session): + dataset_category_filter_options.append( + ( + dataset_category.id, + localization.translate_localizable_dict( + dataset_category.name, request.state.language + ), + ) + ) + workflow_stage_filter_options = [] + for workflow_stage in await stage_queries.collect_all_workflow_stages( + session + ): + workflow_stage_filter_options.append( + ( + workflow_stage.id, + localization.translate_localizable_dict( + workflow_stage.name, request.state.language + ), + ) + ) ( items, num_total, @@ -1038,6 +1086,14 @@ async def get(self, request: Request): "items": serialized_items, "geojson_features": json.dumps(geojson_features), "pagination": pagination_info, + "dataset_categories": dataset_category_filter_options, + "workflow_stages": workflow_stage_filter_options, + "filter_projects_datalist": [ + build_project_compound_name(request, i) for i in some_projects + ], + "filter_missions_datalist": [ + build_mission_compound_name(request, i) for i in some_missions + ], "map_bounds": { "min_lon": min_lon, "min_lat": min_lat, diff --git a/src/seis_lab_data/webapp/templates/macros.html b/src/seis_lab_data/webapp/templates/macros.html index 3a88203..d266248 100644 --- a/src/seis_lab_data/webapp/templates/macros.html +++ b/src/seis_lab_data/webapp/templates/macros.html @@ -197,18 +197,26 @@

{{ _("link") | capitalize }} {{ loop.index0 + 1 }}

{% endmacro %} -{% macro render_search_component(search_url, label_text, initial_value="", set_autofocus=False, extra_on_input_js="") %} -
-
- + +{% macro render_search_component( + search_url, + label_text, + initial_value="", + set_autofocus=False, + extra_on_input_js="" +) %} +
+
+ {{ _("link") | capitalize }} {{ loop.index0 + 1 }} > {{ _("searching...") | capitalize }}
+
{% endmacro %} diff --git a/src/seis_lab_data/webapp/templates/macros/checkbox.html b/src/seis_lab_data/webapp/templates/macros/checkbox.html index 7e92f94..d32fd06 100644 --- a/src/seis_lab_data/webapp/templates/macros/checkbox.html +++ b/src/seis_lab_data/webapp/templates/macros/checkbox.html @@ -28,3 +28,25 @@
{% endmacro %} + + +{% macro search_checkbox( + id, + initial_value, + bind_signal, + search_url, + label +) %} +
+ + +
+{% endmacro %} diff --git a/src/seis_lab_data/webapp/templates/macros/search.html b/src/seis_lab_data/webapp/templates/macros/search.html new file mode 100644 index 0000000..832349b --- /dev/null +++ b/src/seis_lab_data/webapp/templates/macros/search.html @@ -0,0 +1,50 @@ +{% from "macros/checkbox.html" import search_checkbox %} + +{% macro search_component( + search_url, + label_text, + initial_value="", + set_autofocus=False, + extra_on_input_js="" +) %} +
+
+ + + + + {{ search_checkbox( + "search-only-internal", + "true", + "filterOnlyInternal", + search_url, + _("show only internal items") | capitalize + ) }} + + +
+
+{% endmacro %} diff --git a/src/seis_lab_data/webapp/templates/macros/select.html b/src/seis_lab_data/webapp/templates/macros/select.html new file mode 100644 index 0000000..3764cae --- /dev/null +++ b/src/seis_lab_data/webapp/templates/macros/select.html @@ -0,0 +1,25 @@ +{% macro search_select( + id, + options, + bind_signal, + search_url, + label, + selected_option=None +) %} +
+
+ + +
+
+{% endmacro %} diff --git a/src/seis_lab_data/webapp/templates/projects/detail-component.html b/src/seis_lab_data/webapp/templates/projects/detail-component.html index a69c84b..b64b457 100644 --- a/src/seis_lab_data/webapp/templates/projects/detail-component.html +++ b/src/seis_lab_data/webapp/templates/projects/detail-component.html @@ -1,6 +1,7 @@ {% import "macros.html" as macros %} {% import "macros-buttons-list-items.html" as macros_buttons_list_items %} {% import "macros-maps.html" as macros_maps %} +{% from "macros/search.html" import search_component %}
>

{{ _("survey missions") | capitalize }}

- {{ macros.render_search_component( + {{ search_component( url_for("projects:get_project_missions_list_component", project_id=item.id), _("search project survey-missions"), initial_value=search_initial_value | default("", true) diff --git a/src/seis_lab_data/webapp/templates/projects/list.html b/src/seis_lab_data/webapp/templates/projects/list.html index 1a781e8..f1f0f98 100644 --- a/src/seis_lab_data/webapp/templates/projects/list.html +++ b/src/seis_lab_data/webapp/templates/projects/list.html @@ -4,6 +4,7 @@ #} {% extends "base.html" %} {% import "macros.html" as macros %} +{% from "macros/checkbox.html" import search_checkbox %} {% block head %} {{ super() }} @@ -58,14 +59,13 @@

{{ _("search projects") | capitalize }} - + {{ search_checkbox( + "search-only-internal", + "false", + "filterOnlyInternal", + url_for("projects:get_list_component"), + _("show only internal projects") | capitalize + ) }}