mirror of
https://github.com/inventree/InvenTree.git
synced 2025-08-07 04:12: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:
@@ -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
|
||||||
|
@@ -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
|
||||||
|
|
||||||
|
@@ -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():
|
||||||
|
@@ -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."""
|
||||||
|
|
||||||
|
|
||||||
|
@@ -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'),
|
||||||
|
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -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'
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
@@ -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
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@@ -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
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
Reference in New Issue
Block a user