diff --git a/openwisp_controller/config/templates/admin/config/device/change_form.html b/openwisp_controller/config/templates/admin/config/device/change_form.html index e97999f75..cad348ad9 100644 --- a/openwisp_controller/config/templates/admin/config/device/change_form.html +++ b/openwisp_controller/config/templates/admin/config/device/change_form.html @@ -17,7 +17,7 @@ the change form. We utilize a distinct form element (id="act_deact_device_form") - specifically for these actions. The form attribute of the submit buttons (Acivate/Deactivate) + specifically for these actions. The form attribute of the submit buttons (Activate/Deactivate) within the submit-row div references this form. By doing so, we ensure that these actions can be submitted independently without causing any disruption to the device form. diff --git a/openwisp_controller/config/tests/test_admin.py b/openwisp_controller/config/tests/test_admin.py index 90bca8a8a..ef36d7dae 100644 --- a/openwisp_controller/config/tests/test_admin.py +++ b/openwisp_controller/config/tests/test_admin.py @@ -1317,7 +1317,7 @@ def test_variable_usage(self): def test_preview_device_config_empty_id(self): path = reverse(f"admin:{self.app_label}_device_preview") - config = json.dumps({"general": {"descripion": "id: {{ id }}"}}) + config = json.dumps({"general": {"description": "id: {{ id }}"}}) data = { "id": "", "name": "test-empty-id", diff --git a/openwisp_controller/config/tests/test_config.py b/openwisp_controller/config/tests/test_config.py index cda1ad2f9..0d3f6772e 100644 --- a/openwisp_controller/config/tests/test_config.py +++ b/openwisp_controller/config/tests/test_config.py @@ -111,7 +111,7 @@ def test_backend_openwrt_different_versions(self): self.assertIsInstance(c.backend_instance, OpenWrt) self.assertEqual(c.backend_instance.dsa, True) - with self.subTest("DSA disabed OpenWrt Firmware"): + with self.subTest("DSA disabled OpenWrt Firmware"): c = Config( backend="netjsonconfig.OpenWrt", device=Device(name="test", os="OpenWrt 19.02.2 r16495-bf0c965af0"), diff --git a/openwisp_controller/config/tests/test_selenium.py b/openwisp_controller/config/tests/test_selenium.py index 4322c00c5..dbb09e899 100644 --- a/openwisp_controller/config/tests/test_selenium.py +++ b/openwisp_controller/config/tests/test_selenium.py @@ -327,7 +327,7 @@ def test_force_delete_device_with_deactivating_config(self): By.CSS_SELECTOR, "#deactivating-warning .messagelist .warning p" ) self.find_element(by=By.CSS_SELECTOR, value="#warning-ack").click() - # After accepting the warning, wee need to wait for the animation + # After accepting the warning, we need to wait for the animation # to complete before trying to interact with the button, # otherwise the test may fail due to the button not being fully # visible or clickable yet. @@ -364,7 +364,7 @@ def test_force_delete_multiple_devices_with_deactivating_config(self): By.CSS_SELECTOR, "#deactivating-warning .messagelist .warning p" ) self.find_element(by=By.CSS_SELECTOR, value="#warning-ack").click() - # After accepting the warning, wee need to wait for the animation + # After accepting the warning, we need to wait for the animation # to complete before trying to interact with the button, # otherwise the test may fail due to the button not being fully # visible or clickable yet. @@ -630,7 +630,15 @@ def get_chrome_webdriver(cls): return webdriver.Chrome(options=options) def _is_unsaved_changes_alert_present(self): - for entry in self.get_browser_logs(): + alert = self.web_driver.execute_script( + "var alert = sessionStorage.getItem('unsaved_changes_alert');" + "sessionStorage.removeItem('unsaved_changes_alert');" + "return alert;" + ) + if alert and "You haven't saved your changes yet!" in alert: + return True + + for entry in self.get_browser_logs() or []: if ( entry["level"] == "WARNING" and "You haven't saved your changes yet!" in entry["message"] @@ -641,6 +649,7 @@ def _is_unsaved_changes_alert_present(self): def _override_unsaved_changes_alert(self): self.web_driver.execute_script( 'django.jQuery(window).on("beforeunload", function(e) {' + " sessionStorage.setItem('unsaved_changes_alert', e.returnValue);" " console.warn(e.returnValue); });" ) diff --git a/openwisp_controller/config/tests/test_views.py b/openwisp_controller/config/tests/test_views.py index fb6c1e5a3..91585a999 100644 --- a/openwisp_controller/config/tests/test_views.py +++ b/openwisp_controller/config/tests/test_views.py @@ -305,7 +305,7 @@ def test_get_default_values_authorization(self): def test_get_default_values_same_keys(self): self._login() - # Atleast 4 templates are required to create enough entropy in database + # At least 4 templates are required to create enough entropy in database # to make the test fail consistently without patch template1 = self._create_template(name="VNI 1", default_values={"vn1": "1"}) template2 = self._create_template( diff --git a/openwisp_controller/config/whois/tests/tests.py b/openwisp_controller/config/whois/tests/tests.py index fd8b73fc5..275e74108 100644 --- a/openwisp_controller/config/whois/tests/tests.py +++ b/openwisp_controller/config/whois/tests/tests.py @@ -1178,7 +1178,7 @@ class TestWHOISSelenium(CreateWHOISMixin, SeleniumTestMixin, StaticLiveServerTes def test_whois_device_admin(self): def _assert_no_js_errors(): browser_logs = [] - for log in self.get_browser_logs(): + for log in self.get_browser_logs() or []: if self.browser == "chrome" and log["source"] != "console-api": continue elif log["message"] in ["wrong event specified: touchleave"]: diff --git a/openwisp_controller/connection/base/models.py b/openwisp_controller/connection/base/models.py index c79484964..17a06f7bb 100644 --- a/openwisp_controller/connection/base/models.py +++ b/openwisp_controller/connection/base/models.py @@ -107,7 +107,7 @@ class AbstractCredentials(ConnectorMixin, ShareableOrgMixinUniqueName, BaseModel """ # Controls the number of objects which can be stored in memory - # before commiting them to database during bulk auto add operation. + # before committing them to database during bulk auto add operation. chunk_size = 1000 connector = models.CharField( @@ -564,7 +564,7 @@ def _save_without_resurrecting(self): def _schedule_command(self): """ - executes ``launch_command`` celery taks in the background + executes ``launch_command`` celery tasks in the background once changes are committed to the database """ transaction.on_commit(lambda: launch_command.delay(self.pk)) diff --git a/openwisp_controller/connection/commands.py b/openwisp_controller/connection/commands.py index 0e83011ec..9ad6c6a15 100644 --- a/openwisp_controller/connection/commands.py +++ b/openwisp_controller/connection/commands.py @@ -58,7 +58,7 @@ "title": _("Confirm Password"), }, }, - "message": _("Your password must be atleast 6 characters long"), + "message": _("Your password must be at least 6 characters long"), "additionalProperties": False, "definitions": { "password_regex": { diff --git a/openwisp_controller/connection/connectors/openwrt/ssh.py b/openwisp_controller/connection/connectors/openwrt/ssh.py index f0907f783..fc19db6a9 100644 --- a/openwisp_controller/connection/connectors/openwrt/ssh.py +++ b/openwisp_controller/connection/connectors/openwrt/ssh.py @@ -11,7 +11,7 @@ class OpenWrt(Ssh): def update_config(self): try: output, exit_code = self.exec_command( - # "openwisp_config" for backword compatibility + # "openwisp_config" for backward compatibility "(openwisp-config --version || openwisp_config --version) 2>/dev/null" ) except Exception as error: diff --git a/openwisp_controller/connection/connectors/ssh.py b/openwisp_controller/connection/connectors/ssh.py index a6a1b87d9..52a585304 100644 --- a/openwisp_controller/connection/connectors/ssh.py +++ b/openwisp_controller/connection/connectors/ssh.py @@ -138,7 +138,7 @@ def _connect(self, address): """ Tries to instantiate the SSH connection, if the connection fails, it tries again - by disabling the new deafult HostKeyAlgorithms + by disabling the new default HostKeyAlgorithms used by newer versions of Paramiko """ params = self.params @@ -187,11 +187,11 @@ def exec_command( # paramiko expects timeout as a float timeout = float(timeout) logger.info("Executing command: {0}".format(command)) - # execute commmand + # execute command try: start_cmd = time.perf_counter() stdin, stdout, stderr = self.shell.exec_command(command, timeout=timeout) - # re-raise socket.timeout to avoid being catched + # re-raise socket.timeout to avoid being caught # by the subsequent `except Exception as e` block except socket.timeout: raise socket.timeout() diff --git a/openwisp_controller/connection/tests/test_notifications.py b/openwisp_controller/connection/tests/test_notifications.py index 3c45305ba..2c6db1b03 100644 --- a/openwisp_controller/connection/tests/test_notifications.py +++ b/openwisp_controller/connection/tests/test_notifications.py @@ -52,7 +52,7 @@ def test_default_notification_type_already_unregistered(self): # by some other module # Unregister "config_error" and "device_registered" notification - # types to avoid getting rasing ImproperlyConfigured exceptions + # types to avoid getting raising ImproperlyConfigured exceptions unregister_notification_type("connection_is_not_working") unregister_notification_type("connection_is_working") diff --git a/openwisp_controller/connection/tests/test_selenium.py b/openwisp_controller/connection/tests/test_selenium.py index db740dac3..e75af19d3 100644 --- a/openwisp_controller/connection/tests/test_selenium.py +++ b/openwisp_controller/connection/tests/test_selenium.py @@ -55,7 +55,7 @@ def test_command_widget_on_device(self): self.find_element(by=By.CSS_SELECTOR, value="#ow-command-confirm-yes").click() self.assertEqual(Command.objects.count(), 1) - # TODO: Selenium tests does not support websocket connections. + # TODO: Selenium tests do not support websocket connections. # Thus, we need to refresh the page. Remove this when support for # websockets is added. self.open(path) diff --git a/openwisp_controller/connection/tests/utils.py b/openwisp_controller/connection/tests/utils.py index ce122a25a..e8b477791 100644 --- a/openwisp_controller/connection/tests/utils.py +++ b/openwisp_controller/connection/tests/utils.py @@ -17,7 +17,7 @@ class SshServer(Server): def _run(self, *args, **kwargs): """ Hides 'Bad file descriptor' system issue which - does not affect the effectivness of the tests + does not affect the effectiveness of the tests """ try: return super()._run(*args, **kwargs) diff --git a/openwisp_controller/geo/tests/pytest.py b/openwisp_controller/geo/tests/pytest.py index e377520c4..fbe3a0e86 100644 --- a/openwisp_controller/geo/tests/pytest.py +++ b/openwisp_controller/geo/tests/pytest.py @@ -160,7 +160,7 @@ async def test_common_location_org_isolation(self): connected, _ = await admin_communicator.connect() assert connected - # Updating co-ordinates for org1_location should notify org1_user and admin, + # Updating coordinates for org1_location should notify org1_user and admin, await self._save_location(str(org1_location.pk)) org1_response = await org1_communicator.receive_json_from(timeout=1) assert org1_response["id"] == str(org1_location.pk) @@ -181,7 +181,7 @@ async def test_common_location_org_isolation(self): connected, _ = await org2_communicator.connect() assert connected - # Updating co-ordinates for org2_location should notify org2_user and admin, + # Updating coordinates for org2_location should notify org2_user and admin, await self._save_location(str(org2_location.pk)) org2_response = await org2_communicator.receive_json_from(timeout=1) assert org2_response["id"] == str(org2_location.pk) diff --git a/openwisp_controller/geo/tests/test_api.py b/openwisp_controller/geo/tests/test_api.py index d50e7a833..156550692 100644 --- a/openwisp_controller/geo/tests/test_api.py +++ b/openwisp_controller/geo/tests/test_api.py @@ -185,14 +185,14 @@ def test_deactivated_device(self): url = "{0}?key={1}".format(reverse(self.url_name, args=[device.pk]), device.key) device.deactivate() - with self.subTest("Test retrieving device co-ordinates"): + with self.subTest("Test retrieving device coordinates"): response = self.client.get( url, content_type="application/json", ) self.assertEqual(response.status_code, 200) - with self.subTest("Test updating device co-ordinates"): + with self.subTest("Test updating device coordinates"): response = self.client.put( url, content_type="application/json", diff --git a/openwisp_controller/geo/tests/test_selenium.py b/openwisp_controller/geo/tests/test_selenium.py index 1a24354c9..25995edae 100644 --- a/openwisp_controller/geo/tests/test_selenium.py +++ b/openwisp_controller/geo/tests/test_selenium.py @@ -4,7 +4,22 @@ from django.test import tag from django.urls.base import reverse from django_loci.tests import TestAdminMixin +from unittest.mock import MagicMock +from django_loci.base import geocoding_views from django_loci.tests.base.test_selenium import BaseTestDeviceAdminSelenium +from time import sleep +from selenium.webdriver import ActionChains +from selenium.webdriver.support import expected_conditions as EC +from selenium.webdriver.support.ui import WebDriverWait + +class MockLocation: + def __init__(self, address="Lazio 00185, ITA", latitude=41.898903, longitude=12.512124): + self.address = address + self.latitude = latitude + self.longitude = longitude + +geocoding_views.geocode = MagicMock(return_value=MockLocation()) +geocoding_views.reverse_geocode = MagicMock(return_value=MockLocation()) from selenium.webdriver.common.by import By from swapper import load_model @@ -63,6 +78,39 @@ def _fill_device_form(self): self.find_element(by=By.CLASS_NAME, value="select2-results__option").click() super()._fill_device_form() + def test_real_time_update_address_field(self): + # Changing the address in tab 1 should update it in tab 0 in real time without a page reload. + # We increase the alert check timeout to 10s to prevent TimeoutException on slow CI runs. + location = self._create_location() + self.login() + url = reverse(f"admin:{self.app_label}_location_change", args=[location.id]) + self.open(url) + self.web_driver.switch_to.new_window("tab") + tabs = self.web_driver.window_handles + self.web_driver.switch_to.window(tabs[-1]) + self.open(url) + address_input = self.find_element(by=By.ID, value="id_address") + self.assertEqual(address_input.get_attribute("value"), location.address) + self.find_element( + by=By.XPATH, value='//a[@class="leaflet-draw-draw-marker"]' + ).click() + elem = self.find_element(by=By.ID, value="id_geometry-map") + ActionChains(self.web_driver).move_to_element(elem).move_by_offset( + 30, 15 + ).click().perform() + alert = WebDriverWait(self.web_driver, 10).until(EC.alert_is_present()) + alert.accept() + sleep(0.05) + new_address = "Lazio 00185, ITA" + address_input = self.find_element(by=By.ID, value="id_address") + self.assertIn(new_address, address_input.get_attribute("value")) + self.wait_for("element_to_be_clickable", by=By.NAME, value="_continue").click() + self.web_driver.close() + initial_tab = tabs.index(tabs[-1]) - 1 + self.web_driver.switch_to.window(tabs[initial_tab]) + address_input = self.find_element(by=By.ID, value="id_address") + self.assertIn(new_address, address_input.get_attribute("value")) + @tag("selenium_tests") class TestDeviceAdminReadonly( @@ -108,10 +156,19 @@ def test_unsaved_changes_readonly(self): # so we just need to ensure it's set as expected self.web_driver.execute_script( 'django.jQuery(window).on("beforeunload", function(e) {' + " sessionStorage.setItem('unsaved_changes_alert', e.returnValue);" " console.warn(e.returnValue); });" ) self.web_driver.refresh() - for entry in self.get_browser_logs(): + alert = self.web_driver.execute_script( + "var alert = sessionStorage.getItem('unsaved_changes_alert');" + "sessionStorage.removeItem('unsaved_changes_alert');" + "return alert;" + ) + if alert and "You haven't saved your changes yet!" in alert: + self.fail("Unsaved changes alert displayed without any change") + + for entry in self.get_browser_logs() or []: if ( entry["level"] == "WARNING" and "You haven't saved your changes yet!" in entry["message"] diff --git a/openwisp_controller/pki/base/models.py b/openwisp_controller/pki/base/models.py index f93767329..803d8f84a 100644 --- a/openwisp_controller/pki/base/models.py +++ b/openwisp_controller/pki/base/models.py @@ -9,7 +9,24 @@ from ..utils import UnqiueCommonNameMixin +# Avoids "DateTimeField received a naive datetime while time zone support is active" +# warning by returning an aware datetime when USE_TZ is True. +def default_validity_start(): + from datetime import datetime, timedelta + from django.conf import settings + from django.utils import timezone + start = datetime.now() - timedelta(days=1) + start = start.replace(hour=0, minute=0, second=0, microsecond=0) + if settings.USE_TZ: + return timezone.make_aware(start) + return start + + class AbstractCa(ShareableOrgMixin, UnqiueCommonNameMixin, BaseCa): + validity_start = models.DateTimeField( + blank=True, null=True, default=default_validity_start + ) + class Meta(BaseCa.Meta): abstract = True constraints = [ @@ -21,6 +38,9 @@ class Meta(BaseCa.Meta): class AbstractCert(ShareableOrgMixin, UnqiueCommonNameMixin, BaseCert): + validity_start = models.DateTimeField( + blank=True, null=True, default=default_validity_start + ) ca = models.ForeignKey( get_model_name("django_x509", "Ca"), verbose_name=_("CA"), diff --git a/openwisp_controller/pki/migrations/0013_alter_ca_validity_start_alter_cert_validity_start.py b/openwisp_controller/pki/migrations/0013_alter_ca_validity_start_alter_cert_validity_start.py new file mode 100644 index 000000000..1ec87dab7 --- /dev/null +++ b/openwisp_controller/pki/migrations/0013_alter_ca_validity_start_alter_cert_validity_start.py @@ -0,0 +1,30 @@ +import openwisp_controller.pki.base.models +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("pki", "0012_alter_ca_extensions_alter_ca_key_length_and_more"), + ] + + operations = [ + migrations.AlterField( + model_name="ca", + name="validity_start", + field=models.DateTimeField( + blank=True, + default=openwisp_controller.pki.base.models.default_validity_start, + null=True, + ), + ), + migrations.AlterField( + model_name="cert", + name="validity_start", + field=models.DateTimeField( + blank=True, + default=openwisp_controller.pki.base.models.default_validity_start, + null=True, + ), + ), + ] diff --git a/openwisp_controller/subnet_division/base/models.py b/openwisp_controller/subnet_division/base/models.py index b6dd58c57..4ee77d240 100644 --- a/openwisp_controller/subnet_division/base/models.py +++ b/openwisp_controller/subnet_division/base/models.py @@ -132,7 +132,7 @@ def _validate_master_subnet_consistency(self): raise ValidationError( { "number_of_subnets": _( - "The master subnet is too small to acommodate the " + "The master subnet is too small to accommodate the " 'requested "number of subnets" plus the reserved ' "subnet, please increase the size of the master " 'subnet or decrease the "size of subnets" field.' diff --git a/openwisp_controller/subnet_division/rule_types/base.py b/openwisp_controller/subnet_division/rule_types/base.py index 2b51797b8..e22c565ca 100644 --- a/openwisp_controller/subnet_division/rule_types/base.py +++ b/openwisp_controller/subnet_division/rule_types/base.py @@ -177,7 +177,7 @@ def get_config(cls, instance): # check for real existence in DB to workaround # this django-import-export bug: # https://github.com/django-import-export/django-import-export/issues/1078 - # TODO: if that issue is ever solved, we can remove the this block below + # TODO: if that issue is ever solved, we can remove this block below Config = config._meta.model if not Config.objects.filter(pk=config.pk).exists(): raise ObjectDoesNotExist() diff --git a/openwisp_controller/subnet_division/tests/test_models.py b/openwisp_controller/subnet_division/tests/test_models.py index 7137d596c..2377399c8 100644 --- a/openwisp_controller/subnet_division/tests/test_models.py +++ b/openwisp_controller/subnet_division/tests/test_models.py @@ -178,7 +178,7 @@ def test_field_validations(self): rule.full_clean() expected_message_dict = { "number_of_subnets": [ - "The master subnet is too small to acommodate " + "The master subnet is too small to accommodate " 'the requested "number of subnets" plus the ' "reserved subnet, please increase the size of " "the master subnet or decrease the " @@ -279,7 +279,7 @@ def test_slash_32_rule_ipv4_error(self): except ValidationError as e: self.assertIn("number_of_subnets", e.message_dict) self.assertIn( - "The master subnet is too small to acommodate", + "The master subnet is too small to accommodate", e.message_dict["number_of_subnets"][0], ) else: @@ -315,7 +315,7 @@ def test_slash_128_rule_ipv6_error(self): except ValidationError as e: self.assertIn("number_of_subnets", e.message_dict) self.assertIn( - "The master subnet is too small to acommodate", + "The master subnet is too small to accommodate", e.message_dict["number_of_subnets"][0], ) else: @@ -495,7 +495,7 @@ def test_subnets_exhausted(self, mocked_logger, *args): subnet = self._get_master_subnet( "10.0.0.0/29", master_subnet=self.master_subnet ) - # The master subnet can acommodate + # The master subnet can accommodate # this rule only once: # A /29 has 4 /31 slots available # Minus the reserved subnet = 3 diff --git a/openwisp_controller/tests/test_selenium.py b/openwisp_controller/tests/test_selenium.py index 8a3c94f45..208bed20c 100644 --- a/openwisp_controller/tests/test_selenium.py +++ b/openwisp_controller/tests/test_selenium.py @@ -89,7 +89,7 @@ def test_restoring_deleted_device(self, *args): # is logged at WARNING level. # By checking that there are no WARNING level errors logged in the # browser console, we ensure that this issue is not happening. - for error in self.get_browser_logs(): + for error in self.get_browser_logs() or []: if error["level"] == "WARNING" and error["message"] not in [ "wrong event specified: touchleave" ]: diff --git a/tests/openwisp2/sample_pki/migrations/0005_alter_ca_validity_start_alter_cert_validity_start.py b/tests/openwisp2/sample_pki/migrations/0005_alter_ca_validity_start_alter_cert_validity_start.py new file mode 100644 index 000000000..53e86db31 --- /dev/null +++ b/tests/openwisp2/sample_pki/migrations/0005_alter_ca_validity_start_alter_cert_validity_start.py @@ -0,0 +1,32 @@ +# Generated by Django 5.2.15 on 2026-06-16 16:34 + +import openwisp_controller.pki.base.models +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("sample_pki", "0004_alter_ca_extensions_alter_ca_key_length_and_more"), + ] + + operations = [ + migrations.AlterField( + model_name="ca", + name="validity_start", + field=models.DateTimeField( + blank=True, + default=openwisp_controller.pki.base.models.default_validity_start, + null=True, + ), + ), + migrations.AlterField( + model_name="cert", + name="validity_start", + field=models.DateTimeField( + blank=True, + default=openwisp_controller.pki.base.models.default_validity_start, + null=True, + ), + ), + ]