From 3a7809d7109e683180e5854d3d433136bb27e7f9 Mon Sep 17 00:00:00 2001 From: Ricardo Garcia Silva Date: Tue, 21 Jul 2026 14:04:46 +0100 Subject: [PATCH 1/5] Added published filter for projects --- src/seis_lab_data/db/queries/projects.py | 3 +++ src/seis_lab_data/operations/projects.py | 2 ++ src/seis_lab_data/webapp/filters.py | 11 ++++++++++ .../webapp/templates/macros/checkbox.html | 22 +++++++++++++++++++ .../webapp/templates/projects/list.html | 16 +++++++------- 5 files changed, 46 insertions(+), 8 deletions(-) diff --git a/src/seis_lab_data/db/queries/projects.py b/src/seis_lab_data/db/queries/projects.py index 0b8dcb9..e48af89 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_published: bool = False, ) -> tuple[list[models.Project], int | None]: """Return all projects regardless of status. Intended for admin use.""" statement = _build_project_statement( en_name_filter, pt_name_filter, spatial_intersect, temporal_extent ) + if only_published: + 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/operations/projects.py b/src/seis_lab_data/operations/projects.py index 815edc1..f8220b6 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_published: 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_published=only_published) return await project_queries.list_projects(session, **kwargs) else: return await project_queries.list_accessible_projects( diff --git a/src/seis_lab_data/webapp/filters.py b/src/seis_lab_data/webapp/filters.py index caf62e3..9167ac2 100644 --- a/src/seis_lab_data/webapp/filters.py +++ b/src/seis_lab_data/webapp/filters.py @@ -68,6 +68,16 @@ def serialize_to_query_string(self) -> str: return result[1:] if result.startswith("&") else result +@dataclasses.dataclass +class OnlyPublishedFilter(SimpleListFilter): + internal_name = "only_published" + value: bool + + @classmethod + def from_params(cls, params: Mapping[str, str]) -> Self: + return cls(value=bool(params.get("filterOnlyPublished", False))) + + @dataclasses.dataclass class BoundingBoxFilter(SimpleListFilter): internal_name = "spatial_intersect" @@ -316,6 +326,7 @@ def from_params(cls, params: Mapping[str, str], current_language: str) -> Self: filters: dict[str, SimpleListFilter | LanguageDependantListFilter] = {} for simple_type in ( BoundingBoxFilter, + OnlyPublishedFilter, TemporalExtentFilter, EnNameFilter, PtNameFilter, 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/projects/list.html b/src/seis_lab_data/webapp/templates/projects/list.html index 1a781e8..5830cf3 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-published", + "false", + "filterOnlyPublished", + url_for("projects:get_list_component"), + _("show only published items") | capitalize + ) }}

> {{ _("searching...") | capitalize }} + {% 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/survey-missions/detail-component.html b/src/seis_lab_data/webapp/templates/survey-missions/detail-component.html index b995541..3e342e7 100644 --- a/src/seis_lab_data/webapp/templates/survey-missions/detail-component.html +++ b/src/seis_lab_data/webapp/templates/survey-missions/detail-component.html @@ -2,6 +2,7 @@ {% import "macros-buttons.html" as macros_buttons %} {% 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-related-records") | capitalize }}

- {{ macros.render_search_component( + {{ search_component( url_for("survey_missions:get_mission_records_list_component", survey_mission_id=item.id), _("search survey-related records"), initial_value=search_initial_value | default("", true), diff --git a/src/seis_lab_data/webapp/templates/survey-related-records/list.html b/src/seis_lab_data/webapp/templates/survey-related-records/list.html index cfe4c82..d6b095b 100644 --- a/src/seis_lab_data/webapp/templates/survey-related-records/list.html +++ b/src/seis_lab_data/webapp/templates/survey-related-records/list.html @@ -1,6 +1,7 @@ {% extends "base.html" %} {% import "macros.html" as macros %} {% from "macros/checkbox.html" import search_checkbox %} +{% from "macros/select.html" import search_select %} {% block head %} {{ super() }} @@ -92,6 +93,20 @@
+ {{ search_select( + "search-dataset-category", + dataset_categories, + "filterDatasetCategory", + url_for("survey_related_records:get_list_component"), + _("dataset category") | capitalize + ) }} + {{ search_select( + "search-workflow-stage", + workflow_stages, + "filterWorkflowStage", + url_for("survey_related_records:get_list_component"), + _("workflow stage") | capitalize + ) }} {{ macros.render_temporal_extent_search_component( url_for("survey_related_records:get_list_component"), _("Temporal extent begin"), From 1f55b67b7b2afa8955316255fc7ceb57dd58d602 Mon Sep 17 00:00:00 2001 From: Ricardo Garcia Silva Date: Thu, 23 Jul 2026 17:59:27 +0100 Subject: [PATCH 5/5] Added parent mission and project filters to general records listing page --- .../db/queries/datasetcategories.py | 11 ++-- src/seis_lab_data/db/queries/discovery.py | 4 +- .../db/queries/surveyrelatedrecords.py | 55 +++++++++++++------ .../db/queries/workflowstages.py | 11 ++-- .../operations/surveyrelatedrecords.py | 2 + .../schemas/surveyrelatedrecords.py | 11 ++-- src/seis_lab_data/webapp/filters.py | 38 +++++++++++++ src/seis_lab_data/webapp/routes/common.py | 30 +++++++++- src/seis_lab_data/webapp/routes/projects.py | 2 + .../webapp/routes/surveymissions.py | 2 + .../webapp/routes/surveyrelatedrecords.py | 29 ++++++++++ .../survey-related-records/list-item.html | 9 +++ .../survey-related-records/list.html | 15 +++++ tests/test_db_queries.py | 27 +++++++++ 14 files changed, 214 insertions(+), 32 deletions(-) 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/surveyrelatedrecords.py b/src/seis_lab_data/db/queries/surveyrelatedrecords.py index 25a7d19..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,6 +24,7 @@ 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, @@ -62,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( @@ -100,6 +108,7 @@ 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, @@ -118,16 +127,22 @@ 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, ) @@ -153,14 +168,14 @@ 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, ) @@ -183,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, @@ -197,6 +213,7 @@ async def list_published_survey_related_records( ) -> tuple[list[models.SurveyRelatedRecord], int | None]: statement = _build_survey_related_record_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, @@ -241,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, @@ -256,6 +274,7 @@ async def list_accessible_survey_related_records( statement = _restrict_to_accessible( _build_survey_related_record_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, @@ -377,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, @@ -393,6 +413,7 @@ async def list_survey_related_records( """Return all records. Intended for admin use.""" statement = _build_survey_related_record_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, 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/operations/surveyrelatedrecords.py b/src/seis_lab_data/operations/surveyrelatedrecords.py index c8d4eb3..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, @@ -351,6 +352,7 @@ async def list_survey_related_records( ) -> 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, 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 c440c47..d653575 100644 --- a/src/seis_lab_data/webapp/filters.py +++ b/src/seis_lab_data/webapp/filters.py @@ -3,6 +3,7 @@ import json import logging import uuid +import re from typing import ( Mapping, Protocol, @@ -232,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 @@ -428,6 +464,8 @@ def from_params(cls, params: Mapping[str, str], current_language: str) -> Self: 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 1527e59..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__) @@ -1443,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 bfc4d56..4da1b15 100644 --- a/src/seis_lab_data/webapp/routes/surveyrelatedrecords.py +++ b/src/seis_lab_data/webapp/routes/surveyrelatedrecords.py @@ -31,6 +31,7 @@ ) 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, ) @@ -44,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, ) @@ -57,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, @@ -998,6 +1003,24 @@ 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 @@ -1065,6 +1088,12 @@ async def get(self, request: Request): "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/survey-related-records/list-item.html b/src/seis_lab_data/webapp/templates/survey-related-records/list-item.html index 19af991..a34cef3 100644 --- a/src/seis_lab_data/webapp/templates/survey-related-records/list-item.html +++ b/src/seis_lab_data/webapp/templates/survey-related-records/list-item.html @@ -26,6 +26,14 @@
{{ item.name|translate_localizable_string|default(_( {% if item.dataset_category %}{{ item.dataset_category.name | translate_localizable_string }}{% else %}{{ _("unknown category") }}{% endif %} {% if item.workflow_stage %}{{ item.workflow_stage.name | translate_localizable_string }}{% else %}{{ _("unknown stage") }}{% endif %}
+
+ {{ icons.projects }} + {{ item.survey_mission.project.name | translate_localizable_string }} + {{ icons.survey_missions }} + {{ item.survey_mission.name | translate_localizable_string }} +
+ + {{ icons['status_' + item.status.value] }} {{ item.status.value }} @@ -54,6 +62,7 @@
{{ item.name|translate_localizable_string|default(_(
  • {{ item.description|translate_localizable_string|default(_("description not available"), True) }}
  • +
  • {{ item.record_assets | map(attribute="relative_path") | join(",") }}
  • diff --git a/src/seis_lab_data/webapp/templates/survey-related-records/list.html b/src/seis_lab_data/webapp/templates/survey-related-records/list.html index d6b095b..456d2a9 100644 --- a/src/seis_lab_data/webapp/templates/survey-related-records/list.html +++ b/src/seis_lab_data/webapp/templates/survey-related-records/list.html @@ -2,6 +2,7 @@ {% import "macros.html" as macros %} {% from "macros/checkbox.html" import search_checkbox %} {% from "macros/select.html" import search_select %} +{% from "macros/datalist.html" import search_datalist %} {% block head %} {{ super() }} @@ -93,6 +94,20 @@
    + {{ search_datalist( + "search-project-datalist", + filter_projects_datalist, + "filterProject", + url_for("survey_related_records:get_list_component"), + _("project") | capitalize + ) }} + {{ search_datalist( + "search-mission-datalist", + filter_missions_datalist, + "filterMission", + url_for("survey_related_records:get_list_component"), + _("mission") | capitalize + ) }} {{ search_select( "search-dataset-category", dataset_categories, diff --git a/tests/test_db_queries.py b/tests/test_db_queries.py index 3bcd1a5..5417b86 100644 --- a/tests/test_db_queries.py +++ b/tests/test_db_queries.py @@ -66,3 +66,30 @@ async def test_list_survey_related_records( session, survey_mission_id=survey_mission_id_filter, include_total=True ) assert total == expected_total + + +@pytest.mark.parametrize( + "project_id_filter, expected_total", + [ + pytest.param(None, 2), + pytest.param( + identifiers.ProjectId(uuid.UUID("74f07051-1aa9-4c08-bc27-3ecf101ab5b3")), 2 + ), + pytest.param( + identifiers.ProjectId(uuid.UUID("8f931331-15c3-4899-846c-38470f6bcb5a")), 0 + ), + ], +) +@pytest.mark.integration +@pytest.mark.asyncio +async def test_list_survey_related_records_project_id_filter( + sample_survey_related_records, + db_session_maker, + project_id_filter, + expected_total, +): + async with db_session_maker() as session: + survey_records, total = await record_queries.list_survey_related_records( + session, project_id=project_id_filter, include_total=True + ) + assert total == expected_total