2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-08-07 12:22:11 +00:00

Email history enhancements (#10109)

* Improve formatting for <ConfigValueList>

- 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
This commit is contained in:
Oliver
2025-08-01 12:32:22 +10:00
committed by GitHub
parent 44bcf2c7e8
commit 4895370d0d
9 changed files with 118 additions and 24 deletions

View File

@@ -40,6 +40,7 @@ Configuration of basic server settings:
{{ globalsetting("INVENTREE_DELETE_TASKS_DAYS") }} {{ globalsetting("INVENTREE_DELETE_TASKS_DAYS") }}
{{ globalsetting("INVENTREE_DELETE_ERRORS_DAYS") }} {{ globalsetting("INVENTREE_DELETE_ERRORS_DAYS") }}
{{ globalsetting("INVENTREE_DELETE_NOTIFICATIONS_DAYS") }} {{ globalsetting("INVENTREE_DELETE_NOTIFICATIONS_DAYS") }}
{{ globalsetting("INVENTREE_DELETE_EMAIL_DAYS") }}
### Login Settings ### Login Settings

View File

@@ -1,12 +1,15 @@
"""InvenTree API version information.""" """InvenTree API version information."""
# InvenTree API version # 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.""" """Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
INVENTREE_API_TEXT = """ 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 v376 -> 2025-08-01 : https://github.com/inventree/InvenTree/pull/10108
- Fix search fields for ReturnOrderLineItem API list endpoint - Fix search fields for ReturnOrderLineItem API list endpoint

View File

@@ -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') @tracer.start_as_current_span('check_for_updates')
@scheduled_task(ScheduledTask.DAILY) @scheduled_task(ScheduledTask.DAILY)
def check_for_updates(): def check_for_updates():

View File

@@ -43,6 +43,7 @@ from InvenTree.mixins import (
ListAPI, ListAPI,
ListCreateAPI, ListCreateAPI,
RetrieveAPI, RetrieveAPI,
RetrieveDestroyAPI,
RetrieveUpdateAPI, RetrieveUpdateAPI,
RetrieveUpdateDestroyAPI, RetrieveUpdateDestroyAPI,
) )
@@ -842,7 +843,7 @@ class EmailMessageMixin:
permission_classes = [IsSuperuserOrSuperScope] permission_classes = [IsSuperuserOrSuperScope]
class EmailMessageList(EmailMessageMixin, ListAPI): class EmailMessageList(EmailMessageMixin, BulkDeleteMixin, ListAPI):
"""List view for email objects.""" """List view for email objects."""
filter_backends = SEARCH_ORDER_FILTER 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.""" """Detail view for an email object."""

View File

@@ -331,6 +331,15 @@ SYSTEM_SETTINGS: dict[str, InvenTreeSettingsKeyType] = {
'units': _('days'), 'units': _('days'),
'validator': [int, MinValueValidator(7)], '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': { 'BARCODE_ENABLE': {
'name': _('Barcode Support'), 'name': _('Barcode Support'),
'description': _('Enable barcode scanner support in the web interface'), 'description': _('Enable barcode scanner support in the web interface'),

View File

@@ -1,10 +1,11 @@
import { Code, Text } from '@mantine/core'; import { Table } from '@mantine/core';
import { ApiEndpoints, apiUrl } from '@lib/index'; import { ApiEndpoints, apiUrl } from '@lib/index';
import { Trans } from '@lingui/react/macro'; import { Trans } from '@lingui/react/macro';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { api } from '../../App'; import { api } from '../../App';
import { formatDate } from '../../defaults/formatters';
export function ConfigValueList({ keys }: Readonly<{ keys: string[] }>) { export function ConfigValueList({ keys }: Readonly<{ keys: string[] }>) {
const { data, isLoading } = useQuery({ const { data, isLoading } = useQuery({
@@ -28,14 +29,32 @@ export function ConfigValueList({ keys }: Readonly<{ keys: string[] }>) {
return ( return (
<span> <span>
{totalData.map((vals) => ( <Table withColumnBorders withTableBorder striped>
<Text key={vals.key}> <Table.Thead>
<Trans> <Table.Tr>
<Code>{vals.key}</Code> is set via {vals.value?.source} and was last <Table.Th>
set {vals.value.accessed} <Trans>Setting</Trans>
</Trans> </Table.Th>
</Text> <Table.Th>
))} <Trans>Source</Trans>
</Table.Th>
<Table.Th>
<Trans>Updated</Trans>
</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{totalData.map((vals) => (
<Table.Tr key={vals.key}>
<Table.Td>{vals.key}</Table.Td>
<Table.Td>{vals.value?.source}</Table.Td>
<Table.Td>
{formatDate(vals.value?.accessed, { showTime: true })}
</Table.Td>
</Table.Tr>
))}
</Table.Tbody>
</Table>
</span> </span>
); );
} }

View File

@@ -64,7 +64,8 @@ export default function SystemSettings() {
'INVENTREE_BACKUP_DAYS', 'INVENTREE_BACKUP_DAYS',
'INVENTREE_DELETE_TASKS_DAYS', 'INVENTREE_DELETE_TASKS_DAYS',
'INVENTREE_DELETE_ERRORS_DAYS', 'INVENTREE_DELETE_ERRORS_DAYS',
'INVENTREE_DELETE_NOTIFICATIONS_DAYS' 'INVENTREE_DELETE_NOTIFICATIONS_DAYS',
'INVENTREE_DELETE_EMAIL_DAYS'
]} ]}
/> />
) )

View File

@@ -369,7 +369,9 @@ export function DateColumn(props: TableColumnProps): TableColumn {
title: t`Date`, title: t`Date`,
switchable: true, switchable: true,
render: (record: any) => render: (record: any) =>
formatDate(resolveItem(record, props.accessor ?? 'date')), formatDate(resolveItem(record, props.accessor ?? 'date'), {
showTime: props.extra?.showTime
}),
...props ...props
}; };
} }

View File

@@ -1,11 +1,17 @@
import { ActionButton } from '@lib/components/ActionButton'; import { ActionButton } from '@lib/components/ActionButton';
import { RowDeleteAction } from '@lib/components/RowActions';
import { ApiEndpoints } from '@lib/enums/ApiEndpoints'; import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
import { apiUrl } from '@lib/functions/Api'; import { apiUrl } from '@lib/functions/Api';
import { t } from '@lingui/core/macro'; import { t } from '@lingui/core/macro';
import { Badge } from '@mantine/core';
import { IconTestPipe } from '@tabler/icons-react'; import { IconTestPipe } from '@tabler/icons-react';
import { useMemo } from 'react'; import { useCallback, useMemo, useState } from 'react';
import { useCreateApiFormModal } from '../../hooks/UseForm'; import {
useCreateApiFormModal,
useDeleteApiFormModal
} from '../../hooks/UseForm';
import { useTable } from '../../hooks/UseTable'; import { useTable } from '../../hooks/UseTable';
import { useUserState } from '../../states/UserState';
import { DateColumn } from '../ColumnRenderers'; import { DateColumn } from '../ColumnRenderers';
import { InvenTreeTable } from '../InvenTreeTable'; import { InvenTreeTable } from '../InvenTreeTable';
@@ -20,6 +26,8 @@ export function EmailTable() {
} }
}); });
const user = useUserState();
const tableActions = useMemo(() => { const tableActions = useMemo(() => {
return [ return [
<ActionButton <ActionButton
@@ -31,7 +39,17 @@ export function EmailTable() {
]; ];
}, []); }, []);
const table = useTable('emails', 'id'); const table = useTable('emails', 'pk');
const [selectedEmailId, setSelectedEmailId] = useState<string>('');
const deleteEmail = useDeleteApiFormModal({
url: ApiEndpoints.email_list,
pk: selectedEmailId,
title: t`Delete Email`,
successMessage: t`Email deleted successfully`,
table: table
});
const tableColumns = useMemo(() => { const tableColumns = useMemo(() => {
return [ return [
@@ -57,17 +75,17 @@ export function EmailTable() {
render: (record: any) => { render: (record: any) => {
switch (record.status) { switch (record.status) {
case 'A': case 'A':
return t`Announced`; return <Badge color='blue'>{t`Announced`}</Badge>;
case 'S': case 'S':
return t`Sent`; return <Badge color='blue'>{t`Sent`}</Badge>;
case 'F': case 'F':
return t`Failed`; return <Badge color='red'>{t`Failed`}</Badge>;
case 'D': case 'D':
return t`Delivered`; return <Badge color='green'>{t`Delivered`}</Badge>;
case 'R': case 'R':
return t`Read`; return <Badge color='green'>{t`Read`}</Badge>;
case 'C': case 'C':
return t`Confirmed`; return <Badge color='green'>{t`Confirmed`}</Badge>;
} }
return '-'; return '-';
}, },
@@ -86,21 +104,41 @@ export function EmailTable() {
accessor: 'timestamp', accessor: 'timestamp',
title: t`Timestamp`, title: t`Timestamp`,
sortable: true, 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 ( return (
<> <>
{sendTestMail.modal} {sendTestMail.modal}
{deleteEmail.modal}
<InvenTreeTable <InvenTreeTable
tableState={table} tableState={table}
url={apiUrl(ApiEndpoints.email_list)} url={apiUrl(ApiEndpoints.email_list)}
columns={tableColumns} columns={tableColumns}
props={{ props={{
rowActions: rowactions,
enableSearch: true, enableSearch: true,
enableColumnSwitching: true, enableColumnSwitching: true,
enableSelection: true,
enableBulkDelete: true,
tableActions: tableActions tableActions: tableActions
}} }}
/> />