diff --git a/.gitignore b/.gitignore index f1e2ccaf1..06aa766d1 100644 --- a/.gitignore +++ b/.gitignore @@ -118,3 +118,5 @@ private-media/ **/data_migration/legacy_data/ .db-backups/ + +docs/sql-scripts/kb/ diff --git a/boranga/components/occurrence/api.py b/boranga/components/occurrence/api.py index dbf537571..34214189b 100644 --- a/boranga/components/occurrence/api.py +++ b/boranga/components/occurrence/api.py @@ -331,7 +331,7 @@ def get_date(filter_date): filter_occurrence_name = request.POST.get("filter_occurrence_name") if filter_occurrence_name and not filter_occurrence_name.lower() == "all": - queryset = queryset.filter(occurrence__occurrence_name__icontains=filter_occurrence_name) + queryset = queryset.filter(occurrence__id=filter_occurrence_name) filter_common_name = request.POST.get("filter_common_name") if filter_common_name and not filter_common_name.lower() == "all": @@ -2569,7 +2569,7 @@ def filter_queryset(self, request, queryset, view): filter_occurrence_name = request.POST.get("filter_occurrence_name") if filter_occurrence_name and not filter_occurrence_name.lower() == "all": - queryset = queryset.filter(occurrence_name__icontains=filter_occurrence_name) + queryset = queryset.filter(id=filter_occurrence_name) filter_scientific_name = request.POST.get("filter_scientific_name") if filter_scientific_name and not filter_scientific_name.lower() == "all": diff --git a/boranga/components/occurrence/serializers.py b/boranga/components/occurrence/serializers.py index 41409316b..3fb85f8cc 100644 --- a/boranga/components/occurrence/serializers.py +++ b/boranga/components/occurrence/serializers.py @@ -1463,6 +1463,7 @@ class BaseOccurrenceReportSerializer(BaseModelSerializer): has_main_observer = serializers.BooleanField(read_only=True) is_submitter = serializers.SerializerMethodField() can_user_edit = serializers.SerializerMethodField() + can_user_copy = serializers.SerializerMethodField() common_names = serializers.SerializerMethodField(read_only=True) class Meta: @@ -1514,6 +1515,7 @@ class Meta: "number_of_observers", "has_main_observer", "is_submitter", + "can_user_copy", "record_source", "comments", "ocr_for_occ_number", @@ -1623,6 +1625,10 @@ def get_can_user_edit(self, obj): request = self.context["request"] return obj.can_user_edit(request) + def get_can_user_copy(self, obj): + request = self.context["request"] + return obj.submitter == request.user.id or is_occurrence_assessor(request) + class OccurrenceReportSerializer(BaseOccurrenceReportSerializer): submitter = serializers.SerializerMethodField(read_only=True) diff --git a/boranga/components/spatial/utils.py b/boranga/components/spatial/utils.py index 2f3da0483..ecc5cc25d 100644 --- a/boranga/components/spatial/utils.py +++ b/boranga/components/spatial/utils.py @@ -563,8 +563,11 @@ def save_geometry( is_new_geometry = False # Capture existing state to detect whether geometry data actually changes pre_save_geometry_wkb = geometry.geometry.ewkb if geometry.geometry else None - pre_save_original_ewkb = geometry.original_geometry_ewkb - pre_save_buffer_radius = geometry.buffer_radius + # BinaryField returns memoryview from DB; normalise to bytes for comparison + pre_save_original_ewkb = ( + bytes(geometry.original_geometry_ewkb) if geometry.original_geometry_ewkb else None + ) + pre_save_buffer_radius = getattr(geometry, "buffer_radius", None) else: logger.info(f"Creating new geometry for {instance_model_name}: {instance}") @@ -605,7 +608,12 @@ def save_geometry( geometry_changed = ( (geometry_instance.geometry.ewkb if geometry_instance.geometry else None) != pre_save_geometry_wkb - or geometry_instance.original_geometry_ewkb != pre_save_original_ewkb + or ( + bytes(geometry_instance.original_geometry_ewkb) + if geometry_instance.original_geometry_ewkb + else None + ) + != pre_save_original_ewkb or geometry_instance.buffer_radius != pre_save_buffer_radius ) if geometry_changed: diff --git a/boranga/components/species_and_communities/models.py b/boranga/components/species_and_communities/models.py index 60ce3930a..569fad0c6 100644 --- a/boranga/components/species_and_communities/models.py +++ b/boranga/components/species_and_communities/models.py @@ -1389,6 +1389,7 @@ class SpeciesUserAction(UserAction): ACTION_REINSTATE_SPECIES = "Reinstate Species {}" ACTION_EDIT_SPECIES = "Edit Species {}" ACTION_CREATE_SPECIES = "Create new species {}" + ACTION_ACTIVATE_SPECIES = "Activated species {}" ACTION_SAVE_SPECIES = "Save Species {}" ACTION_MAKE_HISTORICAL = "Make Species {} historical" ACTION_IMAGE_UPDATE = "Species Image document updated for Species {}" @@ -2305,6 +2306,7 @@ class CommunityUserAction(UserAction): ACTION_DISCARD_COMMUNITY = "Discard Community {}" ACTION_REINSTATE_COMMUNITY = "Reinstate Community {}" ACTION_CREATE_COMMUNITY = "Create new community {}" + ACTION_ACTIVATE_COMMUNITY = "Activated community {}" ACTION_SAVE_COMMUNITY = "Save Community {}" ACTION_RENAME_COMMUNITY_MADE_HISTORICAL = "Community {} renamed to {} and made historical" ACTION_RENAME_COMMUNITY_RETAINED = "Community {} renamed to {} but left active" diff --git a/boranga/components/species_and_communities/utils.py b/boranga/components/species_and_communities/utils.py index 440598961..5d2ef9e92 100755 --- a/boranga/components/species_and_communities/utils.py +++ b/boranga/components/species_and_communities/utils.py @@ -48,13 +48,13 @@ def species_form_submit(species_instance, request, split=False, rename=False): # Create a log entry for the proposal species_instance.log_user_action( - SpeciesUserAction.ACTION_CREATE_SPECIES.format(species_instance.species_number), + SpeciesUserAction.ACTION_ACTIVATE_SPECIES.format(species_instance.species_number), request, ) # Create a log entry for the user request.user.log_user_action( - SpeciesUserAction.ACTION_CREATE_SPECIES.format(species_instance.species_number), + SpeciesUserAction.ACTION_ACTIVATE_SPECIES.format(species_instance.species_number), request, ) @@ -80,13 +80,13 @@ def community_form_submit(community_instance, request): # Create a log entry for the proposal community_instance.log_user_action( - CommunityUserAction.ACTION_CREATE_COMMUNITY.format(community_instance.community_number), + CommunityUserAction.ACTION_ACTIVATE_COMMUNITY.format(community_instance.community_number), request, ) # Create a log entry for the user request.user.log_user_action( - CommunityUserAction.ACTION_CREATE_COMMUNITY.format(community_instance.community_number), + CommunityUserAction.ACTION_ACTIVATE_COMMUNITY.format(community_instance.community_number), request, ) diff --git a/boranga/frontend/boranga/src/components/common/occurrence_community_dashboard.vue b/boranga/frontend/boranga/src/components/common/occurrence_community_dashboard.vue index 09f7ed85b..cf25507fe 100644 --- a/boranga/frontend/boranga/src/components/common/occurrence_community_dashboard.vue +++ b/boranga/frontend/boranga/src/components/common/occurrence_community_dashboard.vue @@ -1200,7 +1200,7 @@ export default { }, }) .on('select2:select', function (e) { - let data = e.params.data.text; + let data = e.params.data.id; vm.filterOCCCommunityOccurrenceName = data; sessionStorage.setItem( 'filterOCCCommunityOccurrenceNameText', diff --git a/boranga/frontend/boranga/src/components/common/occurrence_fauna_dashboard.vue b/boranga/frontend/boranga/src/components/common/occurrence_fauna_dashboard.vue index dda6d43dd..9e5215dd3 100644 --- a/boranga/frontend/boranga/src/components/common/occurrence_fauna_dashboard.vue +++ b/boranga/frontend/boranga/src/components/common/occurrence_fauna_dashboard.vue @@ -1225,7 +1225,7 @@ export default { }, }) .on('select2:select', function (e) { - let data = e.params.data.text; + let data = e.params.data.id; vm.filterOCCFaunaOccurrenceName = data; sessionStorage.setItem( 'filterOCCFaunaOccurrenceNameText', diff --git a/boranga/frontend/boranga/src/components/common/occurrence_flora_dashboard.vue b/boranga/frontend/boranga/src/components/common/occurrence_flora_dashboard.vue index c83ac340c..afe82d6a6 100644 --- a/boranga/frontend/boranga/src/components/common/occurrence_flora_dashboard.vue +++ b/boranga/frontend/boranga/src/components/common/occurrence_flora_dashboard.vue @@ -1148,7 +1148,7 @@ export default { }, }) .on('select2:select', function (e) { - let data = e.params.data.text; + let data = e.params.data.id; vm.filterOCCFloraOccurrenceName = data; sessionStorage.setItem( 'filterOCCFloraOccurrenceNameText', diff --git a/boranga/frontend/boranga/src/components/common/occurrence_report_community_dashboard.vue b/boranga/frontend/boranga/src/components/common/occurrence_report_community_dashboard.vue index 1b72e8145..9b6976c2b 100644 --- a/boranga/frontend/boranga/src/components/common/occurrence_report_community_dashboard.vue +++ b/boranga/frontend/boranga/src/components/common/occurrence_report_community_dashboard.vue @@ -1673,6 +1673,7 @@ export default { term: params.term, type: 'public', group_type_id: vm.group_type_id, + active_only: false, }; return query; }, diff --git a/boranga/frontend/boranga/src/components/common/occurrence_report_fauna_dashboard.vue b/boranga/frontend/boranga/src/components/common/occurrence_report_fauna_dashboard.vue index 1bb5e7a5d..47c71162d 100644 --- a/boranga/frontend/boranga/src/components/common/occurrence_report_fauna_dashboard.vue +++ b/boranga/frontend/boranga/src/components/common/occurrence_report_fauna_dashboard.vue @@ -1677,6 +1677,7 @@ export default { term: params.term, type: 'public', group_type_id: vm.group_type_id, + active_only: false, }; return query; }, diff --git a/boranga/frontend/boranga/src/components/common/occurrence_report_flora_dashboard.vue b/boranga/frontend/boranga/src/components/common/occurrence_report_flora_dashboard.vue index 9d13cd367..23c019544 100644 --- a/boranga/frontend/boranga/src/components/common/occurrence_report_flora_dashboard.vue +++ b/boranga/frontend/boranga/src/components/common/occurrence_report_flora_dashboard.vue @@ -1422,6 +1422,7 @@ export default { term: params.term, type: 'public', group_type_id: vm.group_type_id, + active_only: false, }; return query; }, diff --git a/boranga/frontend/boranga/src/components/internal/occurrence/occurrence.vue b/boranga/frontend/boranga/src/components/internal/occurrence/occurrence.vue index 2a0747b42..f16afa646 100644 --- a/boranga/frontend/boranga/src/components/internal/occurrence/occurrence.vue +++ b/boranga/frontend/boranga/src/components/internal/occurrence/occurrence.vue @@ -33,6 +33,7 @@ /> +
+
+ \u{1F512} System READ-ONLY for data verification +
+
+ `; + + document.body.appendChild(container); +} + +function hideReadOnlyBanner() { + document + .getElementById(READ_ONLY_TOAST_ID) + ?.closest('div[style]') + ?.remove(); +} + +/** Fetches the current read-only status from the server and updates window.env + banner. */ +function refreshReadOnlyStatus() { + return originalFetch('/api/read_only_status') + .then((r) => r.json()) + .then((data) => { + window.env = window.env || {}; + window.env.read_only = !!data.read_only; + if (window.env.read_only) { + showReadOnlyBanner(); + } else { + hideReadOnlyBanner(); + } + return window.env.read_only; + }) + .catch(() => false); +} + +// Check on initial page load. +refreshReadOnlyStatus(); + // Do NOT make the outer wrapper async; otherwise window.fetch becomes a Promise. window.fetch = ((orig) => { + const WRITE_METHODS = new Set(['POST', 'PUT', 'PATCH', 'DELETE']); + return async (...args) => { // Normalize URL (string | URL | Request) let url; @@ -49,6 +100,43 @@ window.fetch = ((orig) => { const sameOrigin = url.origin === window.location.origin; const isApi = sameOrigin && url.pathname.startsWith('/api'); + // Determine request method + const method = ( + (args.length > 1 && args[1]?.method) || + (args[0] instanceof Request && args[0].method) || + 'GET' + ).toUpperCase(); + + // Re-check read-only status from the server on every write attempt. + // DataTables uses POST on _paginated endpoints when the querystring is too + // long — these are read-only list queries and must not be blocked. + if ( + isApi && + WRITE_METHODS.has(method) && + !url.pathname.includes('_paginated') + ) { + const isReadOnly = await refreshReadOnlyStatus(); + if (isReadOnly) { + swal.fire({ + iconHtml: + '', + customClass: { + icon: 'border-0', + confirmButton: 'btn btn-primary', + }, + title: 'Read-Only Mode', + text: 'The system is currently in read-only mode for data verification. No changes can be made at this time.', + }); + // Throw an AbortError so the fetch promise rejects silently — + // component reject handlers typically just log and show nothing, + // preventing a second error dialog from overwriting the lock dialog. + throw new DOMException( + 'Request blocked: system is in read-only mode.', + 'AbortError' + ); + } + } + // Merge headers let headers = new Headers(); if (args.length > 1 && args[1]?.headers) { diff --git a/boranga/middleware.py b/boranga/middleware.py index 07b0d33f1..f752d6cf0 100755 --- a/boranga/middleware.py +++ b/boranga/middleware.py @@ -42,6 +42,50 @@ def __call__(self, request): return redirect(path_ft + "?next=" + quote_plus(request.get_full_path())) +class ReadOnlyMiddleware: + """Blocks all write requests (POST/PUT/PATCH/DELETE) to the API when + settings.DATA_VERIFICATION_READ_ONLY is truthy. Django admin, authentication, + and other non-API paths are exempt so that admin and ORM-level operations still + work. POST requests to *_paginated endpoints are also allowed through because + DataTables uses POST when the querystring is too long — these are read-only + list queries, not writes.""" + + EXEMPT_PATH_PREFIXES = ( + "/admin/", + "/ledger/", + "/sso/", + "/logout", + "/ssologin", + ) + WRITE_METHODS = frozenset({"POST", "PUT", "PATCH", "DELETE"}) + + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + from django.conf import settings as django_settings + + if ( + getattr(django_settings, "DATA_VERIFICATION_READ_ONLY", False) + and request.method in self.WRITE_METHODS + and request.path.startswith("/api/") + and not any(request.path.startswith(p) for p in self.EXEMPT_PATH_PREFIXES) + # Allow DataTables POST queries on paginated endpoints (read-only list queries) + and "_paginated" not in request.path + ): + from django.http import JsonResponse + + return JsonResponse( + { + "detail": "The system is currently in read-only mode for data verification. " + "No changes can be made at this time." + }, + status=503, + ) + + return self.get_response(request) + + class RevisionOverrideMiddleware(RevisionMiddleware): """ Wraps the entire request in a revision. diff --git a/boranga/settings.py b/boranga/settings.py index 5f1a94f31..d9a0c0964 100755 --- a/boranga/settings.py +++ b/boranga/settings.py @@ -28,6 +28,7 @@ DISABLE_EMAIL = env("DISABLE_EMAIL", False) SHOW_TESTS_URL = env("SHOW_TESTS_URL", False) SHOW_DEBUG_TOOLBAR = env("SHOW_DEBUG_TOOLBAR", False) +DATA_VERIFICATION_READ_ONLY = env("DATA_VERIFICATION_READ_ONLY", False) TIME_ZONE = "Australia/Perth" SILENCE_SYSTEM_CHECKS = env("SILENCE_SYSTEM_CHECKS", False) @@ -156,6 +157,7 @@ def show_toolbar(request): } MIDDLEWARE_CLASSES += [ + "boranga.middleware.ReadOnlyMiddleware", "boranga.middleware.FirstTimeNagScreenMiddleware", "whitenoise.middleware.WhiteNoiseMiddleware", ] diff --git a/boranga/urls.py b/boranga/urls.py index d720fb298..ef2cd4469 100755 --- a/boranga/urls.py +++ b/boranga/urls.py @@ -191,6 +191,7 @@ def trigger_error(request): router.registry.sort(key=lambda x: x[0]) api_patterns = [ + re_path(r"^api/read_only_status$", views.ReadOnlyStatusView.as_view(), name="read-only-status"), re_path(r"^api/profile$", users_api.GetProfile.as_view(), name="get-profile"), re_path( r"^api/geojson_to_shapefile$", diff --git a/boranga/views.py b/boranga/views.py index e12dd2777..9e5ba757c 100644 --- a/boranga/views.py +++ b/boranga/views.py @@ -5,9 +5,10 @@ from django.conf import settings from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin from django.core.management import call_command -from django.http import HttpResponse +from django.http import HttpResponse, JsonResponse from django.shortcuts import redirect, render from django.utils.http import url_has_allowed_host_and_scheme +from django.views import View from django.views.generic import DetailView from django.views.generic.base import TemplateView @@ -167,6 +168,15 @@ class BorangaFurtherInformationView(TemplateView): template_name = "boranga/further_info.html" +class ReadOnlyStatusView(View): + """Returns the current read-only status. Never cached.""" + + def get(self, request): + response = JsonResponse({"read_only": getattr(settings, "DATA_VERIFICATION_READ_ONLY", False)}) + response["Cache-Control"] = "no-store" + return response + + class ManagementCommandsView(LoginRequiredMixin, UserPassesTestMixin, TemplateView): template_name = "boranga/mgt-commands.html" diff --git a/docs/sql-scripts/CommunityOCCBoundaryActive.sql b/docs/sql-scripts/CommunityOCCBoundaryActive.sql index a8258233c..50bb3f3d7 100644 --- a/docs/sql-scripts/CommunityOCCBoundaryActive.sql +++ b/docs/sql-scripts/CommunityOCCBoundaryActive.sql @@ -8,12 +8,13 @@ -- - OCC Processing Status = Active only -- - Community must have a current Approved Conservation Status -- --- NOTE: OCC_MOD_BY returns an integer user ID from the ledger accounts_emailuser --- table which lives in a separate database (ledger_db). A cross-database join is --- not possible in standard PostgreSQL. If human-readable names are required, --- either use dblink / postgres_fdw, or resolve IDs in application code. --- -- NOTE: OBS_DATE is sourced from boranga_occhabitatcondition (habitat.obs_date). +-- +-- IMPORTANT — KB does not allow comments in SQL queries. Before pasting this +-- script into KB, strip all comments using: +-- python scripts/strip_sql_comments.py docs/sql-scripts/CommunityOCCBoundaryActive.sql +-- Or write the result to a file for easy copying: +-- python scripts/strip_sql_comments.py docs/sql-scripts/CommunityOCCBoundaryActive.sql > tmp.sql -- ============================================================================= WITH diff --git a/docs/sql-scripts/CommunityOCCBoundaryAll.sql b/docs/sql-scripts/CommunityOCCBoundaryAll.sql index bbcd2b400..0398a309e 100644 --- a/docs/sql-scripts/CommunityOCCBoundaryAll.sql +++ b/docs/sql-scripts/CommunityOCCBoundaryAll.sql @@ -6,12 +6,16 @@ -- One row per OccurrenceGeometry (Polygon type) for Community Occurrences. -- Returns ALL processing statuses. -- --- NOTE: OCC_MOD_BY returns an integer user ID from the ledger accounts_emailuser --- table which lives in a separate database (ledger_db). A cross-database join is --- not possible in standard PostgreSQL. If human-readable names are required, --- either use dblink / postgres_fdw, or resolve IDs in application code. +-- NOTE: OCC_MOD_BY is resolved via accounts_emailuser and returns +-- first_name || ' ' || last_name for the last user to modify the record. -- -- NOTE: OBS_DATE is sourced from boranga_occhabitatcondition (habitat.obs_date). +-- +-- IMPORTANT — KB does not allow comments in SQL queries. Before pasting this +-- script into KB, strip all comments using: +-- python scripts/strip_sql_comments.py docs/sql-scripts/CommunityOCCBoundaryAll.sql +-- Or write the result to a file for easy copying: +-- python scripts/strip_sql_comments.py docs/sql-scripts/CommunityOCCBoundaryAll.sql > tmp.sql -- ============================================================================= WITH @@ -215,7 +219,7 @@ SELECT END AS OCC_SOURCE, occ.processing_status AS OCC_STATUS, TO_CHAR(occ.datetime_updated, 'YYYY-MM-DD HH24:MI:SS') AS OCC_MOD_DA, - occ.last_modified_by AS OCC_MOD_BY, + (u_mod.first_name || ' ' || u_mod.last_name) AS OCC_MOD_BY, occ.lodgement_date AS LODG_DATE, -- Region / District @@ -247,4 +251,5 @@ LEFT JOIN loc ON occ.id = loc.occurrence_id LEFT JOIN obs_detail ON occ.id = obs_detail.occurrence_id LEFT JOIN identification ON occ.id = identification.occurrence_id LEFT JOIN habitat ON occ.id = habitat.occurrence_id +LEFT JOIN accounts_emailuser u_mod ON occ.last_modified_by = u_mod.id ORDER BY occ.occurrence_number, geom.geom_id; diff --git a/docs/sql-scripts/CommunityOCCBuffersActive.sql b/docs/sql-scripts/CommunityOCCBuffersActive.sql index 7c18ca27b..6d0f340f5 100644 --- a/docs/sql-scripts/CommunityOCCBuffersActive.sql +++ b/docs/sql-scripts/CommunityOCCBuffersActive.sql @@ -8,12 +8,13 @@ -- - OCC Processing Status = Active only -- - Community must have a current Approved Conservation Status -- --- NOTE: OCC_MOD_BY returns an integer user ID from the ledger accounts_emailuser --- table which lives in a separate database (ledger_db). A cross-database join is --- not possible in standard PostgreSQL. If human-readable names are required, --- either use dblink / postgres_fdw, or resolve IDs in application code. --- -- NOTE: OBS_DATE is sourced from boranga_occhabitatcondition (habitat.obs_date). +-- +-- IMPORTANT — KB does not allow comments in SQL queries. Before pasting this +-- script into KB, strip all comments using: +-- python scripts/strip_sql_comments.py docs/sql-scripts/CommunityOCCBuffersActive.sql +-- Or write the result to a file for easy copying: +-- python scripts/strip_sql_comments.py docs/sql-scripts/CommunityOCCBuffersActive.sql > tmp.sql -- ============================================================================= WITH diff --git a/docs/sql-scripts/CommunityOCCBuffersAll.sql b/docs/sql-scripts/CommunityOCCBuffersAll.sql index 1a618e7c2..7d2bf5a9f 100644 --- a/docs/sql-scripts/CommunityOCCBuffersAll.sql +++ b/docs/sql-scripts/CommunityOCCBuffersAll.sql @@ -6,12 +6,16 @@ -- One row per BufferGeometry for Community Occurrences. -- Returns ALL processing statuses. -- --- NOTE: OCC_MOD_BY returns an integer user ID from the ledger accounts_emailuser --- table which lives in a separate database (ledger_db). A cross-database join is --- not possible in standard PostgreSQL. If human-readable names are required, --- either use dblink / postgres_fdw, or resolve IDs in application code. +-- NOTE: OCC_MOD_BY is resolved via accounts_emailuser and returns +-- first_name || ' ' || last_name for the last user to modify the record. -- -- NOTE: OBS_DATE is sourced from boranga_occhabitatcondition (habitat.obs_date). +-- +-- IMPORTANT — KB does not allow comments in SQL queries. Before pasting this +-- script into KB, strip all comments using: +-- python scripts/strip_sql_comments.py docs/sql-scripts/CommunityOCCBuffersAll.sql +-- Or write the result to a file for easy copying: +-- python scripts/strip_sql_comments.py docs/sql-scripts/CommunityOCCBuffersAll.sql > tmp.sql -- ============================================================================= WITH @@ -218,7 +222,7 @@ SELECT END AS OCC_SOURCE, occ.processing_status AS OCC_STATUS, TO_CHAR(occ.datetime_updated, 'YYYY-MM-DD HH24:MI:SS') AS OCC_MOD_DA, - occ.last_modified_by AS OCC_MOD_BY, + (u_mod.first_name || ' ' || u_mod.last_name) AS OCC_MOD_BY, occ.lodgement_date AS LODG_DATE, -- Region / District @@ -250,4 +254,5 @@ LEFT JOIN loc ON occ.id = loc.occurrence_id LEFT JOIN obs_detail ON occ.id = obs_detail.occurrence_id LEFT JOIN identification ON occ.id = identification.occurrence_id LEFT JOIN habitat ON occ.id = habitat.occurrence_id +LEFT JOIN accounts_emailuser u_mod ON occ.last_modified_by = u_mod.id ORDER BY occ.occurrence_number, buf.occ_geom_id; diff --git a/docs/sql-scripts/CommunityOCCSites.sql b/docs/sql-scripts/CommunityOCCSites.sql index d57aa8f20..2a09c855d 100644 --- a/docs/sql-scripts/CommunityOCCSites.sql +++ b/docs/sql-scripts/CommunityOCCSites.sql @@ -8,6 +8,12 @@ -- -- NOTE: OBS_DATE (observation_date) is not available on the Occurrence model -- and has been excluded from all OCC reports pending further review. +-- +-- IMPORTANT — KB does not allow comments in SQL queries. Before pasting this +-- script into KB, strip all comments using: +-- python scripts/strip_sql_comments.py docs/sql-scripts/CommunityOCCSites.sql +-- Or write the result to a file for easy copying: +-- python scripts/strip_sql_comments.py docs/sql-scripts/CommunityOCCSites.sql > tmp.sql -- ============================================================================= WITH diff --git a/docs/sql-scripts/CommunityORFPoints.sql b/docs/sql-scripts/CommunityORFPoints.sql index 80c955238..a1eb7a5c4 100644 --- a/docs/sql-scripts/CommunityORFPoints.sql +++ b/docs/sql-scripts/CommunityORFPoints.sql @@ -6,10 +6,14 @@ -- One row per OccurrenceReportGeometry (Point type) for Community OCRs. -- Returns ALL processing statuses. -- --- NOTE: ORF_MOD_BY returns an integer user ID from the ledger accounts_emailuser --- table which lives in a separate database (ledger_db). A cross-database join is --- not possible in standard PostgreSQL. If human-readable names are required, --- either use dblink / postgres_fdw, or resolve IDs in application code. +-- NOTE: ORF_MOD_BY is resolved via accounts_emailuser and returns +-- first_name || ' ' || last_name for the last user to modify the record. +-- +-- IMPORTANT — KB does not allow comments in SQL queries. Before pasting this +-- script into KB, strip all comments using: +-- python scripts/strip_sql_comments.py docs/sql-scripts/CommunityORFPoints.sql +-- Or write the result to a file for easy copying: +-- python scripts/strip_sql_comments.py docs/sql-scripts/CommunityORFPoints.sql > tmp.sql -- ============================================================================= WITH @@ -217,7 +221,7 @@ SELECT ocr.record_source AS OCR_SOURCE, ocr.processing_status AS ORF_STATUS, TO_CHAR(ocr.datetime_updated, 'YYYY-MM-DD HH24:MI:SS') AS ORF_MOD_DA, - ocr.last_modified_by AS ORF_MOD_BY, + (u_mod.first_name || ' ' || u_mod.last_name) AS ORF_MOD_BY, ocr.lodgement_date AS LODG_DATE, -- Region / District @@ -248,4 +252,5 @@ LEFT JOIN observer ON ocr.id = observer.occurrence_report_id LEFT JOIN obs_detail ON ocr.id = obs_detail.occurrence_report_id LEFT JOIN identification ON ocr.id = identification.occurrence_report_id LEFT JOIN habitat ON ocr.id = habitat.occurrence_report_id +LEFT JOIN accounts_emailuser u_mod ON ocr.last_modified_by = u_mod.id ORDER BY ocr.occurrence_report_number, geom.geom_id; diff --git a/docs/sql-scripts/CommunityORFPolygons.sql b/docs/sql-scripts/CommunityORFPolygons.sql index 9ff39cdf6..034a29bbe 100644 --- a/docs/sql-scripts/CommunityORFPolygons.sql +++ b/docs/sql-scripts/CommunityORFPolygons.sql @@ -6,10 +6,14 @@ -- One row per OccurrenceReportGeometry (Polygon type) for Community OCRs. -- Returns ALL processing statuses. -- --- NOTE: ORF_MOD_BY returns an integer user ID from the ledger accounts_emailuser --- table which lives in a separate database (ledger_db). A cross-database join is --- not possible in standard PostgreSQL. If human-readable names are required, --- either use dblink / postgres_fdw, or resolve IDs in application code. +-- NOTE: ORF_MOD_BY is resolved via accounts_emailuser and returns +-- first_name || ' ' || last_name for the last user to modify the record. +-- +-- IMPORTANT — KB does not allow comments in SQL queries. Before pasting this +-- script into KB, strip all comments using: +-- python scripts/strip_sql_comments.py docs/sql-scripts/CommunityORFPolygons.sql +-- Or write the result to a file for easy copying: +-- python scripts/strip_sql_comments.py docs/sql-scripts/CommunityORFPolygons.sql > tmp.sql -- ============================================================================= WITH @@ -225,7 +229,7 @@ SELECT ocr.record_source AS OCR_SOURCE, ocr.processing_status AS ORF_STATUS, TO_CHAR(ocr.datetime_updated, 'YYYY-MM-DD HH24:MI:SS') AS ORF_MOD_DA, - ocr.last_modified_by AS ORF_MOD_BY, + (u_mod.first_name || ' ' || u_mod.last_name) AS ORF_MOD_BY, ocr.lodgement_date AS LODG_DATE, -- Region / District @@ -256,4 +260,5 @@ LEFT JOIN observer ON ocr.id = observer.occurrence_report_id LEFT JOIN obs_detail ON ocr.id = obs_detail.occurrence_report_id LEFT JOIN identification ON ocr.id = identification.occurrence_report_id LEFT JOIN habitat ON ocr.id = habitat.occurrence_report_id +LEFT JOIN accounts_emailuser u_mod ON ocr.last_modified_by = u_mod.id ORDER BY ocr.occurrence_report_number, geom.geom_id; diff --git a/docs/sql-scripts/FaunaOCCBuffers.sql b/docs/sql-scripts/FaunaOCCBuffers.sql index 9cca3a691..12bf486fe 100644 --- a/docs/sql-scripts/FaunaOCCBuffers.sql +++ b/docs/sql-scripts/FaunaOCCBuffers.sql @@ -6,16 +6,17 @@ -- One row per BufferGeometry for Fauna Occurrences. -- Returns ALL processing statuses. -- --- NOTE: OCC_MOD_BY returns an integer user ID from the ledger accounts_emailuser --- table which lives in a separate database (ledger_db). A cross-database join is --- not possible in standard PostgreSQL. If human-readable names are required, --- either use dblink / postgres_fdw, or resolve IDs in application code. --- -- NOTE: OBS_DATE is sourced from boranga_occanimalobservation (animal_obs.obs_date). -- -- NOTE: primary_detection_method, secondary_sign, and reproductive_state are -- MultiSelectFields that store comma-separated IDs. They are resolved to -- display names via lateral unnest joins to their respective lookup tables. +-- +-- IMPORTANT — KB does not allow comments in SQL queries. Before pasting this +-- script into KB, strip all comments using: +-- python scripts/strip_sql_comments.py docs/sql-scripts/FaunaOCCBuffers.sql +-- Or write the result to a file for easy copying: +-- python scripts/strip_sql_comments.py docs/sql-scripts/FaunaOCCBuffers.sql > tmp.sql -- ============================================================================= WITH diff --git a/docs/sql-scripts/FaunaOCCPointsActive.sql b/docs/sql-scripts/FaunaOCCPointsActive.sql index ad16d5165..607adc96b 100644 --- a/docs/sql-scripts/FaunaOCCPointsActive.sql +++ b/docs/sql-scripts/FaunaOCCPointsActive.sql @@ -8,16 +8,17 @@ -- - OCC Processing Status = Active only -- - Species must have a current Approved Conservation Status -- --- NOTE: OCC_MOD_BY returns an integer user ID from the ledger accounts_emailuser --- table which lives in a separate database (ledger_db). A cross-database join is --- not possible in standard PostgreSQL. If human-readable names are required, --- either use dblink / postgres_fdw, or resolve IDs in application code. --- -- NOTE: OBS_DATE is sourced from boranga_occanimalobservation (animal_obs.obs_date). -- -- NOTE: primary_detection_method, secondary_sign, and reproductive_state are -- MultiSelectFields that store comma-separated IDs. They are resolved to -- display names via lateral unnest joins to their respective lookup tables. +-- +-- IMPORTANT — KB does not allow comments in SQL queries. Before pasting this +-- script into KB, strip all comments using: +-- python scripts/strip_sql_comments.py docs/sql-scripts/FaunaOCCPointsActive.sql +-- Or write the result to a file for easy copying: +-- python scripts/strip_sql_comments.py docs/sql-scripts/FaunaOCCPointsActive.sql > tmp.sql -- ============================================================================= WITH diff --git a/docs/sql-scripts/FaunaOCCPointsAll.sql b/docs/sql-scripts/FaunaOCCPointsAll.sql index 4245a113f..e41e6e1e1 100644 --- a/docs/sql-scripts/FaunaOCCPointsAll.sql +++ b/docs/sql-scripts/FaunaOCCPointsAll.sql @@ -6,16 +6,20 @@ -- One row per OccurrenceGeometry (Point type) for Fauna Occurrences. -- Returns ALL processing statuses. -- --- NOTE: OCC_MOD_BY returns an integer user ID from the ledger accounts_emailuser --- table which lives in a separate database (ledger_db). A cross-database join is --- not possible in standard PostgreSQL. If human-readable names are required, --- either use dblink / postgres_fdw, or resolve IDs in application code. +-- NOTE: OCC_MOD_BY is resolved via accounts_emailuser and returns +-- first_name || ' ' || last_name for the last user to modify the record. -- -- NOTE: OBS_DATE is sourced from boranga_occanimalobservation (animal_obs.obs_date). -- -- NOTE: primary_detection_method, secondary_sign, and reproductive_state are -- MultiSelectFields that store comma-separated IDs. They are resolved to -- display names via lateral unnest joins to their respective lookup tables. +-- +-- IMPORTANT — KB does not allow comments in SQL queries. Before pasting this +-- script into KB, strip all comments using: +-- python scripts/strip_sql_comments.py docs/sql-scripts/FaunaOCCPointsAll.sql +-- Or write the result to a file for easy copying: +-- python scripts/strip_sql_comments.py docs/sql-scripts/FaunaOCCPointsAll.sql > tmp.sql -- ============================================================================= WITH @@ -281,7 +285,7 @@ SELECT END AS OCC_SOURCE, occ.processing_status AS OCC_STATUS, TO_CHAR(occ.datetime_updated, 'YYYY-MM-DD HH24:MI:SS') AS OCC_MOD_DA, - occ.last_modified_by AS OCC_MOD_BY, + (u_mod.first_name || ' ' || u_mod.last_name) AS OCC_MOD_BY, occ.lodgement_date AS LODG_DATE, -- Region / District @@ -312,4 +316,5 @@ LEFT JOIN obs_detail ON occ.id = obs_detail.occurrence_id LEFT JOIN animal_obs ON occ.id = animal_obs.occurrence_id LEFT JOIN identification ON occ.id = identification.occurrence_id LEFT JOIN habitat ON occ.id = habitat.occurrence_id +LEFT JOIN accounts_emailuser u_mod ON occ.last_modified_by = u_mod.id ORDER BY occ.occurrence_number, geom.geom_id; diff --git a/docs/sql-scripts/FaunaOCCSites.sql b/docs/sql-scripts/FaunaOCCSites.sql index 528449361..b8a4c40a6 100644 --- a/docs/sql-scripts/FaunaOCCSites.sql +++ b/docs/sql-scripts/FaunaOCCSites.sql @@ -8,6 +8,12 @@ -- -- NOTE: OBS_DATE and OBS_TIME are not available on the Occurrence model -- and have been excluded from all OCC reports pending further review. +-- +-- IMPORTANT — KB does not allow comments in SQL queries. Before pasting this +-- script into KB, strip all comments using: +-- python scripts/strip_sql_comments.py docs/sql-scripts/FaunaOCCSites.sql +-- Or write the result to a file for easy copying: +-- python scripts/strip_sql_comments.py docs/sql-scripts/FaunaOCCSites.sql > tmp.sql -- ============================================================================= WITH diff --git a/docs/sql-scripts/FaunaORFPoints.sql b/docs/sql-scripts/FaunaORFPoints.sql index 6c6404461..1f939d429 100644 --- a/docs/sql-scripts/FaunaORFPoints.sql +++ b/docs/sql-scripts/FaunaORFPoints.sql @@ -6,14 +6,18 @@ -- One row per OccurrenceReportGeometry (Point type) for Fauna OCRs. -- Returns ALL processing statuses. -- --- NOTE: ORF_MOD_BY returns an integer user ID from the ledger accounts_emailuser --- table which lives in a separate database (ledger_db). A cross-database join is --- not possible in standard PostgreSQL. If human-readable names are required, --- either use dblink / postgres_fdw, or resolve IDs in application code. +-- NOTE: ORF_MOD_BY is resolved via accounts_emailuser and returns +-- first_name || ' ' || last_name for the last user to modify the record. -- -- NOTE: primary_detection_method, secondary_sign, and reproductive_state are -- MultiSelectFields that store comma-separated IDs. They are resolved to -- display names via lateral unnest joins to their respective lookup tables. +-- +-- IMPORTANT — KB does not allow comments in SQL queries. Before pasting this +-- script into KB, strip all comments using: +-- python scripts/strip_sql_comments.py docs/sql-scripts/FaunaORFPoints.sql +-- Or write the result to a file for easy copying: +-- python scripts/strip_sql_comments.py docs/sql-scripts/FaunaORFPoints.sql > tmp.sql -- ============================================================================= WITH @@ -307,7 +311,7 @@ SELECT ocr.record_source AS OCR_SOURCE, ocr.processing_status AS ORF_STATUS, TO_CHAR(ocr.datetime_updated, 'YYYY-MM-DD HH24:MI:SS') AS ORF_MOD_DA, - ocr.last_modified_by AS ORF_MOD_BY, + (u_mod.first_name || ' ' || u_mod.last_name) AS ORF_MOD_BY, ocr.lodgement_date AS LODG_DATE, -- Region / District @@ -340,4 +344,5 @@ LEFT JOIN animal_obs ON ocr.id = animal_obs.occurrence_report_id LEFT JOIN identification ON ocr.id = identification.occurrence_report_id LEFT JOIN obs_time ON ocr.observation_time_id = obs_time.id LEFT JOIN habitat ON ocr.id = habitat.occurrence_report_id +LEFT JOIN accounts_emailuser u_mod ON ocr.last_modified_by = u_mod.id ORDER BY ocr.occurrence_report_number, geom.geom_id; diff --git a/docs/sql-scripts/FloraOCCBoundaryActive.sql b/docs/sql-scripts/FloraOCCBoundaryActive.sql index c064ccea1..41e12f162 100644 --- a/docs/sql-scripts/FloraOCCBoundaryActive.sql +++ b/docs/sql-scripts/FloraOCCBoundaryActive.sql @@ -8,12 +8,13 @@ -- - OCC Processing Status = Active only -- - Species must have a current Approved Conservation Status -- --- NOTE: OCC_MOD_BY returns an integer user ID from the ledger accounts_emailuser --- table which lives in a separate database (ledger_db). A cross-database join is --- not possible in standard PostgreSQL. If human-readable names are required, --- either use dblink / postgres_fdw, or resolve IDs in application code. --- -- NOTE: OBS_DATE is sourced from boranga_occplantcount (plant_count.obs_date). +-- +-- IMPORTANT — KB does not allow comments in SQL queries. Before pasting this +-- script into KB, strip all comments using: +-- python scripts/strip_sql_comments.py docs/sql-scripts/FloraOCCBoundaryActive.sql +-- Or write the result to a file for easy copying: +-- python scripts/strip_sql_comments.py docs/sql-scripts/FloraOCCBoundaryActive.sql > tmp.sql -- ============================================================================= WITH diff --git a/docs/sql-scripts/FloraOCCBoundaryAll.sql b/docs/sql-scripts/FloraOCCBoundaryAll.sql index e31ba61c7..ced02e598 100644 --- a/docs/sql-scripts/FloraOCCBoundaryAll.sql +++ b/docs/sql-scripts/FloraOCCBoundaryAll.sql @@ -6,12 +6,16 @@ -- One row per OccurrenceGeometry (Polygon type) for Flora Occurrences. -- Returns ALL processing statuses. -- --- NOTE: OCC_MOD_BY returns an integer user ID from the ledger accounts_emailuser --- table which lives in a separate database (ledger_db). A cross-database join is --- not possible in standard PostgreSQL. If human-readable names are required, --- either use dblink / postgres_fdw, or resolve IDs in application code. +-- NOTE: OCC_MOD_BY is resolved via accounts_emailuser and returns +-- first_name || ' ' || last_name for the last user to modify the record. -- -- NOTE: OBS_DATE is sourced from boranga_occplantcount (plant_count.obs_date). +-- +-- IMPORTANT — KB does not allow comments in SQL queries. Before pasting this +-- script into KB, strip all comments using: +-- python scripts/strip_sql_comments.py docs/sql-scripts/FloraOCCBoundaryAll.sql +-- Or write the result to a file for easy copying: +-- python scripts/strip_sql_comments.py docs/sql-scripts/FloraOCCBoundaryAll.sql > tmp.sql -- ============================================================================= WITH @@ -236,7 +240,7 @@ SELECT END AS OCC_SOURCE, occ.processing_status AS OCC_STATUS, TO_CHAR(occ.datetime_updated, 'YYYY-MM-DD HH24:MI:SS') AS OCC_MOD_DA, - occ.last_modified_by AS OCC_MOD_BY, + (u_mod.first_name || ' ' || u_mod.last_name) AS OCC_MOD_BY, occ.lodgement_date AS LODG_DATE, -- Region / District @@ -267,4 +271,5 @@ LEFT JOIN obs_detail ON occ.id = obs_detail.occurrence_id LEFT JOIN plant_count ON occ.id = plant_count.occurrence_id LEFT JOIN identification ON occ.id = identification.occurrence_id LEFT JOIN habitat ON occ.id = habitat.occurrence_id +LEFT JOIN accounts_emailuser u_mod ON occ.last_modified_by = u_mod.id ORDER BY occ.occurrence_number, geom.geom_id; diff --git a/docs/sql-scripts/FloraOCCBuffers.sql b/docs/sql-scripts/FloraOCCBuffers.sql index aab1695e3..6c7243d84 100644 --- a/docs/sql-scripts/FloraOCCBuffers.sql +++ b/docs/sql-scripts/FloraOCCBuffers.sql @@ -6,12 +6,13 @@ -- One row per BufferGeometry for Flora Occurrences. -- Returns ALL processing statuses. -- --- NOTE: OCC_MOD_BY returns an integer user ID from the ledger accounts_emailuser --- table which lives in a separate database (ledger_db). A cross-database join is --- not possible in standard PostgreSQL. If human-readable names are required, --- either use dblink / postgres_fdw, or resolve IDs in application code. --- -- NOTE: OBS_DATE is sourced from boranga_occplantcount (plant_count.obs_date). +-- +-- IMPORTANT — KB does not allow comments in SQL queries. Before pasting this +-- script into KB, strip all comments using: +-- python scripts/strip_sql_comments.py docs/sql-scripts/FloraOCCBuffers.sql +-- Or write the result to a file for easy copying: +-- python scripts/strip_sql_comments.py docs/sql-scripts/FloraOCCBuffers.sql > tmp.sql -- ============================================================================= WITH diff --git a/docs/sql-scripts/FloraOCCSites.sql b/docs/sql-scripts/FloraOCCSites.sql index 6d4401d94..a1751b92d 100644 --- a/docs/sql-scripts/FloraOCCSites.sql +++ b/docs/sql-scripts/FloraOCCSites.sql @@ -8,6 +8,12 @@ -- -- NOTE: OBS_DATE (observation_date) is not available on the Occurrence model -- and has been excluded from all OCC reports pending further review. +-- +-- IMPORTANT — KB does not allow comments in SQL queries. Before pasting this +-- script into KB, strip all comments using: +-- python scripts/strip_sql_comments.py docs/sql-scripts/FloraOCCSites.sql +-- Or write the result to a file for easy copying: +-- python scripts/strip_sql_comments.py docs/sql-scripts/FloraOCCSites.sql > tmp.sql -- ============================================================================= WITH diff --git a/docs/sql-scripts/FloraORFPoints.sql b/docs/sql-scripts/FloraORFPoints.sql index 738302071..daace8a5d 100644 --- a/docs/sql-scripts/FloraORFPoints.sql +++ b/docs/sql-scripts/FloraORFPoints.sql @@ -6,10 +6,14 @@ -- One row per OccurrenceReportGeometry (Point type) for Flora OCRs. -- Returns ALL processing statuses. -- --- NOTE: ORF_MOD_BY returns an integer user ID from the ledger accounts_emailuser --- table which lives in a separate database (ledger_db). A cross-database join is --- not possible in standard PostgreSQL. If human-readable names are required, --- either use dblink / postgres_fdw, or resolve IDs in application code. +-- NOTE: ORF_MOD_BY is resolved via accounts_emailuser and returns +-- first_name || ' ' || last_name for the last user to modify the record. +-- +-- IMPORTANT — KB does not allow comments in SQL queries. Before pasting this +-- script into KB, strip all comments using: +-- python scripts/strip_sql_comments.py docs/sql-scripts/FloraORFPoints.sql +-- Or write the result to a file for easy copying: +-- python scripts/strip_sql_comments.py docs/sql-scripts/FloraORFPoints.sql > tmp.sql -- ============================================================================= WITH @@ -248,7 +252,7 @@ SELECT ocr.record_source AS OCR_SOURCE, ocr.processing_status AS ORF_STATUS, TO_CHAR(ocr.datetime_updated, 'YYYY-MM-DD HH24:MI:SS') AS ORF_MOD_DA, - ocr.last_modified_by AS ORF_MOD_BY, + (u_mod.first_name || ' ' || u_mod.last_name) AS ORF_MOD_BY, ocr.lodgement_date AS LODG_DATE, -- Region / District @@ -280,4 +284,5 @@ LEFT JOIN obs_detail ON ocr.id = obs_detail.occurrence_report_id LEFT JOIN plant_count ON ocr.id = plant_count.occurrence_report_id LEFT JOIN identification ON ocr.id = identification.occurrence_report_id LEFT JOIN habitat ON ocr.id = habitat.occurrence_report_id +LEFT JOIN accounts_emailuser u_mod ON ocr.last_modified_by = u_mod.id ORDER BY ocr.occurrence_report_number, geom.geom_id; diff --git a/docs/sql-scripts/FloraORFPolygons.sql b/docs/sql-scripts/FloraORFPolygons.sql index 3b78eadf2..296ff49d8 100644 --- a/docs/sql-scripts/FloraORFPolygons.sql +++ b/docs/sql-scripts/FloraORFPolygons.sql @@ -6,10 +6,14 @@ -- One row per OccurrenceReportGeometry (Polygon type) for Flora OCRs. -- Returns ALL processing statuses. -- --- NOTE: ORF_MOD_BY returns an integer user ID from the ledger accounts_emailuser --- table which lives in a separate database (ledger_db). A cross-database join is --- not possible in standard PostgreSQL. If human-readable names are required, --- either use dblink / postgres_fdw, or resolve IDs in application code. +-- NOTE: ORF_MOD_BY is resolved via accounts_emailuser and returns +-- first_name || ' ' || last_name for the last user to modify the record. +-- +-- IMPORTANT — KB does not allow comments in SQL queries. Before pasting this +-- script into KB, strip all comments using: +-- python scripts/strip_sql_comments.py docs/sql-scripts/FloraORFPolygons.sql +-- Or write the result to a file for easy copying: +-- python scripts/strip_sql_comments.py docs/sql-scripts/FloraORFPolygons.sql > tmp.sql -- ============================================================================= WITH @@ -256,7 +260,7 @@ SELECT ocr.record_source AS OCR_SOURCE, ocr.processing_status AS ORF_STATUS, TO_CHAR(ocr.datetime_updated, 'YYYY-MM-DD HH24:MI:SS') AS ORF_MOD_DA, - ocr.last_modified_by AS ORF_MOD_BY, + (u_mod.first_name || ' ' || u_mod.last_name) AS ORF_MOD_BY, ocr.lodgement_date AS LODG_DATE, -- Region / District @@ -288,4 +292,5 @@ LEFT JOIN obs_detail ON ocr.id = obs_detail.occurrence_report_id LEFT JOIN plant_count ON ocr.id = plant_count.occurrence_report_id LEFT JOIN identification ON ocr.id = identification.occurrence_report_id LEFT JOIN habitat ON ocr.id = habitat.occurrence_report_id +LEFT JOIN accounts_emailuser u_mod ON ocr.last_modified_by = u_mod.id ORDER BY ocr.occurrence_report_number, geom.geom_id; diff --git a/docs/sql-scripts/READ_ME.md b/docs/sql-scripts/READ_ME.md new file mode 100644 index 000000000..2f1582dd5 --- /dev/null +++ b/docs/sql-scripts/READ_ME.md @@ -0,0 +1,5 @@ +KB does not support sql comments. + +To generate the sql scripts into the kb sub folder with comments removed, simply run: + +for f in docs/sql-scripts/*.sql; do python scripts/strip_sql_comments.py "$f" > "docs/sql-scripts/kb/$(basename "$f")"; done \ No newline at end of file diff --git a/scripts/strip_sql_comments.py b/scripts/strip_sql_comments.py new file mode 100644 index 000000000..f7314c9c0 --- /dev/null +++ b/scripts/strip_sql_comments.py @@ -0,0 +1,129 @@ +#!/usr/bin/env python3 +""" +Strip comments from SQL files in docs/sql-scripts/ and print the result. + +Usage: + python scripts/strip_sql_comments.py [file ...] + +Without arguments, processes all .sql files in docs/sql-scripts/. +With arguments, processes only the named files (paths or bare filenames). + +The original files are never modified. +""" + +import re +import sys +from pathlib import Path + +SQL_DIR = Path(__file__).resolve().parent.parent / "docs" / "sql-scripts" + + +def strip_sql_comments(sql: str) -> str: + """Remove -- line comments and /* ... */ block comments from SQL text.""" + # Pattern handles: + # - single-quoted strings 'it''s a "test"' (preserve content) + # - double-quoted identifiers "my col" (preserve content) + # - block comments /* ... */ (remove) + # - line comments -- ... (remove) + # + # Approach: tokenise left-to-right, keeping literals intact. + + result = [] + i = 0 + n = len(sql) + + while i < n: + # --- Single-quoted string literal --- + if sql[i] == "'": + j = i + 1 + while j < n: + if sql[j] == "'" and j + 1 < n and sql[j + 1] == "'": + j += 2 # escaped quote inside string + elif sql[j] == "'": + j += 1 + break + else: + j += 1 + result.append(sql[i:j]) + i = j + + # --- Double-quoted identifier --- + elif sql[i] == '"': + j = i + 1 + while j < n: + if sql[j] == '"' and j + 1 < n and sql[j + 1] == '"': + j += 2 + elif sql[j] == '"': + j += 1 + break + else: + j += 1 + result.append(sql[i:j]) + i = j + + # --- Block comment /* ... */ --- + elif sql[i : i + 2] == "/*": + j = sql.find("*/", i + 2) + if j == -1: + # Unterminated block comment — drop the rest + break + i = j + 2 + + # --- Line comment -- ... --- + elif sql[i : i + 2] == "--": + j = sql.find("\n", i + 2) + if j == -1: + break # comment runs to EOF + i = j # keep the newline so line numbers stay roughly intact + + else: + result.append(sql[i]) + i += 1 + + text = "".join(result) + + # Collapse runs of blank lines (>2 consecutive) left behind by removed comments + text = re.sub(r"\n{3,}", "\n\n", text) + return text.strip() + + +def resolve_files(args: list[str]) -> list[Path]: + if not args: + return sorted(SQL_DIR.glob("*.sql")) + + paths = [] + for arg in args: + p = Path(arg) + if not p.is_absolute() and not p.exists(): + # Try treating it as a bare filename inside SQL_DIR + candidate = SQL_DIR / arg + if candidate.exists(): + p = candidate + if not p.exists(): + print(f"Warning: file not found: {arg}", file=sys.stderr) + continue + paths.append(p.resolve()) + return paths + + +def main() -> None: + files = resolve_files(sys.argv[1:]) + + if not files: + print("No SQL files found.", file=sys.stderr) + sys.exit(1) + + for path in files: + if len(files) > 1: + print(f"-- ===== {path.name} =====") + print() + + sql = path.read_text(encoding="utf-8") + print(strip_sql_comments(sql)) + + if len(files) > 1: + print() + + +if __name__ == "__main__": + main()