diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d5ef652602..e08b7445f4d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,47 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). +## [7.12.0](https://github.com/specify/specify7/compare/v7.11.2...v7.12.0) (1 April 2026) + +### Added + +* Introduces the **Guided Setup Tool**, a step-by-step wizard for new Specify installations that enables the initial creation of institutions, divisions, and disciplines ([#7647](https://github.com/specify/specify7/pull/7647), [#7674](https://github.com/specify/specify7/pull/7674)) +* Adds the **System Configuration Tool**, a new administrative interface for managing high-level system settings and infrastructure configurations without direct database access ([#7312](https://github.com/specify/specify7/pull/7312), [#7647](https://github.com/specify/specify7/pull/7647)) +* Introduces a **UI Branding Refresh**, featuring new logos, updated color palettes, and modern graphics, including accessibility improvements to meet WCAG 2.1 AA compliance ([#7788](https://github.com/specify/specify7/pull/7788)) +* Adds a **Visual Editor for Field Formatters**, providing a visual interface for configuration and reducing the need for manual XML/JSON editing ([#5075](https://github.com/specify/specify7/pull/5075)) +* Adds a **Collection Preferences UI** for managing collection-specific settings directly within the application ([#7557](https://github.com/specify/specify7/pull/7557), [#7608](https://github.com/specify/specify7/pull/7608)) +* Adds the **Tree Import** feature, enabling the direct import of new default tree data for Taxon, Storage, and other hierarchical trees ([#6429](https://github.com/specify/specify7/pull/6429)) +* Adds support for using **Interaction Identifiers** (preparation identifiers) when creating or adding items in the Interactions modules, including Loans ([#7644](https://github.com/specify/specify7/pull/7644)) +* Adds support for the **Components** data model, enabling the capture of constituent parts as named or numbered parts of a Collection Object ([#6721](https://github.com/specify/specify7/pull/6721)) +* Adds zoom support for images within the attachment previewer ([#7526](https://github.com/specify/specify7/pull/7526)) +* Adds Batch Edit support for attachment-related tables to streamline metadata management ([#7453](https://github.com/specify/specify7/pull/7453)) +* Adds the `ALLOW_SUPPORT_LOGIN` environment variable to Docker to facilitate troubleshooting by support staff ([#7399](https://github.com/specify/specify7/pull/7399)) +* Adds support for automatic database user creation for new Specify 7 instances ([#6389](https://github.com/specify/specify7/pull/6389)) + +### Changed + +* Updates the **Query Builder "NOT" logic** to include records with empty (null) values by default for "In", "Contains", or "=" comparisons ([#7477](https://github.com/specify/specify7/pull/7477), [#7651](https://github.com/specify/specify7/pull/7651)) +* Enhances the **Catalog Number Search** to intelligently detect if a number is numeric or alphanumeric before searching to prevent casting errors ([#7469](https://github.com/specify/specify7/pull/7469)) +* Moves attachment downloading for record sets to the backend to improve performance ([#6625](https://github.com/specify/specify7/pull/6625)) +* Changes **Object Formatters** to automatically apply date formatting to temporal fields ([#7807](https://github.com/specify/specify7/pull/7807)) +* Replaces legacy table icons with modern `SvgIcon` components ([#7429](https://github.com/specify/specify7/pull/7429)) +* Upgrades the backend framework **Django to version 4.2.27** ([#7591](https://github.com/specify/specify7/pull/7591)) + +### Fixed + +* Fixes an issue in **Workbench** where attachment imports could become stuck ([#7798](https://github.com/specify/specify7/pull/7798)) +* Fixes an issue where deleting a dataset in Workbench incorrectly navigated the user away from the page ([#7519](https://github.com/specify/specify7/pull/7519)) +* Fixes an issue where **Quantity Resolved** enforcement was not correctly handled in validation ([#7670](https://github.com/specify/specify7/pull/7670)) +* Fixes the **Auto-populate** preference during record merging ([#7478](https://github.com/specify/specify7/pull/7478)) +* Fixes an issue preventing the cloning of **Collection Object Attribute (COA)** values ([#7538](https://github.com/specify/specify7/pull/7538)) +* Fixes ordering issues in tree queries ([#7528](https://github.com/specify/specify7/pull/7528)) and bad structures on taxon imports ([#7765](https://github.com/specify/specify7/pull/7765)) +* Fixes a regression that prevented manual typing in tree rank picklists ([#7597](https://github.com/specify/specify7/pull/7597)) +* Fixes an issue that caused broken transactions during autonumbering ([#7671](https://github.com/specify/specify7/pull/7671)) +* Implements a "Delete Blockers" hotfix to resolve stability issues during record deletion ([#7833](https://github.com/specify/specify7/pull/7833)) +* Fixes an issue in **Firefox** where "Download All" failed for single attachments ([#6619](https://github.com/specify/specify7/pull/6619)) +* Fixes an issue with multi-select functionality in embedded record sets ([#7796](https://github.com/specify/specify7/pull/7796)) +* Fixes Host Taxon disambiguation cases in query results ([#7509](https://github.com/specify/specify7/pull/7509)) + ## [7.11.4](https://github.com/specify/specify7/compare/v7.11.3...v7.11.4) (5 February 2026) ### Fixed @@ -12,7 +53,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). * Fixes an issue with auto-incrementing for the 'Treatment Number' field in 'Treatment Event' ([#7560](https://github.com/specify/specify7/issues/7560) - *Reported by SDNHM and CSIRO*) * Solves an issue that prevented the upload of records with auto-incrementing fields when other users are creating records in the same table ([#4894](https://github.com/specify/specify7/issues/4894) - *Reported by RBGE and others*) - ## [7.11.3](https://github.com/specify/specify7/compare/v7.11.2.1..v7.11.3) (12 November 2025) ### Added diff --git a/config/common/common.views.xml b/config/common/common.views.xml index 50d60fc1402..7f8fff71ad7 100644 --- a/config/common/common.views.xml +++ b/config/common/common.views.xml @@ -5552,7 +5552,7 @@ --> - + subform on the Agent form. 100px,2px,473px,5px,120px,2px,125px,0px,15px,p:g diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index 8c392bd3e71..e52a68dc66e 100755 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -13,7 +13,7 @@ if [ "$1" = 've/bin/gunicorn' ] || [ "$1" = 've/bin/python' ]; then ./sp7_db_setup_check.sh # Setup db users and run mirgations # ve/bin/python manage.py base_specify_migration # ve/bin/python manage.py migrate - # ve/bin/python manage.py run_key_migration_functions # Uncomment if you want the key migration functions to run on startup. + ve/bin/python manage.py run_key_migration_functions # Uncomment if you want the key migration functions to run on startup. set -e fi exec "$@" diff --git a/specifyweb/backend/delete_blockers/views.py b/specifyweb/backend/delete_blockers/views.py index b579dc0defa..034a5339b2c 100644 --- a/specifyweb/backend/delete_blockers/views.py +++ b/specifyweb/backend/delete_blockers/views.py @@ -22,8 +22,6 @@ def delete_blockers(request, model, id): using = router.db_for_write(obj.__class__, instance=obj) if obj._meta.model_name == 'discipline': # Special case for discipline - if not request.specify_user.is_admin(): - return http.HttpResponseForbidden('Specifyuser must be an institution admin') guard_blockers = get_discipline_delete_guard_blockers(obj) if guard_blockers: result = guard_blockers @@ -54,4 +52,4 @@ def _collect_delete_blockers(obj, using) -> list[dict]: ]) def flatten(l): - return [item for sublist in l for item in sublist] \ No newline at end of file + return [item for sublist in l for item in sublist] diff --git a/specifyweb/backend/stored_queries/format.py b/specifyweb/backend/stored_queries/format.py index 079a9674db6..947d48b9a87 100644 --- a/specifyweb/backend/stored_queries/format.py +++ b/specifyweb/backend/stored_queries/format.py @@ -406,7 +406,9 @@ def _dateformat(self, specify_field, field): if specify_field.type == "java.sql.Timestamp": return func.date_format(field, "%Y-%m-%dT%H:%i:%s") - prec_fld = getattr(field.class_, specify_field.name + 'Precision', None) + prec_fld = None + if hasattr(field, 'class_'): + prec_fld = getattr(field.class_, specify_field.name + 'Precision', None) # format_expr = ( # case( diff --git a/specifyweb/backend/trees/urls.py b/specifyweb/backend/trees/urls.py index acb3809e12a..cff4ea19989 100644 --- a/specifyweb/backend/trees/urls.py +++ b/specifyweb/backend/trees/urls.py @@ -26,4 +26,5 @@ re_path(r'^create_default_tree/status/(?P[^/]+)/$', views.default_tree_upload_status), re_path(r'^create_default_tree/abort/(?P[^/]+)/$', views.abort_default_tree_creation), path('default_tree_mapping/', views.default_tree_mapping), + path('db_encoding/', views.get_db_encoding), ] \ No newline at end of file diff --git a/specifyweb/backend/trees/views.py b/specifyweb/backend/trees/views.py index 1e4068b05b9..bd04ec2522f 100644 --- a/specifyweb/backend/trees/views.py +++ b/specifyweb/backend/trees/views.py @@ -928,3 +928,16 @@ def default_tree_mapping(request) -> http.HttpResponse: return http.JsonResponse({'error': f'Default tree mapping is invalid: {e}'}, status=400) return http.JsonResponse(tree_cfg) + +def get_db_encoding(request): + cursor = connection.cursor() + + cursor.execute("SELECT @@character_set_database;") + row = cursor.fetchone() + + encoding = row[0] if row else None + + return http.HttpResponse( + json.dumps({"encoding": encoding}), + content_type='application/json' + ) \ No newline at end of file diff --git a/specifyweb/frontend/js_src/lib/components/AppResources/__tests__/CreateAppResource.test.tsx b/specifyweb/frontend/js_src/lib/components/AppResources/__tests__/CreateAppResource.test.tsx index 8db4698e4d4..9b3d5f19f4e 100644 --- a/specifyweb/frontend/js_src/lib/components/AppResources/__tests__/CreateAppResource.test.tsx +++ b/specifyweb/frontend/js_src/lib/components/AppResources/__tests__/CreateAppResource.test.tsx @@ -1,13 +1,14 @@ +import { waitFor } from '@testing-library/react'; import React from 'react'; + +import { clearIdStore } from '../../../hooks/useId'; +import { requireContext } from '../../../tests/helpers'; import { mount } from '../../../tests/reactUtils'; -import { CreateAppResource } from '../Create'; import { TestComponentWrapperRouter } from '../../../tests/utils'; -import { testAppResources } from './testAppResources'; -import { requireContext } from '../../../tests/helpers'; -import { UnloadProtectsContext } from '../../Router/UnloadProtect'; -import { clearIdStore } from '../../../hooks/useId'; import { LoadingContext } from '../../Core/Contexts'; -import { waitFor } from '@testing-library/react'; +import { UnloadProtectsContext } from '../../Router/UnloadProtect'; +import { CreateAppResource } from '../Create'; +import { testAppResources } from './testAppResources'; requireContext(); @@ -22,9 +23,9 @@ describe('CreateAppResource', () => { const { getByRole } = mount( @@ -42,9 +43,9 @@ describe('CreateAppResource', () => { const { getAllByRole, user, getByRole } = mount( @@ -56,9 +57,9 @@ describe('CreateAppResource', () => { await user.click(appResourceButton); // This is a lot more cleaner than the inner HTML - expect(getByRole('dialog').textContent).toMatchInlineSnapshot( - `"Select Resource TypeTypeDocumentationLabelDocumentation(opens in a new tab)ReportDocumentation(opens in a new tab)Default User PreferencesDocumentation(opens in a new tab)Leaflet LayersDocumentation(opens in a new tab)RSS Export FeedDocumentation(opens in a new tab)Express Search ConfigDocumentation(opens in a new tab)Type SearchesDocumentation(opens in a new tab)Web LinksDocumentation(opens in a new tab)Field FormattersDocumentation(opens in a new tab)Record FormattersDocumentation(opens in a new tab)Data Entry TablesDocumentation(opens in a new tab)Interactions TablesDocumentation(opens in a new tab)Other XML ResourceOther JSON ResourceOther Properties ResourceOther ResourceCancel"` - ); + expect(getByRole('dialog').textContent).toMatchInlineSnapshot(`"Select Resource TypeTypeDocumentationLabelDocumentationReportDocumentationDefault User PreferencesDocumentationLeaflet LayersDocumentationRSS Export FeedDocumentationExpress Search ConfigDocumentationType SearchesDocumentationWeb LinksDocumentationField FormattersDocumentationRecord FormattersDocumentationData Entry TablesDocumentationInteractions TablesDocumentationOther XML ResourceOther JSON ResourceOther Properties ResourceOther ResourceCancel"` + +); }); test('simple Form type (mimetype undefined)', async () => { @@ -66,9 +67,9 @@ describe('CreateAppResource', () => { const promiseHandler = jest.fn(); const { getAllByRole, user, getByRole, asFragment } = mount( @@ -86,8 +87,10 @@ describe('CreateAppResource', () => { expect(asFragment).toThrowErrorMatchingSnapshot(``); }); } catch { - // This is hacky. Essentially, we want to wait till the dialog gets populated - // since the useAsyncState won't resolve immediately. + /* + * This is hacky. Essentially, we want to wait till the dialog gets populated + * since the useAsyncState won't resolve immediately. + */ } expect(getByRole('dialog').textContent).toMatchInlineSnapshot( diff --git a/specifyweb/frontend/js_src/lib/components/Atoms/Link.tsx b/specifyweb/frontend/js_src/lib/components/Atoms/Link.tsx index 9497a48c842..d762b6ab951 100644 --- a/specifyweb/frontend/js_src/lib/components/Atoms/Link.tsx +++ b/specifyweb/frontend/js_src/lib/components/Atoms/Link.tsx @@ -1,7 +1,6 @@ import React from 'react'; import type { LocalizedString } from 'typesafe-i18n'; -import { commonText } from '../../localization/common'; import type { IR, RA, RR } from '../../utils/types'; import { className } from './className'; import type { IconProps } from './Icons'; @@ -35,20 +34,23 @@ const linkComponent = = RR>( export const Link = { Default: linkComponent('Link.Default', className.link), - NewTab: linkComponent('Link.NewTab', className.link, (props) => ({ - ...props, - target: '_blank', - rel: 'noopener', - children: ( - <> - {props.children} - - {commonText.opensInNewTab()} - {icons.externalLink} - - - ), - })), + NewTab: linkComponent('Link.NewTab', className.link, (props) => { + const hasChildren = React.Children.count(props.children) > 0; + return { + ...props, + target: '_blank', + rel: 'noopener', + 'aria-label': + props['aria-label'] ?? + (hasChildren ? undefined : (props.title ?? props.href)), + children: ( + <> + {props.children} + {icons.externalLink} + + ), + }; + }), Small: linkComponent<{ /* * A class name that is responsible for text and background color diff --git a/specifyweb/frontend/js_src/lib/components/Atoms/__tests__/__snapshots__/DataEntry.test.ts.snap b/specifyweb/frontend/js_src/lib/components/Atoms/__tests__/__snapshots__/DataEntry.test.ts.snap index 129331c1e93..1d35add6612 100644 --- a/specifyweb/frontend/js_src/lib/components/Atoms/__tests__/__snapshots__/DataEntry.test.ts.snap +++ b/specifyweb/frontend/js_src/lib/components/Atoms/__tests__/__snapshots__/DataEntry.test.ts.snap @@ -272,13 +272,8 @@ exports[`DataEntry.visit no resource 2`] = ` title="Open in New Tab" >