From 4895370d0daedd4ee6cf780ced7b17b8ab540fbd Mon Sep 17 00:00:00 2001 From: Oliver Date: Fri, 1 Aug 2025 12:32:22 +1000 Subject: [PATCH] Email history enhancements (#10109) * Improve formatting for - Only used for email settings currently * API updates: - Allow delete operations of email record - Allow bulk delete operations of email record * Table updates: - Improved table rendering - Allow delete ops * Display timestamp in email table * Add setting to control email cleanup interval * Add scheduled task to delete old emails * Bump API version --- docs/docs/settings/global.md | 1 + .../InvenTree/InvenTree/api_version.py | 5 +- src/backend/InvenTree/InvenTree/tasks.py | 20 +++++++ src/backend/InvenTree/common/api.py | 5 +- .../InvenTree/common/setting/system.py | 9 +++ .../components/settings/ConfigValueList.tsx | 37 +++++++++--- .../pages/Index/Settings/SystemSettings.tsx | 3 +- src/frontend/src/tables/ColumnRenderers.tsx | 4 +- .../src/tables/settings/EmailTable.tsx | 58 +++++++++++++++---- 9 files changed, 118 insertions(+), 24 deletions(-) diff --git a/docs/docs/settings/global.md b/docs/docs/settings/global.md index 668d989332..93301810ba 100644 --- a/docs/docs/settings/global.md +++ b/docs/docs/settings/global.md @@ -40,6 +40,7 @@ Configuration of basic server settings: {{ globalsetting("INVENTREE_DELETE_TASKS_DAYS") }} {{ globalsetting("INVENTREE_DELETE_ERRORS_DAYS") }} {{ globalsetting("INVENTREE_DELETE_NOTIFICATIONS_DAYS") }} +{{ globalsetting("INVENTREE_DELETE_EMAIL_DAYS") }} ### Login Settings diff --git a/src/backend/InvenTree/InvenTree/api_version.py b/src/backend/InvenTree/InvenTree/api_version.py index 74f70f4175..2b3c6747ed 100644 --- a/src/backend/InvenTree/InvenTree/api_version.py +++ b/src/backend/InvenTree/InvenTree/api_version.py @@ -1,12 +1,15 @@ """InvenTree API version information.""" # InvenTree API version -INVENTREE_API_VERSION = 376 +INVENTREE_API_VERSION = 377 """Increment this API version number whenever there is a significant change to the API that any clients need to know about.""" INVENTREE_API_TEXT = """ +v377 -> 2025-08-01 : https://github.com/inventree/InvenTree/pull/10109 + - Allow email records to be deleted via the API + v376 -> 2025-08-01 : https://github.com/inventree/InvenTree/pull/10108 - Fix search fields for ReturnOrderLineItem API list endpoint diff --git a/src/backend/InvenTree/InvenTree/tasks.py b/src/backend/InvenTree/InvenTree/tasks.py index ac10a91c4c..80d297c03d 100644 --- a/src/backend/InvenTree/InvenTree/tasks.py +++ b/src/backend/InvenTree/InvenTree/tasks.py @@ -466,6 +466,26 @@ def delete_old_notifications(): ) +@tracer.start_as_current_span('delete_old_emails') +@scheduled_task(ScheduledTask.DAILY) +def delete_old_emails(): + """Delete old email messages.""" + try: + from common.models import EmailMessage + + days = get_global_setting('INVENTREE_DELETE_EMAIL_DAYS', 30) + threshold = timezone.now() - timedelta(days=days) + + emails = EmailMessage.objects.filter(timestamp__lte=threshold) + + if emails.count() > 0: + logger.info('Deleted %s old email messages', emails.count()) + emails.delete() + + except AppRegistryNotReady: + logger.info("Could not perform 'delete_old_emails' - App registry not ready") + + @tracer.start_as_current_span('check_for_updates') @scheduled_task(ScheduledTask.DAILY) def check_for_updates(): diff --git a/src/backend/InvenTree/common/api.py b/src/backend/InvenTree/common/api.py index f8863a5f7e..1946d3a2de 100644 --- a/src/backend/InvenTree/common/api.py +++ b/src/backend/InvenTree/common/api.py @@ -43,6 +43,7 @@ from InvenTree.mixins import ( ListAPI, ListCreateAPI, RetrieveAPI, + RetrieveDestroyAPI, RetrieveUpdateAPI, RetrieveUpdateDestroyAPI, ) @@ -842,7 +843,7 @@ class EmailMessageMixin: permission_classes = [IsSuperuserOrSuperScope] -class EmailMessageList(EmailMessageMixin, ListAPI): +class EmailMessageList(EmailMessageMixin, BulkDeleteMixin, ListAPI): """List view for email objects.""" filter_backends = SEARCH_ORDER_FILTER @@ -865,7 +866,7 @@ class EmailMessageList(EmailMessageMixin, ListAPI): ] -class EmailMessageDetail(EmailMessageMixin, RetrieveAPI): +class EmailMessageDetail(EmailMessageMixin, RetrieveDestroyAPI): """Detail view for an email object.""" diff --git a/src/backend/InvenTree/common/setting/system.py b/src/backend/InvenTree/common/setting/system.py index 9a933fb117..eb91897aea 100644 --- a/src/backend/InvenTree/common/setting/system.py +++ b/src/backend/InvenTree/common/setting/system.py @@ -331,6 +331,15 @@ SYSTEM_SETTINGS: dict[str, InvenTreeSettingsKeyType] = { 'units': _('days'), 'validator': [int, MinValueValidator(7)], }, + 'INVENTREE_DELETE_EMAIL_DAYS': { + 'name': _('Email Deletion Interval'), + 'description': _( + 'Email messages will be deleted after specified number of days' + ), + 'default': 30, + 'units': _('days'), + 'validator': [int, MinValueValidator(7)], + }, 'BARCODE_ENABLE': { 'name': _('Barcode Support'), 'description': _('Enable barcode scanner support in the web interface'), diff --git a/src/frontend/src/components/settings/ConfigValueList.tsx b/src/frontend/src/components/settings/ConfigValueList.tsx index 4efb6cdebb..184f09f62c 100644 --- a/src/frontend/src/components/settings/ConfigValueList.tsx +++ b/src/frontend/src/components/settings/ConfigValueList.tsx @@ -1,10 +1,11 @@ -import { Code, Text } from '@mantine/core'; +import { Table } from '@mantine/core'; import { ApiEndpoints, apiUrl } from '@lib/index'; import { Trans } from '@lingui/react/macro'; import { useQuery } from '@tanstack/react-query'; import { useMemo } from 'react'; import { api } from '../../App'; +import { formatDate } from '../../defaults/formatters'; export function ConfigValueList({ keys }: Readonly<{ keys: string[] }>) { const { data, isLoading } = useQuery({ @@ -28,14 +29,32 @@ export function ConfigValueList({ keys }: Readonly<{ keys: string[] }>) { return ( - {totalData.map((vals) => ( - - - {vals.key} is set via {vals.value?.source} and was last - set {vals.value.accessed} - - - ))} + + + + + Setting + + + Source + + + Updated + + + + + {totalData.map((vals) => ( + + {vals.key} + {vals.value?.source} + + {formatDate(vals.value?.accessed, { showTime: true })} + + + ))} + +
); } diff --git a/src/frontend/src/pages/Index/Settings/SystemSettings.tsx b/src/frontend/src/pages/Index/Settings/SystemSettings.tsx index d467a059a6..8148dee83c 100644 --- a/src/frontend/src/pages/Index/Settings/SystemSettings.tsx +++ b/src/frontend/src/pages/Index/Settings/SystemSettings.tsx @@ -64,7 +64,8 @@ export default function SystemSettings() { 'INVENTREE_BACKUP_DAYS', 'INVENTREE_DELETE_TASKS_DAYS', 'INVENTREE_DELETE_ERRORS_DAYS', - 'INVENTREE_DELETE_NOTIFICATIONS_DAYS' + 'INVENTREE_DELETE_NOTIFICATIONS_DAYS', + 'INVENTREE_DELETE_EMAIL_DAYS' ]} /> ) diff --git a/src/frontend/src/tables/ColumnRenderers.tsx b/src/frontend/src/tables/ColumnRenderers.tsx index ef004dcb61..e10c19ab48 100644 --- a/src/frontend/src/tables/ColumnRenderers.tsx +++ b/src/frontend/src/tables/ColumnRenderers.tsx @@ -369,7 +369,9 @@ export function DateColumn(props: TableColumnProps): TableColumn { title: t`Date`, switchable: true, render: (record: any) => - formatDate(resolveItem(record, props.accessor ?? 'date')), + formatDate(resolveItem(record, props.accessor ?? 'date'), { + showTime: props.extra?.showTime + }), ...props }; } diff --git a/src/frontend/src/tables/settings/EmailTable.tsx b/src/frontend/src/tables/settings/EmailTable.tsx index cfa1caaea9..8e8eeb089f 100644 --- a/src/frontend/src/tables/settings/EmailTable.tsx +++ b/src/frontend/src/tables/settings/EmailTable.tsx @@ -1,11 +1,17 @@ import { ActionButton } from '@lib/components/ActionButton'; +import { RowDeleteAction } from '@lib/components/RowActions'; import { ApiEndpoints } from '@lib/enums/ApiEndpoints'; import { apiUrl } from '@lib/functions/Api'; import { t } from '@lingui/core/macro'; +import { Badge } from '@mantine/core'; import { IconTestPipe } from '@tabler/icons-react'; -import { useMemo } from 'react'; -import { useCreateApiFormModal } from '../../hooks/UseForm'; +import { useCallback, useMemo, useState } from 'react'; +import { + useCreateApiFormModal, + useDeleteApiFormModal +} from '../../hooks/UseForm'; import { useTable } from '../../hooks/UseTable'; +import { useUserState } from '../../states/UserState'; import { DateColumn } from '../ColumnRenderers'; import { InvenTreeTable } from '../InvenTreeTable'; @@ -20,6 +26,8 @@ export function EmailTable() { } }); + const user = useUserState(); + const tableActions = useMemo(() => { return [ (''); + + const deleteEmail = useDeleteApiFormModal({ + url: ApiEndpoints.email_list, + pk: selectedEmailId, + title: t`Delete Email`, + successMessage: t`Email deleted successfully`, + table: table + }); const tableColumns = useMemo(() => { return [ @@ -57,17 +75,17 @@ export function EmailTable() { render: (record: any) => { switch (record.status) { case 'A': - return t`Announced`; + return {t`Announced`}; case 'S': - return t`Sent`; + return {t`Sent`}; case 'F': - return t`Failed`; + return {t`Failed`}; case 'D': - return t`Delivered`; + return {t`Delivered`}; case 'R': - return t`Read`; + return {t`Read`}; case 'C': - return t`Confirmed`; + return {t`Confirmed`}; } return '-'; }, @@ -86,21 +104,41 @@ export function EmailTable() { accessor: 'timestamp', title: t`Timestamp`, sortable: true, - switchable: true + switchable: true, + extra: { showTime: true } }) ]; }, []); + const rowactions = useCallback( + (record: any) => { + return [ + RowDeleteAction({ + onClick: () => { + setSelectedEmailId(record.pk); + deleteEmail.open(); + }, + hidden: !user.isStaff() + }) + ]; + }, + [user] + ); + return ( <> {sendTestMail.modal} + {deleteEmail.modal}