Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
8646b82
fix: run_key_migration_functions always
grantfitzsimmons Mar 24, 2026
573f744
Feat: Prevent user with non UTF-8 encoding to create geo tree
Mar 25, 2026
36533bb
Fix: Allow non admin to have access to discipline resource
Mar 25, 2026
752bc30
Lint code with ESLint and Prettier
Mar 25, 2026
18484f6
Allow non-admin access to discipline resource
CarolineDenis Mar 25, 2026
68c1fef
Merge pull request #7851 from specify/issue-7848-1
grantfitzsimmons Mar 26, 2026
158d872
feat: update CHANGELOG
grantfitzsimmons Mar 26, 2026
7c82b53
Merge pull request #7843 from specify/v7_12_0
grantfitzsimmons Mar 26, 2026
7a62095
Merge branch 'v7.12.0-prerelease' into issue-7842-3
grantfitzsimmons Mar 26, 2026
efc16cc
Fix: Remove login required for encoding route
Mar 26, 2026
9c0421d
Merge pull request #7850 from specify/issue-7842-3
CarolineDenis Mar 26, 2026
3c94db0
Check if _class attribute exists first
alesan99 Mar 27, 2026
d8a23b0
Merge pull request #7857 from specify/issue-7856
grantfitzsimmons Mar 30, 2026
b436050
fix: remove critters
grantfitzsimmons Apr 1, 2026
70f5d4a
feat: add critterless splash screen
grantfitzsimmons Apr 1, 2026
d0dcf6f
Lint code with ESLint and Prettier
grantfitzsimmons Apr 1, 2026
cf24519
fix(forms): fix minor typo in identifiers
grantfitzsimmons Apr 6, 2026
177aea8
fix: colors for better contrast
grantfitzsimmons Apr 9, 2026
1381138
fix: increase contrast for gray headers
grantfitzsimmons Apr 9, 2026
7a2032c
fix: remove redundant links in query results
grantfitzsimmons Apr 9, 2026
8ddd79c
fix: contrast issues in dark mode and link aria
grantfitzsimmons Apr 9, 2026
7315667
Lint code with ESLint and Prettier
grantfitzsimmons Apr 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 41 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion config/common/common.views.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5552,7 +5552,7 @@
</row>-->
</rows>
</viewdef>
<viewdef name="AgentIdentifiers" class="edu.ku.brc.specify.datamodel.AgentIdentifier" type="form" gettable="edu.ku.brc.af.ui.forms.DataGetterForObj" settable="edu.ku.brc.af.ui.forms.DataSetterForObj" useresourcelabels="true">
<viewdef name="AgentIdentifier" class="edu.ku.brc.specify.datamodel.AgentIdentifier" type="form" gettable="edu.ku.brc.af.ui.forms.DataGetterForObj" settable="edu.ku.brc.af.ui.forms.DataSetterForObj" useresourcelabels="true">
<desc>subform on the Agent form.</desc>
<enableRules/>
<columnDef>100px,2px,473px,5px,120px,2px,125px,0px,15px,p:g</columnDef>
Expand Down
2 changes: 1 addition & 1 deletion docker-entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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 "$@"
4 changes: 1 addition & 3 deletions specifyweb/backend/delete_blockers/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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]
return [item for sublist in l for item in sublist]
4 changes: 3 additions & 1 deletion specifyweb/backend/stored_queries/format.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
1 change: 1 addition & 0 deletions specifyweb/backend/trees/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,5 @@
re_path(r'^create_default_tree/status/(?P<task_id>[^/]+)/$', views.default_tree_upload_status),
re_path(r'^create_default_tree/abort/(?P<task_id>[^/]+)/$', views.abort_default_tree_creation),
path('default_tree_mapping/', views.default_tree_mapping),
path('db_encoding/', views.get_db_encoding),
]
13 changes: 13 additions & 0 deletions specifyweb/backend/trees/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'
)
Original file line number Diff line number Diff line change
@@ -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();

Expand All @@ -22,9 +23,9 @@ describe('CreateAppResource', () => {

const { getByRole } = mount(
<TestComponentWrapperRouter
context={{ getSet: [testAppResources, setter] }}
initialEntries={['/resources/create/discipline_3']}
path="resources/create/:directoryKey"
context={{ getSet: [testAppResources, setter] }}
>
<UnloadProtectsContext.Provider value={[]}>
<CreateAppResource />
Expand All @@ -42,9 +43,9 @@ describe('CreateAppResource', () => {

const { getAllByRole, user, getByRole } = mount(
<TestComponentWrapperRouter
context={{ getSet: [testAppResources, setter] }}
initialEntries={['/resources/create/discipline_3']}
path="resources/create/:directoryKey"
context={{ getSet: [testAppResources, setter] }}
>
<UnloadProtectsContext.Provider value={[]}>
<CreateAppResource />
Expand All @@ -56,19 +57,19 @@ 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 () => {
const setter = jest.fn();
const promiseHandler = jest.fn();
const { getAllByRole, user, getByRole, asFragment } = mount(
<TestComponentWrapperRouter
context={{ getSet: [testAppResources, setter] }}
initialEntries={['/resources/create/discipline_3']}
path="resources/create/:directoryKey"
context={{ getSet: [testAppResources, setter] }}
>
<UnloadProtectsContext.Provider value={[]}>
<LoadingContext.Provider value={promiseHandler}>
Expand All @@ -86,8 +87,10 @@ describe('CreateAppResource', () => {
expect(asFragment).toThrowErrorMatchingSnapshot(`<DocumentFragment />`);
});
} 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(
Expand Down
32 changes: 17 additions & 15 deletions specifyweb/frontend/js_src/lib/components/Atoms/Link.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -35,20 +34,23 @@ const linkComponent = <EXTRA_PROPS extends IR<unknown> = RR<never, never>>(

export const Link = {
Default: linkComponent('Link.Default', className.link),
NewTab: linkComponent('Link.NewTab', className.link, (props) => ({
...props,
target: '_blank',
rel: 'noopener',
children: (
<>
{props.children}
<span title={commonText.opensInNewTab()}>
<span className="sr-only">{commonText.opensInNewTab()}</span>
{icons.externalLink}
</span>
</>
),
})),
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}
<span aria-hidden>{icons.externalLink}</span>
</>
),
};
}),
Small: linkComponent<{
/*
* A class name that is responsible for text and background color
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -272,13 +272,8 @@ exports[`DataEntry.visit no resource 2`] = `
title="Open in New Tab"
>
<span
title="(opens in a new tab)"
aria-hidden="true"
>
<span
class="sr-only"
>
(opens in a new tab)
</span>
<svg
aria-hidden="true"
class="w-6 h-6 flex-shrink-0"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,13 +101,8 @@ exports[`Link.NewTab renders without errors 1`] = `
>
Close
<span
title="(opens in a new tab)"
aria-hidden="true"
>
<span
class="sr-only"
>
(opens in a new tab)
</span>
<svg
aria-hidden="true"
class="w-6 h-6 flex-shrink-0"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ exports[`H2 renders without errors 1`] = `
exports[`H3 renders without errors 1`] = `
<DocumentFragment>
<h3
class="text-gray-500 dark:text-neutral-400"
class="text-gray-600 dark:text-neutral-400"
>
View
</h3>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ export const className = {
formStyles,
limitedWidth: `max-w-[var(--max-field-width)]`,
headerPrimary: 'font-semibold text-black dark:text-white',
headerGray: 'text-gray-500 dark:text-neutral-400',
headerGray: 'text-gray-600 dark:text-neutral-400',
// These values must be synchronised with main.css
dataEntryGrid: 'data-entry-grid',
formFooter:
Expand Down
2 changes: 1 addition & 1 deletion specifyweb/frontend/js_src/lib/components/Header/Logo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ export function Logo({
src={collapsedLogo}
/>
)}
<span className="sr-only">{commonText.goToHomepage()}</span>
<span className="sr-only text-white">{commonText.goToHomepage()}</span>
</a>
</h1>
);
Expand Down
20 changes: 16 additions & 4 deletions specifyweb/frontend/js_src/lib/components/HomePage/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,16 @@ export function WelcomeView(): JSX.Element {
);
}

function getCritterlessWelcomePageImage(isDarkMode: boolean): string {
return isDarkMode
? '/static/img/critterless_splash_screen_dark.svg'
: '/static/img/critterless_splash_screen.svg';
}

function WelcomeScreenContent(): JSX.Element {
const [mode] = userPreferences.use('welcomePage', 'general', 'mode');
const [source] = userPreferences.use('welcomePage', 'general', 'source');
const isDarkMode = useDarkMode();

return mode === 'embeddedWebpage' ? (
<iframe
Expand All @@ -66,21 +73,26 @@ function WelcomeScreenContent(): JSX.Element {
title={welcomeText.pageTitle()}
/>
) : mode === 'default' ? (
<DefaultSplashScreen />
<DefaultSplashScreen source={getDefaultWelcomePageImage(isDarkMode)} />
) : mode === 'critterless' ? (
<DefaultSplashScreen source={getCritterlessWelcomePageImage(isDarkMode)} />
) : (
<img alt="" className="h-full" src={source} />
);
}

function DefaultSplashScreen(): JSX.Element {
function DefaultSplashScreen({
source,
}: {
readonly source: string;
}): JSX.Element {
const hueDifference = useHueDifference();
const isDarkMode = useDarkMode();
return (
<div className="relative">
<img
alt=""
className="w-[800px]"
src={getDefaultWelcomePageImage(isDarkMode)}
src={source}
style={{ filter: `hue-rotate(${hueDifference}deg)` }}
/>
{/* The following gradients in the divs are here to apply a fade out effect on the image */}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ export function Notifications({
'aria-live': 'polite',
className:
unreadCount > 0 && !isOpen
? '[&:not(:hover)]:!text-brand-300 [&:not(:hover)]:dark:!text-brand-400'
? '[&:not(:hover)]:!text-brand-200 [&:not(:hover)]:dark:!text-brand-400'
: undefined,
disabled: notificationCount === 0,
}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@ export function FontFamilyPreferenceItem({
}

export type WelcomePageMode =
| 'critterless'
| 'customImage'
| 'default'
| 'embeddedWebpage'
Expand All @@ -250,6 +251,10 @@ const welcomePageModes: PreferenceItem<WelcomePageMode> = {
value: 'default',
title: preferencesText.defaultImage(),
},
{
value: 'critterless',
title: preferencesText.critterless(),
},
{
value: 'taxonTiles',
title: welcomeText.taxonTiles(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -352,7 +352,7 @@ export const userPreferenceDefinitions = {
title: preferencesText.accentColor2(),
requiresReload: false,
visible: true,
defaultValue: '#a4af83',
defaultValue: '#ACB389',
renderer: ColorPickerPreferenceItem,
container: 'label',
}),
Expand Down
Loading
Loading