diff --git a/.github/workflows/check_translations.yaml b/.github/workflows/check_translations.yaml index 910ecdda9b..b408ab5d74 100644 --- a/.github/workflows/check_translations.yaml +++ b/.github/workflows/check_translations.yaml @@ -21,10 +21,16 @@ jobs: INVENTREE_MEDIA_ROOT: ./media INVENTREE_STATIC_ROOT: ./static INVENTREE_BACKUP_DIR: ./backup + python_version: 3.9 steps: - name: Checkout Code uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # pin@v3.1.0 + - name: Set Up Python ${{ env.python_version }} + uses: actions/setup-python@13ae5bb136fac2878aff31522b9efb785519f984 # pin@v4.3.0 + with: + python-version: ${{ env.python_version }} + cache: 'pip' - name: Install Dependencies run: | sudo apt-get update diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml index 9d30cb9a79..f6be48d665 100644 --- a/.github/workflows/docker.yaml +++ b/.github/workflows/docker.yaml @@ -30,10 +30,15 @@ jobs: id-token: write env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - + python_version: 3.9 steps: - name: Check out repo uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # pin@v3.1.0 + - name: Set Up Python ${{ env.python_version }} + uses: actions/setup-python@13ae5bb136fac2878aff31522b9efb785519f984 # pin@v4.3.0 + with: + python-version: ${{ env.python_version }} + cache: 'pip' - name: Version Check run: | pip install requests diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml.disabled similarity index 100% rename from .github/workflows/release.yml rename to .github/workflows/release.yml.disabled diff --git a/InvenTree/build/test_build.py b/InvenTree/build/test_build.py index 23925eba17..6561e08903 100644 --- a/InvenTree/build/test_build.py +++ b/InvenTree/build/test_build.py @@ -557,11 +557,13 @@ class BuildTest(BuildTestBase): category='build.new_build', ) - self.assertEqual(messages.count(), 2) + self.assertEqual(messages.count(), 1) self.assertFalse(messages.filter(user__pk=2).exists()) - self.assertTrue(messages.filter(user__pk=3).exists()) + # Inactive users do not receive notifications + self.assertFalse(messages.filter(user__pk=3).exists()) + self.assertTrue(messages.filter(user__pk=4).exists()) diff --git a/InvenTree/common/notifications.py b/InvenTree/common/notifications.py index 1d4e7ae47b..0928fad6d0 100644 --- a/InvenTree/common/notifications.py +++ b/InvenTree/common/notifications.py @@ -243,8 +243,9 @@ class UIMessageNotification(SingleNotificationMethod): METHOD_NAME = 'ui_message' def get_targets(self): - """Just return the targets - no tricks here.""" - return self.targets + """Only send notifications for active users""" + + return [target for target in self.targets if target.is_active] def send(self, target): """Send a UI notification to a user.""" diff --git a/InvenTree/common/tests.py b/InvenTree/common/tests.py index 9720508d10..b305073045 100644 --- a/InvenTree/common/tests.py +++ b/InvenTree/common/tests.py @@ -778,7 +778,8 @@ class NotificationTest(InvenTreeAPITestCase): messages = NotificationMessage.objects.all() # As there are three staff users (including the 'test' user) we expect 30 notifications - self.assertEqual(messages.count(), 30) + # However, one user is marked as i nactive + self.assertEqual(messages.count(), 20) # Only 10 messages related to *this* user my_notifications = messages.filter(user=self.user) @@ -822,7 +823,7 @@ class NotificationTest(InvenTreeAPITestCase): # Only 7 notifications should have been deleted, # as the notifications associated with other users must remain untouched - self.assertEqual(NotificationMessage.objects.count(), 23) + self.assertEqual(NotificationMessage.objects.count(), 13) self.assertEqual(NotificationMessage.objects.filter(user=self.user).count(), 3) diff --git a/InvenTree/order/test_sales_order.py b/InvenTree/order/test_sales_order.py index 2d00f69ac6..98db41ff42 100644 --- a/InvenTree/order/test_sales_order.py +++ b/InvenTree/order/test_sales_order.py @@ -267,7 +267,7 @@ class SalesOrderTest(TestCase): category='order.overdue_sales_order', ) - self.assertEqual(len(messages), 2) + self.assertEqual(len(messages), 1) def test_new_so_notification(self): """Test that a notification is sent when a new SalesOrder is created. diff --git a/InvenTree/order/tests.py b/InvenTree/order/tests.py index 8c66b3a53a..4a758c3d6e 100644 --- a/InvenTree/order/tests.py +++ b/InvenTree/order/tests.py @@ -326,7 +326,12 @@ class OrderTest(TestCase): user__id=user_id, ) - self.assertTrue(messages.exists()) + # User ID 3 is inactive, and thus should not receive notifications + if user_id == 3: + self.assertFalse(messages.exists()) + continue + else: + self.assertTrue(messages.exists()) msg = messages.first() diff --git a/InvenTree/plugin/builtin/integration/core_notifications.py b/InvenTree/plugin/builtin/integration/core_notifications.py index d21dc8c46f..2176ab9feb 100644 --- a/InvenTree/plugin/builtin/integration/core_notifications.py +++ b/InvenTree/plugin/builtin/integration/core_notifications.py @@ -6,6 +6,7 @@ from django.utils.translation import gettext_lazy as _ from allauth.account.models import EmailAddress import common.models +import InvenTree.helpers import InvenTree.tasks from plugin import InvenTreePlugin from plugin.mixins import BulkNotificationMethod, SettingsMixin @@ -61,7 +62,12 @@ class CoreNotificationsPlugin(SettingsMixin, InvenTreePlugin): allowed_users = [] for user in self.targets: - allows_emails = self.usersetting(user) + + if not user.is_active: + # Ignore any users who have been deactivated + continue + + allows_emails = InvenTree.helpers.str2bool(self.usersetting(user)) if allows_emails: allowed_users.append(user) diff --git a/InvenTree/templates/js/translated/build.js b/InvenTree/templates/js/translated/build.js index fa1ec5bab4..13532db35b 100644 --- a/InvenTree/templates/js/translated/build.js +++ b/InvenTree/templates/js/translated/build.js @@ -1036,9 +1036,12 @@ function loadBuildOutputTable(build_info, options={}) { // Now that the allocations have been grouped by stock item, // we can update each row in the table, // using the pk value of each row (stock item) + + var data = []; + rows.forEach(function(row) { row.allocations = allocations[row.pk] || []; - $(table).bootstrapTable('updateByUniqueId', row.pk, row, true); + data.push(row); var n_completed_lines = 0; @@ -1066,6 +1069,9 @@ function loadBuildOutputTable(build_info, options={}) { } }); }); + + // Reload table with updated data + $(table).bootstrapTable('load', data); } } ); @@ -1108,6 +1114,7 @@ function loadBuildOutputTable(build_info, options={}) { { success: function(results) { + var data = []; // Iterate through each row and find matching test results rows.forEach(function(row) { var test_results = {}; @@ -1124,8 +1131,10 @@ function loadBuildOutputTable(build_info, options={}) { row.passed_tests = test_results; - $(table).bootstrapTable('updateByUniqueId', row.pk, row, true); + data.push(row); }); + + $(table).bootstrapTable('load', row); } } ); @@ -1466,18 +1475,20 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { function redrawAllocationData() { // Force a refresh of each row in the table // Note we cannot call 'refresh' because we are passing data from memory - // var rows = $(table).bootstrapTable('getData'); // How many rows are fully allocated? var allocated_rows = 0; - bom_items.forEach(function(row) { - $(table).bootstrapTable('updateByUniqueId', row.pk, row, true); + for (var idx = 0; idx < bom_items.length; idx++) { + var row = bom_items[idx]; if (isRowFullyAllocated(row)) { - allocated_rows += 1; + allocated_rows++; } - }); + } + + // Reload table data + $(table).bootstrapTable('load', bom_items); // Find the top-level progess bar for this build output var output_progress_bar = $(`#output-progress-${outputId}`); @@ -1675,7 +1686,7 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { setupCallbacks(); }, sortable: true, - showColumns: false, + showColumns: true, detailView: true, detailFilter: function(index, row) { return allocatedQuantity(row) > 0; @@ -1806,6 +1817,7 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { field: 'sub_part_detail.full_name', title: '{% trans "Required Part" %}', sortable: true, + switchable: false, formatter: function(value, row) { var url = `/part/${row.sub_part}/`; var thumb = row.sub_part_detail.thumbnail; @@ -1830,16 +1842,37 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { field: 'reference', title: '{% trans "Reference" %}', sortable: true, + switchable: true, + }, + { + field: 'consumable', + title: '{% trans "Consumable" %}', + sortable: true, + switchable: true, + formatter: function(value) { + return yesNoLabel(value); + } + }, + { + field: 'optional', + title: '{% trans "Optional" %}', + sortable: true, + switchable: true, + formatter: function(value) { + return yesNoLabel(value); + } }, { field: 'quantity', title: '{% trans "Quantity Per" %}', sortable: true, + switchable: false, }, { field: 'available_stock', title: '{% trans "Available" %}', sortable: true, + switchable: true, formatter: function(value, row) { var url = `/part/${row.sub_part_detail.pk}/?display=part-stock`; @@ -1903,6 +1936,7 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { field: 'allocated', title: '{% trans "Allocated" %}', sortable: true, + switchable: false, formatter: function(value, row) { var required = requiredQuantity(row); var allocated = row.consumable ? required : allocatedQuantity(row); @@ -1943,6 +1977,8 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { { field: 'actions', title: '{% trans "Actions" %}', + switchable: false, + sortable: false, formatter: function(value, row) { if (row.consumable) {