From 8750b4f076aa862823cfd86cfa2cebdc7db8f4d7 Mon Sep 17 00:00:00 2001 From: Xinlyu Wang Date: Thu, 21 Nov 2024 09:14:32 +0800 Subject: [PATCH 01/52] Add local SQL Server setup with auto-restore --- sqlserver.Dockerfile | 13 +++++++++++++ .gitignore | 1 + docker-compose.yml | 18 ++++++++++++++++++ init-sqlserver.sh | 30 ++++++++++++++++++++++++++++++ 4 files changed, 62 insertions(+) create mode 100644 sqlserver.Dockerfile create mode 100755 init-sqlserver.sh diff --git a/ sqlserver.Dockerfile b/ sqlserver.Dockerfile new file mode 100644 index 000000000..67dbdfab8 --- /dev/null +++ b/ sqlserver.Dockerfile @@ -0,0 +1,13 @@ +FROM mcr.microsoft.com/mssql/server:2022-latest + +# Install sqlpackage +RUN apt-get update && apt-get install -y wget unzip \ + && wget -progress=bar:force -q -O sqlpackage.zip https://go.microsoft.com/fwlink/?linkid=2185670 \ + && unzip -qq sqlpackage.zip -d /opt/sqlpackage \ + && chmod +x /opt/sqlpackage/sqlpackage \ + && ln -s /opt/sqlpackage/sqlpackage /usr/local/bin/sqlpackage \ + && rm sqlpackage.zip + +# Copy initialization script +COPY scripts/init-sqlserver.sh /docker-entrypoint-initdb.d/ +RUN chmod +x /docker-entrypoint-initdb.d/init-sqlserver.sh \ No newline at end of file diff --git a/.gitignore b/.gitignore index 5fe44dfc3..130782bdd 100644 --- a/.gitignore +++ b/.gitignore @@ -81,3 +81,4 @@ taxonomy/fixtures/test_tax_* *.csv *.pgdump compose.yaml +*.bacpac diff --git a/docker-compose.yml b/docker-compose.yml index bd9f751ab..e09cddf41 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,6 +12,21 @@ services: volumes: - ./init-db.sql:/docker-entrypoint-initdb.d/init-db.sql + sqlserver: + build: + context: . + dockerfile: sqlserver.Dockerfile + environment: + - ACCEPT_EULA=Y + - MSSQL_SA_PASSWORD=YourStrong@Passw0rd + - DB_NAME=turtle_tagging_uat + ports: + - "1433:1433" + volumes: + - sqlserver_data:/var/opt/mssql + - ./turtle_tagging_uat-2024-10-4-10-27.bacpac:/var/opt/mssql/backup/turtle_tagging_uat.bacpac + command: /bin/bash -c "/opt/mssql/bin/sqlservr & /docker-entrypoint-initdb.d/init-sqlserver.sh" + web: build: . env_file: @@ -23,3 +38,6 @@ services: volumes: - .:/app restart: always + +volumes: + sqlserver_data: diff --git a/init-sqlserver.sh b/init-sqlserver.sh new file mode 100755 index 000000000..114f632df --- /dev/null +++ b/init-sqlserver.sh @@ -0,0 +1,30 @@ +#!/bin/bash +set -e + +# 等待SQL Server启动 +/opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P "$MSSQL_SA_PASSWORD" -Q "SELECT 1" >/dev/null 2>&1 +while [ $? -ne 0 ]; do + echo "Waiting for SQL Server to start..." + sleep 1 + /opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P "$MSSQL_SA_PASSWORD" -Q "SELECT 1" >/dev/null 2>&1 +done + +echo "SQL Server is up" + +# 检查数据库是否已存在 +DB_EXISTS=$(/opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P "$MSSQL_SA_PASSWORD" -Q "SELECT COUNT(*) FROM sys.databases WHERE name = '${DB_NAME}'" -h -1) + +if [ $DB_EXISTS -eq 0 ]; then + echo "Restoring database..." + /opt/mssql-tools/bin/sqlpackage \ + /Action:Import \ + /SourceFile:/var/opt/mssql/backup/turtle_tagging_uat.bacpac \ + /TargetServerName:localhost \ + /TargetDatabaseName:$DB_NAME \ + /TargetUser:sa \ + /TargetPassword:$MSSQL_SA_PASSWORD + + echo "Database restored" +else + echo "Database already exists, skipping restore" +fi \ No newline at end of file From b8bf921ec4576d33f9826876024bdc8bdfeebe1e Mon Sep 17 00:00:00 2001 From: Xinlyu Wang Date: Thu, 21 Nov 2024 09:15:21 +0800 Subject: [PATCH 02/52] Add batch curation tool to admin tools page --- wamtram2/templates/wamtram2/admin_tools.html | 10 +++++----- .../wamtram2/batch_curation_list.html | 4 ++-- .../wamtram2/entry_curation_list.html | 16 +++++++++------- .../wamtram2/trtentrybatch_detail.html | 4 ++-- wamtram2/urls.py | 2 +- wamtram2/views.py | 18 +++++++++++++++--- 6 files changed, 34 insertions(+), 20 deletions(-) diff --git a/wamtram2/templates/wamtram2/admin_tools.html b/wamtram2/templates/wamtram2/admin_tools.html index 8f6047a8d..f4f6fd4bd 100644 --- a/wamtram2/templates/wamtram2/admin_tools.html +++ b/wamtram2/templates/wamtram2/admin_tools.html @@ -151,15 +151,15 @@

Template Management

- +
- +
- +
-

Batch Management

-

Manage and review data entry batches.

+

Batch Curation

+

Review and manage data entry batches for curation.

diff --git a/wamtram2/templates/wamtram2/batch_curation_list.html b/wamtram2/templates/wamtram2/batch_curation_list.html index c8953ce82..9f727b8c1 100644 --- a/wamtram2/templates/wamtram2/batch_curation_list.html +++ b/wamtram2/templates/wamtram2/batch_curation_list.html @@ -156,7 +156,7 @@ function viewBatch() { if (selectedBatchId) { - window.location.href = "{% url 'wamtram2:batch_entries' 0 %}".replace('0', selectedBatchId); + window.location.href = "{% url 'wamtram2:entries_curation' 0 %}".replace('0', selectedBatchId); } } @@ -189,7 +189,7 @@ $('.grid-row').click(function() { const batchId = $(this).data('batch-id'); - window.location.href = "{% url 'wamtram2:batch_entries' 0 %}".replace('0', batchId); + window.location.href = "{% url 'wamtram2:entries_curation' 0 %}".replace('0', batchId); }); $('#saveColumnSettings').click(function() { diff --git a/wamtram2/templates/wamtram2/entry_curation_list.html b/wamtram2/templates/wamtram2/entry_curation_list.html index fb5389d31..a49004ec4 100644 --- a/wamtram2/templates/wamtram2/entry_curation_list.html +++ b/wamtram2/templates/wamtram2/entry_curation_list.html @@ -102,11 +102,11 @@

Entries for Batch {{ batch_id }}

@@ -190,13 +190,15 @@

Entries for Batch {{ batch_id }}

{{ entry.get_sex_display|default:'-' }}
-
{% elif column.field == 'place_code' %} diff --git a/wamtram2/templates/wamtram2/trtentrybatch_detail.html b/wamtram2/templates/wamtram2/trtentrybatch_detail.html index 8d2ac68f1..dfe1e6c09 100644 --- a/wamtram2/templates/wamtram2/trtentrybatch_detail.html +++ b/wamtram2/templates/wamtram2/trtentrybatch_detail.html @@ -143,10 +143,10 @@
{% if request.user.is_superuser or request.user|has_group:"WAMTRAM2_STAFF" or request.user|has_group:"WAMTRAM2_TEAM_LEADER"%} - Validate this Batch + Validate this Batch {% endif %} {% if request.user.is_superuser %} - Add this batch to the database + Add this batch to the database {% endif %}
diff --git a/wamtram2/urls.py b/wamtram2/urls.py index 4a17e6c95..601802436 100644 --- a/wamtram2/urls.py +++ b/wamtram2/urls.py @@ -51,6 +51,6 @@ path('curation/transfer-observations-by-tag/', views.TransferObservationsByTagView.as_view(), name='transfer_observations_by_tag'), path('curation/nesting-seasons/', views.NestingSeasonListView.as_view(), name='nesting_season_list'), path('curation/batches/', views.BatchCurationView.as_view(), name='batch_curation'), - path('curation/batch//entries/', views.EntryCurationView.as_view(), name='batch_entries'), + path('curation/batch//entries/', views.EntryCurationView.as_view(), name='entries_curation'), path('save-entry-changes/', views.SaveEntryChangesView.as_view(), name='save_entry_changes'), ] diff --git a/wamtram2/views.py b/wamtram2/views.py index 7ca4ab457..790c23c27 100644 --- a/wamtram2/views.py +++ b/wamtram2/views.py @@ -663,7 +663,13 @@ def get(self, request, *args, **kwargs): messages.add_message( request, messages.ERROR, "Database error: {}".format(e) ) - return redirect("wamtram2:entry_batch_detail", batch_id=self.kwargs["batch_id"]) + + return_to = request.GET.get('return_to') + + if return_to == 'curation': + return redirect("wamtram2:entries_curation", batch_id=self.kwargs["batch_id"]) + else: + return redirect("wamtram2:entry_batch_detail", batch_id=self.kwargs["batch_id"]) class DeleteEntryView(LoginRequiredMixin,DeleteView): @@ -731,7 +737,13 @@ def get(self, request, *args, **kwargs): messages.add_message( request, messages.ERROR, "Database error: {}".format(e) ) - return redirect("wamtram2:entry_batch_detail", batch_id=self.kwargs["batch_id"]) + + return_to = request.GET.get('return_to') + + if return_to == 'curation': + return redirect("wamtram2:entries_curation", batch_id=self.kwargs["batch_id"]) + else: + return redirect("wamtram2:entry_batch_detail", batch_id=self.kwargs["batch_id"]) class FindTurtleView(LoginRequiredMixin, View): """ @@ -3495,7 +3507,7 @@ def get_queryset(self): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - + context['sex_choices'] = TrtDataEntry.SEX_CHOICES if not context.get('object_list'): context['object_list'] = [] From ea9c3e54f913efc9670e8edca14f44bf823fccf9 Mon Sep 17 00:00:00 2001 From: Xinlyu Wang Date: Thu, 21 Nov 2024 12:33:02 +0800 Subject: [PATCH 03/52] Add batch curation tool to admin tools page --- .gitignore | 1 + wamtram2/templates/wamtram2/admin_tools.html | 10 +++++----- .../wamtram2/batch_curation_list.html | 4 ++-- .../wamtram2/entry_curation_list.html | 16 +++++++++------- .../wamtram2/trtentrybatch_detail.html | 4 ++-- wamtram2/urls.py | 2 +- wamtram2/views.py | 18 +++++++++++++++--- 7 files changed, 35 insertions(+), 20 deletions(-) diff --git a/.gitignore b/.gitignore index 5fe44dfc3..130782bdd 100644 --- a/.gitignore +++ b/.gitignore @@ -81,3 +81,4 @@ taxonomy/fixtures/test_tax_* *.csv *.pgdump compose.yaml +*.bacpac diff --git a/wamtram2/templates/wamtram2/admin_tools.html b/wamtram2/templates/wamtram2/admin_tools.html index 8f6047a8d..f4f6fd4bd 100644 --- a/wamtram2/templates/wamtram2/admin_tools.html +++ b/wamtram2/templates/wamtram2/admin_tools.html @@ -151,15 +151,15 @@

Template Management

- +
- +
- +
-

Batch Management

-

Manage and review data entry batches.

+

Batch Curation

+

Review and manage data entry batches for curation.

diff --git a/wamtram2/templates/wamtram2/batch_curation_list.html b/wamtram2/templates/wamtram2/batch_curation_list.html index c8953ce82..9f727b8c1 100644 --- a/wamtram2/templates/wamtram2/batch_curation_list.html +++ b/wamtram2/templates/wamtram2/batch_curation_list.html @@ -156,7 +156,7 @@ function viewBatch() { if (selectedBatchId) { - window.location.href = "{% url 'wamtram2:batch_entries' 0 %}".replace('0', selectedBatchId); + window.location.href = "{% url 'wamtram2:entries_curation' 0 %}".replace('0', selectedBatchId); } } @@ -189,7 +189,7 @@ $('.grid-row').click(function() { const batchId = $(this).data('batch-id'); - window.location.href = "{% url 'wamtram2:batch_entries' 0 %}".replace('0', batchId); + window.location.href = "{% url 'wamtram2:entries_curation' 0 %}".replace('0', batchId); }); $('#saveColumnSettings').click(function() { diff --git a/wamtram2/templates/wamtram2/entry_curation_list.html b/wamtram2/templates/wamtram2/entry_curation_list.html index fb5389d31..a49004ec4 100644 --- a/wamtram2/templates/wamtram2/entry_curation_list.html +++ b/wamtram2/templates/wamtram2/entry_curation_list.html @@ -102,11 +102,11 @@

Entries for Batch {{ batch_id }}

@@ -190,13 +190,15 @@

Entries for Batch {{ batch_id }}

{{ entry.get_sex_display|default:'-' }}
-
{% elif column.field == 'place_code' %} diff --git a/wamtram2/templates/wamtram2/trtentrybatch_detail.html b/wamtram2/templates/wamtram2/trtentrybatch_detail.html index 8d2ac68f1..dfe1e6c09 100644 --- a/wamtram2/templates/wamtram2/trtentrybatch_detail.html +++ b/wamtram2/templates/wamtram2/trtentrybatch_detail.html @@ -143,10 +143,10 @@
{% if request.user.is_superuser or request.user|has_group:"WAMTRAM2_STAFF" or request.user|has_group:"WAMTRAM2_TEAM_LEADER"%} - Validate this Batch + Validate this Batch {% endif %} {% if request.user.is_superuser %} - Add this batch to the database + Add this batch to the database {% endif %}
diff --git a/wamtram2/urls.py b/wamtram2/urls.py index 4a17e6c95..601802436 100644 --- a/wamtram2/urls.py +++ b/wamtram2/urls.py @@ -51,6 +51,6 @@ path('curation/transfer-observations-by-tag/', views.TransferObservationsByTagView.as_view(), name='transfer_observations_by_tag'), path('curation/nesting-seasons/', views.NestingSeasonListView.as_view(), name='nesting_season_list'), path('curation/batches/', views.BatchCurationView.as_view(), name='batch_curation'), - path('curation/batch//entries/', views.EntryCurationView.as_view(), name='batch_entries'), + path('curation/batch//entries/', views.EntryCurationView.as_view(), name='entries_curation'), path('save-entry-changes/', views.SaveEntryChangesView.as_view(), name='save_entry_changes'), ] diff --git a/wamtram2/views.py b/wamtram2/views.py index 7ca4ab457..790c23c27 100644 --- a/wamtram2/views.py +++ b/wamtram2/views.py @@ -663,7 +663,13 @@ def get(self, request, *args, **kwargs): messages.add_message( request, messages.ERROR, "Database error: {}".format(e) ) - return redirect("wamtram2:entry_batch_detail", batch_id=self.kwargs["batch_id"]) + + return_to = request.GET.get('return_to') + + if return_to == 'curation': + return redirect("wamtram2:entries_curation", batch_id=self.kwargs["batch_id"]) + else: + return redirect("wamtram2:entry_batch_detail", batch_id=self.kwargs["batch_id"]) class DeleteEntryView(LoginRequiredMixin,DeleteView): @@ -731,7 +737,13 @@ def get(self, request, *args, **kwargs): messages.add_message( request, messages.ERROR, "Database error: {}".format(e) ) - return redirect("wamtram2:entry_batch_detail", batch_id=self.kwargs["batch_id"]) + + return_to = request.GET.get('return_to') + + if return_to == 'curation': + return redirect("wamtram2:entries_curation", batch_id=self.kwargs["batch_id"]) + else: + return redirect("wamtram2:entry_batch_detail", batch_id=self.kwargs["batch_id"]) class FindTurtleView(LoginRequiredMixin, View): """ @@ -3495,7 +3507,7 @@ def get_queryset(self): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - + context['sex_choices'] = TrtDataEntry.SEX_CHOICES if not context.get('object_list'): context['object_list'] = [] From 2e84d0713b7e406f3c83c0522a8934c71c4dd7f2 Mon Sep 17 00:00:00 2001 From: Xinlyu Wang Date: Thu, 21 Nov 2024 12:36:59 +0800 Subject: [PATCH 04/52] Remove local SQL Server setup with auto-restore --- Dockerfile | 3 ++- docker-compose.yml | 20 +------------------- wait-for-db.sh | 2 +- 3 files changed, 4 insertions(+), 21 deletions(-) diff --git a/Dockerfile b/Dockerfile index 2acc5a6fd..4aa90c20a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,3 +1,4 @@ + # syntax=docker/dockerfile:1 # Prepare the base environment. FROM python:3.11.8-slim as builder_base_wastd @@ -77,4 +78,4 @@ ENV PYTHONUNBUFFERED=1 \ ENTRYPOINT ["/app/entrypoint.sh"] -CMD ["gunicorn", "wastd.wsgi:application", "--bind", "0.0.0.0:8080", "--timeout", "120", "--log-level", "debug"] \ No newline at end of file +CMD ["gunicorn", "wastd.wsgi:application", "--bind", "0.0.0.0:8080", "--timeout", "120", "--log-level", "debug"] diff --git a/docker-compose.yml b/docker-compose.yml index e09cddf41..5887fef5d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,21 +12,6 @@ services: volumes: - ./init-db.sql:/docker-entrypoint-initdb.d/init-db.sql - sqlserver: - build: - context: . - dockerfile: sqlserver.Dockerfile - environment: - - ACCEPT_EULA=Y - - MSSQL_SA_PASSWORD=YourStrong@Passw0rd - - DB_NAME=turtle_tagging_uat - ports: - - "1433:1433" - volumes: - - sqlserver_data:/var/opt/mssql - - ./turtle_tagging_uat-2024-10-4-10-27.bacpac:/var/opt/mssql/backup/turtle_tagging_uat.bacpac - command: /bin/bash -c "/opt/mssql/bin/sqlservr & /docker-entrypoint-initdb.d/init-sqlserver.sh" - web: build: . env_file: @@ -37,7 +22,4 @@ services: - db volumes: - .:/app - restart: always - -volumes: - sqlserver_data: + restart: always \ No newline at end of file diff --git a/wait-for-db.sh b/wait-for-db.sh index 4897e1cc0..64e740fcf 100755 --- a/wait-for-db.sh +++ b/wait-for-db.sh @@ -14,4 +14,4 @@ until PGPASSWORD="$POSTGRES_PASSWORD" psql -h "$host" -U "$POSTGRES_USER" -d "$P done >&2 echo "Postgres is up - executing command" -exec $cmd +exec $cmd \ No newline at end of file From ded57630706bad90c0884675796985ddfbb5c285 Mon Sep 17 00:00:00 2001 From: Rick Wang Date: Thu, 21 Nov 2024 12:39:50 +0800 Subject: [PATCH 05/52] Remove local SQL Server setup with auto-restore --- sqlserver.Dockerfile | 13 ------------- init-sqlserver.sh | 30 ------------------------------ 2 files changed, 43 deletions(-) delete mode 100644 sqlserver.Dockerfile delete mode 100755 init-sqlserver.sh diff --git a/ sqlserver.Dockerfile b/ sqlserver.Dockerfile deleted file mode 100644 index 67dbdfab8..000000000 --- a/ sqlserver.Dockerfile +++ /dev/null @@ -1,13 +0,0 @@ -FROM mcr.microsoft.com/mssql/server:2022-latest - -# Install sqlpackage -RUN apt-get update && apt-get install -y wget unzip \ - && wget -progress=bar:force -q -O sqlpackage.zip https://go.microsoft.com/fwlink/?linkid=2185670 \ - && unzip -qq sqlpackage.zip -d /opt/sqlpackage \ - && chmod +x /opt/sqlpackage/sqlpackage \ - && ln -s /opt/sqlpackage/sqlpackage /usr/local/bin/sqlpackage \ - && rm sqlpackage.zip - -# Copy initialization script -COPY scripts/init-sqlserver.sh /docker-entrypoint-initdb.d/ -RUN chmod +x /docker-entrypoint-initdb.d/init-sqlserver.sh \ No newline at end of file diff --git a/init-sqlserver.sh b/init-sqlserver.sh deleted file mode 100755 index 114f632df..000000000 --- a/init-sqlserver.sh +++ /dev/null @@ -1,30 +0,0 @@ -#!/bin/bash -set -e - -# 等待SQL Server启动 -/opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P "$MSSQL_SA_PASSWORD" -Q "SELECT 1" >/dev/null 2>&1 -while [ $? -ne 0 ]; do - echo "Waiting for SQL Server to start..." - sleep 1 - /opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P "$MSSQL_SA_PASSWORD" -Q "SELECT 1" >/dev/null 2>&1 -done - -echo "SQL Server is up" - -# 检查数据库是否已存在 -DB_EXISTS=$(/opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P "$MSSQL_SA_PASSWORD" -Q "SELECT COUNT(*) FROM sys.databases WHERE name = '${DB_NAME}'" -h -1) - -if [ $DB_EXISTS -eq 0 ]; then - echo "Restoring database..." - /opt/mssql-tools/bin/sqlpackage \ - /Action:Import \ - /SourceFile:/var/opt/mssql/backup/turtle_tagging_uat.bacpac \ - /TargetServerName:localhost \ - /TargetDatabaseName:$DB_NAME \ - /TargetUser:sa \ - /TargetPassword:$MSSQL_SA_PASSWORD - - echo "Database restored" -else - echo "Database already exists, skipping restore" -fi \ No newline at end of file From 764076aa6a11646036a1d5f189eca816141671e7 Mon Sep 17 00:00:00 2001 From: Xinlyu Wang Date: Tue, 26 Nov 2024 12:22:50 +0800 Subject: [PATCH 06/52] Remove duplicate message --- wamtram2/templates/wamtram2/add_person.html | 3 +-- wamtram2/templates/wamtram2/export_form.html | 1 - wamtram2/templates/wamtram2/tag_register.html | 1 - wamtram2/templates/wamtram2/template_manage.html | 3 +-- wamtram2/templates/wamtram2/transfer_observation.html | 3 +-- 5 files changed, 3 insertions(+), 8 deletions(-) diff --git a/wamtram2/templates/wamtram2/add_person.html b/wamtram2/templates/wamtram2/add_person.html index a5932acc8..8bdd6c201 100644 --- a/wamtram2/templates/wamtram2/add_person.html +++ b/wamtram2/templates/wamtram2/add_person.html @@ -56,8 +56,7 @@ - - {% include "includes/messages.html" %} + diff --git a/wamtram2/templates/wamtram2/export_form.html b/wamtram2/templates/wamtram2/export_form.html index 89cf83532..36698d9f2 100644 --- a/wamtram2/templates/wamtram2/export_form.html +++ b/wamtram2/templates/wamtram2/export_form.html @@ -37,7 +37,6 @@

Export Data

- {% include "includes/messages.html" %} diff --git a/wamtram2/templates/wamtram2/tag_register.html b/wamtram2/templates/wamtram2/tag_register.html index 20ff3c387..3449fa906 100644 --- a/wamtram2/templates/wamtram2/tag_register.html +++ b/wamtram2/templates/wamtram2/tag_register.html @@ -25,7 +25,6 @@

Register Tags

- {% include "includes/messages.html" %} diff --git a/wamtram2/templates/wamtram2/template_manage.html b/wamtram2/templates/wamtram2/template_manage.html index 7fadfd3ea..f9e53e358 100644 --- a/wamtram2/templates/wamtram2/template_manage.html +++ b/wamtram2/templates/wamtram2/template_manage.html @@ -35,9 +35,8 @@

Template Management

- {% include "includes/messages.html" %} - +
Create New Template
diff --git a/wamtram2/templates/wamtram2/transfer_observation.html b/wamtram2/templates/wamtram2/transfer_observation.html index 7ed9ca392..6f79f629d 100644 --- a/wamtram2/templates/wamtram2/transfer_observation.html +++ b/wamtram2/templates/wamtram2/transfer_observation.html @@ -23,8 +23,7 @@ - - {% include "includes/messages.html" %} + From 137f2875051ff28a2819d47d9191199758d2ef89 Mon Sep 17 00:00:00 2001 From: Xinlyu Wang Date: Tue, 26 Nov 2024 14:15:24 +0800 Subject: [PATCH 07/52] Add curation observation page --- wamtram2/static/js/observation_management.js | 217 +++++++++++ .../wamtram2/observation_management.html | 352 ++++++++++++++++++ wamtram2/urls.py | 3 + wamtram2/views.py | 193 ++++++++++ 4 files changed, 765 insertions(+) create mode 100644 wamtram2/static/js/observation_management.js create mode 100644 wamtram2/templates/wamtram2/observation_management.html diff --git a/wamtram2/static/js/observation_management.js b/wamtram2/static/js/observation_management.js new file mode 100644 index 000000000..d48e28f73 --- /dev/null +++ b/wamtram2/static/js/observation_management.js @@ -0,0 +1,217 @@ +$(document).ready(function() { + // Global variables + let currentObservationId = null; + const formFields = new Set(); + let originalData = {}; + + // Initialize form validation and other components + function initialize() { + initializeFormValidation(); + initializeDropdowns(); + bindEventHandlers(); + setupAutoSave(); + } + + // Form validation + function initializeFormValidation() { + $('form input, form select').each(function() { + if ($(this).prop('required')) { + formFields.add($(this).attr('name')); + } + }); + } + + // Initialize select2 dropdowns + function initializeDropdowns() { + $('.select2-dropdown').select2({ + theme: 'bootstrap4', + width: '100%' + }); + } + + // Bind event handlers + function bindEventHandlers() { + // Save button click + $('#saveChanges').click(handleSave); + + // New record button click + $('#addNew').click(handleNewRecord); + + // Filter changes + $('#tagSearch, #placeFilter, #dateFilter, #statusFilter').on('change', handleFilter); + + // Form field changes + $('form input, form select').on('change', function() { + $(this).addClass('modified'); + checkFormValidity(); + }); + + // Add damage record button + $('#addDamage').click(addDamageRecord); + + // Add measurement button + $('#addMeasurement').click(addMeasurementRow); + + // Coordinate conversion + $('.coordinate-input').on('change', convertCoordinates); + } + + // Handle save operation + async function handleSave() { + if (!validateForm()) { + showErrorMessage('Please fill in all required fields'); + return; + } + + showLoadingOverlay(); + try { + const formData = collectFormData(); + const response = await $.ajax({ + url: '/api/observations/', + method: currentObservationId ? 'PUT' : 'POST', + data: JSON.stringify(formData), + contentType: 'application/json' + }); + + if (response.status === 'success') { + showSuccessMessage('Changes saved successfully'); + updateOriginalData(formData); + if (!currentObservationId) { + currentObservationId = response.observation_id; + } + } else { + throw new Error(response.message); + } + } catch (error) { + showErrorMessage('Error saving changes: ' + error.message); + } finally { + hideLoadingOverlay(); + } + } + + // Collect form data + function collectFormData() { + const data = { + basic_info: {}, + tag_info: { + recorded_tags: [], + recorded_pit_tags: [] + }, + measurements: [], + damage_records: [], + location: {} + }; + + // Collect basic information + $('#basicInfo input, #basicInfo select').each(function() { + data.basic_info[$(this).attr('name')] = $(this).val(); + }); + + // Collect tag information + $('#tagInfo .tag-row').each(function() { + const tagData = { + tag_id: $(this).find('[name="tag_id"]').val(), + tag_type: $(this).find('[name="tag_type"]').val(), + tag_position: $(this).find('[name="tag_position"]').val(), + tag_state: $(this).find('[name="tag_state"]').val() + }; + data.tag_info.recorded_tags.push(tagData); + }); + + // Collect measurements + $('#measurements .measurement-row').each(function() { + const measurementData = { + measurement_type: $(this).find('[name="measurement_type"]').val(), + measurement_value: $(this).find('[name="measurement_value"]').val() + }; + data.measurements.push(measurementData); + }); + + // Collect damage records + $('#damage .damage-record').each(function() { + const damageData = { + body_part: $(this).find('[name="body_part"]').val(), + damage_code: $(this).find('[name="damage_code"]').val() + }; + data.damage_records.push(damageData); + }); + + // Collect location information + $('#location input, #location select').each(function() { + data.location[$(this).attr('name')] = $(this).val(); + }); + + return data; + } + + // Load observation data + async function loadObservation(observationId) { + showLoadingOverlay(); + try { + const response = await $.get(`/api/observations/${observationId}/`); + if (response.status === 'success') { + populateForm(response.data); + currentObservationId = observationId; + updateOriginalData(response.data); + } else { + throw new Error(response.message); + } + } catch (error) { + showErrorMessage('Error loading observation: ' + error.message); + } finally { + hideLoadingOverlay(); + } + } + + // Populate form with data + function populateForm(data) { + clearForm(); + + // Populate basic information + for (const [key, value] of Object.entries(data.basic_info)) { + $(`[name="${key}"]`).val(value); + } + + // Populate tags + data.tag_info.recorded_tags.forEach(tag => { + addTagRow(tag); + }); + + // Populate measurements + data.measurements.forEach(measurement => { + addMeasurementRow(measurement); + }); + + // Populate damage records + data.damage_records.forEach(damage => { + addDamageRecord(damage); + }); + + // Populate location + for (const [key, value] of Object.entries(data.location)) { + $(`[name="${key}"]`).val(value); + } + } + + // Utility functions + function showLoadingOverlay() { + $('.loading-overlay').show(); + } + + function hideLoadingOverlay() { + $('.loading-overlay').hide(); + } + + function showSuccessMessage(message) { + // Implement your preferred notification method + alert(message); + } + + function showErrorMessage(message) { + // Implement your preferred notification method + alert(message); + } + + // Initialize the page + initialize(); +}); \ No newline at end of file diff --git a/wamtram2/templates/wamtram2/observation_management.html b/wamtram2/templates/wamtram2/observation_management.html new file mode 100644 index 000000000..5586ebd07 --- /dev/null +++ b/wamtram2/templates/wamtram2/observation_management.html @@ -0,0 +1,352 @@ +{% extends "base_wastd.html" %} +{% load static bootstrap4 %} + +{% block extra_style %} + {{ block.super }} + {{ form.media.css }} + + + +{% endblock %} + +{% block page_content_inner %} +
+ +
+
+
+

Observation Management

+
+ + +
+
+
+
+ + +
+
+
+ +
+ +
+
+
+
+ +
+
+ +
+
+ +
+
+ + +
+
+ + + + +
+ +
+
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+ +
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+
+ + +
+
+

Flipper Tags

+
+
+
Left Flipper
+
+ + +
+
+ + +
+
+
+
Right Flipper
+
+ + +
+
+ + +
+
+
+ +

PIT Tags

+
+
+
+ + +
+
+
+
+ + +
+
+
+
+
+ + +
+
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+ +
+ +
+ + +
+
+ + +
+
+

Standard Damage Records

+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+ +
+ +
+ + +
+
+ + +
+
+
+
+
+ + +
+
+
+
+ + +
+
+
+ +
+
+
Latitude
+
+ + + +
+
+
+
Longitude
+
+ + + +
+
+
+
+
+
+
+
+
+ + + +{% endblock %} + +{% block extra_script %} + +{% endblock %} \ No newline at end of file diff --git a/wamtram2/urls.py b/wamtram2/urls.py index 601802436..661747d65 100644 --- a/wamtram2/urls.py +++ b/wamtram2/urls.py @@ -53,4 +53,7 @@ path('curation/batches/', views.BatchCurationView.as_view(), name='batch_curation'), path('curation/batch//entries/', views.EntryCurationView.as_view(), name='entries_curation'), path('save-entry-changes/', views.SaveEntryChangesView.as_view(), name='save_entry_changes'), + path('curation/observations/', views.ObservationManagementView.as_view(), name='observation_management'), + path('api/observations/', views.ObservationDataView.as_view(), name='observation_data'), + path('api/observations//', views.ObservationDataView.as_view(), name='observation_detail'), ] diff --git a/wamtram2/views.py b/wamtram2/views.py index 790c23c27..223898185 100644 --- a/wamtram2/views.py +++ b/wamtram2/views.py @@ -40,6 +40,7 @@ TrtEntryBatches, TrtDataEntry, TrtPersons, + TrtBeachPositions, TrtObservations, Template, TrtTagStates, @@ -55,6 +56,10 @@ TrtBodyParts, TrtDamageCodes, TrtYesNo, + TrtRecordedTags, + TrtRecordedPitTags, + TrtMeasurements, + TrtDamage ) from .forms import TrtDataEntryForm, SearchForm, TrtEntryBatchesForm, TemplateForm, BatchesCodeForm, TrtPersonsForm, TagRegisterForm @@ -3733,3 +3738,191 @@ def post(self, request): }) + +class ObservationManagementView(LoginRequiredMixin, TemplateView): + template_name = 'wamtram2/observation_management.html' + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context.update({ + 'places': TrtPlaces.objects.all(), + 'damage_codes': TrtDamageCodes.objects.all(), + 'measurement_types': TrtMeasurementTypes.objects.all(), + 'activities': TrtActivities.objects.all(), + 'beach_positions': TrtBeachPositions.objects.all(), + 'tag_states': TrtTagStates.objects.all(), + }) + return context + +class ObservationDataView(LoginRequiredMixin, View): + @transaction.atomic + def post(self, request): + try: + data = json.loads(request.body) + observation_id = data.get('observation_id') + + if observation_id: + observation = TrtObservations.objects.get(pk=observation_id) + else: + observation = TrtObservations() + + # Update basic info + basic_info = data.get('basic_info', {}) + for field, value in basic_info.items(): + if hasattr(observation, field): + setattr(observation, field, value) + + # Handle special fields + if 'observation_date' in basic_info: + observation.observation_date = datetime.strptime( + basic_info['observation_date'], + '%Y-%m-%d' + ) + + observation.save() + + # Update related records + self._update_tags(observation, data.get('tag_info', {})) + self._update_measurements(observation, data.get('measurements', [])) + self._update_damage_records(observation, data.get('damage_records', [])) + self._update_location(observation, data.get('location', {})) + + return JsonResponse({ + 'status': 'success', + 'observation_id': observation.observation_id + }) + + except ValidationError as e: + return JsonResponse({ + 'status': 'error', + 'message': str(e) + }, status=400) + except Exception as e: + return JsonResponse({ + 'status': 'error', + 'message': str(e) + }, status=500) + + def get(self, request, observation_id=None): + try: + if observation_id: + observation = TrtObservations.objects.get(pk=observation_id) + data = self._get_observation_data(observation) + return JsonResponse({'status': 'success', 'data': data}) + else: + # Handle list view with filters + observations = self._filter_observations(request) + data = [self._get_observation_summary(obs) for obs in observations] + return JsonResponse({'status': 'success', 'data': data}) + except TrtObservations.DoesNotExist: + return JsonResponse({ + 'status': 'error', + 'message': 'Observation not found' + }, status=404) + except Exception as e: + return JsonResponse({ + 'status': 'error', + 'message': str(e) + }, status=500) + + def _get_observation_data(self, observation): + """Get full observation data""" + return { + 'basic_info': { + 'observation_id': observation.observation_id, + 'turtle_id': observation.turtle_id, + 'observation_date': observation.observation_date.strftime('%Y-%m-%d'), + 'alive': observation.alive, + 'nesting': observation.nesting, + # Add other basic fields + }, + 'tag_info': { + 'recorded_tags': list(observation.trtrecordedtags_set.values()), + 'recorded_pit_tags': list(observation.trtrecordedpittags_set.values()), + }, + 'measurements': list(observation.trtmeasurements_set.values()), + 'damage_records': list(observation.trtdamage_set.values()), + 'location': { + 'place_code': observation.place_code, + 'latitude': observation.latitude, + 'longitude': observation.longitude, + # Add other location fields + } + } + + def _filter_observations(self, request): + """Filter observations based on request parameters""" + observations = TrtObservations.objects.all() + + tag_id = request.GET.get('tag_id') + if tag_id: + observations = observations.filter( + trtrecordedtags__tag_id=tag_id + ).distinct() + + place_code = request.GET.get('place_code') + if place_code: + observations = observations.filter(place_code=place_code) + + date = request.GET.get('date') + if date: + observations = observations.filter( + observation_date__date=datetime.strptime(date, '%Y-%m-%d') + ) + + return observations + + def _get_observation_summary(self, observation): + """Get summary data for observation list""" + return { + 'observation_id': observation.observation_id, + 'turtle_id': observation.turtle_id, + 'observation_date': observation.observation_date.strftime('%Y-%m-%d'), + 'place_code': observation.place_code, + 'status': observation.status + } + + # Helper methods for updating related records + def _update_tags(self, observation, tag_data): + """Update tag records""" + observation.trtrecordedtags_set.all().delete() + observation.trtrecordedpittags_set.all().delete() + + for tag in tag_data.get('recorded_tags', []): + TrtRecordedTags.objects.create( + observation=observation, + **tag + ) + + for pit_tag in tag_data.get('recorded_pit_tags', []): + TrtRecordedPitTags.objects.create( + observation=observation, + **pit_tag + ) + + def _update_measurements(self, observation, measurements): + """Update measurement records""" + observation.trtmeasurements_set.all().delete() + for measurement in measurements: + TrtMeasurements.objects.create( + observation=observation, + **measurement + ) + + def _update_damage_records(self, observation, damage_records): + """Update damage records""" + observation.trtdamage_set.all().delete() + for damage in damage_records: + TrtDamage.objects.create( + observation=observation, + **damage + ) + + def _update_location(self, observation, location_data): + """Update location information""" + for field, value in location_data.items(): + if hasattr(observation, field): + setattr(observation, field, value) + observation.save() + + \ No newline at end of file From 31abe8f52157b5569635937f87184fa661107924 Mon Sep 17 00:00:00 2001 From: Rick Wang Date: Tue, 26 Nov 2024 14:16:11 +0800 Subject: [PATCH 08/52] Fix icon display in admin tools page --- wamtram2/templates/wamtram2/admin_tools.html | 98 ++++++++++---------- wamtram2/views.py | 2 +- 2 files changed, 51 insertions(+), 49 deletions(-) diff --git a/wamtram2/templates/wamtram2/admin_tools.html b/wamtram2/templates/wamtram2/admin_tools.html index f4f6fd4bd..8b263c851 100644 --- a/wamtram2/templates/wamtram2/admin_tools.html +++ b/wamtram2/templates/wamtram2/admin_tools.html @@ -1,52 +1,54 @@ {% extends "base_wastd.html" %} -{% load static %} +{% load static bootstrap4 %} {% block extra_style %} - +{{ block.super }} + {{ form.media.css }} + {% endblock %} {% block page_content_inner %} @@ -77,7 +79,7 @@

Tag Registration

-

PIT Tags List

+

PIT Tags Management

View and manage all PIT tags in the system.

+ + + `); + }); } // Handle save operation diff --git a/wamtram2/templates/wamtram2/observation_management.html b/wamtram2/templates/wamtram2/observation_management.html index 5586ebd07..bbef1b005 100644 --- a/wamtram2/templates/wamtram2/observation_management.html +++ b/wamtram2/templates/wamtram2/observation_management.html @@ -41,11 +41,8 @@

Observation Management

- +
diff --git a/wamtram2/views.py b/wamtram2/views.py index ccd5ef1f5..c316a23d6 100644 --- a/wamtram2/views.py +++ b/wamtram2/views.py @@ -1686,6 +1686,7 @@ def search_places(request): return JsonResponse(list(places), safe=False) + class ExportDataView(LoginRequiredMixin, View): template_name = 'wamtram2/export_form.html' @@ -3832,54 +3833,103 @@ def _get_observation_data(self, observation): 'observation_id': observation.observation_id, 'turtle_id': observation.turtle_id, 'observation_date': observation.observation_date.strftime('%Y-%m-%d'), + 'observation_time': observation.observation_time.strftime('%H:%M') if observation.observation_time else '', 'alive': observation.alive, 'nesting': observation.nesting, - # Add other basic fields + 'activity_code': observation.activity_code, + 'beach_position_code': observation.beach_position_code, + 'status': observation.status }, 'tag_info': { - 'recorded_tags': list(observation.trtrecordedtags_set.values()), - 'recorded_pit_tags': list(observation.trtrecordedpittags_set.values()), + 'recorded_tags': [{ + 'tag_id': tag.tag_id, + 'tag_type': tag.tag_type, + 'tag_position': tag.tag_position, + 'tag_state': tag.tag_state + } for tag in observation.trtrecordedtags_set.all()], + 'recorded_pit_tags': [{ + 'tag_id': tag.tag_id, + 'tag_position': tag.tag_position, + 'tag_state': tag.tag_state + } for tag in observation.trtrecordedpittags_set.all()] }, - 'measurements': list(observation.trtmeasurements_set.values()), - 'damage_records': list(observation.trtdamage_set.values()), + 'measurements': [{ + 'measurement_type': m.measurement_type, + 'measurement_value': m.measurement_value + } for m in observation.trtmeasurements_set.all()], + 'damage_records': [{ + 'body_part': d.body_part, + 'damage_code': d.damage_code + } for d in observation.trtdamage_set.all()], 'location': { 'place_code': observation.place_code, - 'latitude': observation.latitude, - 'longitude': observation.longitude, - # Add other location fields + 'datum_code': observation.datum_code, + 'latitude_degrees': observation.latitude_degrees, + 'latitude_minutes': observation.latitude_minutes, + 'latitude_seconds': observation.latitude_seconds, + 'longitude_degrees': observation.longitude_degrees, + 'longitude_minutes': observation.longitude_minutes, + 'longitude_seconds': observation.longitude_seconds } } - def _filter_observations(self, request): """Filter observations based on request parameters""" observations = TrtObservations.objects.all() - tag_id = request.GET.get('tag_id') - if tag_id: - observations = observations.filter( - trtrecordedtags__tag_id=tag_id - ).distinct() + search_term = request.GET.get('search') + if search_term: + tag_parts = search_term.split() + q_objects = Q() + for part in tag_parts: + q_objects |= ( + Q(trtrecordedtags__tag_id__icontains=part) | + Q(trtrecordedpittags__tag_id__icontains=part) | + Q(turtle_id__icontains=part) + ) + observations = observations.filter(q_objects) - place_code = request.GET.get('place_code') + place_code = request.GET.get('place') if place_code: observations = observations.filter(place_code=place_code) date = request.GET.get('date') if date: - observations = observations.filter( - observation_date__date=datetime.strptime(date, '%Y-%m-%d') - ) + try: + date_obj = datetime.strptime(date, '%Y-%m-%d') + observations = observations.filter(observation_date__date=date_obj) + except ValueError: + pass - return observations - + status = request.GET.get('status') + if status: + if status == 'Initial Nesting': + observations = observations.filter(nesting='Y', status='I') + elif status == 'Subsequent Nesting': + observations = observations.filter(nesting='Y', status='S') + elif status == 'Non-nesting': + observations = observations.filter(nesting='N') + + observations = observations.select_related('place').prefetch_related('trtrecordedtags_set', 'trtrecordedpittags_set').distinct().order_by('-observation_date', '-observation_time') + + + return observations.order_by('-observation_date') def _get_observation_summary(self, observation): """Get summary data for observation list""" + tags = list(observation.trtrecordedtags_set.values_list('tag_id', flat=True)) + pit_tags = list(observation.trtrecordedpittags_set.values_list('tag_id', flat=True)) return { 'observation_id': observation.observation_id, 'turtle_id': observation.turtle_id, 'observation_date': observation.observation_date.strftime('%Y-%m-%d'), + 'observation_time': observation.observation_time.strftime('%H:%M') if observation.observation_time else '', 'place_code': observation.place_code, - 'status': observation.status + 'place_description': observation.place.description if observation.place else '', + 'status': observation.status, + 'nesting': observation.nesting, + 'tags': tags, + 'pit_tags': pit_tags, + 'total_tags': len(tags), + 'total_pit_tags': len(pit_tags) } # Helper methods for updating related records From 7d418d073dd66b6e94eec88b65e4665c10383f0d Mon Sep 17 00:00:00 2001 From: Xinlyu Wang Date: Wed, 27 Nov 2024 10:29:18 +0800 Subject: [PATCH 10/52] Add curation observation page --- .../wamtram2/observation_management.html | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/wamtram2/templates/wamtram2/observation_management.html b/wamtram2/templates/wamtram2/observation_management.html index bbef1b005..63078f84f 100644 --- a/wamtram2/templates/wamtram2/observation_management.html +++ b/wamtram2/templates/wamtram2/observation_management.html @@ -9,6 +9,22 @@ {% endblock %} +{% block extra_head %} + + {{ form.media }} +{% endblock extra_head %} + +{% block breadcrumbs %} + + +{% endblock %} + {% block page_content_inner %}
@@ -344,6 +360,7 @@
Longitude
{% endblock %} -{% block extra_script %} +{% block extra_js %} +{{ block.super }} {% endblock %} \ No newline at end of file From fecc5faaa9a83235899a941811396f103c776b78 Mon Sep 17 00:00:00 2001 From: Xinlyu Wang Date: Wed, 27 Nov 2024 11:04:57 +0800 Subject: [PATCH 11/52] Fix curation observation page --- .../static/css/observation_management.css | 62 ++++++ wamtram2/static/js/observation_management.js | 191 +++++++++++++++++- .../wamtram2/observation_management.html | 23 ++- 3 files changed, 256 insertions(+), 20 deletions(-) create mode 100644 wamtram2/static/css/observation_management.css diff --git a/wamtram2/static/css/observation_management.css b/wamtram2/static/css/observation_management.css new file mode 100644 index 000000000..d8fe279f2 --- /dev/null +++ b/wamtram2/static/css/observation_management.css @@ -0,0 +1,62 @@ +.container-fluid { + padding: 20px; +} + + +.section-card { + background-color: #fff; + border: 1px solid #dee2e6; + border-radius: 5px; + padding: 20px; + margin-bottom: 20px; + box-shadow: 0 0 10px rgba(0,0,0,0.1); +} + +.nav-tabs { + margin-bottom: 20px; +} + +.tab-content { + padding: 20px 0; +} + +.form-row { + margin-bottom: 15px; +} + +#searchResults { + margin-top: 20px; +} + +#searchResults th { + background-color: #f8f9fa; + border-bottom: 2px solid #dee2e6; +} + +#searchResults td { + vertical-align: middle; +} + +.modified { + background-color: #fff3cd; +} + +.readonly-field { + background-color: #e9ecef; + cursor: not-allowed; +} + + +@media (max-width: 768px) { + .container-fluid { + padding: 10px; + } + + .section-card { + padding: 15px; + } + + .form-row { + margin-bottom: 10px; + } +} \ No newline at end of file diff --git a/wamtram2/static/js/observation_management.js b/wamtram2/static/js/observation_management.js index 04fb67141..6fd2e07bc 100644 --- a/wamtram2/static/js/observation_management.js +++ b/wamtram2/static/js/observation_management.js @@ -9,7 +9,7 @@ $(document).ready(function() { initializeFormValidation(); initializeDropdowns(); bindEventHandlers(); - setupAutoSave(); + // setupAutoSave(); } // Form validation @@ -23,7 +23,7 @@ $(document).ready(function() { // Initialize select2 dropdowns function initializeDropdowns() { - $('.select2-dropdown').select2({ + $('select').select2({ theme: 'bootstrap4', width: '100%' }); @@ -84,10 +84,6 @@ $(document).ready(function() { function bindEventHandlers() { // Save button click $('#saveChanges').click(handleSave); - - // New record button click - $('#addNew').click(handleNewRecord); - // Filter changes $('#tagSearch, #placeFilter, #dateFilter, #statusFilter').on('change', handleFilter); @@ -115,6 +111,134 @@ $(document).ready(function() { }); $('#placeFilter, #dateFilter, #statusFilter').change(handleSearch); + + $(document).on('click', '.remove-tag', function() { + $(this).closest('.tag-row').remove(); + }); + + $(document).on('click', '.remove-measurement', function() { + $(this).closest('.measurement-row').remove(); + }); + + $(document).on('click', '.remove-damage', function() { + $(this).closest('.damage-record').remove(); + }); + } + + function clearForm() { + $('#basicInfo input, #basicInfo select').val(''); + + $('#tagInfo .tag-row').remove(); + + $('#measurements .measurement-row').remove(); + + $('#damage .damage-record').remove(); + + $('#location input, #location select').val(''); + + $('.modified').removeClass('modified'); + currentObservationId = null; + } + + function updateOriginalData(data) { + originalData = JSON.parse(JSON.stringify(data)); + } + + function validateForm() { + let isValid = true; + let firstInvalidField = null; + + formFields.forEach(fieldName => { + const field = $(`[name="${fieldName}"]`); + if (!field.val()) { + field.addClass('is-invalid'); + if (!firstInvalidField) { + firstInvalidField = field; + } + isValid = false; + } else { + field.removeClass('is-invalid'); + } + }); + if (firstInvalidField) { + firstInvalidField.focus(); + $('html, body').animate({ + scrollTop: firstInvalidField.offset().top - 100 + }, 500); + } + + return isValid; + } + + function addTagRow(tagData = {}) { + const tagRow = $(` +
+
+ +
+
+ +
+
+ +
+
+ `); + $('#tagInfo .tag-container').append(tagRow); + } + + function addMeasurementRow(measurementData = {}) { + const measurementRow = $(` +
+
+ +
+
+ +
+
+ +
+
+ `); + $('#measurements .measurement-container').append(measurementRow); + } + + function addDamageRecord(damageData = {}) { + const damageRecord = $(` +
+
+ +
+
+ +
+
+ +
+
+ `); + $('#damage .damage-container').append(damageRecord); } async function handleSearch() { @@ -369,13 +493,60 @@ $(document).ready(function() { } function showSuccessMessage(message) { - // Implement your preferred notification method - alert(message); + const alertHtml = ` + + `; + $('.container-fluid').prepend(alertHtml); } function showErrorMessage(message) { - // Implement your preferred notification method - alert(message); + const alertHtml = ` + + `; + $('.container-fluid').prepend(alertHtml); + } + + // function setupAutoSave() { + // let autoSaveTimer; + // let autoSaveNotification; + + // $('form').on('change', 'input, select', function() { + // clearTimeout(autoSaveTimer); + + // if (!autoSaveNotification) { + // autoSaveNotification = $('
Changes will be auto-saved...
'); + // $('body').append(autoSaveNotification); + // } + + // autoSaveTimer = setTimeout(() => { + // handleSave().then(() => { + // if (autoSaveNotification) { + // autoSaveNotification.remove(); + // autoSaveNotification = null; + // } + // }); + // }, 30000); + // }); + // } + + $(window).on('beforeunload', function() { + if (hasUnsavedChanges()) { + return "You have unsaved changes. Are you sure you want to leave?"; + } + }); + + function hasUnsavedChanges() { + return $('.modified').length > 0; } // Initialize the page diff --git a/wamtram2/templates/wamtram2/observation_management.html b/wamtram2/templates/wamtram2/observation_management.html index 63078f84f..3bbef6f8f 100644 --- a/wamtram2/templates/wamtram2/observation_management.html +++ b/wamtram2/templates/wamtram2/observation_management.html @@ -4,16 +4,14 @@ {% block extra_style %} {{ block.super }} {{ form.media.css }} + + + {% endblock %} -{% block extra_head %} - - {{ form.media }} -{% endblock extra_head %} - {% block breadcrumbs %}
@@ -352,15 +350,20 @@
Longitude
- -
+ +
+
+
+
+
+ + + + + + + + + + + + + + +
Observation IDTurtle IDObservation Date/TimePlaceStatusAction
+
+
+
+
+
+ + + +
From 05e9713625694b1f33aa9d031ac35dcd7f857215 Mon Sep 17 00:00:00 2001 From: Rick Wang Date: Wed, 27 Nov 2024 14:57:56 +0800 Subject: [PATCH 16/52] Fix curation observation page --- wamtram2/models.py | 2 +- .../wamtram2/observation_management.html | 1 + wamtram2/views.py | 62 +++++++++++-------- 3 files changed, 38 insertions(+), 27 deletions(-) diff --git a/wamtram2/models.py b/wamtram2/models.py index d54316cad..922c0ed01 100644 --- a/wamtram2/models.py +++ b/wamtram2/models.py @@ -2205,7 +2205,7 @@ class TrtSpecies(models.Model): common_name = models.CharField( db_column="COMMON_NAME", max_length=50, blank=True, null=True ) # Field name made lowercase. - old_species_code = models.CharField( + old_species_code = models.observation_status( db_column="OLD_SPECIES_CODE", max_length=2, blank=True, null=True ) # Field name made lowercase. hide_dataentry = models.BooleanField( diff --git a/wamtram2/templates/wamtram2/observation_management.html b/wamtram2/templates/wamtram2/observation_management.html index da588112a..6d54b2be9 100644 --- a/wamtram2/templates/wamtram2/observation_management.html +++ b/wamtram2/templates/wamtram2/observation_management.html @@ -400,6 +400,7 @@
Longitude
{% block extra_js %} {{ block.super }} + {% endblock %} \ No newline at end of file diff --git a/wamtram2/views.py b/wamtram2/views.py index 89fe52e6c..f20a64493 100644 --- a/wamtram2/views.py +++ b/wamtram2/views.py @@ -3898,48 +3898,58 @@ def get(self, request, observation_id=None): def _get_observation_data(self, observation): """Get full observation data""" + try: + damage = observation.trtdamage + damage_data = { + 'body_part': damage.body_part.body_part if damage.body_part else None, + 'damage_code': damage.damage_code.damage_code if damage.damage_code else None, + 'damage_cause_code': damage.damage_cause_code.damage_cause_code if damage.damage_cause_code else None, + 'comments': damage.comments + } + except TrtDamage.DoesNotExist: + damage_data = None + return { 'basic_info': { 'observation_id': observation.observation_id, 'turtle_id': observation.turtle_id, 'observation_date': observation.observation_date.strftime('%Y-%m-%d'), 'observation_time': observation.observation_time.strftime('%H:%M') if observation.observation_time else '', - 'alive': observation.alive, - 'nesting': observation.nesting, - 'activity_code': observation.activity_code, - 'beach_position_code': observation.beach_position_code, - 'status': observation.status + 'alive': str(observation.alive), + 'nesting': str(observation.nesting), + 'activity_code': str(observation.activity_code), + 'beach_position_code': str(observation.beach_position_code), + 'status': str(observation.observation_status) }, 'tag_info': { 'recorded_tags': [{ - 'tag_id': tag.tag_id, - 'tag_type': tag.tag_type, - 'tag_position': tag.tag_position, - 'tag_state': tag.tag_state + 'tag_id': str(tag.tag_id), + 'tag_side': str(tag.side), + 'tag_position': str(tag.tag_position), + 'tag_state': str(tag.tag_state) } for tag in observation.trtrecordedtags_set.all()], 'recorded_pit_tags': [{ - 'tag_id': tag.tag_id, - 'tag_position': tag.tag_position, - 'tag_state': tag.tag_state + 'tag_id': str(tag.pittag_id), + 'tag_position': str(tag.pit_tag_position), + 'tag_state': str(tag.pit_tag_state) } for tag in observation.trtrecordedpittags_set.all()] }, 'measurements': [{ - 'measurement_type': m.measurement_type, - 'measurement_value': m.measurement_value + 'measurement_type': str(m.measurement_type), + 'measurement_value': str(m.measurement_value) } for m in observation.trtmeasurements_set.all()], - 'damage_records': [{ - 'body_part': d.body_part, - 'damage_code': d.damage_code - } for d in observation.trtdamage_set.all()], + + 'damage_records': [damage_data] if damage_data else [], + 'location': { - 'place_code': observation.place_code, - 'datum_code': observation.datum_code, - 'latitude_degrees': observation.latitude_degrees, - 'latitude_minutes': observation.latitude_minutes, - 'latitude_seconds': observation.latitude_seconds, - 'longitude_degrees': observation.longitude_degrees, - 'longitude_minutes': observation.longitude_minutes, - 'longitude_seconds': observation.longitude_seconds + 'place_code': str(observation.place_code), + 'datum_code': str(observation.datum_code), + 'latitude_degrees': str(observation.latitude_degrees), + 'latitude_minutes': str(observation.latitude_minutes), + 'latitude_seconds': str(observation.latitude_seconds), + 'longitude_degrees': str(observation.longitude_degrees), + 'longitude_minutes': str(observation.longitude_minutes), + 'longitude_seconds': str(observation.longitude_seconds) } } From 9a994a9d6b9896d655e7be78ca14da65d9ed52bd Mon Sep 17 00:00:00 2001 From: Xinlyu Wang Date: Wed, 27 Nov 2024 15:52:50 +0800 Subject: [PATCH 17/52] Fix curation observation page --- wamtram2/static/js/observation_management.js | 203 +++++++++++------- .../wamtram2/observation_management.html | 201 ++++++++++------- wamtram2/views.py | 11 +- 3 files changed, 252 insertions(+), 163 deletions(-) diff --git a/wamtram2/static/js/observation_management.js b/wamtram2/static/js/observation_management.js index 9d048d1e9..e335212db 100644 --- a/wamtram2/static/js/observation_management.js +++ b/wamtram2/static/js/observation_management.js @@ -99,6 +99,26 @@ $(document).ready(function() { // Add measurement button $('#addMeasurement').click(addMeasurementRow); + $('#addTag').click(function() { + const newRow = $('.tag-row-template .tag-row').clone(); + newRow.show(); + $('.tag-container').append(newRow); + newRow.find('select').select2({ + theme: 'bootstrap4', + width: '100%' + }); + }); + + $('#addPitTag').click(function() { + const newRow = $('.pit-tag-row-template .pit-tag-row').clone(); + newRow.show(); + $('.pit-tag-container').append(newRow); + newRow.find('select').select2({ + theme: 'bootstrap4', + width: '100%' + }); + }); + // // Coordinate conversion // $('.coordinate-input').on('change', convertCoordinates); @@ -176,74 +196,55 @@ $(document).ready(function() { } function addTagRow(tagData = {}) { - const tagRow = $(` -
-
- -
-
- -
-
- -
-
- `); - $('#tagInfo .tag-container').append(tagRow); + + const newRow = $('#tagRowTemplate .tag-row').clone(); + + if (tagData) { + newRow.find('[name="tag_id"]').val(tagData.tag_id || ''); + newRow.find('[name="side"]').val(tagData.side || ''); + newRow.find('[name="tag_position"]').val(tagData.tag_position || ''); + newRow.find('[name="tag_state"]').val(tagData.tag_state || ''); + newRow.find('[name="barnacles"]').prop('checked', tagData.barnacles === 'True'); + } + + $('.tag-container').append(newRow); + + newRow.find('select').select2({ + theme: 'bootstrap4', + width: '100%' + }); } function addMeasurementRow(measurementData = {}) { - const measurementRow = $(` -
-
- -
-
- -
-
- -
-
- `); - $('#measurements .measurement-container').append(measurementRow); + const newRow = $('#measurementRowTemplate .measurement-row').clone(); + + if (measurementData) { + newRow.find('[name="measurement_type"]').val(measurementData.measurement_type || ''); + newRow.find('[name="measurement_value"]').val(measurementData.measurement_value || ''); + } + + $('.measurement-container').append(newRow); + + newRow.find('select').select2({ + theme: 'bootstrap4', + width: '100%' + }); } function addDamageRecord(damageData = {}) { - const damageRecord = $(` -
-
- -
-
- -
-
- -
-
- `); - $('#damage .damage-container').append(damageRecord); + const newRow = $('#damageRowTemplate .damage-record').clone(); + + if (damageData) { + newRow.find('[name="body_part"]').val(damageData.body_part || ''); + newRow.find('[name="damage_code"]').val(damageData.damage_code || ''); + } + + $('.damage-container').append(newRow); + + newRow.find('select').select2({ + theme: 'bootstrap4', + width: '100%' + }); } async function handleSearch() { @@ -450,28 +451,80 @@ $(document).ready(function() { // Populate basic information for (const [key, value] of Object.entries(data.basic_info)) { - $(`[name="${key}"]`).val(value); + const field = $(`[name="${key}"]`); + if (field.length) { + field.val(value).trigger('change'); + } } // Populate tags - data.tag_info.recorded_tags.forEach(tag => { - addTagRow(tag); - }); + if (data.tag_info) { + // Clear existing tag rows + $('.tag-container .tag-row:not(.tag-row-template)').remove(); + $('.pit-tag-container .pit-tag-row:not(.pit-tag-row-template)').remove(); + + // Add recorded tags + data.tag_info.recorded_tags.forEach(tag => { + const newRow = $('.tag-row-template .tag-row').clone(); + newRow.show(); + newRow.find('[name="tag_id"]').val(tag.tag_id); + newRow.find('[name="side"]').val(tag.side); ; + newRow.find('[name="tag_position"]').val(tag.tag_position); + newRow.find('[name="tag_state"]').val(tag.tag_state); + newRow.find('[name="barnacles"]').prop('checked', tag.barnacles === 'True'); + $('.tag-container').append(newRow); + newRow.find('select').select2({ + theme: 'bootstrap4', + width: '100%' + }); + }); + + // Add recorded PIT tags + data.tag_info.recorded_pit_tags.forEach(tag => { + const newRow = $('.pit-tag-row-template .pit-tag-row').clone(); + newRow.show(); + newRow.find('[name="pittag_id"]').val(tag.tag_id); + newRow.find('[name="pit_tag_position"]').val(tag.tag_position); + newRow.find('[name="tag_state"]').val(tag.tag_state); + $('.pit-tag-container').append(newRow); + newRow.find('select').select2({ + theme: 'bootstrap4', + width: '100%' + }); + }); + } // Populate measurements - data.measurements.forEach(measurement => { - addMeasurementRow(measurement); - }); + if (data.measurements) { + $('#measurements .measurement-row').remove(); + data.measurements.forEach(measurement => { + addMeasurementRow(measurement); + }); + } // Populate damage records - data.damage_records.forEach(damage => { - addDamageRecord(damage); - }); + if (data.damage_records) { + $('#damage .damage-record').remove(); + data.damage_records.forEach(damage => { + addDamageRecord(damage); + }); + } - // Populate location - for (const [key, value] of Object.entries(data.location)) { - $(`[name="${key}"]`).val(value); + // Populate location information + if (data.location) { + for (const [key, value] of Object.entries(data.location)) { + const field = $(`[name="${key}"]`); + if (field.length) { + field.val(value).trigger('change'); + } + } } + + // 重新初始化所有 select2 下拉框 + $('select').trigger('change').select2({ + theme: 'bootstrap4', + width: '100%' + }); } // Utility functions diff --git a/wamtram2/templates/wamtram2/observation_management.html b/wamtram2/templates/wamtram2/observation_management.html index 6d54b2be9..465ca44bc 100644 --- a/wamtram2/templates/wamtram2/observation_management.html +++ b/wamtram2/templates/wamtram2/observation_management.html @@ -209,127 +209,162 @@

Observation Management

Flipper Tags

-
-
-
Left Flipper
-
- - -
-
- - -
-
-
-
Right Flipper
-
- - -
-
- - + +
+ + +
+ +

PIT Tags

-
-
-
- - -
-
-
-
- - + +
+ + +
+ +
-
-
-
- - + +
-
- -
-
- -
-
- -
@@ -331,7 +312,7 @@

PIT Tags

Add Measurement
- +
diff --git a/wamtram2/templates/wamtram2/transfer_observation.html b/wamtram2/templates/wamtram2/transfer_observation.html index 6f79f629d..f156de47e 100644 --- a/wamtram2/templates/wamtram2/transfer_observation.html +++ b/wamtram2/templates/wamtram2/transfer_observation.html @@ -23,7 +23,7 @@ - + diff --git a/wamtram2/templates/wamtram2/turtle_management.html b/wamtram2/templates/wamtram2/turtle_management.html new file mode 100644 index 000000000..063f237e6 --- /dev/null +++ b/wamtram2/templates/wamtram2/turtle_management.html @@ -0,0 +1,424 @@ +{% extends "base_wastd.html" %} +{% load static bootstrap4 %} + +{% block extra_style %} + {{ block.super }} + {{ form.media.css }} + + + + + + +{% endblock %} + +{% block breadcrumbs %} + + +{% endblock %} + +{% block page_content_inner %} +
+ +
+
+
+

Turtle Management

+
+
+
+ + +
+
+
+ +
+ +
+
+
+
+
+ +
+ +
+
+
+
+
+ +
+ +
+
+
+
+
+ +
+ +
+
+
+
+ + +
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + +
Turtle IDSpeciesTurtle NameSexCause of DeathTurtle StatusDate EnteredComments
+
+
+
+
+
+ + + + + +
+
+ + + +
+ +
+
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+ +
+
+
+ + +
+
+
+
+ + +
+
+
+
+
+ + +
+
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+ +
+
+
+ + +
+
+
+
+ + +
+
+
+
+
+ + +
+
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+
+ +
+
+
+ + +
+
+ + + +
+ +
+
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+ +
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+
+ + +
+
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+ +
+
+
+ + +
+
+
+
+ + +
+
+
+
+
+ + +
+
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+
+ +
+
+
+
+ + +
+
+ Loading... +
+
+ + +
+{% endblock %} + +{% block extra_js %} +{{ block.super }} + + + + +{% endblock %} \ No newline at end of file diff --git a/wamtram2/urls.py b/wamtram2/urls.py index 4a600329f..6e27b4da3 100644 --- a/wamtram2/urls.py +++ b/wamtram2/urls.py @@ -57,4 +57,6 @@ path('api/observations/', views.ObservationDataView.as_view(), name='observation_data'), path('api/observations//', views.ObservationDataView.as_view(), name='observation_detail'), path('api/get-places/', views.ObservationDataView.as_view(), name='api_get_places'), + path('curation/turtle-management/', views.TurtleManagementView.as_view(), name='turtle_management'), + path('api/turtle-search/', views.TurtleManagementView.as_view(), name='turtle_search'), ] diff --git a/wamtram2/views.py b/wamtram2/views.py index 5967a29c5..ea9e9d762 100644 --- a/wamtram2/views.py +++ b/wamtram2/views.py @@ -4065,4 +4065,68 @@ def _update_location(self, observation, location_data): setattr(observation, field, value) observation.save() + +class TurtleManagementView(TemplateView): + template_name = 'wamtram2/turtle_management.html' + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context['sex_choices'] = SEX_CHOICES + return context + + def get(self, request, *args, **kwargs): + if request.headers.get('X-Requested-With') == 'XMLHttpRequest': + return self.handle_ajax_request(request) + return super().get(request, *args, **kwargs) + + def handle_ajax_request(self, request): + turtle_id = request.GET.get('turtle_id') + tag_id = request.GET.get('tag_id') + pit_tag_id = request.GET.get('pit_tag_id') + other_id = request.GET.get('other_id') + + queryset = None + + if turtle_id: + queryset = TrtTurtles.objects.filter(turtle_id=turtle_id) + elif tag_id: + turtle_ids = TrtTags.objects.filter( + tag_id__icontains=tag_id + ).values_list('turtle_id', flat=True) + queryset = TrtTurtles.objects.filter(turtle_id__in=turtle_ids) + elif pit_tag_id: + turtle_ids = TrtPitTags.objects.filter( + pittag_id__icontains=pit_tag_id + ).values_list('turtle_id', flat=True) + queryset = TrtTurtles.objects.filter(turtle_id__in=turtle_ids) + elif other_id: + turtle_ids = TrtIdentification.objects.filter( + identifier__icontains=other_id + ).values_list('turtle_id', flat=True) + queryset = TrtTurtles.objects.filter(turtle_id__in=turtle_ids) + + if queryset is None: + return JsonResponse({ + 'status': 'error', + 'message': 'No search criteria provided' + }) + + turtle_data = [] + for turtle in queryset: + turtle_data.append({ + 'turtle_id': turtle.turtle_id, + 'species': str(turtle.species_code), + 'turtle_name': turtle.turtle_name or '', + 'sex': dict(SEX_CHOICES).get(turtle.sex, ''), + 'cause_of_death': str(turtle.cause_of_death) if turtle.cause_of_death else '', + 'turtle_status': str(turtle.turtle_status) if turtle.turtle_status else '', + 'date_entered': turtle.date_entered.strftime('%Y-%m-%d') if turtle.date_entered else '', + 'comments': turtle.comments or '' + }) + + return JsonResponse({ + 'status': 'success', + 'data': turtle_data + }) + \ No newline at end of file From 88acff2a3c4b77f8527e5544c747e2916b3f4a5b Mon Sep 17 00:00:00 2001 From: Xinlyu Wang Date: Thu, 28 Nov 2024 11:43:56 +0800 Subject: [PATCH 21/52] Add curation turtle manage page --- wamtram2/static/css/turtle_management.css | 112 +++++ wamtram2/static/js/turtle_management.js | 73 +++ .../wamtram2/observation_management.html | 21 +- .../wamtram2/transfer_observation.html | 2 +- .../templates/wamtram2/turtle_management.html | 424 ++++++++++++++++++ wamtram2/urls.py | 2 + wamtram2/views.py | 64 +++ 7 files changed, 677 insertions(+), 21 deletions(-) create mode 100644 wamtram2/static/css/turtle_management.css create mode 100644 wamtram2/static/js/turtle_management.js create mode 100644 wamtram2/templates/wamtram2/turtle_management.html diff --git a/wamtram2/static/css/turtle_management.css b/wamtram2/static/css/turtle_management.css new file mode 100644 index 000000000..10125c4c1 --- /dev/null +++ b/wamtram2/static/css/turtle_management.css @@ -0,0 +1,112 @@ + +.table-container { + overflow-y: auto; + position: relative; + height: 40vh; + margin: 1rem 0; + border: 1px solid #dee2e6; + border-radius: 0.25rem; +} + + +.table { + width: 100%; + margin-bottom: 0; + color: #212529; +} + + +.table thead th { + position: sticky; + top: 0; + background: #f8f9fa; + z-index: 1; + font-weight: 500; + border-bottom: 2px solid #dee2e6; +} + + +.table tbody tr:hover { + background-color: rgba(0,0,0,.075); + cursor: pointer; +} + + +.section-card { + background-color: #fff; + border: 1px solid #dee2e6; + border-radius: 5px; + padding: 20px; + margin-bottom: 20px; + box-shadow: 0 0 10px rgba(0,0,0,0.1); +} + + +.nav-tabs { + margin-bottom: 20px; + border-bottom: 1px solid #dee2e6; +} + +.nav-tabs .nav-link { + color: #495057; + border: 1px solid transparent; + border-top-left-radius: .25rem; + border-top-right-radius: .25rem; +} + +.nav-tabs .nav-link.active { + color: #495057; + background-color: #fff; + border-color: #dee2e6 #dee2e6 #fff; +} + + +.form-row { + margin-bottom: 15px; +} + +.form-control.readonly-field { + background-color: #e9ecef; + cursor: not-allowed; +} + + +.modified { + background-color: #fff3cd; +} + +@media (max-width: 768px) { + .container-fluid { + padding: 10px; + } + + .section-card { + padding: 15px; + } + + .table { + font-size: 0.875rem; + } + + .table th, + .table td { + padding: 0.5rem; + } +} + +.search-area { + margin-bottom: 1.5rem; +} + +.search-area .input-group { + margin-bottom: 0.5rem; +} + + +.tag-info-area { + margin-top: 1.5rem; +} + +.observation-area { + margin-top: 1.5rem; +} \ No newline at end of file diff --git a/wamtram2/static/js/turtle_management.js b/wamtram2/static/js/turtle_management.js new file mode 100644 index 000000000..a46434051 --- /dev/null +++ b/wamtram2/static/js/turtle_management.js @@ -0,0 +1,73 @@ +document.addEventListener('DOMContentLoaded', function() { + + const searchButtons = document.querySelectorAll('[id$="SearchBtn"]'); + const turtleTable = document.getElementById('turtleInfoTable'); + const tableBody = turtleTable.querySelector('tbody'); + const noResultsDiv = document.getElementById('noResults'); + const loadingSpinner = document.querySelector('.loading-spinner'); + const loadingOverlay = document.querySelector('.loading-overlay'); + + + async function handleSearch(searchType, searchValue) { + try { + + loadingSpinner.style.display = 'block'; + loadingOverlay.style.display = 'block'; + + tableBody.innerHTML = ''; + noResultsDiv.style.display = 'none'; + + const params = new URLSearchParams(); + params.append(searchType, searchValue); + + const response = await fetch(`/api/turtle-search/?${params.toString()}`, { + headers: { + 'X-Requested-With': 'XMLHttpRequest' + } + }); + + const data = await response.json(); + + if (data.status === 'success' && data.data.length > 0) { + data.data.forEach(turtle => { + const row = document.createElement('tr'); + row.innerHTML = ` + ${turtle.turtle_id} + ${turtle.species} + ${turtle.turtle_name} + ${turtle.sex} + ${turtle.cause_of_death} + ${turtle.turtle_status} + ${turtle.date_entered} + ${turtle.comments} + `; + tableBody.appendChild(row); + }); + } else { + noResultsDiv.style.display = 'block'; + } + } catch (error) { + const alertDiv = document.createElement('div'); + alertDiv.className = 'alert alert-danger'; + alertDiv.textContent = 'An error occurred while searching. Please try again.'; + turtleTable.parentNode.insertBefore(alertDiv, turtleTable); + + setTimeout(() => alertDiv.remove(), 3000); + } finally { + loadingSpinner.style.display = 'none'; + loadingOverlay.style.display = 'none'; + } + } + + searchButtons.forEach(button => { + button.addEventListener('click', function() { + const searchInput = this.parentElement.previousElementSibling; + const searchType = searchInput.id.replace('Search', '').toLowerCase(); + const searchValue = searchInput.value.trim(); + + if (searchValue) { + handleSearch(searchType, searchValue); + } + }); + }); +}); \ No newline at end of file diff --git a/wamtram2/templates/wamtram2/observation_management.html b/wamtram2/templates/wamtram2/observation_management.html index 465ca44bc..5cae0d48a 100644 --- a/wamtram2/templates/wamtram2/observation_management.html +++ b/wamtram2/templates/wamtram2/observation_management.html @@ -34,9 +34,6 @@

Observation Management

- {% comment %} {% endcomment %}
@@ -54,22 +51,6 @@

Observation Management

-
- -
-
- -
-
- -
@@ -331,7 +312,7 @@

PIT Tags

Add Measurement
- +
diff --git a/wamtram2/templates/wamtram2/transfer_observation.html b/wamtram2/templates/wamtram2/transfer_observation.html index 6f79f629d..f156de47e 100644 --- a/wamtram2/templates/wamtram2/transfer_observation.html +++ b/wamtram2/templates/wamtram2/transfer_observation.html @@ -23,7 +23,7 @@ - + diff --git a/wamtram2/templates/wamtram2/turtle_management.html b/wamtram2/templates/wamtram2/turtle_management.html new file mode 100644 index 000000000..063f237e6 --- /dev/null +++ b/wamtram2/templates/wamtram2/turtle_management.html @@ -0,0 +1,424 @@ +{% extends "base_wastd.html" %} +{% load static bootstrap4 %} + +{% block extra_style %} + {{ block.super }} + {{ form.media.css }} + + + + + + +{% endblock %} + +{% block breadcrumbs %} + + +{% endblock %} + +{% block page_content_inner %} +
+ +
+
+
+

Turtle Management

+
+
+
+ + +
+
+
+ +
+ +
+
+
+
+
+ +
+ +
+
+
+
+
+ +
+ +
+
+
+
+
+ +
+ +
+
+
+
+ + +
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + +
Turtle IDSpeciesTurtle NameSexCause of DeathTurtle StatusDate EnteredComments
+
+
+
+
+
+ + + + + +
+
+ + + +
+ +
+
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+ +
+
+
+ + +
+
+
+
+ + +
+
+
+
+
+ + +
+
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+ +
+
+
+ + +
+
+
+
+ + +
+
+
+
+
+ + +
+
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+
+ +
+
+
+ + +
+
+ + + +
+ +
+
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+ +
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+
+ + +
+
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+ +
+
+
+ + +
+
+
+
+ + +
+
+
+
+
+ + +
+
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+
+ +
+
+
+
+ + +
+
+ Loading... +
+
+ + +
+{% endblock %} + +{% block extra_js %} +{{ block.super }} + + + + +{% endblock %} \ No newline at end of file diff --git a/wamtram2/urls.py b/wamtram2/urls.py index 4a600329f..6e27b4da3 100644 --- a/wamtram2/urls.py +++ b/wamtram2/urls.py @@ -57,4 +57,6 @@ path('api/observations/', views.ObservationDataView.as_view(), name='observation_data'), path('api/observations//', views.ObservationDataView.as_view(), name='observation_detail'), path('api/get-places/', views.ObservationDataView.as_view(), name='api_get_places'), + path('curation/turtle-management/', views.TurtleManagementView.as_view(), name='turtle_management'), + path('api/turtle-search/', views.TurtleManagementView.as_view(), name='turtle_search'), ] diff --git a/wamtram2/views.py b/wamtram2/views.py index ab33d195f..3d4c7862d 100644 --- a/wamtram2/views.py +++ b/wamtram2/views.py @@ -4065,4 +4065,68 @@ def _update_location(self, observation, location_data): setattr(observation, field, value) observation.save() + +class TurtleManagementView(TemplateView): + template_name = 'wamtram2/turtle_management.html' + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context['sex_choices'] = SEX_CHOICES + return context + + def get(self, request, *args, **kwargs): + if request.headers.get('X-Requested-With') == 'XMLHttpRequest': + return self.handle_ajax_request(request) + return super().get(request, *args, **kwargs) + + def handle_ajax_request(self, request): + turtle_id = request.GET.get('turtle_id') + tag_id = request.GET.get('tag_id') + pit_tag_id = request.GET.get('pit_tag_id') + other_id = request.GET.get('other_id') + + queryset = None + + if turtle_id: + queryset = TrtTurtles.objects.filter(turtle_id=turtle_id) + elif tag_id: + turtle_ids = TrtTags.objects.filter( + tag_id__icontains=tag_id + ).values_list('turtle_id', flat=True) + queryset = TrtTurtles.objects.filter(turtle_id__in=turtle_ids) + elif pit_tag_id: + turtle_ids = TrtPitTags.objects.filter( + pittag_id__icontains=pit_tag_id + ).values_list('turtle_id', flat=True) + queryset = TrtTurtles.objects.filter(turtle_id__in=turtle_ids) + elif other_id: + turtle_ids = TrtIdentification.objects.filter( + identifier__icontains=other_id + ).values_list('turtle_id', flat=True) + queryset = TrtTurtles.objects.filter(turtle_id__in=turtle_ids) + + if queryset is None: + return JsonResponse({ + 'status': 'error', + 'message': 'No search criteria provided' + }) + + turtle_data = [] + for turtle in queryset: + turtle_data.append({ + 'turtle_id': turtle.turtle_id, + 'species': str(turtle.species_code), + 'turtle_name': turtle.turtle_name or '', + 'sex': dict(SEX_CHOICES).get(turtle.sex, ''), + 'cause_of_death': str(turtle.cause_of_death) if turtle.cause_of_death else '', + 'turtle_status': str(turtle.turtle_status) if turtle.turtle_status else '', + 'date_entered': turtle.date_entered.strftime('%Y-%m-%d') if turtle.date_entered else '', + 'comments': turtle.comments or '' + }) + + return JsonResponse({ + 'status': 'success', + 'data': turtle_data + }) + \ No newline at end of file From 88dc9aa0098ebb00b5709fa322fd6046894110f5 Mon Sep 17 00:00:00 2001 From: Xinlyu Wang Date: Thu, 28 Nov 2024 12:05:51 +0800 Subject: [PATCH 22/52] Fix curation turtle manage page --- wamtram2/models.py | 2 +- wamtram2/static/js/turtle_management.js | 36 +++---- .../templates/wamtram2/turtle_management.html | 97 ++++++++++++------- 3 files changed, 84 insertions(+), 51 deletions(-) diff --git a/wamtram2/models.py b/wamtram2/models.py index 922c0ed01..d54316cad 100644 --- a/wamtram2/models.py +++ b/wamtram2/models.py @@ -2205,7 +2205,7 @@ class TrtSpecies(models.Model): common_name = models.CharField( db_column="COMMON_NAME", max_length=50, blank=True, null=True ) # Field name made lowercase. - old_species_code = models.observation_status( + old_species_code = models.CharField( db_column="OLD_SPECIES_CODE", max_length=2, blank=True, null=True ) # Field name made lowercase. hide_dataentry = models.BooleanField( diff --git a/wamtram2/static/js/turtle_management.js b/wamtram2/static/js/turtle_management.js index a46434051..226b79fe0 100644 --- a/wamtram2/static/js/turtle_management.js +++ b/wamtram2/static/js/turtle_management.js @@ -1,8 +1,10 @@ document.addEventListener('DOMContentLoaded', function() { + console.log('DOM Content Loaded'); const searchButtons = document.querySelectorAll('[id$="SearchBtn"]'); - const turtleTable = document.getElementById('turtleInfoTable'); - const tableBody = turtleTable.querySelector('tbody'); + console.log('Found search buttons:', searchButtons.length); + + const searchResultForm = document.getElementById('searchResultForm'); const noResultsDiv = document.getElementById('noResults'); const loadingSpinner = document.querySelector('.loading-spinner'); const loadingOverlay = document.querySelector('.loading-overlay'); @@ -14,7 +16,8 @@ document.addEventListener('DOMContentLoaded', function() { loadingSpinner.style.display = 'block'; loadingOverlay.style.display = 'block'; - tableBody.innerHTML = ''; + const inputs = searchResultForm.querySelectorAll('input'); + inputs.forEach(input => input.value = ''); noResultsDiv.style.display = 'none'; const params = new URLSearchParams(); @@ -29,28 +32,24 @@ document.addEventListener('DOMContentLoaded', function() { const data = await response.json(); if (data.status === 'success' && data.data.length > 0) { - data.data.forEach(turtle => { - const row = document.createElement('tr'); - row.innerHTML = ` - ${turtle.turtle_id} - ${turtle.species} - ${turtle.turtle_name} - ${turtle.sex} - ${turtle.cause_of_death} - ${turtle.turtle_status} - ${turtle.date_entered} - ${turtle.comments} - `; - tableBody.appendChild(row); + const turtle = data.data[0]; + searchResultForm.style.display = 'block'; + + Object.keys(turtle).forEach(key => { + const input = searchResultForm.querySelector(`input[name="${key}"]`); + if (input) { + input.value = turtle[key]; + } }); } else { noResultsDiv.style.display = 'block'; + searchResultForm.style.display = 'none'; } } catch (error) { const alertDiv = document.createElement('div'); alertDiv.className = 'alert alert-danger'; alertDiv.textContent = 'An error occurred while searching. Please try again.'; - turtleTable.parentNode.insertBefore(alertDiv, turtleTable); + searchResultForm.parentNode.insertBefore(alertDiv, searchResultForm); setTimeout(() => alertDiv.remove(), 3000); } finally { @@ -60,8 +59,11 @@ document.addEventListener('DOMContentLoaded', function() { } searchButtons.forEach(button => { + console.log('Adding click listener to button:', button.id); button.addEventListener('click', function() { + console.log('Button clicked:', this.id); const searchInput = this.parentElement.previousElementSibling; + console.log('Search input:', searchInput.id, 'value:', searchInput.value); const searchType = searchInput.id.replace('Search', '').toLowerCase(); const searchValue = searchInput.value.trim(); diff --git a/wamtram2/templates/wamtram2/turtle_management.html b/wamtram2/templates/wamtram2/turtle_management.html index 063f237e6..009f256f7 100644 --- a/wamtram2/templates/wamtram2/turtle_management.html +++ b/wamtram2/templates/wamtram2/turtle_management.html @@ -40,7 +40,7 @@

Turtle Management

-
@@ -50,7 +50,7 @@

Turtle Management

-
@@ -60,7 +60,7 @@

Turtle Management

-
@@ -70,7 +70,7 @@

Turtle Management

-
@@ -78,37 +78,68 @@

Turtle Management

- +
-
- - - - - - - - - - - - - - - - - - - - - - - - -
Turtle IDSpeciesTurtle NameSexCause of DeathTurtle StatusDate EnteredComments
+
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+ +
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+ +
+
+
+ + +
+
+
@@ -124,7 +155,7 @@

Turtle Management

- +
@@ -262,7 +293,7 @@

Turtle Management

- +
From f802825cfd5a061824edcb64288555c660dddfa5 Mon Sep 17 00:00:00 2001 From: Rick Wang Date: Thu, 28 Nov 2024 12:32:20 +0800 Subject: [PATCH 23/52] Fix curation turtle page --- wamtram2/static/js/turtle_management.js | 11 +++++++++-- wamtram2/views.py | 3 +++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/wamtram2/static/js/turtle_management.js b/wamtram2/static/js/turtle_management.js index 226b79fe0..fd98532ee 100644 --- a/wamtram2/static/js/turtle_management.js +++ b/wamtram2/static/js/turtle_management.js @@ -23,7 +23,7 @@ document.addEventListener('DOMContentLoaded', function() { const params = new URLSearchParams(); params.append(searchType, searchValue); - const response = await fetch(`/api/turtle-search/?${params.toString()}`, { + const response = await fetch(`/wamtram2/api/turtle-search/?${params.toString()}`, { headers: { 'X-Requested-With': 'XMLHttpRequest' } @@ -57,6 +57,12 @@ document.addEventListener('DOMContentLoaded', function() { loadingOverlay.style.display = 'none'; } } + const searchTypeMapping = { + 'turtleid': 'turtle_id', + 'flippertag': 'tag_id', + 'pittag': 'pit_tag_id', + 'otheridentification': 'other_id' + }; searchButtons.forEach(button => { console.log('Adding click listener to button:', button.id); @@ -64,7 +70,8 @@ document.addEventListener('DOMContentLoaded', function() { console.log('Button clicked:', this.id); const searchInput = this.parentElement.previousElementSibling; console.log('Search input:', searchInput.id, 'value:', searchInput.value); - const searchType = searchInput.id.replace('Search', '').toLowerCase(); + let searchType = searchInput.id.replace('Search', '').toLowerCase(); + searchType = searchTypeMapping[searchType] || searchType; const searchValue = searchInput.value.trim(); if (searchValue) { diff --git a/wamtram2/views.py b/wamtram2/views.py index ea9e9d762..fc46f1672 100644 --- a/wamtram2/views.py +++ b/wamtram2/views.py @@ -4080,11 +4080,14 @@ def get(self, request, *args, **kwargs): return super().get(request, *args, **kwargs) def handle_ajax_request(self, request): + print("Received AJAX request with parameters:", request.GET) turtle_id = request.GET.get('turtle_id') tag_id = request.GET.get('tag_id') pit_tag_id = request.GET.get('pit_tag_id') other_id = request.GET.get('other_id') + print(f"Searching for turtle_id: {turtle_id}, tag_id: {tag_id}, pit_tag_id: {pit_tag_id}, other_id: {other_id}") + queryset = None if turtle_id: From 66a5f1aaa89e9c13e5074e4783eb5c8f073c46d6 Mon Sep 17 00:00:00 2001 From: Xinlyu Wang Date: Thu, 28 Nov 2024 12:46:46 +0800 Subject: [PATCH 24/52] Enhance Turtle Management UI with Save Confirmations --- wamtram2/static/js/turtle_management.js | 161 +++++++++++++++--- .../templates/wamtram2/turtle_management.html | 79 ++++++++- wamtram2/urls.py | 1 + wamtram2/views.py | 36 +++- 4 files changed, 252 insertions(+), 25 deletions(-) diff --git a/wamtram2/static/js/turtle_management.js b/wamtram2/static/js/turtle_management.js index fd98532ee..c607b76bc 100644 --- a/wamtram2/static/js/turtle_management.js +++ b/wamtram2/static/js/turtle_management.js @@ -2,27 +2,93 @@ document.addEventListener('DOMContentLoaded', function() { console.log('DOM Content Loaded'); const searchButtons = document.querySelectorAll('[id$="SearchBtn"]'); - console.log('Found search buttons:', searchButtons.length); - const searchResultForm = document.getElementById('searchResultForm'); const noResultsDiv = document.getElementById('noResults'); const loadingSpinner = document.querySelector('.loading-spinner'); const loadingOverlay = document.querySelector('.loading-overlay'); + const saveButton = document.getElementById('saveTurtleBtn'); + + console.log('Found search buttons:', searchButtons.length); + + const searchTypeMapping = { + 'turtleid': 'turtle_id', + 'flippertag': 'tag_id', + 'pittag': 'pit_tag_id', + 'otheridentification': 'other_id' + }; + + const saveConfirmationModal = new bootstrap.Modal(document.getElementById('saveConfirmationModal')); + const unsavedChangesModal = new bootstrap.Modal(document.getElementById('unsavedChangesModal')); + let originalFormData = {}; + let hasUnsavedChanges = false; + let pendingSearchData = null; + + function saveOriginalFormData() { + originalFormData = {}; + const formElements = searchResultForm.querySelectorAll('input, select'); + formElements.forEach(element => { + originalFormData[element.name] = element.value; + }); + } + + function getFormChanges() { + const changes = {}; + const currentFormData = {}; + const formElements = searchResultForm.querySelectorAll('input, select'); + + formElements.forEach(element => { + const value = element.value; + if (value !== originalFormData[element.name]) { + const label = element.previousElementSibling?.textContent || element.name; + changes[label] = { + old: originalFormData[element.name] || '(empty)', + new: value || '(empty)' + }; + } + }); + + return changes; + } + + function showSaveConfirmation(changes) { + const changesContent = document.getElementById('changesContent'); + if (Object.keys(changes).length === 0) { + changesContent.innerHTML = '

No changes detected.

'; + return false; + } + let html = '
The following changes will be saved:
    '; + for (const [field, values] of Object.entries(changes)) { + html += `
  • ${field}:
    + From: ${values.old}
    + To: ${values.new}
  • `; + } + html += '
'; + changesContent.innerHTML = html; + saveConfirmationModal.show(); + return true; + } async function handleSearch(searchType, searchValue) { + if (hasUnsavedChanges) { + pendingSearchData = { searchType, searchValue }; + unsavedChangesModal.show(); + return; + } + try { - loadingSpinner.style.display = 'block'; loadingOverlay.style.display = 'block'; - const inputs = searchResultForm.querySelectorAll('input'); - inputs.forEach(input => input.value = ''); + const formElements = searchResultForm.querySelectorAll('input, select'); + formElements.forEach(element => element.value = ''); noResultsDiv.style.display = 'none'; const params = new URLSearchParams(); params.append(searchType, searchValue); + console.log('Fetching from:', `/wamtram2/api/turtle-search/?${params.toString()}`); + const response = await fetch(`/wamtram2/api/turtle-search/?${params.toString()}`, { headers: { 'X-Requested-With': 'XMLHttpRequest' @@ -30,39 +96,81 @@ document.addEventListener('DOMContentLoaded', function() { }); const data = await response.json(); + console.log('Search response:', data); if (data.status === 'success' && data.data.length > 0) { const turtle = data.data[0]; searchResultForm.style.display = 'block'; + Object.keys(turtle).forEach(key => { - const input = searchResultForm.querySelector(`input[name="${key}"]`); - if (input) { - input.value = turtle[key]; + const element = searchResultForm.querySelector(`input[name="${key}"], select[name="${key}"]`); + if (element) { + element.value = turtle[key]; } }); + saveOriginalFormData(); + hasUnsavedChanges = false; } else { noResultsDiv.style.display = 'block'; searchResultForm.style.display = 'none'; } } catch (error) { - const alertDiv = document.createElement('div'); - alertDiv.className = 'alert alert-danger'; - alertDiv.textContent = 'An error occurred while searching. Please try again.'; - searchResultForm.parentNode.insertBefore(alertDiv, searchResultForm); - - setTimeout(() => alertDiv.remove(), 3000); + console.error('Search error:', error); + showAlert('An error occurred while searching. Please try again.', 'danger'); + } finally { + loadingSpinner.style.display = 'none'; + loadingOverlay.style.display = 'none'; + } + } + + async function handleSave() { + try { + loadingSpinner.style.display = 'block'; + loadingOverlay.style.display = 'block'; + + const formData = {}; + const formElements = searchResultForm.querySelectorAll('input, select'); + formElements.forEach(element => { + formData[element.name] = element.value; + }); + + const csrfToken = document.querySelector('[name=csrfmiddlewaretoken]').value; + + const response = await fetch('/wamtram2/api/turtle-update/', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': csrfToken, + 'X-Requested-With': 'XMLHttpRequest' + }, + body: JSON.stringify(formData) + }); + + const data = await response.json(); + console.log('Save response:', data); + + if (data.status === 'success') { + showAlert('Changes saved successfully!', 'success'); + } else { + showAlert(data.message || 'Error saving changes', 'danger'); + } + } catch (error) { + console.error('Save error:', error); + showAlert('An error occurred while saving. Please try again.', 'danger'); } finally { loadingSpinner.style.display = 'none'; loadingOverlay.style.display = 'none'; } } - const searchTypeMapping = { - 'turtleid': 'turtle_id', - 'flippertag': 'tag_id', - 'pittag': 'pit_tag_id', - 'otheridentification': 'other_id' - }; + + function showAlert(message, type) { + const alertDiv = document.createElement('div'); + alertDiv.className = `alert alert-${type} mt-3`; + alertDiv.textContent = message; + searchResultForm.parentNode.insertBefore(alertDiv, searchResultForm); + setTimeout(() => alertDiv.remove(), 3000); + } searchButtons.forEach(button => { console.log('Adding click listener to button:', button.id); @@ -79,4 +187,17 @@ document.addEventListener('DOMContentLoaded', function() { } }); }); + + if (saveButton) { + saveButton.addEventListener('click', function() { + const changes = getFormChanges(); + if (showSaveConfirmation(changes)) { + handleSave(); + } + }); + } + + searchResultForm.addEventListener('change', function() { + hasUnsavedChanges = true; + }); }); \ No newline at end of file diff --git a/wamtram2/templates/wamtram2/turtle_management.html b/wamtram2/templates/wamtram2/turtle_management.html index 009f256f7..772ebcda4 100644 --- a/wamtram2/templates/wamtram2/turtle_management.html +++ b/wamtram2/templates/wamtram2/turtle_management.html @@ -84,6 +84,15 @@

Turtle Management

+ {% csrf_token %} + +
+
+ +
+
@@ -94,7 +103,12 @@

Turtle Management

- +
@@ -106,7 +120,12 @@

Turtle Management

- +
@@ -115,13 +134,23 @@

Turtle Management

- +
- +
@@ -444,6 +473,48 @@

Turtle Management

+ + + + + + {% endblock %} {% block extra_js %} diff --git a/wamtram2/urls.py b/wamtram2/urls.py index 6e27b4da3..d643e7ab1 100644 --- a/wamtram2/urls.py +++ b/wamtram2/urls.py @@ -59,4 +59,5 @@ path('api/get-places/', views.ObservationDataView.as_view(), name='api_get_places'), path('curation/turtle-management/', views.TurtleManagementView.as_view(), name='turtle_management'), path('api/turtle-search/', views.TurtleManagementView.as_view(), name='turtle_search'), + path('api/turtle-update/', views.TurtleManagementView.as_view(), name='turtle_update'), ] diff --git a/wamtram2/views.py b/wamtram2/views.py index fc46f1672..a59109616 100644 --- a/wamtram2/views.py +++ b/wamtram2/views.py @@ -59,7 +59,9 @@ TrtRecordedTags, TrtRecordedPitTags, TrtMeasurements, - TrtDamage + TrtDamage, + TrtCauseOfDeath, + TrtTurtleStatus ) from .forms import TrtDataEntryForm, SearchForm, TrtEntryBatchesForm, TemplateForm, BatchesCodeForm, TrtPersonsForm, TagRegisterForm @@ -4072,6 +4074,10 @@ class TurtleManagementView(TemplateView): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context['sex_choices'] = SEX_CHOICES + context['cause_of_death_choices'] = TrtCauseOfDeath.objects.all() + context['turtle_status_choices'] = TrtTurtleStatus.objects.all() + context['species_choices'] = TrtSpecies.objects.all() + return context def get(self, request, *args, **kwargs): @@ -4079,6 +4085,34 @@ def get(self, request, *args, **kwargs): return self.handle_ajax_request(request) return super().get(request, *args, **kwargs) + def post(self, request, *args, **kwargs): + if request.headers.get('X-Requested-With') == 'XMLHttpRequest': + try: + data = json.loads(request.body) + turtle = TrtTurtles.objects.get(turtle_id=data['turtle_id']) + + turtle.turtle_name = data['turtle_name'] + turtle.species_code = data['species'] + turtle.sex = data['sex'] + turtle.cause_of_death = data['cause_of_death'] + turtle.comments = data['comments'] + turtle.turtle_status = data['turtle_status'] + + turtle.save() + + return JsonResponse({ + 'status': 'success', + 'message': 'Turtle information updated successfully' + }) + except Exception as e: + return JsonResponse({ + 'status': 'error', + 'message': str(e) + }) + return super().post(request, *args, **kwargs) + + + def handle_ajax_request(self, request): print("Received AJAX request with parameters:", request.GET) turtle_id = request.GET.get('turtle_id') From 695454c01c9da04a44fafab689547b2dca92b2ba Mon Sep 17 00:00:00 2001 From: Xinlyu Wang Date: Thu, 28 Nov 2024 13:07:53 +0800 Subject: [PATCH 25/52] Debug curation turtle page --- wamtram2/static/js/turtle_management.js | 9 ++++-- .../templates/wamtram2/turtle_management.html | 28 +++++++++++++------ wamtram2/views.py | 9 ++++-- 3 files changed, 32 insertions(+), 14 deletions(-) diff --git a/wamtram2/static/js/turtle_management.js b/wamtram2/static/js/turtle_management.js index c607b76bc..39572a3b9 100644 --- a/wamtram2/static/js/turtle_management.js +++ b/wamtram2/static/js/turtle_management.js @@ -100,13 +100,16 @@ document.addEventListener('DOMContentLoaded', function() { if (data.status === 'success' && data.data.length > 0) { const turtle = data.data[0]; + console.log('Received turtle data:', turtle); + searchResultForm.style.display = 'block'; - Object.keys(turtle).forEach(key => { - const element = searchResultForm.querySelector(`input[name="${key}"], select[name="${key}"]`); + const element = searchResultForm.querySelector(`[name="${key}"]`); + console.log(`Setting ${key} = ${turtle[key]}, Found element:`, element); if (element) { - element.value = turtle[key]; + element.value = turtle[key] || ''; + console.log(`After setting ${key}, value is:`, element.value); } }); saveOriginalFormData(); diff --git a/wamtram2/templates/wamtram2/turtle_management.html b/wamtram2/templates/wamtram2/turtle_management.html index 772ebcda4..557ca9ee7 100644 --- a/wamtram2/templates/wamtram2/turtle_management.html +++ b/wamtram2/templates/wamtram2/turtle_management.html @@ -38,6 +38,7 @@

Turtle Management

+
+
- + +
+
+
+ +
+
-
- -
- {% for status in turtle_status_choices %} {% endfor %}
-
+
diff --git a/wamtram2/views.py b/wamtram2/views.py index a59109616..1b5fb4d47 100644 --- a/wamtram2/views.py +++ b/wamtram2/views.py @@ -4077,6 +4077,7 @@ def get_context_data(self, **kwargs): context['cause_of_death_choices'] = TrtCauseOfDeath.objects.all() context['turtle_status_choices'] = TrtTurtleStatus.objects.all() context['species_choices'] = TrtSpecies.objects.all() + context['location_choices'] = TrtPlaces.objects.all() return context @@ -4094,6 +4095,7 @@ def post(self, request, *args, **kwargs): turtle.turtle_name = data['turtle_name'] turtle.species_code = data['species'] turtle.sex = data['sex'] + turtle.location_code = data['location'] turtle.cause_of_death = data['cause_of_death'] turtle.comments = data['comments'] turtle.turtle_status = data['turtle_status'] @@ -4154,12 +4156,15 @@ def handle_ajax_request(self, request): 'turtle_id': turtle.turtle_id, 'species': str(turtle.species_code), 'turtle_name': turtle.turtle_name or '', - 'sex': dict(SEX_CHOICES).get(turtle.sex, ''), + 'sex': turtle.sex if turtle.sex else '', 'cause_of_death': str(turtle.cause_of_death) if turtle.cause_of_death else '', 'turtle_status': str(turtle.turtle_status) if turtle.turtle_status else '', 'date_entered': turtle.date_entered.strftime('%Y-%m-%d') if turtle.date_entered else '', - 'comments': turtle.comments or '' + 'comments': turtle.comments or '', + 'location': str(turtle.location_code) if turtle.location_code else '' }) + + print(f"Sending data: {turtle_data[-1]}") return JsonResponse({ 'status': 'success', From 2fc6451d5eb955c2802c91bfe29a5515b702de06 Mon Sep 17 00:00:00 2001 From: Rick Wang Date: Thu, 28 Nov 2024 13:52:08 +0800 Subject: [PATCH 26/52] Fix curation turtle page --- wamtram2/static/css/turtle_management.css | 7 +++++++ wamtram2/templates/wamtram2/turtle_management.html | 12 ++++++------ wamtram2/views.py | 10 +++++----- 3 files changed, 18 insertions(+), 11 deletions(-) diff --git a/wamtram2/static/css/turtle_management.css b/wamtram2/static/css/turtle_management.css index 10125c4c1..0f6bafa7b 100644 --- a/wamtram2/static/css/turtle_management.css +++ b/wamtram2/static/css/turtle_management.css @@ -1,4 +1,11 @@ +.form-label { + color: #495057; + font-size: 0.9rem; + text-transform: uppercase; + letter-spacing: 0.5px; +} + .table-container { overflow-y: auto; position: relative; diff --git a/wamtram2/templates/wamtram2/turtle_management.html b/wamtram2/templates/wamtram2/turtle_management.html index 557ca9ee7..56b680ae9 100644 --- a/wamtram2/templates/wamtram2/turtle_management.html +++ b/wamtram2/templates/wamtram2/turtle_management.html @@ -38,7 +38,7 @@

Turtle Management

- +
- +
- +
- +
@@ -125,7 +125,7 @@

Turtle Management

diff --git a/wamtram2/views.py b/wamtram2/views.py index 1b5fb4d47..d2721b86c 100644 --- a/wamtram2/views.py +++ b/wamtram2/views.py @@ -4077,7 +4077,7 @@ def get_context_data(self, **kwargs): context['cause_of_death_choices'] = TrtCauseOfDeath.objects.all() context['turtle_status_choices'] = TrtTurtleStatus.objects.all() context['species_choices'] = TrtSpecies.objects.all() - context['location_choices'] = TrtPlaces.objects.all() + context['location_choices'] = TrtLocations.objects.all() return context @@ -4154,14 +4154,14 @@ def handle_ajax_request(self, request): for turtle in queryset: turtle_data.append({ 'turtle_id': turtle.turtle_id, - 'species': str(turtle.species_code), + 'species': turtle.species_code.species_code, 'turtle_name': turtle.turtle_name or '', 'sex': turtle.sex if turtle.sex else '', - 'cause_of_death': str(turtle.cause_of_death) if turtle.cause_of_death else '', - 'turtle_status': str(turtle.turtle_status) if turtle.turtle_status else '', + 'cause_of_death': turtle.cause_of_death.cause_of_death if turtle.cause_of_death else '', + 'turtle_status': turtle.turtle_status.turtle_status if turtle.turtle_status else '', 'date_entered': turtle.date_entered.strftime('%Y-%m-%d') if turtle.date_entered else '', 'comments': turtle.comments or '', - 'location': str(turtle.location_code) if turtle.location_code else '' + 'location': turtle.location_code.location_code if turtle.location_code else '' }) print(f"Sending data: {turtle_data[-1]}") From df7442cf72930f59818e97e46169e5393a6332bd Mon Sep 17 00:00:00 2001 From: Xinlyu Wang Date: Thu, 28 Nov 2024 13:55:21 +0800 Subject: [PATCH 27/52] Test save function in curation turtle page --- wamtram2/static/js/turtle_management.js | 49 ++++++++++++++++++++++--- wamtram2/views.py | 12 +++++- 2 files changed, 54 insertions(+), 7 deletions(-) diff --git a/wamtram2/static/js/turtle_management.js b/wamtram2/static/js/turtle_management.js index 39572a3b9..ab935004f 100644 --- a/wamtram2/static/js/turtle_management.js +++ b/wamtram2/static/js/turtle_management.js @@ -17,8 +17,14 @@ document.addEventListener('DOMContentLoaded', function() { 'otheridentification': 'other_id' }; - const saveConfirmationModal = new bootstrap.Modal(document.getElementById('saveConfirmationModal')); - const unsavedChangesModal = new bootstrap.Modal(document.getElementById('unsavedChangesModal')); + const saveConfirmationModalElement = document.getElementById('saveConfirmationModal'); + const unsavedChangesModalElement = document.getElementById('unsavedChangesModal'); + + const saveConfirmationModal = saveConfirmationModalElement ? + new bootstrap.Modal(saveConfirmationModalElement) : null; + const unsavedChangesModal = unsavedChangesModalElement ? + new bootstrap.Modal(unsavedChangesModalElement) : null; + let originalFormData = {}; let hasUnsavedChanges = false; let pendingSearchData = null; @@ -51,7 +57,17 @@ document.addEventListener('DOMContentLoaded', function() { } function showSaveConfirmation(changes) { + if (!saveConfirmationModal) { + console.error('Save confirmation modal not found'); + return false; + } + const changesContent = document.getElementById('changesContent'); + if (!changesContent) { + console.error('Changes content element not found'); + return false; + } + if (Object.keys(changes).length === 0) { changesContent.innerHTML = '

No changes detected.

'; return false; @@ -70,7 +86,7 @@ document.addEventListener('DOMContentLoaded', function() { } async function handleSearch(searchType, searchValue) { - if (hasUnsavedChanges) { + if (hasUnsavedChanges && unsavedChangesModal) { pendingSearchData = { searchType, searchValue }; unsavedChangesModal.show(); return; @@ -139,6 +155,10 @@ document.addEventListener('DOMContentLoaded', function() { }); const csrfToken = document.querySelector('[name=csrfmiddlewaretoken]').value; + if (!csrfToken) { + throw new Error('CSRF token not found'); + } + const response = await fetch('/wamtram2/api/turtle-update/', { method: 'POST', @@ -195,12 +215,29 @@ document.addEventListener('DOMContentLoaded', function() { saveButton.addEventListener('click', function() { const changes = getFormChanges(); if (showSaveConfirmation(changes)) { - handleSave(); + document.getElementById('confirmSaveBtn')?.addEventListener('click', function() { + saveConfirmationModal?.hide(); + handleSave(); + }); } }); } - searchResultForm.addEventListener('change', function() { - hasUnsavedChanges = true; + if (searchResultForm) { + searchResultForm.addEventListener('change', function() { + hasUnsavedChanges = true; + }); + } + + document.getElementById('discardChangesBtn')?.addEventListener('click', function() { + unsavedChangesModal?.hide(); + hasUnsavedChanges = false; + if (pendingSearchData) { + const { searchType, searchValue } = pendingSearchData; + pendingSearchData = null; + handleSearch(searchType, searchValue); + } }); + + clearForm(); }); \ No newline at end of file diff --git a/wamtram2/views.py b/wamtram2/views.py index d2721b86c..3f213832e 100644 --- a/wamtram2/views.py +++ b/wamtram2/views.py @@ -4090,6 +4090,9 @@ def post(self, request, *args, **kwargs): if request.headers.get('X-Requested-With') == 'XMLHttpRequest': try: data = json.loads(request.body) + + print("Received data:", data) + turtle = TrtTurtles.objects.get(turtle_id=data['turtle_id']) turtle.turtle_name = data['turtle_name'] @@ -4106,11 +4109,18 @@ def post(self, request, *args, **kwargs): 'status': 'success', 'message': 'Turtle information updated successfully' }) + except json.JSONDecodeError as e: + print("JSON decode error:", str(e)) + return JsonResponse({ + 'status': 'error', + 'message': 'Invalid JSON data' + }, status=400) except Exception as e: + print("Error:", str(e)) return JsonResponse({ 'status': 'error', 'message': str(e) - }) + }, status=500) return super().post(request, *args, **kwargs) From 78638d58c11d5beac4aaead615de5e5b090f4013 Mon Sep 17 00:00:00 2001 From: Xinlyu Wang Date: Thu, 28 Nov 2024 14:44:54 +0800 Subject: [PATCH 28/52] Fix the csrf token bug --- .../templates/wamtram2/turtle_management.html | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/wamtram2/templates/wamtram2/turtle_management.html b/wamtram2/templates/wamtram2/turtle_management.html index 56b680ae9..f8c7dc039 100644 --- a/wamtram2/templates/wamtram2/turtle_management.html +++ b/wamtram2/templates/wamtram2/turtle_management.html @@ -24,6 +24,8 @@ {% endblock %} {% block page_content_inner %} +{% csrf_token %} +
@@ -88,7 +90,6 @@

Turtle Management

- {% csrf_token %}
@@ -485,11 +486,11 @@

Turtle Management

-