mirror of
https://github.com/inventree/InvenTree.git
synced 2025-06-16 20:15:44 +00:00
Merge remote-tracking branch 'inventree/master'
This commit is contained in:
@ -385,7 +385,7 @@ class InvenTreeAttachment(models.Model):
|
|||||||
'link': _('Missing external link'),
|
'link': _('Missing external link'),
|
||||||
})
|
})
|
||||||
|
|
||||||
if self.attachment.name.lower().endswith('.svg'):
|
if self.attachment and self.attachment.name.lower().endswith('.svg'):
|
||||||
self.attachment.file.file = self.clean_svg(self.attachment)
|
self.attachment.file.file = self.clean_svg(self.attachment)
|
||||||
|
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
@ -226,15 +226,16 @@ def heartbeat():
|
|||||||
|
|
||||||
@scheduled_task(ScheduledTask.DAILY)
|
@scheduled_task(ScheduledTask.DAILY)
|
||||||
def delete_successful_tasks():
|
def delete_successful_tasks():
|
||||||
"""Delete successful task logs which are more than a month old."""
|
"""Delete successful task logs which are older than a specified period"""
|
||||||
try:
|
try:
|
||||||
from django_q.models import Success
|
from django_q.models import Success
|
||||||
except AppRegistryNotReady: # pragma: no cover
|
|
||||||
logger.info("Could not perform 'delete_successful_tasks' - App registry not ready")
|
|
||||||
return
|
|
||||||
|
|
||||||
threshold = timezone.now() - timedelta(days=30)
|
from common.models import InvenTreeSetting
|
||||||
|
|
||||||
|
days = InvenTreeSetting.get_setting('INVENTREE_DELETE_TASKS_DAYS', 30)
|
||||||
|
threshold = timezone.now() - timedelta(days=days)
|
||||||
|
|
||||||
|
# Delete successful tasks
|
||||||
results = Success.objects.filter(
|
results = Success.objects.filter(
|
||||||
started__lte=threshold
|
started__lte=threshold
|
||||||
)
|
)
|
||||||
@ -243,6 +244,34 @@ def delete_successful_tasks():
|
|||||||
logger.info(f"Deleting {results.count()} successful task records")
|
logger.info(f"Deleting {results.count()} successful task records")
|
||||||
results.delete()
|
results.delete()
|
||||||
|
|
||||||
|
except AppRegistryNotReady: # pragma: no cover
|
||||||
|
logger.info("Could not perform 'delete_successful_tasks' - App registry not ready")
|
||||||
|
|
||||||
|
|
||||||
|
@scheduled_task(ScheduledTask.DAILY)
|
||||||
|
def delete_failed_tasks():
|
||||||
|
"""Delete failed task logs which are older than a specified period"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
from django_q.models import Failure
|
||||||
|
|
||||||
|
from common.models import InvenTreeSetting
|
||||||
|
|
||||||
|
days = InvenTreeSetting.get_setting('INVENTREE_DELETE_TASKS_DAYS', 30)
|
||||||
|
threshold = timezone.now() - timedelta(days=days)
|
||||||
|
|
||||||
|
# Delete failed tasks
|
||||||
|
results = Failure.objects.filter(
|
||||||
|
started__lte=threshold
|
||||||
|
)
|
||||||
|
|
||||||
|
if results.count() > 0:
|
||||||
|
logger.info(f"Deleting {results.count()} failed task records")
|
||||||
|
results.delete()
|
||||||
|
|
||||||
|
except AppRegistryNotReady: # pragma: no cover
|
||||||
|
logger.info("Could not perform 'delete_failed_tasks' - App registry not ready")
|
||||||
|
|
||||||
|
|
||||||
@scheduled_task(ScheduledTask.DAILY)
|
@scheduled_task(ScheduledTask.DAILY)
|
||||||
def delete_old_error_logs():
|
def delete_old_error_logs():
|
||||||
@ -250,8 +279,10 @@ def delete_old_error_logs():
|
|||||||
try:
|
try:
|
||||||
from error_report.models import Error
|
from error_report.models import Error
|
||||||
|
|
||||||
# Delete any error logs more than 30 days old
|
from common.models import InvenTreeSetting
|
||||||
threshold = timezone.now() - timedelta(days=30)
|
|
||||||
|
days = InvenTreeSetting.get_setting('INVENTREE_DELETE_ERRORS_DAYS', 30)
|
||||||
|
threshold = timezone.now() - timedelta(days=days)
|
||||||
|
|
||||||
errors = Error.objects.filter(
|
errors = Error.objects.filter(
|
||||||
when__lte=threshold,
|
when__lte=threshold,
|
||||||
@ -264,7 +295,37 @@ def delete_old_error_logs():
|
|||||||
except AppRegistryNotReady: # pragma: no cover
|
except AppRegistryNotReady: # pragma: no cover
|
||||||
# Apps not yet loaded
|
# Apps not yet loaded
|
||||||
logger.info("Could not perform 'delete_old_error_logs' - App registry not ready")
|
logger.info("Could not perform 'delete_old_error_logs' - App registry not ready")
|
||||||
return
|
|
||||||
|
|
||||||
|
@scheduled_task(ScheduledTask.DAILY)
|
||||||
|
def delete_old_notifications():
|
||||||
|
"""Delete old notification logs"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
from common.models import (InvenTreeSetting, NotificationEntry,
|
||||||
|
NotificationMessage)
|
||||||
|
|
||||||
|
days = InvenTreeSetting.get_setting('INVENTREE_DELETE_NOTIFICATIONS_DAYS', 30)
|
||||||
|
threshold = timezone.now() - timedelta(days=days)
|
||||||
|
|
||||||
|
items = NotificationEntry.objects.filter(
|
||||||
|
updated__lte=threshold
|
||||||
|
)
|
||||||
|
|
||||||
|
if items.count() > 0:
|
||||||
|
logger.info(f"Deleted {items.count()} old notification entries")
|
||||||
|
items.delete()
|
||||||
|
|
||||||
|
items = NotificationMessage.objects.filter(
|
||||||
|
creation__lte=threshold
|
||||||
|
)
|
||||||
|
|
||||||
|
if items.count() > 0:
|
||||||
|
logger.info(f"Deleted {items.count()} old notification messages")
|
||||||
|
items.delete()
|
||||||
|
|
||||||
|
except AppRegistryNotReady:
|
||||||
|
logger.info("Could not perform 'delete_old_notifications' - App registry not ready")
|
||||||
|
|
||||||
|
|
||||||
@scheduled_task(ScheduledTask.DAILY)
|
@scheduled_task(ScheduledTask.DAILY)
|
||||||
|
@ -893,6 +893,39 @@ class InvenTreeSetting(BaseInvenTreeSetting):
|
|||||||
'default': True,
|
'default': True,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
'INVENTREE_DELETE_TASKS_DAYS': {
|
||||||
|
'name': _('Delete Old Tasks'),
|
||||||
|
'description': _('Background task results will be deleted after specified number of days'),
|
||||||
|
'default': 30,
|
||||||
|
'units': 'days',
|
||||||
|
'validator': [
|
||||||
|
int,
|
||||||
|
MinValueValidator(7),
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
'INVENTREE_DELETE_ERRORS_DAYS': {
|
||||||
|
'name': _('Delete Error Logs'),
|
||||||
|
'description': _('Error logs will be deleted after specified number of days'),
|
||||||
|
'default': 30,
|
||||||
|
'units': 'days',
|
||||||
|
'validator': [
|
||||||
|
int,
|
||||||
|
MinValueValidator(7)
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
'INVENTREE_DELETE_NOTIFICATIONS_DAYS': {
|
||||||
|
'name': _('Delete Noficiations'),
|
||||||
|
'description': _('User notifications 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'),
|
'description': _('Enable barcode scanner support'),
|
||||||
|
@ -2350,3 +2350,83 @@ class PartParameterTest(InvenTreeAPITestCase):
|
|||||||
data = response.data
|
data = response.data
|
||||||
|
|
||||||
self.assertEqual(data['data'], '15')
|
self.assertEqual(data['data'], '15')
|
||||||
|
|
||||||
|
|
||||||
|
class PartAttachmentTest(InvenTreeAPITestCase):
|
||||||
|
"""Unit tests for the PartAttachment API endpoint"""
|
||||||
|
|
||||||
|
fixtures = [
|
||||||
|
'category',
|
||||||
|
'part',
|
||||||
|
'location',
|
||||||
|
]
|
||||||
|
|
||||||
|
def test_add_attachment(self):
|
||||||
|
"""Test that we can create a new PartAttachment via the API"""
|
||||||
|
|
||||||
|
url = reverse('api-part-attachment-list')
|
||||||
|
|
||||||
|
# Upload without permission
|
||||||
|
response = self.post(
|
||||||
|
url,
|
||||||
|
{},
|
||||||
|
expected_code=403,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add required permission
|
||||||
|
self.assignRole('part.add')
|
||||||
|
|
||||||
|
# Upload without specifying part (will fail)
|
||||||
|
response = self.post(
|
||||||
|
url,
|
||||||
|
{
|
||||||
|
'comment': 'Hello world',
|
||||||
|
},
|
||||||
|
expected_code=400
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertIn('This field is required', str(response.data['part']))
|
||||||
|
|
||||||
|
# Upload without file OR link (will fail)
|
||||||
|
response = self.post(
|
||||||
|
url,
|
||||||
|
{
|
||||||
|
'part': 1,
|
||||||
|
'comment': 'Hello world',
|
||||||
|
},
|
||||||
|
expected_code=400
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertIn('Missing file', str(response.data['attachment']))
|
||||||
|
self.assertIn('Missing external link', str(response.data['link']))
|
||||||
|
|
||||||
|
# Upload an invalid link (will fail)
|
||||||
|
response = self.post(
|
||||||
|
url,
|
||||||
|
{
|
||||||
|
'part': 1,
|
||||||
|
'link': 'not-a-link.py',
|
||||||
|
},
|
||||||
|
expected_code=400
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertIn('Enter a valid URL', str(response.data['link']))
|
||||||
|
|
||||||
|
link = 'https://www.google.com/test'
|
||||||
|
|
||||||
|
# Upload a valid link (will pass)
|
||||||
|
response = self.post(
|
||||||
|
url,
|
||||||
|
{
|
||||||
|
'part': 1,
|
||||||
|
'link': link,
|
||||||
|
'comment': 'Hello world',
|
||||||
|
},
|
||||||
|
expected_code=201
|
||||||
|
)
|
||||||
|
|
||||||
|
data = response.data
|
||||||
|
|
||||||
|
self.assertEqual(data['part'], 1)
|
||||||
|
self.assertEqual(data['link'], link)
|
||||||
|
self.assertEqual(data['comment'], 'Hello world')
|
||||||
|
@ -24,6 +24,10 @@
|
|||||||
{% include "InvenTree/settings/setting.html" with key="INVENTREE_REQUIRE_CONFIRM" icon="fa-check" %}
|
{% include "InvenTree/settings/setting.html" with key="INVENTREE_REQUIRE_CONFIRM" icon="fa-check" %}
|
||||||
{% include "InvenTree/settings/setting.html" with key="INVENTREE_TREE_DEPTH" icon="fa-sitemap" %}
|
{% include "InvenTree/settings/setting.html" with key="INVENTREE_TREE_DEPTH" icon="fa-sitemap" %}
|
||||||
{% include "InvenTree/settings/setting.html" with key="INVENTREE_BACKUP_ENABLE" icon="fa-hdd" %}
|
{% include "InvenTree/settings/setting.html" with key="INVENTREE_BACKUP_ENABLE" icon="fa-hdd" %}
|
||||||
|
<tr><td colspan='5'></td></tr>
|
||||||
|
{% include "InvenTree/settings/setting.html" with key="INVENTREE_DELETE_TASKS_DAYS" icon="fa-calendar-alt" %}
|
||||||
|
{% include "InvenTree/settings/setting.html" with key="INVENTREE_DELETE_ERRORS_DAYS" icon="fa-calendar-alt" %}
|
||||||
|
{% include "InvenTree/settings/setting.html" with key="INVENTREE_DELETE_NOTIFICATIONS_DAYS" icon="fa-calendar-alt" %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
@ -1308,15 +1308,20 @@ function loadBomTable(table, options={}) {
|
|||||||
|
|
||||||
var data = table.bootstrapTable('getData');
|
var data = table.bootstrapTable('getData');
|
||||||
|
|
||||||
|
var update_required = false;
|
||||||
|
|
||||||
for (var idx = 0; idx < data.length; idx++) {
|
for (var idx = 0; idx < data.length; idx++) {
|
||||||
var row = data[idx];
|
|
||||||
|
|
||||||
if (!row.parentId) {
|
if (!data[idx].parentId) {
|
||||||
row.parentId = parent_id;
|
data[idx].parentId = parent_id;
|
||||||
|
update_required = true;
|
||||||
table.bootstrapTable('updateByUniqueId', row.pk, row, true);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Re-load the table back data
|
||||||
|
if (update_required) {
|
||||||
|
table.bootstrapTable('load', data);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onLoadSuccess: function(data) {
|
onLoadSuccess: function(data) {
|
||||||
|
|
||||||
|
@ -324,7 +324,7 @@ function setupFilterList(tableKey, table, target, options={}) {
|
|||||||
|
|
||||||
// Callback for reloading the table
|
// Callback for reloading the table
|
||||||
element.find(`#reload-${tableKey}`).click(function() {
|
element.find(`#reload-${tableKey}`).click(function() {
|
||||||
$(table).bootstrapTable('refresh');
|
reloadTableFilters(table);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add a callback for downloading table data
|
// Add a callback for downloading table data
|
||||||
|
@ -1301,15 +1301,17 @@ function loadParametricPartTable(table, options={}) {
|
|||||||
|
|
||||||
for (var idx = 0; idx < data.length; idx++) {
|
for (var idx = 0; idx < data.length; idx++) {
|
||||||
var row = data[idx];
|
var row = data[idx];
|
||||||
var pk = row.pk;
|
|
||||||
|
|
||||||
// Make each parameter accessible, based on the "template" columns
|
// Make each parameter accessible, based on the "template" columns
|
||||||
row.parameters.forEach(function(parameter) {
|
row.parameters.forEach(function(parameter) {
|
||||||
row[`parameter_${parameter.template}`] = parameter.data;
|
row[`parameter_${parameter.template}`] = parameter.data;
|
||||||
});
|
});
|
||||||
|
|
||||||
$(table).bootstrapTable('updateByUniqueId', pk, row);
|
data[idx] = row;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update the table
|
||||||
|
$(table).bootstrapTable('load', data);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user