diff --git a/itassets/middleware.py b/itassets/middleware.py index d8730da2..4dbbbf29 100644 --- a/itassets/middleware.py +++ b/itassets/middleware.py @@ -2,8 +2,11 @@ from django.http import HttpResponse, HttpResponseServerError -class HealthCheckMiddleware(object): +class HealthCheckMiddleware: """Middleware to provide healthcheck HTTP endpoints for the system. + Should be placed at the top of the MIDDLEWARE list so that requests + to healthcheck endpoints short-circuit and return a response without + passing through further middleware classes. """ def __init__(self, get_response): @@ -18,8 +21,7 @@ def __call__(self, request): return self.get_response(request) def liveness(self, request): - """Returns that the server is alive and able to serve HTTP responses. - """ + """Returns that the server is alive and able to serve HTTP responses.""" return HttpResponse("OK") def readiness(self, request): diff --git a/itassets/settings.py b/itassets/settings.py index e3bb49aa..20bd2534 100644 --- a/itassets/settings.py +++ b/itassets/settings.py @@ -67,10 +67,12 @@ "django.middleware.security.SecurityMiddleware", "whitenoise.middleware.WhiteNoiseMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.cache.UpdateCacheMiddleware", "django.middleware.common.CommonMiddleware", "django.middleware.csrf.CsrfViewMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.cache.FetchFromCacheMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", "dbca_utils.middleware.SSOLoginMiddleware", ] @@ -110,7 +112,14 @@ "LOCATION": REDIS_CACHE_HOST, } } +else: + CACHES = { + "default": { + "BACKEND": "django.core.cache.backends.dummy.DummyCache", + } + } API_RESPONSE_CACHE_SECONDS = env("API_RESPONSE_CACHE_SECONDS", 60) +CACHE_MIDDLEWARE_SECONDS = env("CACHE_MIDDLEWARE_SECONDS", 60) ADMIN_EMAILS = env("ADMIN_EMAILS", "asi@dbca.wa.gov.au").split(",") SERVICE_DESK_EMAIL = env("SERVICE_DESK_EMAIL", "oim.servicedesk@dbca.wa.gov.au") diff --git a/kustomize/overlays/prod/cronjobs/deptusers-check-ascender/patch.yaml b/kustomize/overlays/prod/cronjobs/deptusers-check-ascender/patch.yaml index f76e92a6..2a465c57 100644 --- a/kustomize/overlays/prod/cronjobs/deptusers-check-ascender/patch.yaml +++ b/kustomize/overlays/prod/cronjobs/deptusers-check-ascender/patch.yaml @@ -3,8 +3,8 @@ kind: CronJob metadata: name: itassets-cronjob spec: - # AWST: 10/40 min past the hour, 08:00-19:00, Mon-Fri - schedule: "10,40 0-10 * * 1-5" + # AWST: 10/40 min past the hour, 07:00-19:00, Mon-Fri + schedule: "10,40 0-10,23 * * 1-5" jobTemplate: spec: activeDeadlineSeconds: 600 diff --git a/kustomize/overlays/prod/deployment_patch.yaml b/kustomize/overlays/prod/deployment_patch.yaml index 9396cd91..d381edf4 100644 --- a/kustomize/overlays/prod/deployment_patch.yaml +++ b/kustomize/overlays/prod/deployment_patch.yaml @@ -197,3 +197,8 @@ spec: secretKeyRef: name: itassets-env-prod key: REDIS_CACHE_HOST + - name: API_RESPONSE_CACHE_SECONDS + valueFrom: + secretKeyRef: + name: itassets-env-prod + key: API_RESPONSE_CACHE_SECONDS diff --git a/kustomize/overlays/prod/kustomization.yaml b/kustomize/overlays/prod/kustomization.yaml index 4fdfd378..9d92fc59 100644 --- a/kustomize/overlays/prod/kustomization.yaml +++ b/kustomize/overlays/prod/kustomization.yaml @@ -35,4 +35,4 @@ patches: - path: postgres_fdw_service_patch.yaml images: - name: ghcr.io/dbca-wa/it-assets - newTag: 2.4.28 + newTag: 2.4.29 diff --git a/kustomize/overlays/uat/deployment_patch.yaml b/kustomize/overlays/uat/deployment_patch.yaml index 1187b2c5..a2d3859d 100644 --- a/kustomize/overlays/uat/deployment_patch.yaml +++ b/kustomize/overlays/uat/deployment_patch.yaml @@ -203,3 +203,8 @@ spec: secretKeyRef: name: itassets-env-uat key: REDIS_CACHE_HOST + - name: API_RESPONSE_CACHE_SECONDS + valueFrom: + secretKeyRef: + name: itassets-env-uat + key: API_RESPONSE_CACHE_SECONDS diff --git a/organisation/ascender.py b/organisation/ascender.py index df27dce6..b96aa79c 100644 --- a/organisation/ascender.py +++ b/organisation/ascender.py @@ -466,6 +466,7 @@ def ascender_user_import_all(): user.ascender_data = job user.ascender_data_updated = timezone.localtime() user.update_from_ascender_data() # This method calls save() + LOGGER.info(f"Updated existing user {user}") elif not DepartmentUser.objects.filter(employee_id=employee_id).exists(): # Ascender record does not exist in our database; conditionally create a new # Azure AD account and DepartmentUser instance for them. diff --git a/organisation/static/js/location_map.js b/organisation/static/js/location_map.js index 02d32c8f..6e8e93c1 100644 --- a/organisation/static/js/location_map.js +++ b/organisation/static/js/location_map.js @@ -15,22 +15,61 @@ const mapboxStreets = L.tileLayer( // Define overlay tile layers. const dbcaRegions = L.tileLayer( - geoserver_wmts_url_overlay + "&layer=cddp:dbca_regions", + geoserver_wmts_url_overlay + "&layer=cddp:kaartdijin-boodja-public_CPT_DBCA_REGIONS", { tileSize: 1024, zoomOffset: -2, }, ); const dbcaDistricts = L.tileLayer( - geoserver_wmts_url_overlay + "&layer=public:dbca_districts_public", + geoserver_wmts_url_overlay + "&layer=cddp:kaartdijin-boodja-public_CPT_DBCA_DISTRICTS", { tileSize: 1024, zoomOffset: -2, }, ); +// Function to define hover effect for location points. +function locationHover(feature, layer) { + layer.bindTooltip( + feature.properties.name, + { className: "leaflet-tooltip-wide" } + ); + layer.bindPopup(`${feature.properties.name}
+${feature.properties.ascender_desc}
+${feature.properties.phone}`); +} + +// Define a clustered layer for DBCA locations, and a GeoJSON layer to contain the data. +const locationsClustered = L.markerClusterGroup(); +const dbcaLocations = L.geoJSON( + null, // Initially empty. + { + onEachFeature: locationHover + }, +); + +// Function to get location data and populate the layer. +function queryLocationsData() { + $.ajax({ + dataType: "json", + url: location_features_url, + data: { format: "geojson" }, + success: function (data) { + // Add the device data to the GeoJSON layer. + dbcaLocations.addData(data); + // Add DBCA locations layer to the map display and zoom to their bounds. + locationsClustered.addLayer(dbcaLocations); + map.addLayer(locationsClustered); + map.fitBounds(dbcaLocations.getBounds()); + }, + }); +}; +// Immediately run the function once to get data. +queryLocationsData(); + // Define map. -const map = L.map('map', { +const map = L.map("map", { crs: L.CRS.EPSG4326, // WGS 84 center: [-31.96, 115.87], minZoom: 4, @@ -54,26 +93,11 @@ L.control.layers(baseMaps, overlayMaps).addTo(map); // Define scale bar L.control.scale({ maxWidth: 500, imperial: false }).addTo(map); -// Function to define hover effect for location points. -function locationHover(feature, layer) { - layer.bindTooltip( - feature.properties.name, - { className: 'leaflet-tooltip-wide' } - ); - layer.bindPopup(` -${feature.properties.name}
-${feature.properties.ascender_desc}
-${feature.properties.phone}`); -} - -// Add locations to the map display and zoom to their bounds. -const locationsLayer = L.geoJson(locationFeatures, { - onEachFeature: locationHover +// Move the map div inside the #locations-tab-pane div and redraw it. +// https://stackoverflow.com/a/63319084/14508 +const mapDiv = document.getElementById("map"); +const mapResizeObserver = new ResizeObserver(() => { + map.invalidateSize(); }); -const locations = L.markerClusterGroup(); -locations.addLayer(locationsLayer); -map.addLayer(locations); -map.fitBounds(locations.getBounds()); - -// Move the map div inside the #locations-tab-pane div. -document.getElementById("locations-tab-pane").appendChild(document.getElementById("map")); +document.getElementById("locations-tab-pane").appendChild(mapDiv); +mapResizeObserver.observe(mapDiv); diff --git a/organisation/templates/organisation/address_book.html b/organisation/templates/organisation/address_book.html index 72990344..3415b843 100644 --- a/organisation/templates/organisation/address_book.html +++ b/organisation/templates/organisation/address_book.html @@ -31,6 +31,14 @@

{{ page_title }}

+ +
+
+ +
+
+
+
- - -
@@ -135,11 +140,12 @@

{{ page_title }}

{% block extra_js %} {{ block.super }} + {% endblock extra_js %} diff --git a/organisation/views.py b/organisation/views.py index 1dd4a5cf..d3ead305 100644 --- a/organisation/views.py +++ b/organisation/views.py @@ -28,13 +28,6 @@ def get_context_data(self, **kwargs): context["previous_pages"] = get_previous_pages(context["page_obj"]) context["next_pages"] = get_next_pages(context["page_obj"]) context["geoserver_url"] = settings.GEOSERVER_URL - context["locations_geojson"] = serialize( - "geojson", - Location.objects.filter(active=True, point__isnull=False, ascender_desc__isnull=False), - geometry_field="point", - srid=4283, - fields=["name", "phone", "ascender_desc"], - ) return context def get_queryset(self): @@ -203,6 +196,16 @@ def get(self, request, *args, **kwargs): # Tailor the API response. if "selectlist" in request.GET: # Smaller response, for use in HTML select lists. locations = [{"id": location.pk, "text": location.name} for location in queryset] + elif "format" in request.GET and request.GET["format"] == "geojson": + # Return the API response in GeoJSON format. + locations = serialize( + "geojson", + Location.objects.filter(active=True, point__isnull=False, ascender_desc__isnull=False), + geometry_field="point", + srid=4283, + fields=["id", "name", "address", "phone", "ascender_desc"], + ) + return HttpResponse(content=locations, content_type="application/json") else: locations = [ { diff --git a/poetry.lock b/poetry.lock index a67afcc3..58cd0244 100644 --- a/poetry.lock +++ b/poetry.lock @@ -602,19 +602,19 @@ python-dateutil = ">=2.4" [[package]] name = "filelock" -version = "3.15.4" +version = "3.16.0" description = "A platform independent file lock." optional = false python-versions = ">=3.8" files = [ - {file = "filelock-3.15.4-py3-none-any.whl", hash = "sha256:6ca1fffae96225dab4c6eaf1c4f4f28cd2568d3ec2a44e15a08520504de468e7"}, - {file = "filelock-3.15.4.tar.gz", hash = "sha256:2207938cbc1844345cb01a5a95524dae30f0ce089eba5b00378295a17e3e90cb"}, + {file = "filelock-3.16.0-py3-none-any.whl", hash = "sha256:f6ed4c963184f4c84dd5557ce8fece759a3724b37b80c6c4f20a2f63a4dc6609"}, + {file = "filelock-3.16.0.tar.gz", hash = "sha256:81de9eb8453c769b63369f87f11131a7ab04e367f8d97ad39dc230daa07e3bec"}, ] [package.extras] -docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] -testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8.0.1)", "pytest (>=7.4.3)", "pytest-asyncio (>=0.21)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)", "virtualenv (>=20.26.2)"] -typing = ["typing-extensions (>=4.8)"] +docs = ["furo (>=2024.8.6)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.6.1)", "diff-cover (>=9.1.1)", "pytest (>=8.3.2)", "pytest-asyncio (>=0.24)", "pytest-cov (>=5)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.26.3)"] +typing = ["typing-extensions (>=4.12.2)"] [[package]] name = "gunicorn" @@ -779,22 +779,22 @@ tests = ["Django (>=3.0)", "Flask (>=1.0)", "Marshmallow (>=3.9)", "SQLAlchemy ( [[package]] name = "msal" -version = "1.30.0" +version = "1.31.0" description = "The Microsoft Authentication Library (MSAL) for Python library enables your app to access the Microsoft Cloud by supporting authentication of users with Microsoft Azure Active Directory accounts (AAD) and Microsoft Accounts (MSA) using industry standard OAuth2 and OpenID Connect." optional = false python-versions = ">=3.7" files = [ - {file = "msal-1.30.0-py3-none-any.whl", hash = "sha256:423872177410cb61683566dc3932db7a76f661a5d2f6f52f02a047f101e1c1de"}, - {file = "msal-1.30.0.tar.gz", hash = "sha256:b4bf00850092e465157d814efa24a18f788284c9a479491024d62903085ea2fb"}, + {file = "msal-1.31.0-py3-none-any.whl", hash = "sha256:96bc37cff82ebe4b160d5fc0f1196f6ca8b50e274ecd0ec5bf69c438514086e7"}, + {file = "msal-1.31.0.tar.gz", hash = "sha256:2c4f189cf9cc8f00c80045f66d39b7c0f3ed45873fd3d1f2af9f22db2e12ff4b"}, ] [package.dependencies] -cryptography = ">=2.5,<45" +cryptography = ">=2.5,<46" PyJWT = {version = ">=1.0.0,<3", extras = ["crypto"]} requests = ">=2.0.0,<3" [package.extras] -broker = ["pymsalruntime (>=0.13.2,<0.17)"] +broker = ["pymsalruntime (>=0.14,<0.18)", "pymsalruntime (>=0.17,<0.18)"] [[package]] name = "nodeenv" @@ -870,19 +870,19 @@ ptyprocess = ">=0.5" [[package]] name = "platformdirs" -version = "4.2.2" +version = "4.3.2" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false python-versions = ">=3.8" files = [ - {file = "platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee"}, - {file = "platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"}, + {file = "platformdirs-4.3.2-py3-none-any.whl", hash = "sha256:eb1c8582560b34ed4ba105009a4badf7f6f85768b30126f351328507b2beb617"}, + {file = "platformdirs-4.3.2.tar.gz", hash = "sha256:9e5e27a08aa095dd127b9f2e764d74254f482fef22b0970773bfba79d091ab8c"}, ] [package.extras] -docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] -type = ["mypy (>=1.8)"] +docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"] +type = ["mypy (>=1.11.2)"] [[package]] name = "pre-commit" @@ -1254,13 +1254,13 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "sentry-sdk" -version = "2.13.0" +version = "2.14.0" description = "Python client for Sentry (https://sentry.io)" optional = false python-versions = ">=3.6" files = [ - {file = "sentry_sdk-2.13.0-py2.py3-none-any.whl", hash = "sha256:6beede8fc2ab4043da7f69d95534e320944690680dd9a963178a49de71d726c6"}, - {file = "sentry_sdk-2.13.0.tar.gz", hash = "sha256:8d4a576f7a98eb2fdb40e13106e41f330e5c79d72a68be1316e7852cf4995260"}, + {file = "sentry_sdk-2.14.0-py2.py3-none-any.whl", hash = "sha256:b8bc3dc51d06590df1291b7519b85c75e2ced4f28d9ea655b6d54033503b5bf4"}, + {file = "sentry_sdk-2.14.0.tar.gz", hash = "sha256:1e0e2eaf6dad918c7d1e0edac868a7bf20017b177f242cefe2a6bcd47955961d"}, ] [package.dependencies] @@ -1415,13 +1415,13 @@ zstd = ["zstandard (>=0.18.0)"] [[package]] name = "virtualenv" -version = "20.26.3" +version = "20.26.4" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.7" files = [ - {file = "virtualenv-20.26.3-py3-none-any.whl", hash = "sha256:8cc4a31139e796e9a7de2cd5cf2489de1217193116a8fd42328f1bd65f434589"}, - {file = "virtualenv-20.26.3.tar.gz", hash = "sha256:4c43a2a236279d9ea36a0d76f98d84bd6ca94ac4e0f4a3b9d46d05e10fea542a"}, + {file = "virtualenv-20.26.4-py3-none-any.whl", hash = "sha256:48f2695d9809277003f30776d155615ffc11328e6a0a8c1f0ec80188d7874a55"}, + {file = "virtualenv-20.26.4.tar.gz", hash = "sha256:c17f4e0f3e6036e9f26700446f85c76ab11df65ff6d8a9cbfad9f71aabfcf23c"}, ] [package.dependencies] @@ -1488,4 +1488,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "3ddc89b7045b4eb4a8a293bf603befc5cf8b443e1075138ecad9512551bfe1ea" +content-hash = "64f3ef62c7be1ba9f60079c57dff431c88c1b1a9ecf54222418d101dc5721838" diff --git a/pyproject.toml b/pyproject.toml index 0a021a5e..582ff22c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "itassets" -version = "2.4.28" +version = "2.4.29" description = "DBCA IT assets (both physical and knowledge-based) management system" authors = ["DBCA OIM "] license = "Apache-2.0" @@ -19,13 +19,13 @@ unicodecsv = "0.14.1" python-dateutil = "2.8.2" webtemplate-dbca = "1.7.1" mixer = "7.2.2" -msal = "1.30.0" +msal = "1.31.0" whitenoise = {version = "6.7.0", extras = ["brotli"]} pysftp = "0.2.9" azure-storage-blob = "12.22.0" django-storages = {version = "1.14.4", extras = ["azure"]} xlsxwriter = "3.2.0" -sentry-sdk = {version = "2.13.0", extras = ["django"]} +sentry-sdk = {version = "2.14.0", extras = ["django"]} redis = "5.0.8" [tool.poetry.group.dev.dependencies]