From 0fd1390fe05474a848b56092549ab4dbeb3a70cd Mon Sep 17 00:00:00 2001
From: Oliver <oliver.henry.walters@gmail.com>
Date: Sat, 22 Oct 2022 12:45:50 +1100
Subject: [PATCH 1/4] Workflow fix (#3830)

* Specify minimum python version for docker build workflow

(cherry picked from commit 18c55b30b81aef66c131a960586b52d655a621f9)

* Specify python version for translation checker

(cherry picked from commit 14360507f5c9d77ed8321755aab25a79fbcf10f9)

* Disable social media workflow

(has never worked, anyway)

(cherry picked from commit 56fbcbeae25b165978d149aa484ae7335f8f67a4)
---
 .github/workflows/check_translations.yaml               | 6 ++++++
 .github/workflows/docker.yaml                           | 7 ++++++-
 .github/workflows/{release.yml => release.yml.disabled} | 0
 3 files changed, 12 insertions(+), 1 deletion(-)
 rename .github/workflows/{release.yml => release.yml.disabled} (100%)

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

From 9d39d5b00f22e5eef7e0ffa6cb6a0f00b926a7f1 Mon Sep 17 00:00:00 2001
From: Oliver <oliver.henry.walters@gmail.com>
Date: Sat, 22 Oct 2022 14:16:55 +1100
Subject: [PATCH 2/4] Improve redraw speed of build allocation table (#3831)

- Reload table data in one go, rather than one row at a time
- Reduces redraw time (in one example) from ~4s to ~0.05s
---
 InvenTree/templates/js/translated/build.js | 11 +++++++----
 1 file changed, 7 insertions(+), 4 deletions(-)

diff --git a/InvenTree/templates/js/translated/build.js b/InvenTree/templates/js/translated/build.js
index fa1ec5bab4..223372a767 100644
--- a/InvenTree/templates/js/translated/build.js
+++ b/InvenTree/templates/js/translated/build.js
@@ -1471,13 +1471,16 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
         // 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}`);

From c120de90ae2336978fb818c86c9850740c67973d Mon Sep 17 00:00:00 2001
From: Oliver <oliver.henry.walters@gmail.com>
Date: Sat, 22 Oct 2022 18:56:23 +1100
Subject: [PATCH 3/4] Build table improvements (#3833)

* Add extra columns to build order table

* Optimize build table update

* Improve loading speed of 'test results' in build output table
---
 InvenTree/templates/js/translated/build.js | 41 +++++++++++++++++++---
 1 file changed, 37 insertions(+), 4 deletions(-)

diff --git a/InvenTree/templates/js/translated/build.js b/InvenTree/templates/js/translated/build.js
index 223372a767..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,7 +1475,6 @@ 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;
@@ -1678,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;
@@ -1809,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;
@@ -1833,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`;
@@ -1906,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);
@@ -1946,6 +1977,8 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
             {
                 field: 'actions',
                 title: '{% trans "Actions" %}',
+                switchable: false,
+                sortable: false,
                 formatter: function(value, row) {
 
                     if (row.consumable) {

From a898ebce4038d5967c1e37503bb58121c469e9a8 Mon Sep 17 00:00:00 2001
From: Oliver <oliver.henry.walters@gmail.com>
Date: Sat, 22 Oct 2022 18:56:38 +1100
Subject: [PATCH 4/4] Fix email notification setting (#3832)

* Coerce setting value to a boolean

* Ignore inactive users when sending notification emails

* Only send UI notifications to active users

* Fixes for unit tests
---
 InvenTree/build/test_build.py                             | 6 ++++--
 InvenTree/common/notifications.py                         | 5 +++--
 InvenTree/common/tests.py                                 | 5 +++--
 InvenTree/order/test_sales_order.py                       | 2 +-
 InvenTree/order/tests.py                                  | 7 ++++++-
 .../plugin/builtin/integration/core_notifications.py      | 8 +++++++-
 6 files changed, 24 insertions(+), 9 deletions(-)

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)