From c3f6b75b302cedf99694acaf5dc3e7489b35e180 Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 20 Oct 2022 22:43:14 +1100 Subject: [PATCH 1/3] Attachment bug fix (#3818) * Prevent name check on null attachment file (cherry picked from commit c4ed1e23a01f278d696c2853337bdde0a682c6c5) * Unit testing for uploading attachments via API (cherry picked from commit 592548065f7b69f58b8aaaaea506e3ec653a63df) --- InvenTree/InvenTree/models.py | 2 +- InvenTree/part/test_api.py | 80 +++++++++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+), 1 deletion(-) diff --git a/InvenTree/InvenTree/models.py b/InvenTree/InvenTree/models.py index 04b9c14bed..cd15800e55 100644 --- a/InvenTree/InvenTree/models.py +++ b/InvenTree/InvenTree/models.py @@ -385,7 +385,7 @@ class InvenTreeAttachment(models.Model): '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) super().save(*args, **kwargs) diff --git a/InvenTree/part/test_api.py b/InvenTree/part/test_api.py index 35179485d0..9555ed5092 100644 --- a/InvenTree/part/test_api.py +++ b/InvenTree/part/test_api.py @@ -2350,3 +2350,83 @@ class PartParameterTest(InvenTreeAPITestCase): data = response.data 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') From 4ca2aa6cd852e3df74ab7cdfb82382dba8a93867 Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 20 Oct 2022 23:27:09 +1100 Subject: [PATCH 2/3] Housekeeping Settings (#3821) * Add new settings for controlling how long logged data is retained * Update existing tasks to use new user-configurable values - Also add a task to delete failed task logs * Add background task to remove old notification logs --- InvenTree/InvenTree/tasks.py | 85 ++++++++++++++++--- InvenTree/common/models.py | 33 +++++++ .../templates/InvenTree/settings/global.html | 4 + 3 files changed, 110 insertions(+), 12 deletions(-) diff --git a/InvenTree/InvenTree/tasks.py b/InvenTree/InvenTree/tasks.py index 1c856bebba..b1c90008ec 100644 --- a/InvenTree/InvenTree/tasks.py +++ b/InvenTree/InvenTree/tasks.py @@ -226,22 +226,51 @@ def heartbeat(): @scheduled_task(ScheduledTask.DAILY) 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: from django_q.models import Success + + 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( + started__lte=threshold + ) + + if results.count() > 0: + logger.info(f"Deleting {results.count()} successful task records") + results.delete() + except AppRegistryNotReady: # pragma: no cover logger.info("Could not perform 'delete_successful_tasks' - App registry not ready") - return - threshold = timezone.now() - timedelta(days=30) - results = Success.objects.filter( - started__lte=threshold - ) +@scheduled_task(ScheduledTask.DAILY) +def delete_failed_tasks(): + """Delete failed task logs which are older than a specified period""" - if results.count() > 0: - logger.info(f"Deleting {results.count()} successful task records") - results.delete() + 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) @@ -250,8 +279,10 @@ def delete_old_error_logs(): try: from error_report.models import Error - # Delete any error logs more than 30 days old - threshold = timezone.now() - timedelta(days=30) + from common.models import InvenTreeSetting + + days = InvenTreeSetting.get_setting('INVENTREE_DELETE_ERRORS_DAYS', 30) + threshold = timezone.now() - timedelta(days=days) errors = Error.objects.filter( when__lte=threshold, @@ -264,7 +295,37 @@ def delete_old_error_logs(): except AppRegistryNotReady: # pragma: no cover # Apps not yet loaded 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) diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py index 582503ae6f..25589c4887 100644 --- a/InvenTree/common/models.py +++ b/InvenTree/common/models.py @@ -893,6 +893,39 @@ class InvenTreeSetting(BaseInvenTreeSetting): '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': { 'name': _('Barcode Support'), 'description': _('Enable barcode scanner support'), diff --git a/InvenTree/templates/InvenTree/settings/global.html b/InvenTree/templates/InvenTree/settings/global.html index 32dee05241..aab649b0bc 100644 --- a/InvenTree/templates/InvenTree/settings/global.html +++ b/InvenTree/templates/InvenTree/settings/global.html @@ -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_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_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" %} From 121d68aa87c691bdc40121ca7bdcb159358fc297 Mon Sep 17 00:00:00 2001 From: Oliver Date: Fri, 21 Oct 2022 17:05:10 +1100 Subject: [PATCH 3/3] Bom table load fix (#3826) * Optimize loading of BOM table - Do not use updateByUniqueId (inefficient!) - Instead, process and reload the entire table * Optimize part parameter table * Revert testing change * javascript linting --- InvenTree/templates/js/translated/bom.js | 15 ++++++++++----- InvenTree/templates/js/translated/filters.js | 2 +- InvenTree/templates/js/translated/part.js | 6 ++++-- 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/InvenTree/templates/js/translated/bom.js b/InvenTree/templates/js/translated/bom.js index 5380afae64..1fe0746396 100644 --- a/InvenTree/templates/js/translated/bom.js +++ b/InvenTree/templates/js/translated/bom.js @@ -1308,15 +1308,20 @@ function loadBomTable(table, options={}) { var data = table.bootstrapTable('getData'); + var update_required = false; + for (var idx = 0; idx < data.length; idx++) { - var row = data[idx]; - if (!row.parentId) { - row.parentId = parent_id; - - table.bootstrapTable('updateByUniqueId', row.pk, row, true); + if (!data[idx].parentId) { + data[idx].parentId = parent_id; + update_required = true; } } + + // Re-load the table back data + if (update_required) { + table.bootstrapTable('load', data); + } }, onLoadSuccess: function(data) { diff --git a/InvenTree/templates/js/translated/filters.js b/InvenTree/templates/js/translated/filters.js index 349b3028ed..d2edcaf5fe 100644 --- a/InvenTree/templates/js/translated/filters.js +++ b/InvenTree/templates/js/translated/filters.js @@ -324,7 +324,7 @@ function setupFilterList(tableKey, table, target, options={}) { // Callback for reloading the table element.find(`#reload-${tableKey}`).click(function() { - $(table).bootstrapTable('refresh'); + reloadTableFilters(table); }); // Add a callback for downloading table data diff --git a/InvenTree/templates/js/translated/part.js b/InvenTree/templates/js/translated/part.js index fbc07bdd09..e99f53bde8 100644 --- a/InvenTree/templates/js/translated/part.js +++ b/InvenTree/templates/js/translated/part.js @@ -1301,15 +1301,17 @@ function loadParametricPartTable(table, options={}) { for (var idx = 0; idx < data.length; idx++) { var row = data[idx]; - var pk = row.pk; // Make each parameter accessible, based on the "template" columns row.parameters.forEach(function(parameter) { row[`parameter_${parameter.template}`] = parameter.data; }); - $(table).bootstrapTable('updateByUniqueId', pk, row); + data[idx] = row; } + + // Update the table + $(table).bootstrapTable('load', data); } }); }