From 1ac8668a5ebeaf18b7c5b647c5a59276fbc34d43 Mon Sep 17 00:00:00 2001 From: Steve Turoscy Date: Fri, 10 Apr 2026 12:18:14 -0400 Subject: [PATCH 1/6] Adding docker and software v4 index --- .gitignore | 3 ++ dgpf1/Dockerfile | 49 +++++++++++++++++ dgpf1/compose.yaml | 21 ++++++++ dgpf1/dgpf1/fields.py | 2 +- dgpf1/dgpf1/settings.py | 52 +++++++++++++++---- dgpf1/dgpf1/urls.py | 2 +- dgpf1/dgpf1/views.py | 10 ++-- dgpf1/entrypoint.sh | 43 +++++++++++++++ dgpf1/requirements.txt | 7 +++ .../components/detail-general-metadata.html | 2 + .../detail-globus-search-metadata.html | 4 +- .../v2/components/search-results.html | 7 +-- dgpf1/templatetags/custom_tags.py | 10 ++++ 13 files changed, 189 insertions(+), 23 deletions(-) create mode 100644 dgpf1/Dockerfile create mode 100644 dgpf1/compose.yaml create mode 100755 dgpf1/entrypoint.sh create mode 100644 dgpf1/requirements.txt create mode 100644 dgpf1/templatetags/custom_tags.py diff --git a/.gitignore b/.gitignore index db9dd30..2e8e623 100644 --- a/.gitignore +++ b/.gitignore @@ -161,3 +161,6 @@ cython_debug/ # Mac .DS_Store + +# Project-specific ignores +local_settings.py \ No newline at end of file diff --git a/dgpf1/Dockerfile b/dgpf1/Dockerfile new file mode 100644 index 0000000..0b2a918 --- /dev/null +++ b/dgpf1/Dockerfile @@ -0,0 +1,49 @@ +# ========================= +# Base stage (shared) +# ========================= +FROM python:3.12-slim AS base + +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 + +WORKDIR /app + + +# ========================= +# Builder +# ========================= +FROM base AS builder + +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential \ + libpq-dev \ + && rm -rf /var/lib/apt/lists/* + +RUN python -m venv /opt/venv +ENV PATH="/opt/venv/bin:$PATH" + +COPY requirements.txt . +RUN pip install --upgrade pip && pip install -r requirements.txt + + +# ========================= +# Runtime +# ========================= +FROM base AS runtime + +RUN apt-get update && apt-get install -y --no-install-recommends \ + libpq5 \ + && rm -rf /var/lib/apt/lists/* + +COPY --from=builder /opt/venv /opt/venv +ENV APP_CONFIG="/app/local_settings.py" +ENV PATH="/opt/venv/bin:$PATH" + +COPY . . + +RUN useradd -m appuser && chown -R appuser /app +USER appuser + +COPY --chmod=755 entrypoint.sh /entrypoint.sh + +ENTRYPOINT ["/entrypoint.sh"] \ No newline at end of file diff --git a/dgpf1/compose.yaml b/dgpf1/compose.yaml new file mode 100644 index 0000000..49c3940 --- /dev/null +++ b/dgpf1/compose.yaml @@ -0,0 +1,21 @@ +services: + db: + image: postgres:16-alpine + container_name: postgres + restart: always + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: postgres + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + networks: + - backend + +networks: + backend: + +volumes: + postgres_data: diff --git a/dgpf1/dgpf1/fields.py b/dgpf1/dgpf1/fields.py index 3d57b00..a4934ea 100644 --- a/dgpf1/dgpf1/fields.py +++ b/dgpf1/dgpf1/fields.py @@ -47,4 +47,4 @@ def detail_result_display_fields(result): except Exception: # If the date cannot be parsed, just leave it. pass - return display_fields + return display_fields \ No newline at end of file diff --git a/dgpf1/dgpf1/settings.py b/dgpf1/dgpf1/settings.py index 6879fa8..18b71ff 100644 --- a/dgpf1/dgpf1/settings.py +++ b/dgpf1/dgpf1/settings.py @@ -52,6 +52,7 @@ INSTALLED_APPS = [ 'provider', + 'whitenoise', 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', @@ -63,6 +64,7 @@ MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', + "whitenoise.middleware.WhiteNoiseMiddleware", 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', @@ -79,8 +81,10 @@ 'DIRS': [BASE_DIR / 'templates'], 'APP_DIRS': True, 'OPTIONS': { + 'debug': DEBUG, 'libraries': { 'settings_value': 'templatetags.get_settings', + 'urldecode': 'templatetags.custom_tags', }, 'context_processors': [ 'django.template.context_processors.debug', @@ -102,20 +106,21 @@ 'default': { 'USER': CONF['DJANGO_USER'], 'PASSWORD': CONF['DJANGO_PASS'], - 'HOST': os.environ.get('PGHOST', CONF.get('DB_HOSTNAME_WRITE', 'localhost')), + 'HOST': os.environ.get('PGHOST', CONF.get('DB_HOSTNAME_WRITE', 'db')), + }, + 'sqlite3': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': BASE_DIR / 'db.sqlite3', } -# 'sqlite3': { -# 'ENGINE': 'django.db.backends.sqlite3', -# 'NAME': BASE_DIR / 'db.sqlite3', -# } } for db in DATABASES: - DATABASES[db]['NAME'] = CONF['DB_DATABASE'] - DATABASES[db]['ENGINE'] = 'django.db.backends.postgresql' - DATABASES[db]['PORT'] = os.environ.get('PGPORT', CONF.get('DB_PORT', '5432')) - DATABASES[db]['CONN_MAX_AGE'] = 600 # Persist DB connections - DATABASES[db]['OPTIONS'] = {'options': '-c search_path=ed_dgpf1,public'} + if db == "default": + DATABASES[db]['NAME'] = CONF['DB_DATABASE'] + DATABASES[db]['ENGINE'] = 'django.db.backends.postgresql' + DATABASES[db]['PORT'] = os.environ.get('PGPORT', CONF.get('DB_PORT', '5432')) + DATABASES[db]['CONN_MAX_AGE'] = 600 # Persist DB connections + DATABASES[db]['OPTIONS'] = {'options': '-c search_path=ed_dgpf1,public'} # Password validation # https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators @@ -167,7 +172,8 @@ # https://docs.djangoproject.com/en/4.2/howto/static-files/ STATIC_URL = 'static/' -STATIC_ROOT = CONF['STATIC_ROOT'] + +STATIC_ROOT = BASE_DIR / 'static' STATICFILES_DIRS = [BASE_DIR / 'staticfiles'] # Default primary key field type @@ -289,6 +295,30 @@ ('general_info', general_info), ('detail_result_display_fields', detail_result_display_fields), ], + }, + 'access-software-v4': { + 'name': 'ACCESS Software Catalog - Beta catalog v4', + 'uuid': '3cc4aeec-55a5-4cd6-96d1-8531aef88e83', + 'facets': [ + {'name': 'Category', 'field_name': 'Category'}, + {'name': 'Creation Time', 'field_name': 'CreationTime'}, + {'name': 'Handle Key', 'field_name': 'HandleKey'}, + {'name': 'Handle Type', 'field_name': 'HandleType'}, + {'name': 'Info Group ID', 'field_name': 'Info_GroupID'}, + {'name': 'Info Group Name', 'field_name': 'Info_GroupName'}, + {'name': 'Info Resource ID', 'field_name': 'Info_ResourceID'}, + {'name': 'Info Resource Name', 'field_name': 'Info_ResourceName'}, + {'name': 'Keywords', 'field_name': 'Keywords'}, + {'name': 'Name', 'field_name': 'Name'}, + {'name': 'Organization ID', 'field_name': 'Organization_ID'}, + {'name': 'Organization Name', 'field_name': 'Organization_Name'}, + {'name': 'Support Status', 'field_name': 'SupportStatus'}, + {'name': 'URL', 'field_name': 'URL'}, + {'name': 'Version', 'field_name': 'Version'}, + ], + 'facet_modifiers': [ + 'globus_portal_framework.modifiers.facets.drop_empty', + ], } } diff --git a/dgpf1/dgpf1/urls.py b/dgpf1/dgpf1/urls.py index 5455778..3cc4f6f 100644 --- a/dgpf1/dgpf1/urls.py +++ b/dgpf1/dgpf1/urls.py @@ -30,6 +30,6 @@ path('', include('globus_portal_framework.urls')), # Provides Login urls for Globus Auth path('', include('social_django.urls', namespace='social')), - path('favicon.ico', views.favicon), + # path('favicon.ico', views.favicon), path('dump.html', views.Debug_Details, name='debug-details'), ] diff --git a/dgpf1/dgpf1/views.py b/dgpf1/dgpf1/views.py index 0a7a42f..0105837 100644 --- a/dgpf1/dgpf1/views.py +++ b/dgpf1/dgpf1/views.py @@ -11,11 +11,11 @@ from dgpf1.download import download -@require_GET -@cache_control(max_age=60 * 60 * 24, immutable=True, public=True) # one day -def favicon(request: HttpRequest) -> HttpResponse: - file = open(settings.STATIC_ROOT + '/img/favicon.ico', 'rb') - return FileResponse(file) +# @require_GET +# @cache_control(max_age=60 * 60 * 24, immutable=True, public=True) # one day +# def favicon(request: HttpRequest) -> HttpResponse: +# file = open(settings.STATIC_ROOT + '/img/favicon.ico', 'rb') +# return FileResponse(file) def Debug_Details(request, format=None, **kwargs): return debug.technical_500_response(request, *sys.exc_info(), status_code=400) diff --git a/dgpf1/entrypoint.sh b/dgpf1/entrypoint.sh new file mode 100755 index 0000000..6b1300b --- /dev/null +++ b/dgpf1/entrypoint.sh @@ -0,0 +1,43 @@ +#!/bin/sh +set -e + +# Default values (can be overridden via env) +: "${DJANGO_WSGI_MODULE:=dgpf1.wsgi:application}" +: "${GUNICORN_BIND:=0.0.0.0:8000}" +: "${GUNICORN_WORKERS:=3}" +: "${GUNICORN_THREADS:=2}" +: "${GUNICORN_TIMEOUT:=120}" + +# Optional: wait for database +if [ "$WAIT_FOR_DB" = "true" ]; then + echo "Waiting for database..." + while ! nc -z "$DB_HOST" "$DB_PORT"; do + sleep 1 + done + echo "Database is up!" +fi + +# Optional: run migrations +if [ "$RUN_MIGRATIONS" = "true" ]; then + echo "Running migrations..." + python manage.py migrate --noinput +fi + +# Optional: collect static files +if [ "$COLLECTSTATIC" = "true" ]; then + echo "Collecting static files..." + python manage.py collectstatic --noinput +fi + +# If user passes a custom command, run it instead +if [ "$#" -gt 0 ]; then + exec "$@" +fi + +# Default: start Gunicorn +echo "Starting Gunicorn..." +exec gunicorn "$DJANGO_WSGI_MODULE" \ + --bind "$GUNICORN_BIND" \ + --workers "$GUNICORN_WORKERS" \ + --threads "$GUNICORN_THREADS" \ + --timeout "$GUNICORN_TIMEOUT" \ No newline at end of file diff --git a/dgpf1/requirements.txt b/dgpf1/requirements.txt new file mode 100644 index 0000000..f108569 --- /dev/null +++ b/dgpf1/requirements.txt @@ -0,0 +1,7 @@ +Django==4.2 +django-globus-portal-framework==0.4.15 +psycopg2==2.9.11 +pytz==2026.1 +pandas==3.0.1 +gunicorn==25.3.0 +whitenoise==6.12.0 \ No newline at end of file diff --git a/dgpf1/templates/globus-portal-framework/v2/components/detail-general-metadata.html b/dgpf1/templates/globus-portal-framework/v2/components/detail-general-metadata.html index 300d4d5..28232ef 100644 --- a/dgpf1/templates/globus-portal-framework/v2/components/detail-general-metadata.html +++ b/dgpf1/templates/globus-portal-framework/v2/components/detail-general-metadata.html @@ -1,6 +1,8 @@ +{% load urldecode %} {% block detail_general_metadata %} + {{ request.path|urldecode }} {% for item in detail_result_display_fields %} diff --git a/dgpf1/templates/globus-portal-framework/v2/components/detail-globus-search-metadata.html b/dgpf1/templates/globus-portal-framework/v2/components/detail-globus-search-metadata.html index b8a8d6d..880a1b5 100644 --- a/dgpf1/templates/globus-portal-framework/v2/components/detail-globus-search-metadata.html +++ b/dgpf1/templates/globus-portal-framework/v2/components/detail-globus-search-metadata.html @@ -1,9 +1,9 @@ - +{% load urldecode %}
{{item.display_name}}
- +
subject{{ subject }}{{ subject|urldecode }}
diff --git a/dgpf1/templates/globus-portal-framework/v2/components/search-results.html b/dgpf1/templates/globus-portal-framework/v2/components/search-results.html index 5af7984..dd9a66b 100644 --- a/dgpf1/templates/globus-portal-framework/v2/components/search-results.html +++ b/dgpf1/templates/globus-portal-framework/v2/components/search-results.html @@ -1,5 +1,6 @@ -{% load static %} +{% load static %} +{% load urldecode %} {% block headextras %}{% endblock %}
@@ -23,14 +24,14 @@
{{search.total}} training items found
- {{result.all.0.Abstract|safe}} + {{result.all.0.Abstract|default:result.all.0.Description|safe}} diff --git a/dgpf1/templatetags/custom_tags.py b/dgpf1/templatetags/custom_tags.py new file mode 100644 index 0000000..58b4ad2 --- /dev/null +++ b/dgpf1/templatetags/custom_tags.py @@ -0,0 +1,10 @@ +from django import template +from urllib.parse import unquote + +register = template.Library() + +@register.filter +def urldecode(value): + if value is None: + return value + return unquote(value) \ No newline at end of file From d9294933ed6152fb3a5ab945b6775903de3f991f Mon Sep 17 00:00:00 2001 From: Steve Turoscy Date: Mon, 20 Apr 2026 09:30:56 -0400 Subject: [PATCH 2/6] Adding scripts and updating Dockerfile --- dgpf1/Dockerfile | 2 +- dgpf1/scripts/docker-run.sh | 7 +++++++ dgpf1/{ => scripts}/entrypoint.sh | 0 3 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 dgpf1/scripts/docker-run.sh rename dgpf1/{ => scripts}/entrypoint.sh (100%) diff --git a/dgpf1/Dockerfile b/dgpf1/Dockerfile index 0b2a918..3f39bf3 100644 --- a/dgpf1/Dockerfile +++ b/dgpf1/Dockerfile @@ -44,6 +44,6 @@ COPY . . RUN useradd -m appuser && chown -R appuser /app USER appuser -COPY --chmod=755 entrypoint.sh /entrypoint.sh +COPY --chmod=755 scripts/entrypoint.sh /entrypoint.sh ENTRYPOINT ["/entrypoint.sh"] \ No newline at end of file diff --git a/dgpf1/scripts/docker-run.sh b/dgpf1/scripts/docker-run.sh new file mode 100644 index 0000000..11dbe6d --- /dev/null +++ b/dgpf1/scripts/docker-run.sh @@ -0,0 +1,7 @@ +docker run --network=dgpf1_backend \ + -e GUNICORN_BIND=0.0.0.0:8000 \ + -e RUN_MIGRATIONS=true \ + -e RUN_COLLECTSTATIC=true \ + -v ${PWD}:/app \ + -p 80:8000 \ + -it operations-search-dgpf:latest \ No newline at end of file diff --git a/dgpf1/entrypoint.sh b/dgpf1/scripts/entrypoint.sh similarity index 100% rename from dgpf1/entrypoint.sh rename to dgpf1/scripts/entrypoint.sh From f092962d1d85ea4293760637307bbaaad5829854 Mon Sep 17 00:00:00 2001 From: Steve Turoscy Date: Mon, 20 Apr 2026 09:33:41 -0400 Subject: [PATCH 3/6] Removing local containers --- dgpf1/Dockerfile | 49 ------------------------------------- dgpf1/compose.yaml | 21 ---------------- dgpf1/scripts/docker-run.sh | 7 ------ dgpf1/scripts/entrypoint.sh | 43 -------------------------------- 4 files changed, 120 deletions(-) delete mode 100644 dgpf1/Dockerfile delete mode 100644 dgpf1/compose.yaml delete mode 100644 dgpf1/scripts/docker-run.sh delete mode 100755 dgpf1/scripts/entrypoint.sh diff --git a/dgpf1/Dockerfile b/dgpf1/Dockerfile deleted file mode 100644 index 3f39bf3..0000000 --- a/dgpf1/Dockerfile +++ /dev/null @@ -1,49 +0,0 @@ -# ========================= -# Base stage (shared) -# ========================= -FROM python:3.12-slim AS base - -ENV PYTHONDONTWRITEBYTECODE=1 \ - PYTHONUNBUFFERED=1 - -WORKDIR /app - - -# ========================= -# Builder -# ========================= -FROM base AS builder - -RUN apt-get update && apt-get install -y --no-install-recommends \ - build-essential \ - libpq-dev \ - && rm -rf /var/lib/apt/lists/* - -RUN python -m venv /opt/venv -ENV PATH="/opt/venv/bin:$PATH" - -COPY requirements.txt . -RUN pip install --upgrade pip && pip install -r requirements.txt - - -# ========================= -# Runtime -# ========================= -FROM base AS runtime - -RUN apt-get update && apt-get install -y --no-install-recommends \ - libpq5 \ - && rm -rf /var/lib/apt/lists/* - -COPY --from=builder /opt/venv /opt/venv -ENV APP_CONFIG="/app/local_settings.py" -ENV PATH="/opt/venv/bin:$PATH" - -COPY . . - -RUN useradd -m appuser && chown -R appuser /app -USER appuser - -COPY --chmod=755 scripts/entrypoint.sh /entrypoint.sh - -ENTRYPOINT ["/entrypoint.sh"] \ No newline at end of file diff --git a/dgpf1/compose.yaml b/dgpf1/compose.yaml deleted file mode 100644 index 49c3940..0000000 --- a/dgpf1/compose.yaml +++ /dev/null @@ -1,21 +0,0 @@ -services: - db: - image: postgres:16-alpine - container_name: postgres - restart: always - environment: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - POSTGRES_DB: postgres - ports: - - "5432:5432" - volumes: - - postgres_data:/var/lib/postgresql/data - networks: - - backend - -networks: - backend: - -volumes: - postgres_data: diff --git a/dgpf1/scripts/docker-run.sh b/dgpf1/scripts/docker-run.sh deleted file mode 100644 index 11dbe6d..0000000 --- a/dgpf1/scripts/docker-run.sh +++ /dev/null @@ -1,7 +0,0 @@ -docker run --network=dgpf1_backend \ - -e GUNICORN_BIND=0.0.0.0:8000 \ - -e RUN_MIGRATIONS=true \ - -e RUN_COLLECTSTATIC=true \ - -v ${PWD}:/app \ - -p 80:8000 \ - -it operations-search-dgpf:latest \ No newline at end of file diff --git a/dgpf1/scripts/entrypoint.sh b/dgpf1/scripts/entrypoint.sh deleted file mode 100755 index 6b1300b..0000000 --- a/dgpf1/scripts/entrypoint.sh +++ /dev/null @@ -1,43 +0,0 @@ -#!/bin/sh -set -e - -# Default values (can be overridden via env) -: "${DJANGO_WSGI_MODULE:=dgpf1.wsgi:application}" -: "${GUNICORN_BIND:=0.0.0.0:8000}" -: "${GUNICORN_WORKERS:=3}" -: "${GUNICORN_THREADS:=2}" -: "${GUNICORN_TIMEOUT:=120}" - -# Optional: wait for database -if [ "$WAIT_FOR_DB" = "true" ]; then - echo "Waiting for database..." - while ! nc -z "$DB_HOST" "$DB_PORT"; do - sleep 1 - done - echo "Database is up!" -fi - -# Optional: run migrations -if [ "$RUN_MIGRATIONS" = "true" ]; then - echo "Running migrations..." - python manage.py migrate --noinput -fi - -# Optional: collect static files -if [ "$COLLECTSTATIC" = "true" ]; then - echo "Collecting static files..." - python manage.py collectstatic --noinput -fi - -# If user passes a custom command, run it instead -if [ "$#" -gt 0 ]; then - exec "$@" -fi - -# Default: start Gunicorn -echo "Starting Gunicorn..." -exec gunicorn "$DJANGO_WSGI_MODULE" \ - --bind "$GUNICORN_BIND" \ - --workers "$GUNICORN_WORKERS" \ - --threads "$GUNICORN_THREADS" \ - --timeout "$GUNICORN_TIMEOUT" \ No newline at end of file From 19a2ac7f9e49d71c90b8d6e63ce164c4ea05061a Mon Sep 17 00:00:00 2001 From: Steve Turoscy Date: Mon, 20 Apr 2026 09:54:24 -0400 Subject: [PATCH 4/6] Removing whitenoise dependency, fixing favicon, removing req.txt file --- dgpf1/dgpf1/settings.py | 2 -- dgpf1/dgpf1/urls.py | 2 +- dgpf1/dgpf1/views.py | 10 +++++----- dgpf1/requirements.txt | 7 ------- 4 files changed, 6 insertions(+), 15 deletions(-) delete mode 100644 dgpf1/requirements.txt diff --git a/dgpf1/dgpf1/settings.py b/dgpf1/dgpf1/settings.py index 18b71ff..645678d 100644 --- a/dgpf1/dgpf1/settings.py +++ b/dgpf1/dgpf1/settings.py @@ -52,7 +52,6 @@ INSTALLED_APPS = [ 'provider', - 'whitenoise', 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', @@ -64,7 +63,6 @@ MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', - "whitenoise.middleware.WhiteNoiseMiddleware", 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', diff --git a/dgpf1/dgpf1/urls.py b/dgpf1/dgpf1/urls.py index 3cc4f6f..5455778 100644 --- a/dgpf1/dgpf1/urls.py +++ b/dgpf1/dgpf1/urls.py @@ -30,6 +30,6 @@ path('', include('globus_portal_framework.urls')), # Provides Login urls for Globus Auth path('', include('social_django.urls', namespace='social')), - # path('favicon.ico', views.favicon), + path('favicon.ico', views.favicon), path('dump.html', views.Debug_Details, name='debug-details'), ] diff --git a/dgpf1/dgpf1/views.py b/dgpf1/dgpf1/views.py index 0105837..0a7a42f 100644 --- a/dgpf1/dgpf1/views.py +++ b/dgpf1/dgpf1/views.py @@ -11,11 +11,11 @@ from dgpf1.download import download -# @require_GET -# @cache_control(max_age=60 * 60 * 24, immutable=True, public=True) # one day -# def favicon(request: HttpRequest) -> HttpResponse: -# file = open(settings.STATIC_ROOT + '/img/favicon.ico', 'rb') -# return FileResponse(file) +@require_GET +@cache_control(max_age=60 * 60 * 24, immutable=True, public=True) # one day +def favicon(request: HttpRequest) -> HttpResponse: + file = open(settings.STATIC_ROOT + '/img/favicon.ico', 'rb') + return FileResponse(file) def Debug_Details(request, format=None, **kwargs): return debug.technical_500_response(request, *sys.exc_info(), status_code=400) diff --git a/dgpf1/requirements.txt b/dgpf1/requirements.txt deleted file mode 100644 index f108569..0000000 --- a/dgpf1/requirements.txt +++ /dev/null @@ -1,7 +0,0 @@ -Django==4.2 -django-globus-portal-framework==0.4.15 -psycopg2==2.9.11 -pytz==2026.1 -pandas==3.0.1 -gunicorn==25.3.0 -whitenoise==6.12.0 \ No newline at end of file From 974fec02438243919168456c7091af212b33cfb0 Mon Sep 17 00:00:00 2001 From: Steve Turoscy Date: Mon, 20 Apr 2026 09:55:58 -0400 Subject: [PATCH 5/6] Formatting issue --- dgpf1/dgpf1/fields.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dgpf1/dgpf1/fields.py b/dgpf1/dgpf1/fields.py index a4934ea..3d57b00 100644 --- a/dgpf1/dgpf1/fields.py +++ b/dgpf1/dgpf1/fields.py @@ -47,4 +47,4 @@ def detail_result_display_fields(result): except Exception: # If the date cannot be parsed, just leave it. pass - return display_fields \ No newline at end of file + return display_fields From 95beb08f57cc86f79502661d23b41ccda68f96f5 Mon Sep 17 00:00:00 2001 From: Steve Turoscy Date: Tue, 18 Aug 2026 11:44:28 -0400 Subject: [PATCH 6/6] Add access-software-v4 search and detail UI - Wire up field mappers (title, general_info, detail_result_display_fields) for the software catalog index, pulling Name as the page/card title - Add index-specific detail template showing properties in a clean table, with URL rendered as a link and list-valued fields as
    - Add index-specific search results template showing software Name as the card link and Description in the card body - Trim facets from 15 down to 6 user-facing filters (Category, Keywords, Support Status, Organization, Resource Group, Resource) - Add combine_info_group facet modifier to merge Info_GroupID/Name into a single "Resource Group" facet (currently commented out) Co-Authored-By: Claude Sonnet 4.6 --- dgpf1/dgpf1/facet_modifiers.py | 14 ++++++ dgpf1/dgpf1/fields.py | 42 ++++++++++++++++ dgpf1/dgpf1/settings.py | 26 +++++----- .../v2/components/search-results.html | 37 ++++++++++++++ .../v2/detail-overview.html | 50 +++++++++++++++++++ 5 files changed, 155 insertions(+), 14 deletions(-) create mode 100644 dgpf1/templates/access-software-v4/globus-portal-framework/v2/components/search-results.html create mode 100644 dgpf1/templates/access-software-v4/globus-portal-framework/v2/detail-overview.html diff --git a/dgpf1/dgpf1/facet_modifiers.py b/dgpf1/dgpf1/facet_modifiers.py index 18719ad..e7abfdb 100644 --- a/dgpf1/dgpf1/facet_modifiers.py +++ b/dgpf1/dgpf1/facet_modifiers.py @@ -18,3 +18,17 @@ def lookup_replace_provider_id(facets: dict) -> dict: "added to the database!") bucket["custom_display_value"] = "Other" return facets + + +def combine_info_group(facets: list) -> list: + """Collapse Info_GroupID and Info_GroupName into a single 'Resource Group' + facet that filters by name. Drops the raw ID facet from display.""" + combined = [] + for facet in facets: + if facet["field_name"] == "Info_GroupID": + continue + if facet["field_name"] == "Info_GroupName": + facet = dict(facet) + facet["name"] = "Resource Group" + combined.append(facet) + return combined diff --git a/dgpf1/dgpf1/fields.py b/dgpf1/dgpf1/fields.py index 3d57b00..b87c6f6 100644 --- a/dgpf1/dgpf1/fields.py +++ b/dgpf1/dgpf1/fields.py @@ -5,6 +5,10 @@ def title(result): return result[0]['Title'] +def software_title(result): + return result[0].get('Name', 'Software Detail') + + def general_info(result): """Return all basic information in the first gmeta entry""" return result[0] @@ -48,3 +52,41 @@ def detail_result_display_fields(result): # If the date cannot be parsed, just leave it. pass return display_fields + + +def software_detail_result_display_fields(result): + possible_date_fields = ["CreationTime"] + info = general_info(result) + leading_fields = [ + {"field_name": "Name"}, + {"field_name": "Category"}, + {"field_name": "Keywords"}, + {"field_name": "SupportStatus", "display_name": "Support Status"}, + {"field_name": "Version"}, + {"field_name": "URL"}, + {"field_name": "Organization_Name", "display_name": "Organization Name"}, + {"field_name": "Organization_ID", "display_name": "Organization ID"}, + {"field_name": "Info_GroupName", "display_name": "Resource Group"}, + {"field_name": "Info_GroupID", "display_name": "Resource Group ID"}, + {"field_name": "Info_ResourceName", "display_name": "Resource Name"}, + {"field_name": "Info_ResourceID", "display_name": "Resource ID"}, + ] + trailing_fields = [ + {"field_name": "HandleKey", "display_name": "Handle Key"}, + {"field_name": "HandleType", "display_name": "Handle Type"}, + {"field_name": "CreationTime", "display_name": "Creation Time"}, + ] + known_field_names = [fl["field_name"] for fl in leading_fields + trailing_fields] + other_fields = [{"field_name": f} for f in info if f not in known_field_names] + display_fields = leading_fields + other_fields + trailing_fields + for item in display_fields: + item["display_name"] = item.get("display_name", item["field_name"].replace("_", " ")) + item["value"] = info.get(item["field_name"]) + item["is_list"] = isinstance(item["value"], list) + if item["field_name"] in possible_date_fields: + try: + item["value"] = datetime.datetime.fromisoformat(item["value"].replace("Z", "+00:00")).strftime("%b %d %Y %H:%M:%S %Z%z") + item["is_list"] = False + except Exception: + pass + return display_fields diff --git a/dgpf1/dgpf1/settings.py b/dgpf1/dgpf1/settings.py index 645678d..d6d9332 100644 --- a/dgpf1/dgpf1/settings.py +++ b/dgpf1/dgpf1/settings.py @@ -19,7 +19,7 @@ import json import os import sys -from dgpf1.fields import title, general_info, detail_result_display_fields +from dgpf1.fields import title, general_info, detail_result_display_fields, software_title, software_detail_result_display_fields #import pdb #pdb.set_trace() @@ -299,26 +299,24 @@ 'uuid': '3cc4aeec-55a5-4cd6-96d1-8531aef88e83', 'facets': [ {'name': 'Category', 'field_name': 'Category'}, - {'name': 'Creation Time', 'field_name': 'CreationTime'}, - {'name': 'Handle Key', 'field_name': 'HandleKey'}, - {'name': 'Handle Type', 'field_name': 'HandleType'}, - {'name': 'Info Group ID', 'field_name': 'Info_GroupID'}, - {'name': 'Info Group Name', 'field_name': 'Info_GroupName'}, - {'name': 'Info Resource ID', 'field_name': 'Info_ResourceID'}, - {'name': 'Info Resource Name', 'field_name': 'Info_ResourceName'}, {'name': 'Keywords', 'field_name': 'Keywords'}, - {'name': 'Name', 'field_name': 'Name'}, - {'name': 'Organization ID', 'field_name': 'Organization_ID'}, - {'name': 'Organization Name', 'field_name': 'Organization_Name'}, {'name': 'Support Status', 'field_name': 'SupportStatus'}, - {'name': 'URL', 'field_name': 'URL'}, - {'name': 'Version', 'field_name': 'Version'}, + {'name': 'Organization', 'field_name': 'Organization_Name'}, + {'name': 'Resource Group', 'field_name': 'Info_GroupName'}, + {'name': 'Resource', 'field_name': 'Info_ResourceName'}, ], 'facet_modifiers': [ 'globus_portal_framework.modifiers.facets.drop_empty', + # 'dgpf1.facet_modifiers.combine_info_group', ], + 'fields': [ + ('title', software_title), + ('general_info', general_info), + ('detail_result_display_fields', software_detail_result_display_fields), + ], + 'template_override_dir': 'access-software-v4', } } PROJECT_TITLE = 'Search Pilot' -APP_VERSION = CONF.get('APP_VERSION', '') +APP_VERSION = CONF.get('APP_VERSION', '') \ No newline at end of file diff --git a/dgpf1/templates/access-software-v4/globus-portal-framework/v2/components/search-results.html b/dgpf1/templates/access-software-v4/globus-portal-framework/v2/components/search-results.html new file mode 100644 index 0000000..f8bbd6a --- /dev/null +++ b/dgpf1/templates/access-software-v4/globus-portal-framework/v2/components/search-results.html @@ -0,0 +1,37 @@ + +{% load static %} +{% load urldecode %} +{% block headextras %}{% endblock %} + +
    +
    +

    Results + {% if search.total %} + + + + {% endif %} +

    + + {% if search.total %} +
    {{search.total}} software items found
    + {% endif %} +
    +
    + +
    + {% for result in search.search_results %} +
    + +
    + {% if result.general_info.Description %} +

    {{ result.general_info.Description }}

    + {% endif %} +
    +
    + {% endfor %} +
    diff --git a/dgpf1/templates/access-software-v4/globus-portal-framework/v2/detail-overview.html b/dgpf1/templates/access-software-v4/globus-portal-framework/v2/detail-overview.html new file mode 100644 index 0000000..ee50c15 --- /dev/null +++ b/dgpf1/templates/access-software-v4/globus-portal-framework/v2/detail-overview.html @@ -0,0 +1,50 @@ +{% extends 'globus-portal-framework/v2/detail-base.html' %} + + +{% block breadcrumb_items %} + + + +{% endblock %} + + +{% block detail_body %} + +
    +
    + + {% block detail_search_content %} +

    {{ title|default:'Software' }} — Properties

    + + {% load urldecode %} +
+ + {% for item in detail_result_display_fields %} + {% if item.value %} + + + + + {% endif %} + {% endfor %} + +
{{ item.display_name }} + {% if item.field_name == "URL" %} + {{ item.value }} + {% elif item.is_list %} +
    + {% for v in item.value %}
  • {{ v }}
  • {% endfor %} +
+ {% else %} + {{ item.value }} + {% endif %} +
+ +
Globus Search Metadata
+ {% include 'globus-portal-framework/v2/components/detail-globus-search-metadata.html' %} + {% endblock %} + +
+
+ +{% endblock %}