diff --git a/kustomize/overlays/prod/deployment_patch.yaml b/kustomize/overlays/prod/deployment_patch.yaml index a932967..cb2ceb4 100644 --- a/kustomize/overlays/prod/deployment_patch.yaml +++ b/kustomize/overlays/prod/deployment_patch.yaml @@ -72,11 +72,11 @@ spec: secretKeyRef: name: resourcetracking-env-prod key: TRACPLUS_URL - - name: MAPPROXY_URL + - name: GEOSERVER_URL valueFrom: secretKeyRef: name: resourcetracking-env-prod - key: MAPPROXY_URL + key: GEOSERVER_URL - name: SENTRY_DSN valueFrom: secretKeyRef: diff --git a/kustomize/overlays/prod/kustomization.yaml b/kustomize/overlays/prod/kustomization.yaml index db4a227..244efbf 100644 --- a/kustomize/overlays/prod/kustomization.yaml +++ b/kustomize/overlays/prod/kustomization.yaml @@ -30,4 +30,4 @@ patches: - path: geoserver_service_patch.yaml images: - name: ghcr.io/dbca-wa/resource_tracking - newTag: 1.4.13 + newTag: 1.4.14 diff --git a/kustomize/overlays/uat/deployment_patch.yaml b/kustomize/overlays/uat/deployment_patch.yaml index b8e9933..87134c5 100644 --- a/kustomize/overlays/uat/deployment_patch.yaml +++ b/kustomize/overlays/uat/deployment_patch.yaml @@ -62,11 +62,11 @@ spec: secretKeyRef: name: resourcetracking-env-uat key: TRACPLUS_URL - - name: MAPPROXY_URL + - name: GEOSERVER_URL valueFrom: secretKeyRef: name: resourcetracking-env-uat - key: MAPPROXY_URL + key: GEOSERVER_URL - name: SENTRY_DSN valueFrom: secretKeyRef: diff --git a/poetry.lock b/poetry.lock index ca1ac36..84bc9ea 100644 --- a/poetry.lock +++ b/poetry.lock @@ -165,13 +165,13 @@ files = [ [[package]] name = "certifi" -version = "2024.7.4" +version = "2024.8.30" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" files = [ - {file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"}, - {file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"}, + {file = "certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8"}, + {file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"}, ] [[package]] @@ -541,13 +541,13 @@ python-mimeparse = ">=0.1.4,<1.5 || >1.5" [[package]] name = "executing" -version = "2.0.1" +version = "2.1.0" description = "Get the currently executing AST node of a frame, and other information" optional = false -python-versions = ">=3.5" +python-versions = ">=3.8" files = [ - {file = "executing-2.0.1-py2.py3-none-any.whl", hash = "sha256:eac49ca94516ccc753f9fb5ce82603156e590b27525a8bc32cce8ae302eb61bc"}, - {file = "executing-2.0.1.tar.gz", hash = "sha256:35afe2ce3affba8ee97f2d69927fa823b08b472b7b994e36a52a964b93d16147"}, + {file = "executing-2.1.0-py2.py3-none-any.whl", hash = "sha256:8d63781349375b5ebccc3142f4b30350c0cd9c79f921cde38be2be4637e98eaf"}, + {file = "executing-2.1.0.tar.gz", hash = "sha256:8ea27ddd260da8150fa5a708269c4a10e76161e2496ec3e587da9e3c0fe4b9ab"}, ] [package.extras] @@ -585,13 +585,13 @@ typing = ["typing-extensions (>=4.8)"] [[package]] name = "gunicorn" -version = "22.0.0" +version = "23.0.0" description = "WSGI HTTP Server for UNIX" optional = false python-versions = ">=3.7" files = [ - {file = "gunicorn-22.0.0-py3-none-any.whl", hash = "sha256:350679f91b24062c86e386e198a15438d53a7a8207235a78ba1b53df4c4378d9"}, - {file = "gunicorn-22.0.0.tar.gz", hash = "sha256:4a0b436239ff76fb33f11c07a16482c521a7e09c1ce3cc293c2330afe01bec63"}, + {file = "gunicorn-23.0.0-py3-none-any.whl", hash = "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d"}, + {file = "gunicorn-23.0.0.tar.gz", hash = "sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec"}, ] [package.dependencies] @@ -620,13 +620,13 @@ license = ["ukkonen"] [[package]] name = "idna" -version = "3.7" +version = "3.8" description = "Internationalized Domain Names in Applications (IDNA)" optional = false -python-versions = ">=3.5" +python-versions = ">=3.6" files = [ - {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, - {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, + {file = "idna-3.8-py3-none-any.whl", hash = "sha256:050b4e5baadcd44d760cedbd2b8e639f2ff89bbc7a5730fcc662954303377aac"}, + {file = "idna-3.8.tar.gz", hash = "sha256:d838c2c0ed6fced7693d5e8ab8e734d5f8fda53a039c0164afb0b82e771e3603"}, ] [[package]] @@ -646,13 +646,13 @@ ipython = {version = ">=7.31.1", markers = "python_version >= \"3.11\""} [[package]] name = "ipython" -version = "8.26.0" +version = "8.27.0" description = "IPython: Productive Interactive Computing" optional = false python-versions = ">=3.10" files = [ - {file = "ipython-8.26.0-py3-none-any.whl", hash = "sha256:e6b347c27bdf9c32ee9d31ae85defc525755a1869f14057e900675b9e8d6e6ff"}, - {file = "ipython-8.26.0.tar.gz", hash = "sha256:1cec0fbba8404af13facebe83d04436a7434c7400e59f47acf467c64abd0956c"}, + {file = "ipython-8.27.0-py3-none-any.whl", hash = "sha256:f68b3cb8bde357a5d7adc9598d57e22a45dfbea19eb6b98286fa3b288c9cd55c"}, + {file = "ipython-8.27.0.tar.gz", hash = "sha256:0b99a2dc9f15fd68692e898e5568725c6d49c527d36a9fb5960ffbdeaa82ff7e"}, ] [package.dependencies] @@ -1025,15 +1025,18 @@ cli = ["click (>=5.0)"] [[package]] name = "python-mimeparse" -version = "1.6.0" +version = "2.0.0" description = "A module provides basic functions for parsing mime-type names and matching them against a list of media-ranges." optional = false -python-versions = "*" +python-versions = ">=3.8" files = [ - {file = "python-mimeparse-1.6.0.tar.gz", hash = "sha256:76e4b03d700a641fd7761d3cd4fdbbdcd787eade1ebfac43f877016328334f78"}, - {file = "python_mimeparse-1.6.0-py2.py3-none-any.whl", hash = "sha256:a295f03ff20341491bfe4717a39cd0a8cc9afad619ba44b77e86b0ab8a2b8282"}, + {file = "python_mimeparse-2.0.0-py3-none-any.whl", hash = "sha256:574062a06f2e1d416535c8d3b83ccc6ebe95941e74e2c5939fc010a12e37cc09"}, + {file = "python_mimeparse-2.0.0.tar.gz", hash = "sha256:5b9a9dcf7aa82465e31bd667f5cb7000604811dce83554f1c8a43693a32cb303"}, ] +[package.extras] +test = ["pytest"] + [[package]] name = "pyyaml" version = "6.0.2" @@ -1119,13 +1122,13 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "sentry-sdk" -version = "2.12.0" +version = "2.13.0" description = "Python client for Sentry (https://sentry.io)" optional = false python-versions = ">=3.6" files = [ - {file = "sentry_sdk-2.12.0-py2.py3-none-any.whl", hash = "sha256:7a8d5163d2ba5c5f4464628c6b68f85e86972f7c636acc78aed45c61b98b7a5e"}, - {file = "sentry_sdk-2.12.0.tar.gz", hash = "sha256:8763840497b817d44c49b3fe3f5f7388d083f2337ffedf008b2cdb63b5c86dc6"}, + {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"}, ] [package.dependencies] @@ -1153,6 +1156,7 @@ httpx = ["httpx (>=0.16.0)"] huey = ["huey (>=2)"] huggingface-hub = ["huggingface-hub (>=0.22)"] langchain = ["langchain (>=0.0.210)"] +litestar = ["litestar (>=2.0.0)"] loguru = ["loguru (>=0.5)"] openai = ["openai (>=1.0.0)", "tiktoken (>=0.3.0)"] opentelemetry = ["opentelemetry-distro (>=0.35b0)"] @@ -1328,4 +1332,4 @@ brotli = ["brotli"] [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "fad4758590f5afe3b25d65d1d58f1af5d8f5387fc5f722371d01d57d852fc444" +content-hash = "64f049424e3b7224c58e0726e0a7cec0a0c724c8941f98d31380534ba8a9abc2" diff --git a/pyproject.toml b/pyproject.toml index 19c7f21..db1c9e0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "resource_tracking" -version = "1.4.13" +version = "1.4.14" description = "DBCA internal corporate application to download and serve data from remote tracking devices." authors = ["DBCA OIM "] license = "Apache-2.0" @@ -13,17 +13,17 @@ psycopg = {version = "3.2.1", extras = ["binary", "pool"]} dbca-utils = "2.0.2" python-dotenv = "1.0.1" dj-database-url = "2.2.0" -gunicorn = "22.0.0" +gunicorn = "23.0.0" django-extensions = "3.2.3" django-tastypie = "0.14.7" django-geojson = "4.1.0" unicodecsv = "0.14.1" whitenoise = {version = "6.7.0", extras = ["brotli"]} azure-storage-blob = "12.22.0" -sentry-sdk = {version = "2.12.0", extras = ["django"]} +sentry-sdk = {version = "2.13.0", extras = ["django"]} [tool.poetry.group.dev.dependencies] -ipython = "^8.26.0" +ipython = "^8.27.0" ipdb = "^0.13.13" pre-commit = "^3.8.0" mixer = "^7.2.2" diff --git a/resource_tracking/settings.py b/resource_tracking/settings.py index 2f2cbb2..a4f8c35 100644 --- a/resource_tracking/settings.py +++ b/resource_tracking/settings.py @@ -34,7 +34,7 @@ # Add scary warning on device edit page for prod PROD_SCARY_WARNING = env("PROD_SCARY_WARNING", False) DEVICE_HTTP_CACHE_TIMEOUT = env("DEVICE_HTTP_CACHE_TIMEOUT", 60) -MAPPROXY_URL = env('MAPPROXY_URL', '') +GEOSERVER_URL = env("GEOSERVER_URL", "") INSTALLED_APPS = [ "whitenoise.runserver_nostatic", "django.contrib.admin", @@ -98,13 +98,10 @@ DATABASES = { # Defined in the DATABASE_URL env variable. "default": dj_database_url.config(), - "fleetcare": dj_database_url.parse(env("FLEETCARE_DATABASE_URL", "sqlite:////tmp/db")) } # Project authentication settings -AUTHENTICATION_BACKENDS = ( - "django.contrib.auth.backends.ModelBackend", -) +AUTHENTICATION_BACKENDS = ("django.contrib.auth.backends.ModelBackend",) # Internationalization LANGUAGE_CODE = "en-us" @@ -145,7 +142,7 @@ "handlers": ["console"], "level": "INFO", }, - } + }, } # Tastypie settings @@ -173,8 +170,12 @@ def sentry_excluded_exceptions(event, hint): # Sentry settings SENTRY_DSN = env("SENTRY_DSN", None) SENTRY_SAMPLE_RATE = env("SENTRY_SAMPLE_RATE", 1.0) # Error sampling rate -SENTRY_TRANSACTION_SAMPLE_RATE = env("SENTRY_TRANSACTION_SAMPLE_RATE", 0.0) # Transaction sampling -SENTRY_PROFILES_SAMPLE_RATE = env("SENTRY_PROFILES_SAMPLE_RATE", 0.0) # Proportion of sampled transactions to profile. +SENTRY_TRANSACTION_SAMPLE_RATE = env( + "SENTRY_TRANSACTION_SAMPLE_RATE", 0.0 +) # Transaction sampling +SENTRY_PROFILES_SAMPLE_RATE = env( + "SENTRY_PROFILES_SAMPLE_RATE", 0.0 +) # Proportion of sampled transactions to profile. SENTRY_ENVIRONMENT = env("SENTRY_ENVIRONMENT", None) if SENTRY_DSN and SENTRY_ENVIRONMENT: import sentry_sdk diff --git a/tracking/harvest.py b/tracking/harvest.py index 5844596..2cec4f8 100644 --- a/tracking/harvest.py +++ b/tracking/harvest.py @@ -54,7 +54,9 @@ def harvest_tracking_email(device_type, purge_email=False): # Fetch the email message. status, message = email_utils.email_fetch(imap, uid) if status != "OK": - LOGGER.error(f"Server response failure on fetching email UID {uid}: {status}") + LOGGER.error( + f"Server response failure on fetching email UID {uid}: {status}" + ) continue # `result` will be a LoggedPoint, or None @@ -102,10 +104,17 @@ def save_mp70(message): # Validate lat/lon values. if not validate_latitude_longitude(data["latitude"], data["longitude"]): - LOGGER.info(f"Bad geometry while parsing MP70 message from device {data['device_id']}: {data['latitude']}, {data['longitude']}") + LOGGER.info( + f"Bad geometry while parsing MP70 message from device {data['device_id']}: {data['latitude']}, {data['longitude']}" + ) return False - device, created = Device.objects.get_or_create(deviceid=data["device_id"]) + try: + device, created = Device.objects.get_or_create(deviceid=data["device_id"]) + except: + LOGGER.error("Exception during creation/query of MP70 device") + LOGGER.error(data) + seen = data["timestamp"] point = f"POINT({data['longitude']} {data['latitude']})" @@ -117,7 +126,9 @@ def save_mp70(message): device.altitude = data["altitude"] device.save() - loggedpoint, created = LoggedPoint.objects.get_or_create(device=device, seen=seen, point=point) + loggedpoint, created = LoggedPoint.objects.get_or_create( + device=device, seen=seen, point=point + ) if created: loggedpoint.source_device_type = "mp70" loggedpoint.heading = data["heading"] @@ -142,10 +153,17 @@ def save_spot(message): # Validate lat/lon values. if not validate_latitude_longitude(data["latitude"], data["longitude"]): - LOGGER.info(f"Bad geometry while parsing Spot message from device {data['device_id']}: {data['latitude']}, {data['longitude']}") + LOGGER.info( + f"Bad geometry while parsing Spot message from device {data['device_id']}: {data['latitude']}, {data['longitude']}" + ) return False - device, created = Device.objects.get_or_create(deviceid=data["device_id"]) + try: + device, created = Device.objects.get_or_create(deviceid=data["device_id"]) + except: + LOGGER.error("Exception during creation/query of Spot device") + LOGGER.error(data) + seen = data["timestamp"] point = f"POINT({data['longitude']} {data['latitude']})" @@ -157,7 +175,9 @@ def save_spot(message): device.altitude = data["altitude"] device.save() - loggedpoint, created = LoggedPoint.objects.get_or_create(device=device, seen=seen, point=point) + loggedpoint, created = LoggedPoint.objects.get_or_create( + device=device, seen=seen, point=point + ) if created: loggedpoint.source_device_type = "spot" loggedpoint.heading = data["heading"] @@ -182,10 +202,17 @@ def save_iriditrak(message): # Validate lat/lon values. if not validate_latitude_longitude(data["latitude"], data["longitude"]): - LOGGER.info(f"Bad geometry while parsing Iriditrak message from device {data['device_id']}: {data['latitude']}, {data['longitude']}") + LOGGER.info( + f"Bad geometry while parsing Iriditrak message from device {data['device_id']}: {data['latitude']}, {data['longitude']}" + ) return False - device, created = Device.objects.get_or_create(deviceid=data["device_id"]) + try: + device, created = Device.objects.get_or_create(deviceid=data["device_id"]) + except: + LOGGER.error("Exception during creation/query of Iriditrak device") + LOGGER.error(data) + seen = data["timestamp"] point = f"POINT({data['longitude']} {data['latitude']})" @@ -197,7 +224,9 @@ def save_iriditrak(message): device.altitude = data["altitude"] device.save() - loggedpoint, created = LoggedPoint.objects.get_or_create(device=device, seen=seen, point=point) + loggedpoint, created = LoggedPoint.objects.get_or_create( + device=device, seen=seen, point=point + ) if created: loggedpoint.source_device_type = "iriditrak" loggedpoint.heading = data["heading"] @@ -224,10 +253,17 @@ def save_dplus(message): # Validate lat/lon values. if not validate_latitude_longitude(data["latitude"], data["longitude"]): - LOGGER.info(f"Bad geometry while parsing DPlus message from device {data['device_id']}: {data['latitude']}, {data['longitude']}") + LOGGER.info( + f"Bad geometry while parsing DPlus message from device {data['device_id']}: {data['latitude']}, {data['longitude']}" + ) return False - device, created = Device.objects.get_or_create(deviceid=data["device_id"]) + try: + device, created = Device.objects.get_or_create(deviceid=data["device_id"]) + except: + LOGGER.error("Exception during creation/query of DPlus device") + LOGGER.error(data) + seen = data["timestamp"] point = f"POINT({data['longitude']} {data['latitude']})" @@ -239,7 +275,9 @@ def save_dplus(message): device.altitude = data["altitude"] device.save() - loggedpoint, created = LoggedPoint.objects.get_or_create(device=device, seen=seen, point=point) + loggedpoint, created = LoggedPoint.objects.get_or_create( + device=device, seen=seen, point=point + ) if created: loggedpoint.source_device_type = "dplus" loggedpoint.heading = data["heading"] @@ -289,10 +327,11 @@ def save_dplus(message): def save_dfes_feed(): - """Download and process the DFES API endpoint (returns GeoJSON), create new devices, update existing. - """ + """Download and process the DFES API endpoint (returns GeoJSON), create new devices, update existing.""" LOGGER.info("Querying DFES API") - resp = requests.get(url=settings.DFES_URL, auth=(settings.DFES_USER, settings.DFES_PASS)) + resp = requests.get( + url=settings.DFES_URL, auth=(settings.DFES_USER, settings.DFES_PASS) + ) resp.raise_for_status() features = resp.json()["features"] LOGGER.info(f"DFES API returned {len(features)} features, processing") @@ -312,11 +351,18 @@ def save_dfes_feed(): # Validate lat/lon values. if not validate_latitude_longitude(data["latitude"], data["longitude"]): - LOGGER.info(f"Bad geometry while parsing data for DFES device {data['device_id']}: {data['latitude']}, {data['longitude']}") + LOGGER.info( + f"Bad geometry while parsing data for DFES device {data['device_id']}: {data['latitude']}, {data['longitude']}" + ) skipped_device += 1 continue - device, created = Device.objects.get_or_create(deviceid=data["device_id"]) + try: + device, created = Device.objects.get_or_create(deviceid=data["device_id"]) + except: + LOGGER.error("Exception during creation/query of DFES device") + LOGGER.error(data) + properties = feature["properties"] if created: @@ -344,7 +390,9 @@ def save_dfes_feed(): device.save() - loggedpoint, created = LoggedPoint.objects.get_or_create(device=device, seen=seen, point=point) + loggedpoint, created = LoggedPoint.objects.get_or_create( + device=device, seen=seen, point=point + ) if created: loggedpoint.source_device_type = "dfes" loggedpoint.heading = data["heading"] @@ -353,14 +401,22 @@ def save_dfes_feed(): loggedpoint.save() logged_points += 1 - LOGGER.info(f"Created {created_device}, updated {updated_device}, skipped {skipped_device}, {logged_points} new logged points") + LOGGER.info( + f"Created {created_device}, updated {updated_device}, skipped {skipped_device}, {logged_points} new logged points" + ) def save_tracplus_feed(): - """Query the TracPlus API, create logged points per device, update existing devices. - """ + """Query the TracPlus API, create logged points per device, update existing devices.""" LOGGER.info("Harvesting TracPlus feed") - content = requests.get(settings.TRACPLUS_URL).content.decode("utf-8") + response = requests.get(settings.TRACPLUS_URL) + + # The TracPlus API frequently throttles requests. + if response.status_code == 429: + LOGGER.warning("TracPlus API returned HTTP 429 Too Many Requests") + return + + content = response.content.decode("utf-8") latest = list(csv.DictReader(content.split("\r\n"))) LOGGER.info(f"{len(latest)} records downloaded, processing") @@ -383,13 +439,25 @@ def save_tracplus_feed(): # Validate lat/lon values. if not validate_latitude_longitude(data["latitude"], data["longitude"]): - LOGGER.info(f"Bad geometry while parsing TracPlus data from device {data['device_id']}: {data['latitude']}, {data['longitude']}") + LOGGER.info( + f"Bad geometry while parsing TracPlus data from device {data['device_id']}: {data['latitude']}, {data['longitude']}" + ) skipped_device += 1 continue - device, created = Device.objects.get_or_create(deviceid=data["device_id"]) + try: + device, created = Device.objects.get_or_create(deviceid=data["device_id"]) + except: + LOGGER.error("Exception during creation/query of TracPlus device") + LOGGER.error(row) + skipped_device += 1 + rego = row["Asset Regn"][:32].strip() - symbol = tracplus_symbol_map[row["Asset Type"]] if row["Asset Type"] in tracplus_symbol_map else None + symbol = ( + tracplus_symbol_map[row["Asset Type"]] + if row["Asset Type"] in tracplus_symbol_map + else None + ) if created: created_device += 1 @@ -413,7 +481,9 @@ def save_tracplus_feed(): device.save() - loggedpoint, created = LoggedPoint.objects.get_or_create(device=device, seen=seen, point=point) + loggedpoint, created = LoggedPoint.objects.get_or_create( + device=device, seen=seen, point=point + ) if created: loggedpoint.source_device_type = "tracplus" loggedpoint.heading = data["heading"] @@ -422,4 +492,6 @@ def save_tracplus_feed(): loggedpoint.save() logged_points += 1 - LOGGER.info(f"Updated {updated_device} devices, created {created_device} devices, skipped {skipped_device} devices, {logged_points} new logged points") + LOGGER.info( + f"Updated {updated_device} devices, created {created_device} devices, skipped {skipped_device} devices, {logged_points} new logged points" + ) diff --git a/tracking/migrations/0023_delete_resourceview.py b/tracking/migrations/0023_delete_resourceview.py new file mode 100644 index 0000000..7d534c6 --- /dev/null +++ b/tracking/migrations/0023_delete_resourceview.py @@ -0,0 +1,16 @@ +# Generated by Django 4.2.15 on 2024-09-02 07:00 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('tracking', '0022_resourceview'), + ] + + operations = [ + migrations.DeleteModel( + name='ResourceView', + ), + ] diff --git a/tracking/models.py b/tracking/models.py index 6de10a2..d391f52 100644 --- a/tracking/models.py +++ b/tracking/models.py @@ -10,37 +10,37 @@ from django.forms import ValidationError import logging -LOGGER = logging.getLogger('tracking') - - -DISTRICT_PERTH_HILLS = 'PHD' -DISTRICT_SWAN_COASTAL = 'SCD' -DISTRICT_SWAN_REGION = 'SWAN' -DISTRICT_BLACKWOOD = 'BWD' -DISTRICT_WELLINGTON = 'WTN' -DISTRICT_SOUTH_WEST_REGION = 'SWR' -DISTRICT_DONNELLY = 'DON' -DISTRICT_FRANKLAND = 'FRK' -DISTRICT_WARREN_REGION = 'WR' -DISTRICT_ALBANY = 'ALB' -DISTRICT_ESPERANCE = 'ESP' -DISTRICT_SOUTH_COAST_REGION = 'SCR' -DISTRICT_EAST_KIMBERLEY = 'EKD' -DISTRICT_WEST_KIMBERLEY = 'WKD' -DISTRICT_KIMBERLEY_REGION = 'KIMB' -DISTRICT_PILBARA_REGION = 'PIL' -DISTRICT_EXMOUTH = 'EXM' -DISTRICT_GOLDFIELDS_REGION = 'GLD' -DISTRICT_GERALDTON = 'GER' -DISTRICT_KALBARRI = 'KLB' -DISTRICT_MOORA = 'MOR' -DISTRICT_SHARK_BAY = 'SHB' -DISTRICT_MIDWEST_REGION = 'MWR' -DISTRICT_CENTRAL_WHEATBELT = 'CWB' -DISTRICT_SOUTHERN_WHEATBELT = 'SWB' -DISTRICT_WHEATBELT_REGION = 'WBR' -DISTRICT_AVIATION = 'AV' -DISTRICT_OTHER = 'OTH' +LOGGER = logging.getLogger("tracking") + + +DISTRICT_PERTH_HILLS = "PHD" +DISTRICT_SWAN_COASTAL = "SCD" +DISTRICT_SWAN_REGION = "SWAN" +DISTRICT_BLACKWOOD = "BWD" +DISTRICT_WELLINGTON = "WTN" +DISTRICT_SOUTH_WEST_REGION = "SWR" +DISTRICT_DONNELLY = "DON" +DISTRICT_FRANKLAND = "FRK" +DISTRICT_WARREN_REGION = "WR" +DISTRICT_ALBANY = "ALB" +DISTRICT_ESPERANCE = "ESP" +DISTRICT_SOUTH_COAST_REGION = "SCR" +DISTRICT_EAST_KIMBERLEY = "EKD" +DISTRICT_WEST_KIMBERLEY = "WKD" +DISTRICT_KIMBERLEY_REGION = "KIMB" +DISTRICT_PILBARA_REGION = "PIL" +DISTRICT_EXMOUTH = "EXM" +DISTRICT_GOLDFIELDS_REGION = "GLD" +DISTRICT_GERALDTON = "GER" +DISTRICT_KALBARRI = "KLB" +DISTRICT_MOORA = "MOR" +DISTRICT_SHARK_BAY = "SHB" +DISTRICT_MIDWEST_REGION = "MWR" +DISTRICT_CENTRAL_WHEATBELT = "CWB" +DISTRICT_SOUTHERN_WHEATBELT = "SWB" +DISTRICT_WHEATBELT_REGION = "WBR" +DISTRICT_AVIATION = "AV" +DISTRICT_OTHER = "OTH" DISTRICT_CHOICES = ( (DISTRICT_SWAN_REGION, "Swan Region"), @@ -70,7 +70,7 @@ (DISTRICT_CENTRAL_WHEATBELT, "Central Wheatbelt"), (DISTRICT_SOUTHERN_WHEATBELT, "Southern Wheatbelt"), (DISTRICT_AVIATION, "Aviation"), - (DISTRICT_OTHER, "Other") + (DISTRICT_OTHER, "Other"), ) SYMBOL_CHOICES = ( @@ -97,7 +97,7 @@ ("boat", "Boat"), ("person", "Person"), ("other", "Other"), - ("unknown", "Unknown") + ("unknown", "Unknown"), ) RAW_EQ_CHOICES = ( @@ -108,7 +108,7 @@ (18, "Emergency Message"), (19, "Remote Command Acknowledge for Emergency Turn Off"), (25, "Start Moving"), - (26, "Stop Moving") + (26, "Stop Moving"), ) SOURCE_DEVICE_TYPE_CHOICES = ( @@ -127,36 +127,83 @@ class Device(models.Model): """A location-tracking device installed in a vehicle for the purposes of monitoring its location and heading over time, plus metadata about the vehicle itself. """ + deviceid = models.CharField(max_length=32, unique=True) - registration = models.CharField(max_length=32, default="No Rego", help_text="e.g. 1QBB157") - rin_number = models.PositiveIntegerField(validators=[MaxValueValidator(999)], verbose_name="Resource Identification Number (RIN)", null=True, blank=True, help_text="Heavy Duty, Gang Truck or Plant only (HD/GT/P automatically prefixed).") - rin_display = models.CharField(max_length=5, null=True, blank=True, verbose_name="RIN") + registration = models.CharField( + max_length=32, default="No Rego", help_text="e.g. 1QBB157" + ) + rin_number = models.PositiveIntegerField( + validators=[MaxValueValidator(999)], + verbose_name="Resource Identification Number (RIN)", + null=True, + blank=True, + help_text="Heavy Duty, Gang Truck or Plant only (HD/GT/P automatically prefixed).", + ) + rin_display = models.CharField( + max_length=5, null=True, blank=True, verbose_name="RIN" + ) symbol = models.CharField(max_length=32, choices=SYMBOL_CHOICES, default="other") - district = models.CharField(max_length=32, choices=DISTRICT_CHOICES, default=DISTRICT_OTHER, verbose_name="Region/District") - district_display = models.CharField(max_length=100, default='Other', verbose_name="District") - usual_driver = models.CharField(max_length=50, null=True, blank=True, help_text="e.g. John Jones") - usual_location = models.CharField(max_length=50, null=True, blank=True, help_text="e.g. Karijini National Park") - current_driver = models.CharField(max_length=50, null=True, blank=True, help_text="e.g. Jodie Jones") + district = models.CharField( + max_length=32, + choices=DISTRICT_CHOICES, + default=DISTRICT_OTHER, + verbose_name="Region/District", + ) + district_display = models.CharField( + max_length=100, default="Other", verbose_name="District" + ) + usual_driver = models.CharField( + max_length=50, null=True, blank=True, help_text="e.g. John Jones" + ) + usual_location = models.CharField( + max_length=50, null=True, blank=True, help_text="e.g. Karijini National Park" + ) + current_driver = models.CharField( + max_length=50, null=True, blank=True, help_text="e.g. Jodie Jones" + ) callsign = models.CharField(max_length=50, null=True, blank=True, help_text="") - callsign_display = models.CharField(max_length=50, null=True, blank=True, verbose_name="Callsign") - contractor_details = models.CharField(max_length=50, null=True, blank=True, help_text="Person engaging contractor is responsible for maintaining contractor resource details") + callsign_display = models.CharField( + max_length=50, null=True, blank=True, verbose_name="Callsign" + ) + contractor_details = models.CharField( + max_length=50, + null=True, + blank=True, + help_text="Person engaging contractor is responsible for maintaining contractor resource details", + ) other_details = models.TextField(null=True, blank=True) - internal_only = models.BooleanField(default=False, help_text="Device will only be shown on internal DBCA resource tracking live view (not to DFES, etc.)") - hidden = models.BooleanField(default=False, help_text="Device hidden from DBCA resource tracking live view") + internal_only = models.BooleanField( + default=False, + help_text="Device will only be shown on internal DBCA resource tracking live view (not to DFES, etc.)", + ) + hidden = models.BooleanField( + default=False, help_text="Device hidden from DBCA resource tracking live view" + ) deleted = models.BooleanField(default=False, verbose_name="Deleted?") fire_use = models.BooleanField(default=None, null=True, verbose_name="Fire use") seen = models.DateTimeField(null=True, editable=False) point = models.PointField(null=True, editable=False) - heading = models.PositiveIntegerField(default=0, help_text="Heading in degrees", editable=False) - velocity = models.PositiveIntegerField(default=0, help_text="Speed in metres/hr", editable=False) - altitude = models.IntegerField(default=0, help_text="Altitude above sea level in metres", editable=False) + heading = models.PositiveIntegerField( + default=0, help_text="Heading in degrees", editable=False + ) + velocity = models.PositiveIntegerField( + default=0, help_text="Speed in metres/hr", editable=False + ) + altitude = models.IntegerField( + default=0, help_text="Altitude above sea level in metres", editable=False + ) message = models.PositiveIntegerField(default=3, choices=RAW_EQ_CHOICES) - source_device_type = models.CharField(max_length=32, choices=SOURCE_DEVICE_TYPE_CHOICES, default="other", db_index=True) + source_device_type = models.CharField( + max_length=32, + choices=SOURCE_DEVICE_TYPE_CHOICES, + default="other", + db_index=True, + ) class Meta: - ordering = ('-seen',) + ordering = ("-seen",) def __str__(self): return f"{self.registration} {self.deviceid}" @@ -172,19 +219,19 @@ def age_minutes(self): @property def age_colour(self): if not self.seen: - return 'red' + return "red" minutes = self.age_minutes if minutes < 60: - return 'green' + return "green" elif minutes < 180: - return 'orange' + return "orange" else: - return 'red' + return "red" @property def age_text(self): # Returns age in humanized form - return naturaltime(self.seen).replace(u'\xa0', u' ') + return naturaltime(self.seen).replace("\xa0", " ") @property def icon(self): @@ -211,9 +258,27 @@ def save(self, force_insert=False, force_update=False, *args, **kwargs): def clean(self): # Clean rin_number - if self.rin_number and self.symbol not in ("heavy duty", "gang truck", "dozer", "grader", "loader", "tender", "float"): - raise ValidationError("Please remove the RIN number or select a symbol from Heavy Duty, Gang Truck, Dozer, Grader, Loader, Tender or Float") - if not self.rin_number and self.symbol in ("heavy duty", "gang truck", "dozer", "grader", "loader", "tender", "float"): + if self.rin_number and self.symbol not in ( + "heavy duty", + "gang truck", + "dozer", + "grader", + "loader", + "tender", + "float", + ): + raise ValidationError( + "Please remove the RIN number or select a symbol from Heavy Duty, Gang Truck, Dozer, Grader, Loader, Tender or Float" + ) + if not self.rin_number and self.symbol in ( + "heavy duty", + "gang truck", + "dozer", + "grader", + "loader", + "tender", + "float", + ): raise ValidationError("Please enter a RIN number") @@ -221,15 +286,27 @@ class LoggedPoint(models.Model): """An instance of the location of a tracking device at a point in time, plus additional metadata where available. """ + device = models.ForeignKey(Device, on_delete=models.PROTECT) seen = models.DateTimeField(editable=False, db_index=True) point = models.PointField(editable=False) - heading = models.PositiveIntegerField(default=0, help_text="Heading in degrees", editable=False) - velocity = models.PositiveIntegerField(default=0, help_text="Speed in metres/hr", editable=False) - altitude = models.IntegerField(default=0, help_text="Altitude above sea level in metres", editable=False) + heading = models.PositiveIntegerField( + default=0, help_text="Heading in degrees", editable=False + ) + velocity = models.PositiveIntegerField( + default=0, help_text="Speed in metres/hr", editable=False + ) + altitude = models.IntegerField( + default=0, help_text="Altitude above sea level in metres", editable=False + ) message = models.PositiveIntegerField(default=3, choices=RAW_EQ_CHOICES) - source_device_type = models.CharField(max_length=32, choices=SOURCE_DEVICE_TYPE_CHOICES, default="other", db_index=True) + source_device_type = models.CharField( + max_length=32, + choices=SOURCE_DEVICE_TYPE_CHOICES, + default="other", + db_index=True, + ) raw = models.TextField(editable=False, null=True, blank=True) @@ -251,53 +328,5 @@ def user_pre_save(sender, instance, **kwargs): def user_post_save(sender, instance, **kwargs): # Add users to the 'Edit Resource Tracking Device' group so users can edit Device details # NOTE: does not work when saving user in Django Admin - g, created = Group.objects.get_or_create(name='Edit Resource Tracking Device') + g, created = Group.objects.get_or_create(name="Edit Resource Tracking Device") instance.groups.add(g) - - -class ResourceView(models.Model): - """An unmanaged Django model to allow ORM usage of the `tracking_resource_tracking_view` - database view. - """ - id = models.IntegerField(primary_key=True) - point = models.PointField(srid=4326) - heading = models.IntegerField() - velocity = models.IntegerField() - altitude = models.IntegerField() - seen = models.DateTimeField() - deviceid = models.CharField(max_length=32) - registration = models.CharField(max_length=32) - rin_display = models.CharField(max_length=5) - current_driver = models.CharField(max_length=50) - callsign = models.CharField(max_length=50) - callsign_display = models.CharField(max_length=50) - usual_driver = models.CharField(max_length=50) - usual_location = models.CharField(max_length=50) - contractor_details = models.CharField(max_length=50) - symbol = models.CharField(max_length=32, choices=SYMBOL_CHOICES) - age = models.FloatField() - symbolid = models.TextField() - district = models.CharField(max_length=32, choices=DISTRICT_CHOICES) - district_display = models.CharField(max_length=100) - source_device_type = models.CharField(max_length=32, choices=SOURCE_DEVICE_TYPE_CHOICES) - - class Meta: - managed = False - db_table = "tracking_resource_tracking_view" - verbose_name = "resource" - ordering = ("-seen",) - - def __str__(self): - seen = self.seen.astimezone(settings.TZ).strftime("%d/%b/%Y %H:%M") - if self.callsign: - return f"{self.callsign} ({self.get_symbol_display()}) seen {seen} AWST" - else: - return f"{self.registration} ({self.get_symbol_display()}) seen {seen} AWST" - - def save(self, *args, **kwargs): - # Disallow all save operations. - raise NotSupportedError("View-only database model") - - def delete(self, *args, **kwargs): - # Disallow all delete operations. - raise NotSupportedError("View-only database model") diff --git a/tracking/static/js/resource_map.js b/tracking/static/js/resource_map.js index 0fc2d48..59cf99e 100644 --- a/tracking/static/js/resource_map.js +++ b/tracking/static/js/resource_map.js @@ -1,51 +1,35 @@ "use strict"; +const geoserver_wmts_url = geoserver_url + "/gwc/service/wmts?service=WMTS&request=GetTile&version=1.0.0&tilematrixset=mercator&tilematrix=mercator:{z}&tilecol={x}&tilerow={y}&format=image/png" // Base layers -const mapboxStreets = L.tileLayer.wms(mapproxy_url, { - layers: 'mapbox-streets', - format: 'image/png', - tileSize: 1024, - zoomOffset: -2, -}); -const landgateOrthomosaic = L.tileLayer.wms(mapproxy_url, { - layers: 'virtual-mosaic', - tileSize: 1024, - zoomOffset: -2, -}); +const mapboxStreets = L.tileLayer( + geoserver_wmts_url + "&layer=dbca:mapbox-streets", +); +const landgateOrthomosaic = L.tileLayer( + geoserver_wmts_url + "&layer=landgate:virtual_mosaic", +); // Overlay layers -const dbcaBushfires = L.tileLayer.wms(mapproxy_url, { - layers: 'dbca-going-bushfires', - format: 'image/png', - transparent: true, - opacity: 0.75, - tileSize: 1024, - zoomOffset: -2, -}); -const dfesBushfires = L.tileLayer.wms(mapproxy_url, { - layers: 'dfes-going-bushfires', - format: 'image/png', - transparent: true, - opacity: 0.75, - tileSize: 1024, - zoomOffset: -2, -}); -const dbcaRegions = L.tileLayer.wms(mapproxy_url, { - layers: 'dbca-regions', - format: 'image/png', - transparent: true, - opacity: 0.75, - tileSize: 1024, - zoomOffset: -2, -}); -const lgaBoundaries = L.tileLayer.wms(mapproxy_url, { - layers: 'lga-boundaries', - format: 'image/png', - transparent: true, - opacity: 0.75, - tileSize: 1024, - zoomOffset: -2, -}); +const dbcaBushfires = L.tileLayer( + geoserver_wmts_url + "&layer=landgate:dbca_going_bushfires_dbca-001", + { + transparent: true, + opacity: 0.75, + } +); +const dfesBushfires = L.tileLayer( + geoserver_wmts_url + "&layer=landgate:authorised_fireshape_dfes-032", + { + transparent: true, + opacity: 0.75, + } +); +const dbcaRegions = L.tileLayer( + geoserver_wmts_url + "&layer=cddp:dbca_regions", +); +const lgaBoundaries = L.tileLayer( + geoserver_wmts_url + "&layer=cddp:local_gov_authority", +); // Icon classes (note that URLs are injected into the base template.) const iconCar = L.icon({ @@ -179,7 +163,7 @@ function refreshTrackedDevicesLayer(trackedDevicesLayer) { // Query the API endpoint for device data. $.getJSON( device_geojson_url, - function(data) { + function (data) { // Add the device data to the GeoJSON layer. trackedDevicesLayer.addData(data); // Success notification. @@ -195,7 +179,7 @@ refreshTrackedDevicesLayer(trackedDevices); // Define map. var map = L.map('map', { - crs: L.CRS.EPSG4326, + crs: L.CRS.EPSG3857, center: [-31.96, 115.87], zoom: 12, minZoom: 4, @@ -221,7 +205,7 @@ var overlayMaps = { L.control.layers(baseMaps, overlayMaps).addTo(map); // Define scale bar -L.control.scale({maxWidth: 500, imperial: false}).addTo(map); +L.control.scale({ maxWidth: 500, imperial: false }).addTo(map); // Device registration search const searchControl = new L.Control.Search({ @@ -238,7 +222,7 @@ map.addControl(searchControl); const refreshButton = L.easyButton( ``, - function(btn, map){ + function (btn, map) { refreshTrackedDevicesLayer(trackedDevices); } ).addTo(map); diff --git a/tracking/templates/tracking/resource_map.html b/tracking/templates/tracking/resource_map.html index 0183ce4..1f0bc63 100644 --- a/tracking/templates/tracking/resource_map.html +++ b/tracking/templates/tracking/resource_map.html @@ -53,7 +53,7 @@