diff --git a/src/seis_lab_data/config.py b/src/seis_lab_data/config.py index 7be533e..1310c9a 100644 --- a/src/seis_lab_data/config.py +++ b/src/seis_lab_data/config.py @@ -55,6 +55,7 @@ class SeisLabDataIconSettings(BaseModel): expand_less: str = "expand_less" expand_more: str = "expand_more" status_draft: str = "design_services" + status_other: str = "question_mark" status_published: str = "public" status_under_validation: str = "sync" survey_missions: str = "directions_boat" diff --git a/src/seis_lab_data/dispatch.py b/src/seis_lab_data/dispatch.py index a9770d8..a8179c1 100644 --- a/src/seis_lab_data/dispatch.py +++ b/src/seis_lab_data/dispatch.py @@ -55,6 +55,7 @@ async def __call__(self, event: events.SeisLabDataEvent) -> None: await self._redis.publish( channel=event.resource_type.get_topic_name(), message=messages.ResourceStatusChangedMessage( + request_id=event.request_id, resource_type=event.resource_type, resource_id=event.resource_id, succeeded=event.succeeded, diff --git a/src/seis_lab_data/operations/projects.py b/src/seis_lab_data/operations/projects.py index a29249d..f99073e 100644 --- a/src/seis_lab_data/operations/projects.py +++ b/src/seis_lab_data/operations/projects.py @@ -1,4 +1,3 @@ -import asyncio import logging import pydantic @@ -94,6 +93,7 @@ async def change_project_status( await event_dispatcher( event_schemas.ResourceStatusChangedEvent( initiator=initiator.id, + request_id=request_id, resource_type=constants.ResourceType.PROJECT, resource_id=str(project_id), succeeded=False, @@ -105,6 +105,7 @@ async def change_project_status( await event_dispatcher( event_schemas.ResourceStatusChangedEvent( initiator=initiator.id, + request_id=request_id, resource_type=constants.ResourceType.PROJECT, resource_id=str(project_id), succeeded=True, @@ -115,6 +116,7 @@ async def change_project_status( async def validate_project( + *, request_id: identifiers.RequestId, project_id: identifiers.ProjectId, initiator: user_schemas.User, @@ -153,8 +155,7 @@ async def validate_project( session=session, event_dispatcher=event_dispatcher, ) - await asyncio.sleep(3) - validation_schemas.ValidProject.model_validate(project) + validation_schemas.ValidProject.model_validate(project, from_attributes=True) except pydantic.ValidationError as err: for error in err.errors(): validation_errors.append( @@ -177,6 +178,14 @@ async def validate_project( session, project, validation_result={"is_valid": True, "errors": None} ) finally: + await change_project_status( + request_id=request_id, + target_status=constants.ProjectStatus.DRAFT, + project_id=project_id, + initiator=initiator, + session=session, + event_dispatcher=event_dispatcher, + ) await event_dispatcher( event_schemas.ValidationEvent( initiator=initiator.id, @@ -189,14 +198,6 @@ async def validate_project( details=str(validation_errors), ) ) - await change_project_status( - request_id=request_id, - target_status=constants.ProjectStatus.DRAFT, - project_id=project_id, - initiator=initiator, - session=session, - event_dispatcher=event_dispatcher, - ) return project @@ -236,6 +237,13 @@ async def update_project( ) return None + await validate_project( + request_id=request_id, + project_id=project_id, + initiator=initiator, + session=session, + event_dispatcher=event_dispatcher, + ) await event_dispatcher( event_schemas.ResourceModificationEvent( initiator=initiator.id, diff --git a/src/seis_lab_data/operations/surveymissions.py b/src/seis_lab_data/operations/surveymissions.py index 4f1060b..4be970f 100644 --- a/src/seis_lab_data/operations/surveymissions.py +++ b/src/seis_lab_data/operations/surveymissions.py @@ -1,4 +1,3 @@ -import asyncio import logging import pydantic @@ -120,6 +119,7 @@ async def change_survey_mission_status( except errors.SeisLabDataError as err: await event_dispatcher( event_schemas.ResourceStatusChangedEvent( + request_id=request_id, initiator=initiator.id, resource_type=constants.ResourceType.MISSION, resource_id=str(survey_mission_id), @@ -131,6 +131,7 @@ async def change_survey_mission_status( return None await event_dispatcher( event_schemas.ResourceStatusChangedEvent( + request_id=request_id, initiator=initiator.id, resource_type=constants.ResourceType.MISSION, resource_id=str(survey_mission_id), @@ -142,6 +143,7 @@ async def change_survey_mission_status( async def validate_survey_mission( + *, request_id: identifiers.RequestId, survey_mission_id: identifiers.SurveyMissionId, initiator: user_schemas.User, @@ -188,8 +190,9 @@ async def validate_survey_mission( session=session, event_dispatcher=event_dispatcher, ) - await asyncio.sleep(3) - validation_schemas.ValidSurveyMission.model_validate(survey_mission) + validation_schemas.ValidSurveyMission.model_validate( + survey_mission, from_attributes=True + ) except pydantic.ValidationError as err: for error in err.errors(): validation_errors.append( @@ -284,6 +287,13 @@ async def update_survey_mission( ) return None + await validate_survey_mission( + request_id=request_id, + survey_mission_id=survey_mission_id, + initiator=initiator, + session=session, + event_dispatcher=event_dispatcher, + ) await event_dispatcher( event_schemas.ResourceModificationEvent( resource_type=constants.ResourceType.MISSION, diff --git a/src/seis_lab_data/operations/surveyrelatedrecords.py b/src/seis_lab_data/operations/surveyrelatedrecords.py index 0e473b6..6db3410 100644 --- a/src/seis_lab_data/operations/surveyrelatedrecords.py +++ b/src/seis_lab_data/operations/surveyrelatedrecords.py @@ -1,4 +1,3 @@ -import asyncio import logging import pydantic @@ -149,6 +148,7 @@ async def change_survey_related_record_status( return None await event_dispatcher( event_schemas.ResourceStatusChangedEvent( + request_id=request_id, initiator=initiator.id, resource_type=constants.ResourceType.RECORD, resource_id=str(survey_related_record_id), @@ -160,6 +160,7 @@ async def change_survey_related_record_status( async def validate_survey_related_record( + *, request_id: identifiers.RequestId, survey_related_record_id: identifiers.SurveyRelatedRecordId, initiator: user_schemas.User, @@ -206,9 +207,8 @@ async def validate_survey_related_record( session=session, event_dispatcher=event_dispatcher, ) - await asyncio.sleep(3) validation_schemas.ValidSurveyRelatedRecord.model_validate( - survey_related_record + survey_related_record, from_attributes=True ) except pydantic.ValidationError as err: for error in err.errors(): @@ -437,6 +437,13 @@ async def update_survey_related_record( updated_survey_related_record = await record_commands.update_survey_related_record( session, survey_related_record, to_update ) + await validate_survey_related_record( + request_id=request_id, + survey_related_record_id=survey_related_record_id, + initiator=initiator, + session=session, + event_dispatcher=event_dispatcher, + ) await event_dispatcher( event_schemas.ResourceModificationEvent( initiator=initiator.id, diff --git a/src/seis_lab_data/schemas/events.py b/src/seis_lab_data/schemas/events.py index 129ae61..6dbd114 100644 --- a/src/seis_lab_data/schemas/events.py +++ b/src/seis_lab_data/schemas/events.py @@ -41,6 +41,7 @@ class BulkResourceModificationEvent(_EventBase): @dataclasses.dataclass(frozen=True, kw_only=True) class ResourceStatusChangedEvent(_EventBase): + request_id: identifiers.RequestId resource_type: constants.ResourceType resource_id: str | None succeeded: bool diff --git a/src/seis_lab_data/schemas/messages.py b/src/seis_lab_data/schemas/messages.py index a27e43d..57ca402 100644 --- a/src/seis_lab_data/schemas/messages.py +++ b/src/seis_lab_data/schemas/messages.py @@ -39,6 +39,7 @@ class BulkResourceModificationMessage(pydantic.BaseModel): class ResourceStatusChangedMessage(pydantic.BaseModel): type: Literal["resource_status_changed"] = "resource_status_changed" + request_id: identifiers.RequestId resource_type: constants.ResourceType resource_id: str | None succeeded: bool diff --git a/src/seis_lab_data/schemas/validation.py b/src/seis_lab_data/schemas/validation.py index 314c0d6..509f4ef 100644 --- a/src/seis_lab_data/schemas/validation.py +++ b/src/seis_lab_data/schemas/validation.py @@ -118,7 +118,7 @@ class ValidSurveyMission(pydantic.BaseModel): status: constants.SurveyMissionStatus temporal_extent_begin: dt.date | None temporal_extent_end: dt.date | None - owner: identifiers.UserId + owner_id: identifiers.UserId relative_path: Annotated[str, pydantic.PlainValidator(ensure_relative_path_exists)] links: list[ValidLinkSchema] = [] bbox_4326: Annotated[ @@ -138,8 +138,7 @@ class ValidSurveyRelatedRecord(pydantic.BaseModel): status: constants.SurveyMissionStatus temporal_extent_begin: dt.date | None temporal_extent_end: dt.date | None - owner: identifiers.UserId - relative_path: Annotated[str, pydantic.PlainValidator(ensure_relative_path_exists)] + owner_id: identifiers.UserId links: list[ValidLinkSchema] = [] bbox_4326: Annotated[ shapely.Polygon, diff --git a/src/seis_lab_data/tasks/projects.py b/src/seis_lab_data/tasks/projects.py index d5cc612..30c2a60 100644 --- a/src/seis_lab_data/tasks/projects.py +++ b/src/seis_lab_data/tasks/projects.py @@ -75,22 +75,3 @@ async def delete_project( session=session, event_dispatcher=settings.get_event_dispatcher(), ) - - -@dramatiq.actor -@decorators.sld_settings -async def validate_project( - raw_request_id: str, - raw_project_id: str, - raw_initiator: str, - *, - settings: config.SeisLabDataSettings, -): - async with settings.get_db_session_maker()() as session: - await project_ops.validate_project( - request_id=identifiers.RequestId(uuid.UUID(raw_request_id)), - project_id=identifiers.ProjectId(uuid.UUID(raw_project_id)), - initiator=user_schemas.User(**json.loads(raw_initiator)), - session=session, - event_dispatcher=settings.get_event_dispatcher(), - ) diff --git a/src/seis_lab_data/tasks/surveymissions.py b/src/seis_lab_data/tasks/surveymissions.py index 6d159f3..19a3408 100644 --- a/src/seis_lab_data/tasks/surveymissions.py +++ b/src/seis_lab_data/tasks/surveymissions.py @@ -83,24 +83,3 @@ async def delete_survey_mission( session=session, event_dispatcher=settings.get_event_dispatcher(), ) - - -@dramatiq.actor -@decorators.sld_settings -async def validate_survey_mission( - raw_request_id: str, - raw_survey_mission_id: str, - raw_initiator: str, - *, - settings: config.SeisLabDataSettings, -): - async with settings.get_db_session_maker()() as session: - await survey_mission_ops.validate_survey_mission( - request_id=identifiers.RequestId(uuid.UUID(raw_request_id)), - survey_mission_id=identifiers.SurveyMissionId( - uuid.UUID(raw_survey_mission_id) - ), - initiator=user_schemas.User(**json.loads(raw_initiator)), - session=session, - event_dispatcher=settings.get_event_dispatcher(), - ) diff --git a/src/seis_lab_data/tasks/surveyrelatedrecords.py b/src/seis_lab_data/tasks/surveyrelatedrecords.py index a5bcb87..f8408d3 100644 --- a/src/seis_lab_data/tasks/surveyrelatedrecords.py +++ b/src/seis_lab_data/tasks/surveyrelatedrecords.py @@ -119,24 +119,3 @@ async def bulk_update_survey_related_records( temporal_extent=selection.temporal_extent, asset_path_fragment_filter=selection.asset_path_fragment_filter, ) - - -@dramatiq.actor -@decorators.sld_settings -async def validate_survey_related_record( - raw_request_id: str, - raw_survey_related_record_id: str, - raw_initiator: str, - *, - settings: config.SeisLabDataSettings, -): - async with settings.get_db_session_maker()() as session: - await record_ops.validate_survey_related_record( - request_id=identifiers.RequestId(uuid.UUID(raw_request_id)), - survey_related_record_id=identifiers.SurveyRelatedRecordId( - uuid.UUID(raw_survey_related_record_id) - ), - initiator=user_schemas.User(**json.loads(raw_initiator)), - session=session, - event_dispatcher=settings.get_event_dispatcher(), - ) diff --git a/src/seis_lab_data/webapp/app.py b/src/seis_lab_data/webapp/app.py index f9b42e3..5d9888c 100644 --- a/src/seis_lab_data/webapp/app.py +++ b/src/seis_lab_data/webapp/app.py @@ -101,7 +101,7 @@ async def lifespan(app: Starlette) -> AsyncIterator[State]: ) jinja_env.filters["secondary_language"] = jinjafilters.get_secondary_language_value jinja_env.filters["translate_enum"] = jinjafilters.translate_enum - jinja_env.filters["get_status_icon_name"] = jinjafilters.get_status_icon_name + jinja_env.filters["status_icon"] = jinjafilters.get_status_icon_name jinja_env.filters["highlight_json"] = jinjafilters.highlight_json jinja_env.filters["asset_url"] = jinjafilters.get_url_for_asset configure_jinja_env(jinja_env) diff --git a/src/seis_lab_data/webapp/jinjafilters.py b/src/seis_lab_data/webapp/jinjafilters.py index f5ac4df..ce9cbba 100644 --- a/src/seis_lab_data/webapp/jinjafilters.py +++ b/src/seis_lab_data/webapp/jinjafilters.py @@ -11,17 +11,31 @@ from pygments.formatters import HtmlFormatter from pygments.lexers import JsonLexer -from ..constants import ( - ProjectStatus, - TranslatableEnumProtocol, -) +from .. import constants from ..schemas.common import Localizable from ..localization import translate_localizable if typing.TYPE_CHECKING: from ..config import SeisLabDataSettings + from ..schemas import projects as project_schemas + from ..schemas import surveymissions as mission_schemas from ..schemas import surveyrelatedrecords as record_schemas + ItemWithStatus = typing.TypeVar( + "ItemWithStatus", + bound=( + project_schemas.ProjectReadDetail, + project_schemas.ProjectReadEmbedded, + project_schemas.ProjectReadListItem, + mission_schemas.SurveyMissionReadDetail, + mission_schemas.SurveyMissionReadEmbedded, + mission_schemas.SurveyMissionReadListItem, + record_schemas.SurveyRelatedRecordReadDetail, + record_schemas.SurveyRelatedRecordReadEmbedded, + record_schemas.SurveyRelatedRecordReadListItem, + ), + ) + logger = logging.getLogger(__name__) @@ -42,17 +56,32 @@ def translate_localizable_string( @pass_context -def get_status_icon_name(context: dict[str, typing.Any], status: ProjectStatus) -> str: +def get_status_icon_name( + context: dict[str, typing.Any], + item: "ItemWithStatus", +) -> str: return { - ProjectStatus.DRAFT: context.get("icons", {}).get("status_draft", ""), - ProjectStatus.UNDER_VALIDATION: context.get("icons", {}).get( - "status_under_validation", "" - ), - ProjectStatus.PUBLISHED: context.get("icons", {}).get("status_published", ""), - }.get(status, "") - - -def translate_enum(value: TranslatableEnumProtocol) -> str: + constants.ProjectStatus.DRAFT: context["icons"]["status_draft"], + constants.ProjectStatus.UNDER_VALIDATION: context["icons"][ + "status_under_validation" + ], + constants.ProjectStatus.PUBLISHED: context["icons"]["status_published"], + constants.SurveyMissionStatus.DRAFT: context["icons"]["status_draft"], + constants.SurveyMissionStatus.UNDER_VALIDATION: context["icons"][ + "status_under_validation" + ], + constants.SurveyMissionStatus.PUBLISHED: context["icons"]["status_published"], + constants.SurveyRelatedRecordStatus.DRAFT: context["icons"]["status_draft"], + constants.SurveyRelatedRecordStatus.UNDER_VALIDATION: context["icons"][ + "status_under_validation" + ], + constants.SurveyRelatedRecordStatus.PUBLISHED: context["icons"][ + "status_published" + ], + }.get(item.status, context["icons"]["status_other"]) + + +def translate_enum(value: constants.TranslatableEnumProtocol) -> str: return value.get_translated_value() @@ -70,14 +99,14 @@ def highlight_json(value: dict) -> Markup: def get_url_for_asset( context: dict[str, typing.Any], asset: "record_schemas.RecordAssetReadDetailEmbedded", - survey_related_record: "record_schemas.SurveyRelatedRecordReadDetail", + item: "record_schemas.SurveyRelatedRecordReadDetail", ) -> str: settings: SeisLabDataSettings = context.get("settings") return "/".join( ( settings.public_url, - survey_related_record.survey_mission.project.root_path, - survey_related_record.survey_mission.relative_path, + item.survey_mission.project.root_path, + item.survey_mission.relative_path, asset.relative_path, ) ) diff --git a/src/seis_lab_data/webapp/routes/projects.py b/src/seis_lab_data/webapp/routes/projects.py index d2ef358..21c1569 100644 --- a/src/seis_lab_data/webapp/routes/projects.py +++ b/src/seis_lab_data/webapp/routes/projects.py @@ -332,6 +332,7 @@ async def stream_to_detail_page(request: Request): ), { "resource_modified": common_handlers.handle_resource_modification_detail_page, + "resource_status_changed": common_handlers.handle_resource_status_changed_detail_page, }, ) @@ -671,7 +672,7 @@ async def get(self, request: Request): "projects/detail.html", context={ "request_id": uuid.uuid4(), - "project": details.item, + "item": details.item, "pagination": details.pagination, "survey_missions": details.children, "search_initial_value": details.children_filter, @@ -1051,17 +1052,6 @@ async def event_streamer(): return DatastarResponse(event_streamer()) -@csrf_protect -@requires_auth -async def trigger_project_validation(request: Request): - project_tasks.validate_project.send( - raw_request_id=str(uuid.uuid4()), - raw_project_id=str(uuid.UUID(request.path_params.get("project_id"))), - raw_initiator=json.dumps(dataclasses.asdict(request.user)), - ) # noqa - return Response(status_code=200) - - routes = [ Route("/", ProjectCollectionEndpoint, name="list"), Route("/stream", stream_to_list_page, name="list_stream"), @@ -1132,12 +1122,6 @@ async def trigger_project_validation(request: Request): methods=["GET"], name="detail_stream", ), - Route( - "/{project_id}/validate", - trigger_project_validation, - methods=["POST"], - name="trigger_validation", - ), Route( "/{project_id}", ProjectDetailEndpoint, diff --git a/src/seis_lab_data/webapp/routes/surveymissions.py b/src/seis_lab_data/webapp/routes/surveymissions.py index d2850c5..6bf9291 100644 --- a/src/seis_lab_data/webapp/routes/surveymissions.py +++ b/src/seis_lab_data/webapp/routes/surveymissions.py @@ -593,7 +593,7 @@ async def get(self, request: Request): "survey-missions/detail.html", context={ "request_id": uuid.uuid4(), - "survey_mission": details.item, + "item": details.item, "pagination": details.pagination, "survey_related_records": details.children, "search_initial_value": details.children_filter, diff --git a/src/seis_lab_data/webapp/routes/surveyrelatedrecords.py b/src/seis_lab_data/webapp/routes/surveyrelatedrecords.py index 025a08d..566f68e 100644 --- a/src/seis_lab_data/webapp/routes/surveyrelatedrecords.py +++ b/src/seis_lab_data/webapp/routes/surveyrelatedrecords.py @@ -1072,7 +1072,7 @@ async def get(self, request: Request): "survey-related-records/detail.html", context={ "request_id": uuid.uuid4(), - "survey_related_record": details.item, + "item": details.item, "permissions": details.permissions, "breadcrumbs": details.breadcrumbs, }, diff --git a/src/seis_lab_data/webapp/streamhandlers/common.py b/src/seis_lab_data/webapp/streamhandlers/common.py index c2d005d..da639eb 100644 --- a/src/seis_lab_data/webapp/streamhandlers/common.py +++ b/src/seis_lab_data/webapp/streamhandlers/common.py @@ -1,11 +1,12 @@ -import dataclasses import json import asyncio import logging import time import uuid from collections.abc import AsyncGenerator +from functools import partial +from datastar_py.consts import ElementPatchMode from datastar_py.sse import ServerSentEventGenerator from datastar_py.starlette import DatastarEvent @@ -14,6 +15,7 @@ subscribers, ) from ...operations import ( + projects as project_ops, surveymissions as mission_ops, surveyrelatedrecords as record_ops, ) @@ -22,11 +24,6 @@ messages as message_schemas, webui as webui_schemas, ) -from ...tasks import ( - projects as project_tasks, - surveymissions as mission_tasks, - surveyrelatedrecords as record_tasks, -) logger = logging.getLogger(__name__) @@ -199,26 +196,6 @@ async def handle_resource_modification_edit_page( if message.resource_id != context.resource_id: return - match message.resource_type: - case constants.ResourceType.PROJECT: - project_tasks.validate_project.send( - raw_request_id=str(message.request_id), - raw_project_id=str(message.resource_id), - raw_initiator=json.dumps(dataclasses.asdict(context.user)), - ) # noqa - case constants.ResourceType.MISSION: - mission_tasks.validate_survey_mission.send( - raw_request_id=str(message.request_id), - raw_survey_mission_id=str(message.resource_id), - raw_initiator=json.dumps(dataclasses.asdict(context.user)), - ) # noqa - case constants.ResourceType.RECORD: - record_tasks.validate_survey_related_record.send( - raw_request_id=str(message.request_id), - raw_survey_related_record_id=str(message.resource_id), - raw_initiator=json.dumps(dataclasses.asdict(context.user)), - ) # noqa - if message.succeeded: notification = webui_schemas.Notification( message=f"{message.resource_type.capitalize()} updated successfully!", @@ -300,7 +277,19 @@ async def _handle_project_modification_detail_page( logger.debug( f"Project {message.resource_id!r} has been updated - re-rendering its details..." ) - # TODO: yield re-render of project details + async for event in flash_ui_message_after_redirect( + webui_schemas.Notification( + message=f"{message.resource_type.capitalize()} {message.resource_id} was {message.modification}", + ) + ): + yield event + yield ServerSentEventGenerator.redirect( + str( + context.url_resolver( + "projects:detail", project_id=project_id + ) + ) + ) case constants.ResourceModification.DELETED: async for event in flash_ui_message_after_redirect( webui_schemas.Notification( @@ -469,6 +458,57 @@ async def _handle_survey_mission_modification_detail_page( ) +async def handle_resource_status_changed_detail_page( + message: message_schemas.ResourceStatusChangedMessage, + context: subscribers.HandlerContext, + done: asyncio.Event | None = None, +) -> AsyncGenerator[DatastarEvent, None]: + if context.resource_id != message.resource_id: + return + + status_partial_template = context.jinja_environment.get_template( + "projects/partial-status.html" + ) + try: + item_getter = { + constants.ResourceType.PROJECT: partial( + project_ops.get_project, + project_id=identifiers.ProjectId(uuid.UUID(message.resource_id)), + ), + constants.ResourceType.MISSION: partial( + mission_ops.get_survey_mission, + survey_mission_id=identifiers.SurveyMissionId( + uuid.UUID(message.resource_id) + ), + ), + constants.ResourceType.RECORD: partial( + record_ops.get_survey_related_record, + survey_related_record_id=identifiers.SurveyRelatedRecordId( + uuid.UUID(message.resource_id) + ), + ), + }[message.resource_type] + except (KeyError, ValueError) as err: + logger.exception("Could not determine item_getter") + async for event in flash_ui_message_same_page( + webui_schemas.Notification( + message=f"Could not determine item_getter: {str(err)} ", + category="error", + ) + ): + yield event + else: + async with context.db_session_factory() as session: + item = await item_getter(initiator=context.user, session=session) + rendered = status_partial_template.render(item=item) + yield ServerSentEventGenerator.patch_elements( + rendered, + selector=webui_schemas.NewItemSelectorInfo.status_selector, + mode=ElementPatchMode.REPLACE, + use_view_transition=True, + ) + + async def handle_resource_modification_detail_page( message: message_schemas.ResourceModificationMessage, context: subscribers.HandlerContext, diff --git a/src/seis_lab_data/webapp/templates/macros-buttons.html b/src/seis_lab_data/webapp/templates/macros-buttons.html index f85e2ba..27b72a1 100644 --- a/src/seis_lab_data/webapp/templates/macros-buttons.html +++ b/src/seis_lab_data/webapp/templates/macros-buttons.html @@ -48,7 +48,7 @@ data-bs-target="#{{ modal_id }}" data-attr:disabled="${{ signal_name }}" > - {{ icons.delete_item }} + {{ icons.delete_item }}