From 9f55439862e6d492fa1f4949830d20f7162b80a5 Mon Sep 17 00:00:00 2001 From: kwhuber Date: Wed, 26 Nov 2025 07:53:09 -0600 Subject: [PATCH 1/6] fix: user defined Form Definition persists across the app --- .../components/AppResources/EditorWrapper.tsx | 26 ++++++++++++++++--- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/AppResources/EditorWrapper.tsx b/specifyweb/frontend/js_src/lib/components/AppResources/EditorWrapper.tsx index 32e9a169c07..5b1b1d7fa12 100644 --- a/specifyweb/frontend/js_src/lib/components/AppResources/EditorWrapper.tsx +++ b/specifyweb/frontend/js_src/lib/components/AppResources/EditorWrapper.tsx @@ -211,9 +211,27 @@ function useInitialData( ): string | false | undefined { return useAsyncState( React.useCallback(async () => { + const escapeXml = (s: string): string => + s + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"'); + + const replaceViewsetName = (data: string | null | undefined): string => { + const xml = data ?? ''; + const resourceName = (resource as any)?.name ?? ''; + if (typeof resourceName !== 'string' || resourceName.length === 0) + return xml; + return xml.replace( + /(]*\bname=)(["])(.*?)\2/, + (_match, p1, p2) => `${p1}${p2}${escapeXml(resourceName)}${p2}` + ); + }; + if (typeof initialDataFrom === 'number') return fetchResource('SpAppResourceData', initialDataFrom).then( - ({ data }) => data ?? '' + ({ data }) => replaceViewsetName(data) ); else if (typeof templateFile === 'string') { if (templateFile.includes('..')) @@ -224,7 +242,7 @@ function useInitialData( return ajax(`/static/config/${templateFile}`, { headers: {}, }) - .then(({ data }) => data ?? '') + .then(({ data }) => replaceViewsetName(data)) .catch(() => ''); } const subType = f.maybe( @@ -239,12 +257,12 @@ function useInitialData( if (useTemplate) return ajax(getAppResourceUrl(type.name, 'quiet'), { headers: {}, - }).then(({ data }) => data); + }).then(({ data }) => replaceViewsetName(data)); } return false; // Run this only once // eslint-disable-next-line react-hooks/exhaustive-deps - }, [initialDataFrom, templateFile]), + }, [initialDataFrom, templateFile, resource]), false )[0]; } From 3679ca51fb6b9090546bebcaca790dd7d2db3473 Mon Sep 17 00:00:00 2001 From: kwhuber Date: Wed, 26 Nov 2025 09:10:52 -0600 Subject: [PATCH 2/6] fix for front-end test --- .../components/AppResources/EditorWrapper.tsx | 36 +++++++++---------- 1 file changed, 16 insertions(+), 20 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/AppResources/EditorWrapper.tsx b/specifyweb/frontend/js_src/lib/components/AppResources/EditorWrapper.tsx index 5b1b1d7fa12..a00345e466d 100644 --- a/specifyweb/frontend/js_src/lib/components/AppResources/EditorWrapper.tsx +++ b/specifyweb/frontend/js_src/lib/components/AppResources/EditorWrapper.tsx @@ -229,22 +229,18 @@ function useInitialData( ); }; - if (typeof initialDataFrom === 'number') - return fetchResource('SpAppResourceData', initialDataFrom).then( - ({ data }) => replaceViewsetName(data) - ); - else if (typeof templateFile === 'string') { - if (templateFile.includes('..')) - console.error( - 'Relative paths not allowed. Path is always relative to /static/config/' - ); - else - return ajax(`/static/config/${templateFile}`, { - headers: {}, - }) - .then(({ data }) => replaceViewsetName(data)) - .catch(() => ''); + if (typeof initialDataFrom === 'number') { + const { data } = await fetchResource('SpAppResourceData', initialDataFrom); + return replaceViewsetName(data); } + if (typeof templateFile === 'string') { + try { + const { data } = await ajax(`/static/config/${templateFile}`, { headers: {} }); + return replaceViewsetName(data); + } catch { + return ''; + } + } const subType = f.maybe( toResource(resource, 'SpAppResource'), getAppResourceType @@ -254,14 +250,14 @@ function useInitialData( const useTemplate = typeof type.name === 'string' && (!('useTemplate' in type) || type.useTemplate); - if (useTemplate) - return ajax(getAppResourceUrl(type.name, 'quiet'), { + if (useTemplate) { + const { data } = await ajax(getAppResourceUrl(type.name, 'quiet'), { headers: {}, - }).then(({ data }) => replaceViewsetName(data)); + }); + return replaceViewsetName(data); + } } return false; - // Run this only once - // eslint-disable-next-line react-hooks/exhaustive-deps }, [initialDataFrom, templateFile, resource]), false )[0]; From 7e06b4d81e4f94c4934ca775d60d093a3e91cbf5 Mon Sep 17 00:00:00 2001 From: kwhuber Date: Mon, 1 Dec 2025 13:08:29 -0600 Subject: [PATCH 3/6] Stop tracking .env file --- .env | 72 ------------------------------------------------------------ 1 file changed, 72 deletions(-) delete mode 100644 .env diff --git a/.env b/.env deleted file mode 100644 index 36aa5654325..00000000000 --- a/.env +++ /dev/null @@ -1,72 +0,0 @@ -DATABASE_HOST=mariadb -DATABASE_PORT=3306 -MYSQL_ROOT_PASSWORD=password -DATABASE_NAME=specify - -# The following are database users with specific roles and privileges. -# If the migrator and app user are not defined, the system will use the master user credentials. -# See documenation https://discourse.specifysoftware.org/t/new-blank-database-creation-database-user-levels/3023 - -# MASTER Database User -# Full database administrator, used for initial setup and migrations requiring elevated privileges. -MASTER_NAME=root -MASTER_PASSWORD=password - -# MIGRATOR Database User -# User with elevated privileges to perform migrations (create/drop/modify tables, etc.), for Django migration steps. -MIGRATOR_NAME=specify_migrator -MIGRATOR_PASSWORD=specify_migrator - -# APP Database User -# Normal runtime database user that performs application-level operations. -APP_USER_NAME=specify_user -APP_USER_PASSWORD=specify_user - -# Enabling this option allows administrators with access to the -# backend Specify instance to log in as any user for support -# purposes without knowing their password. -# https://discourse.specifysoftware.org/t/allow-support-login-documentation/2838 -ALLOW_SUPPORT_LOGIN=false -# The amount of time in seconds each token is valid for -SUPPORT_LOGIN_TTL = 180 - -# Make sure to set the `SECRET_KEY` to a unique value -SECRET_KEY=change_this_to_some_unique_random_string - -ASSET_SERVER_URL=http://host.docker.internal/web_asset_store.xml -# Make sure to set the `ASSET_SERVER_KEY` to a unique value -ASSET_SERVER_KEY=your_asset_server_access_key - -REDIS_HOST=redis -REDIS_PORT=6379 -REDIS_DB_INDEX=0 - -REPORT_RUNNER_HOST=report-runner -REPORT_RUNNER_PORT=8080 - -CELERY_BROKER_URL=redis://redis/0 -CELERY_RESULT_BACKEND=redis://redis/1 - -# Local time zone for this installation. Choices can be found here: -# https://en.wikipedia.org/wiki/List_of_tz_zones_by_name -# although not all choices may be available on all operating systems. -# On Unix systems, a value of None will cause Django to use the same -# timezone as the operating system. -# If running in a Windows environment this must be set to the same as your -# system time zone. -TIME_ZONE = America/Chicago - -# This variable controls the Specify 7 logging level. Possible values -# are: -# * DEBUG: Low level system information for debugging purposes. -# * INFO: General system information. -# * WARNING: Information describing a minor problem that has occurred. -# * ERROR: Information describing a major problem that has occurred. -# * CRITICAL: Information describing a critical problem that has occurred. -LOG_LEVEL=WARNING - -# Set this variable to `true` to run Specify 7 in debug mode. This -# should only be used during development and troubleshooting and not -# during general use. Django applications leak memory when operated -# continuously in debug mode. -SP7_DEBUG=true From 2c38b66ee7c65fee68dd8e92fce314429958ef87 Mon Sep 17 00:00:00 2001 From: kwhuber Date: Wed, 3 Dec 2025 12:11:29 -0600 Subject: [PATCH 4/6] env --- .env | 72 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 .env diff --git a/.env b/.env new file mode 100644 index 00000000000..36aa5654325 --- /dev/null +++ b/.env @@ -0,0 +1,72 @@ +DATABASE_HOST=mariadb +DATABASE_PORT=3306 +MYSQL_ROOT_PASSWORD=password +DATABASE_NAME=specify + +# The following are database users with specific roles and privileges. +# If the migrator and app user are not defined, the system will use the master user credentials. +# See documenation https://discourse.specifysoftware.org/t/new-blank-database-creation-database-user-levels/3023 + +# MASTER Database User +# Full database administrator, used for initial setup and migrations requiring elevated privileges. +MASTER_NAME=root +MASTER_PASSWORD=password + +# MIGRATOR Database User +# User with elevated privileges to perform migrations (create/drop/modify tables, etc.), for Django migration steps. +MIGRATOR_NAME=specify_migrator +MIGRATOR_PASSWORD=specify_migrator + +# APP Database User +# Normal runtime database user that performs application-level operations. +APP_USER_NAME=specify_user +APP_USER_PASSWORD=specify_user + +# Enabling this option allows administrators with access to the +# backend Specify instance to log in as any user for support +# purposes without knowing their password. +# https://discourse.specifysoftware.org/t/allow-support-login-documentation/2838 +ALLOW_SUPPORT_LOGIN=false +# The amount of time in seconds each token is valid for +SUPPORT_LOGIN_TTL = 180 + +# Make sure to set the `SECRET_KEY` to a unique value +SECRET_KEY=change_this_to_some_unique_random_string + +ASSET_SERVER_URL=http://host.docker.internal/web_asset_store.xml +# Make sure to set the `ASSET_SERVER_KEY` to a unique value +ASSET_SERVER_KEY=your_asset_server_access_key + +REDIS_HOST=redis +REDIS_PORT=6379 +REDIS_DB_INDEX=0 + +REPORT_RUNNER_HOST=report-runner +REPORT_RUNNER_PORT=8080 + +CELERY_BROKER_URL=redis://redis/0 +CELERY_RESULT_BACKEND=redis://redis/1 + +# Local time zone for this installation. Choices can be found here: +# https://en.wikipedia.org/wiki/List_of_tz_zones_by_name +# although not all choices may be available on all operating systems. +# On Unix systems, a value of None will cause Django to use the same +# timezone as the operating system. +# If running in a Windows environment this must be set to the same as your +# system time zone. +TIME_ZONE = America/Chicago + +# This variable controls the Specify 7 logging level. Possible values +# are: +# * DEBUG: Low level system information for debugging purposes. +# * INFO: General system information. +# * WARNING: Information describing a minor problem that has occurred. +# * ERROR: Information describing a major problem that has occurred. +# * CRITICAL: Information describing a critical problem that has occurred. +LOG_LEVEL=WARNING + +# Set this variable to `true` to run Specify 7 in debug mode. This +# should only be used during development and troubleshooting and not +# during general use. Django applications leak memory when operated +# continuously in debug mode. +SP7_DEBUG=true From 27e2049eda19283f229f3c8ac4779447bf97b6ca Mon Sep 17 00:00:00 2001 From: kwhuber Date: Fri, 12 Dec 2025 12:01:46 -0600 Subject: [PATCH 5/6] fixes --- .../lib/components/AppResources/Editor.tsx | 18 +++++- .../components/AppResources/EditorWrapper.tsx | 58 +++++++------------ .../lib/components/AppResources/xmlUtils.ts | 23 ++++++++ 3 files changed, 60 insertions(+), 39 deletions(-) create mode 100644 specifyweb/frontend/js_src/lib/components/AppResources/xmlUtils.ts diff --git a/specifyweb/frontend/js_src/lib/components/AppResources/Editor.tsx b/specifyweb/frontend/js_src/lib/components/AppResources/Editor.tsx index f09c0ee09da..41a2fcd560c 100644 --- a/specifyweb/frontend/js_src/lib/components/AppResources/Editor.tsx +++ b/specifyweb/frontend/js_src/lib/components/AppResources/Editor.tsx @@ -46,11 +46,23 @@ import { AppResourcesTab, useEditorTabs } from './Tabs'; import { getScope } from './tree'; import type { ScopedAppResourceDir } from './types'; import { appResourceSubTypes } from './types'; +import { replaceViewsetNameInXml } from './xmlUtils'; export const AppResourceContext = React.createContext< SpecifyResource >(undefined!); +const syncViewsetNameInXml = ( + data: string | null | undefined, + appResource: SpecifyResource +): string => { + const viewSet = toTable(appResource, 'SpViewSetObj'); + const name = viewSet?.get('name'); + if (typeof data !== 'string') return data ?? ''; + if (typeof name !== 'string' || name.length === 0) return data; + return replaceViewsetNameInXml(data, name); +}; + export function AppResourceEditor({ resource, directory, @@ -291,9 +303,13 @@ export function AppResourceEditor({ typeof lastDataRef.current === 'function' ? lastDataRef.current() : lastDataRef.current; + const syncedData = syncViewsetNameInXml( + data === undefined ? resourceData.data : data, + appResource + ); const appResourceData = deserializeResource({ ...resourceData, - data: data === undefined ? resourceData.data : data, + data: syncedData, spAppResource: toTable(appResource, 'SpAppResource')?.get( 'resource_uri' diff --git a/specifyweb/frontend/js_src/lib/components/AppResources/EditorWrapper.tsx b/specifyweb/frontend/js_src/lib/components/AppResources/EditorWrapper.tsx index a00345e466d..8d51a709050 100644 --- a/specifyweb/frontend/js_src/lib/components/AppResources/EditorWrapper.tsx +++ b/specifyweb/frontend/js_src/lib/components/AppResources/EditorWrapper.tsx @@ -32,6 +32,7 @@ import type { AppResourcesOutlet } from './index'; import { globalResourceKey } from './tree'; import type { ScopedAppResourceDir } from './types'; import { appResourceSubTypes } from './types'; +import { replaceViewsetNameInXml } from './xmlUtils'; export function AppResourceView(): JSX.Element { return ; @@ -197,13 +198,6 @@ function useAppResource( ); } -/* - * REFACTOR: - * Split this function up. - * Currently, the resource is not needed until subtype needs to be determined. - * All the functionality that does not depend on resource should be part of a different - * function. - */ function useInitialData( resource: SerializedResource, initialDataFrom: number | undefined, @@ -211,36 +205,26 @@ function useInitialData( ): string | false | undefined { return useAsyncState( React.useCallback(async () => { - const escapeXml = (s: string): string => - s - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"'); - const replaceViewsetName = (data: string | null | undefined): string => { - const xml = data ?? ''; const resourceName = (resource as any)?.name ?? ''; - if (typeof resourceName !== 'string' || resourceName.length === 0) - return xml; - return xml.replace( - /(]*\bname=)(["])(.*?)\2/, - (_match, p1, p2) => `${p1}${p2}${escapeXml(resourceName)}${p2}` - ); + return replaceViewsetNameInXml(data, resourceName); }; - if (typeof initialDataFrom === 'number') { - const { data } = await fetchResource('SpAppResourceData', initialDataFrom); - return replaceViewsetName(data); - } - if (typeof templateFile === 'string') { - try { - const { data } = await ajax(`/static/config/${templateFile}`, { headers: {} }); - return replaceViewsetName(data); - } catch { - return ''; + if (typeof initialDataFrom === 'number') + return fetchResource('SpAppResourceData', initialDataFrom).then( + ({ data }) => replaceViewsetName(data ?? '')); + else if (typeof templateFile === 'string') { + if (templateFile.includes('..')) + console.error( + 'Relative paths not allowed. Path is always relative to /static/config/' + ); + else + return ajax(`/static/config/${templateFile}`, { + headers: {}, + }) + .then(({ data }) => replaceViewsetName(data)) + .catch(() => ''); } - } const subType = f.maybe( toResource(resource, 'SpAppResource'), getAppResourceType @@ -250,15 +234,13 @@ function useInitialData( const useTemplate = typeof type.name === 'string' && (!('useTemplate' in type) || type.useTemplate); - if (useTemplate) { - const { data } = await ajax(getAppResourceUrl(type.name, 'quiet'), { + if (useTemplate) + return ajax(getAppResourceUrl(type.name, 'quiet'), { headers: {}, - }); - return replaceViewsetName(data); - } + }).then(({ data }) => replaceViewsetName(data)); } return false; - }, [initialDataFrom, templateFile, resource]), + }, [initialDataFrom, templateFile]), false )[0]; } diff --git a/specifyweb/frontend/js_src/lib/components/AppResources/xmlUtils.ts b/specifyweb/frontend/js_src/lib/components/AppResources/xmlUtils.ts new file mode 100644 index 00000000000..b8a86f2dc17 --- /dev/null +++ b/specifyweb/frontend/js_src/lib/components/AppResources/xmlUtils.ts @@ -0,0 +1,23 @@ +// shared utilities for XML manipulation in AppResources +export const escapeXml = (value: string): string => + value + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"'); + +// replace the viewset name attribute in XML content +export const replaceViewsetNameInXml = ( + data: string | null | undefined, + resourceName: string +): string => { + const xml = data ?? ''; + if (typeof resourceName !== 'string' || resourceName.length === 0) + return xml; + return xml.replace( + /(]*\bname=)([\"])(.*?)\2/, + (_match, prefix, quote) => `${prefix}${quote}${escapeXml(resourceName)}${quote}` + ); +}; + + From 966023b9fbcf12be182c545b3cb302bce4c3caed Mon Sep 17 00:00:00 2001 From: kwhuber Date: Fri, 12 Dec 2025 18:05:48 +0000 Subject: [PATCH 6/6] Lint code with ESLint and Prettier Triggered by 27e2049eda19283f229f3c8ac4779447bf97b6ca on branch refs/heads/issue-5168 --- .../components/AppResources/EditorWrapper.tsx | 3 ++- .../lib/components/AppResources/xmlUtils.ts | 22 +++++++++---------- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/AppResources/EditorWrapper.tsx b/specifyweb/frontend/js_src/lib/components/AppResources/EditorWrapper.tsx index 8d51a709050..a45b9ee21b7 100644 --- a/specifyweb/frontend/js_src/lib/components/AppResources/EditorWrapper.tsx +++ b/specifyweb/frontend/js_src/lib/components/AppResources/EditorWrapper.tsx @@ -212,7 +212,8 @@ function useInitialData( if (typeof initialDataFrom === 'number') return fetchResource('SpAppResourceData', initialDataFrom).then( - ({ data }) => replaceViewsetName(data ?? '')); + ({ data }) => replaceViewsetName(data ?? '') + ); else if (typeof templateFile === 'string') { if (templateFile.includes('..')) console.error( diff --git a/specifyweb/frontend/js_src/lib/components/AppResources/xmlUtils.ts b/specifyweb/frontend/js_src/lib/components/AppResources/xmlUtils.ts index b8a86f2dc17..a64dd02f6fa 100644 --- a/specifyweb/frontend/js_src/lib/components/AppResources/xmlUtils.ts +++ b/specifyweb/frontend/js_src/lib/components/AppResources/xmlUtils.ts @@ -1,23 +1,21 @@ -// shared utilities for XML manipulation in AppResources +// Shared utilities for XML manipulation in AppResources export const escapeXml = (value: string): string => value - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"'); + .replaceAll('&', '&') + .replaceAll('<', '<') + .replaceAll('>', '>') + .replaceAll('"', '"'); -// replace the viewset name attribute in XML content +// Replace the viewset name attribute in XML content export const replaceViewsetNameInXml = ( data: string | null | undefined, resourceName: string ): string => { const xml = data ?? ''; - if (typeof resourceName !== 'string' || resourceName.length === 0) - return xml; + if (typeof resourceName !== 'string' || resourceName.length === 0) return xml; return xml.replace( - /(]*\bname=)([\"])(.*?)\2/, - (_match, prefix, quote) => `${prefix}${quote}${escapeXml(resourceName)}${quote}` + /(]*\bname=)(")(.*?)\2/, + (_match, prefix, quote) => + `${prefix}${quote}${escapeXml(resourceName)}${quote}` ); }; - -