Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/multi-build-typesense.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ jobs:
security-events: write
steps:
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@0.33.1
uses: aquasecurity/trivy-action@0.34.0
env:
TRIVY_DB_REPOSITORY: public.ecr.aws/aquasecurity/trivy-db
with:
Expand Down
9 changes: 8 additions & 1 deletion .github/workflows/multi-build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,13 @@ jobs:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Log in to Docker Hardened Images Registry
uses: docker/login-action@v3
with:
registry: dhi.io
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}

- name: Build and push by digest
# This step builds and pushes the Docker image using Buildx.
# It uses the docker/build-push-action to build the image with the specified context and platforms.
Expand Down Expand Up @@ -291,7 +298,7 @@ jobs:
security-events: write
steps:
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@0.33.1
uses: aquasecurity/trivy-action@0.34.0
env:
TRIVY_DB_REPOSITORY: public.ecr.aws/aquasecurity/trivy-db
with:
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/secret-scan.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ jobs:
secret-scan:
name: Scan project for secrets
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@v6
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ celerybeat-schedule

# dotenv
.env
.env.act

# virtualenv
venv/
Expand Down
74 changes: 23 additions & 51 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,64 +1,36 @@
# syntax=docker/dockerfile:1
# Prepare the base environment.
FROM python:3.13-slim-bookworm AS builder_base

# Install required OS packages.
RUN apt-get update \
&& apt-get install -y --no-install-recommends build-essential gdal-bin libgdal-dev \
&& rm -rf /var/lib/apt/lists/*

# This approximately follows this guide: https://hynek.me/articles/docker-uv/
# Which creates a standalone environment with the dependencies.
# - Silence uv complaining about not being able to use hard links,
# - tell uv to byte-compile packages for faster application startups,
# - prevent uv from accidentally downloading isolated Python builds,
# - pick a Python,
# - and finally declare `/app` as the target for `uv sync`.
ENV UV_LINK_MODE=copy \
UV_COMPILE_BYTECODE=1 \
UV_PYTHON_DOWNLOADS=never \
UV_PROJECT_ENVIRONMENT=/app/.venv

COPY --from=ghcr.io/astral-sh/uv:0.9 /uv /uvx /bin/

# Since there's no point in shipping lock files, we move them
# into a directory that is NOT copied into the runtime image.
# The trailing slash makes COPY create `/_lock/` automagically.
WORKDIR /_lock
COPY pyproject.toml uv.lock /_lock/

# Synchronize dependencies.
# This layer is cached until uv.lock or pyproject.toml change.
RUN --mount=type=cache,target=/root/.cache uv sync --frozen --no-group dev

##################################################################################

FROM python:3.13-slim-bookworm
FROM dhi.io/python:3.13-debian13-dev AS build-stage
LABEL org.opencontainers.image.authors=asi@dbca.wa.gov.au
LABEL org.opencontainers.image.source=https://github.com/dbca-wa/prs

# Install required OS packages.
RUN apt-get update \
&& apt-get upgrade -y \
&& apt-get install -y --no-install-recommends gdal-bin proj-bin libmagic-dev \
# Install system packages required to run the project
RUN apt-get update -y \
# Python package dependencies: fiona requires libgdal-dev and gcc, python-magic requires libmagic1t64
&& apt-get install -y --no-install-recommends libgdal-dev gcc g++ gdal-bin proj-bin libmagic1t64 \
# Run shared library linker after installing packages
&& ldconfig \
&& rm -rf /var/lib/apt/lists/*

# Create a non-root user.
RUN groupadd -r -g 1000 app \
&& useradd -r -u 1000 -d /app -g app -N app

# Import uv to install dependencies
COPY --from=ghcr.io/astral-sh/uv:0.9 /uv /bin/
WORKDIR /app
COPY --from=builder_base --chown=app:app /app /app
# Make sure we use the virtualenv by default.
# Run Python unbuffered.
ENV PATH=/app/.venv/bin:$PATH \
PYTHONUNBUFFERED=1

# Install the project.
# Install project dependencies
COPY pyproject.toml uv.lock ./
RUN uv sync --no-group dev --link-mode=copy --compile-bytecode --no-python-downloads --frozen \
# Remove uv and lockfile after use
&& rm -rf /bin/uv \
&& rm uv.lock

# Copy the remaining project files to finish building the project
COPY gunicorn.py manage.py pyproject.toml ./
COPY prs2 ./prs2
ENV PYTHONUNBUFFERED=1
ENV PATH="/app/.venv/bin:$PATH"
# Compile scripts and collect static files
RUN python -m compileall prs2 \
&& python manage.py collectstatic --noinput
USER app

# Run the project as the nonroot user
USER nonroot
EXPOSE 8080
CMD ["gunicorn", "prs2.wsgi", "--config", "gunicorn.py"]
2 changes: 1 addition & 1 deletion kustomize/base/celery.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ spec:
cpu: '1000m'
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsUser: 65532
privileged: false
allowPrivilegeEscalation: false
capabilities:
Expand Down
4 changes: 2 additions & 2 deletions kustomize/base/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ spec:
resources:
requests:
memory: '100Mi'
cpu: '5m'
cpu: '10m'
limits:
memory: '2Gi'
cpu: '1000m'
Expand Down Expand Up @@ -61,7 +61,7 @@ spec:
timeoutSeconds: 10
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsUser: 65532
privileged: false
allowPrivilegeEscalation: false
capabilities:
Expand Down
2 changes: 1 addition & 1 deletion kustomize/template/cronjob.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ spec:
value: 'Australia/Perth'
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsUser: 65532
privileged: false
allowPrivilegeEscalation: false
capabilities:
Expand Down
18 changes: 9 additions & 9 deletions prs2/harvester/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -374,29 +374,29 @@ def harvest(self, create_tasks=True, create_locations=True, create_records=True,
else:
addresses = app["ADDRESS_DETAIL"]["DOP_ADDRESS_TYPE"]

for a in addresses:
for address in addresses:
# Use the long/lat info to intersect DBCA regions.
try:
p = Point(x=float(a["LONGITUDE"]), y=float(a["LATITUDE"]))
p = Point(x=float(address["LONGITUDE"]), y=float(address["LATITUDE"]))
for r in Region.objects.all():
if r.region_mpoly and r.region_mpoly.intersects(p) and r not in regions:
regions.append(r)
intersected_region = True
except Exception:
log = f"Address long/lat could not be parsed ({a['LONGITUDE']}, {a['LATITUDE']})"
log = f"Address long/lat could not be parsed ({address['LONGITUDE']}, {address['LATITUDE']})"
LOGGER.warning(log)
self.log = f"{log}\n"
actions.append(f"{datetime.now().isoformat()} {log}")
intersected_region = False

# Use the PIN field to query SLIP for geometry.
if "PIN" in a and a["PIN"]:
if "PIN" in address and address["PIN"]:
try:
resp = query_slip_esri(a["PIN"])
resp = query_slip_esri(address["PIN"])
features = resp["features"] # List of spatial features.
if len(features) > 0:
a["FEATURES"] = features
locations.append(a) # A dict for each address location.
address["FEATURES"] = features
locations.append(address) # A dict for each address location.
# If we haven't yet, use the feature geom to intersect DBCA regions.
if not intersected_region:
for f in features:
Expand All @@ -406,15 +406,15 @@ def harvest(self, create_tasks=True, create_locations=True, create_records=True,
for r in Region.objects.all():
if r.region_mpoly and r.region_mpoly.intersects(p) and r not in regions:
regions.append(r)
log = f"Address PIN {a['PIN']} returned geometry from SLIP"
log = f"Address PIN {address['PIN']} returned geometry from SLIP"
self.log = self.log + f"{log}\n"
LOGGER.info(log)
except Exception as e:
log = f"Error querying Landgate SLIP for spatial data (referral ref. {reference})"
LOGGER.error(log)
LOGGER.exception(e)
else:
log = f"Address PIN could not be parsed ({a['PIN']})"
log = f"Address PIN could not be parsed ({address['PIN']})"
LOGGER.warning(log)
self.log = self.log + f"{log}\n"

Expand Down
23 changes: 14 additions & 9 deletions prs2/referral/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ def __init__(self, *args, **kwargs):
self.form_class = "form-horizontal"
self.form_method = "POST"
self.label_class = "col-xs-12 col-sm-4 col-md-3 col-lg-2"
self.field_class = "col-xs-12 col-sm-8 col-md-6 col-lg-4"
self.field_class = "col-xs-12 col-sm-8 col-md-10"
self.help_text_inline = True
self.attrs = {"novalidate": ""}

Expand Down Expand Up @@ -407,9 +407,10 @@ def __init__(self, *args, **kwargs):
self.fields["infobase_id"].help_text = """To link to an Infobase record,
enter the Infobase object ID exactly as it appears in Infobase (i.e.
case-sensitive, no spaces). E.g.: eA498596"""
self.fields["description"].widget = forms.Textarea(attrs={"cols": "40", "rows": "4"})
# Add in a "Save and add another" button.
save_another_button = Submit("save-another", "Save and add another")
save_another_button.field_classes = " btn btn-default"
save_another_button.field_classes = "btn btn-secondary"
layout = Layout(
"name",
"uploaded_file",
Expand Down Expand Up @@ -528,6 +529,7 @@ def __init__(self, *args, **kwargs):
instead."""
self.fields["description"].help_text = """To comment on the completion
stage of the task, ADD details to this description."""
self.fields["description"].widget = forms.Textarea(attrs={"cols": "40", "rows": "4"})
layout = Layout(
"state",
"complete_date",
Expand Down Expand Up @@ -562,8 +564,8 @@ class TaskStopForm(BaseForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["description"].required = True
self.fields["description"].help_text = """Please describe why the
task was stopped."""
self.fields["description"].help_text = "Please describe why the task was stopped."
self.fields["description"].widget = forms.Textarea(attrs={"cols": "40", "rows": "4"})
layout = Layout(
"stopped_date",
"description",
Expand Down Expand Up @@ -601,8 +603,8 @@ def __init__(self, *args, **kwargs):
self.fields["due_date"].label = "Revised due date"
self.fields["restart_date"].required = True
self.fields["restart_date"].input_formats = settings.DATE_INPUT_FORMATS
self.fields["description"].help_text += """ ADD to the description a
brief explanation for restarting the task."""
self.fields["description"].help_text += " ADD to the description a brief explanation for restarting the task."
self.fields["description"].widget = forms.Textarea(attrs={"cols": "40", "rows": "4"})
layout = Layout(
"due_date",
"restart_date",
Expand Down Expand Up @@ -633,6 +635,7 @@ def __init__(self, *args, **kwargs):
self.fields["due_date"].widget = forms.DateInput(format="%d/%m/%Y")
self.fields["due_date"].input_formats = settings.DATE_INPUT_FORMATS
self.fields["due_date"].required = True
self.fields["description"].widget = forms.Textarea(attrs={"cols": "40", "rows": "4"})
layout = Layout(
"assigned_user",
"email_user",
Expand All @@ -655,8 +658,8 @@ class TaskCancelForm(BaseForm):

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["description"].help_text = """Please record why the task
was cancelled (optional)."""
self.fields["description"].help_text = "Please record why the task was cancelled (optional)."
self.fields["description"].widget = forms.Textarea(attrs={"cols": "40", "rows": "4"})
layout = Layout(
"description", Div(self.save_button, self.cancel_button, css_class="col-sm-offset-4 col-md-offset-3 col-lg-offset-2")
)
Expand Down Expand Up @@ -709,6 +712,7 @@ def __init__(self, *args, **kwargs):
self.fields["restart_date"].widget = forms.DateInput(format="%d/%m/%Y")
self.fields["restart_date"].input_formats = settings.DATE_INPUT_FORMATS
self.fields["state"].queryset = TaskState.objects.current().filter(Q(task_type=self.instance.type) | Q(task_type=None))
self.fields["description"].widget = forms.Textarea(attrs={"cols": "40", "rows": "4"})

layout = Layout(
"assigned_user",
Expand Down Expand Up @@ -804,6 +808,7 @@ def __init__(self, *args, **kwargs):
and "Provide pre-referral/preliminary advice" tasks will be
auto-completed if no due date is recorded."""
self.fields["description"].required = True
self.fields["description"].widget = forms.Textarea(attrs={"cols": "40", "rows": "4"})
layout = Layout(
"assigned_user",
"email_user",
Expand Down Expand Up @@ -908,7 +913,7 @@ def __init__(self, *args, **kwargs):
clicking <strong>Save and add another</strong>.</p>""")
# Add in a "Save and add another" button.
save_another_button = Submit("save-another", "Save and add another")
save_another_button.field_classes = "btn btn-default"
save_another_button.field_classes = "btn btn-secondary"
layout = Layout(
help_html,
"model_condition",
Expand Down
37 changes: 21 additions & 16 deletions prs2/referral/templates/referral/change_form.html
Original file line number Diff line number Diff line change
@@ -1,26 +1,31 @@
{% extends "base_prs.html" %}
{% load static %}
{% load crispy_forms_tags %}

{% block extra_head %}
{{ block.super }}
{{ form.media }}
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-datepicker/1.9.0/css/bootstrap-datepicker.min.css" integrity="sha256-siyOpF/pBWUPgIcQi17TLBkjvNgNQArcmwJB8YvkAgg=" crossorigin="anonymous" />
{{ block.super }}
{{ form.media }}
<link rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-datepicker/1.10.0/css/bootstrap-datepicker.min.css"
integrity="sha512-34s5cpvaNG3BknEWSuOncX28vz97bRI59UnVtEEpFX536A7BtZSJHsDyFoCl8S7Dt2TPzcrCEoHBGeM4SUBDBw=="
crossorigin="anonymous"
referrerpolicy="no-referrer" />
{% endblock %}

{% block page_content_inner %}
<h1>{{ title }}</h1>
{% if form.errors %}
<div class="alert alert-danger" role="alert">Please correct the error(s) below</div>
{% endif %}
{% crispy form %}
<h1>{{ title }}</h1>
{% if form.errors %}<div class="alert alert-danger" role="alert">Please correct the error(s) below</div>{% endif %}
{% crispy form %}
{% endblock %}

{% block extra_js %}
{{ block.super }}
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-datepicker/1.9.0/js/bootstrap-datepicker.min.js" integrity="sha256-bqVeqGdJ7h/lYPq6xrPv/YGzMEb6dNxlfiTUHSgRCp8=" crossorigin="anonymous"></script>
<script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/tinymce/4.6.4/tinymce.min.js"></script>
<script type="text/javascript">
{{ block.super }}
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-datepicker/1.10.0/js/bootstrap-datepicker.min.js"
integrity="sha512-LsnSViqQyaXpD4mBBdRYeP6sRwJiJveh2ZIbW41EBrNmKxgr/LFZIiWT6yr+nycvhvauz8c2nYMhrP80YhG7Cw=="
crossorigin="anonymous"
referrerpolicy="no-referrer"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/tinymce/4.6.4/tinymce.min.js"
integrity="sha512-4heT9RAzAQo0qfuk1t6HkAN+iDHVS9WvoMz7gKIUmMUsyXzcCiMi8qPjQ8I9f6ENNH3fJmnRx5LEoM9OLUsjzA=="
crossorigin="anonymous"
referrerpolicy="no-referrer"></script>
<script type="text/javascript">
// Document ready events:
$(function() {
// Initialise datepicker widgets
Expand Down Expand Up @@ -69,5 +74,5 @@ <h1>{{ title }}</h1>
toolbar: 'undo redo | styleselect | bold italic | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | table link',
statusbar: false
});
</script>
</script>
{% endblock %}
Loading