From 8646b82589d9557928f4cc957d198527ad6d14c7 Mon Sep 17 00:00:00 2001 From: Grant Fitzsimmons <37256050+grantfitzsimmons@users.noreply.github.com> Date: Tue, 24 Mar 2026 16:06:53 -0500 Subject: [PATCH 01/39] fix: run_key_migration_functions always --- docker-entrypoint.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 "$@" From 573f744d6b01b306708942cbf1a862259c3227a5 Mon Sep 17 00:00:00 2001 From: Caroline Denis Date: Wed, 25 Mar 2026 20:06:23 +0100 Subject: [PATCH 02/39] Feat: Prevent user with non UTF-8 encoding to create geo tree Fixes #7842 --- specifyweb/backend/trees/urls.py | 1 + specifyweb/backend/trees/views.py | 14 ++++ .../lib/components/TreeView/CreateTree.tsx | 74 +++++++++++++++---- .../frontend/js_src/lib/localization/tree.ts | 7 ++ 4 files changed, 83 insertions(+), 13 deletions(-) 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..7ac2889c203 100644 --- a/specifyweb/backend/trees/views.py +++ b/specifyweb/backend/trees/views.py @@ -928,3 +928,17 @@ 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) + +@login_maybe_required +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/TreeView/CreateTree.tsx b/specifyweb/frontend/js_src/lib/components/TreeView/CreateTree.tsx index ca214d44a11..ecfe5b0e57c 100644 --- a/specifyweb/frontend/js_src/lib/components/TreeView/CreateTree.tsx +++ b/specifyweb/frontend/js_src/lib/components/TreeView/CreateTree.tsx @@ -31,6 +31,7 @@ import type { TreeInformation } from '../InitialContext/treeRanks'; import { userInformation } from '../InitialContext/userInformation'; import { Dialog } from '../Molecules/Dialog'; import { defaultTreeDefs } from './defaults'; +import { Link } from '../Atoms/Link'; export type TaxonFileDefaultDefinition = { readonly discipline: string; @@ -459,24 +460,71 @@ export function PopulatedTreeList({ ? treeOptions.filter((r) => r.discipline === discipline) : treeOptions; + const fetchDatabaseEncoding = async () => + ajax<{ readonly encoding: string }>(`/trees/db_encoding/`, { + headers: { Accept: 'application/json' }, + method: 'GET', + }).then(({ data }) => data.encoding); + + const [encoding, setEncoding] = React.useState(null) + + React.useEffect(() => { + fetchDatabaseEncoding().then((encoding) => { + setEncoding(encoding) + }) + }, []) + + const isUTF8 = + encoding !== null && + ['utf8', 'utf8mb4'].includes(encoding.toLowerCase()) + return (

    {treeText.populatedTrees()}

    {displayedOptions === undefined ? undefined - : displayedOptions.map((resource, index) => ( -
  • - handleClick(resource)}> - {localized(resource.title)} - -
    - {resource.description} -
    -
    - {`${treeText.source()}: ${resource.src}`} -
    -
  • - ))} + : displayedOptions.map((resource, index) => + { + const isBlockedGeoTree = + resource.title === 'Geology (Minerals)' && !isUTF8 + + const encodingFormat = typeof encoding === 'string' ? encoding : '' + + if (isBlockedGeoTree) { + return ( +
  • + + {localized(resource.title)} + + +
    + {treeText.utf8EncodingWarning({ encoding: encodingFormat})} +
    + + + {treeText.resolveEncoding()} + +
  • + ) + } + + return ( +
  • + handleClick(resource)}> + {localized(resource.title)} + + +
    + {resource.description} +
    + +
    + {`${treeText.source()}: ${resource.src}`} +
    +
  • + ) + } + )}
); } diff --git a/specifyweb/frontend/js_src/lib/localization/tree.ts b/specifyweb/frontend/js_src/lib/localization/tree.ts index 44693ef13d0..c3e1df2516b 100644 --- a/specifyweb/frontend/js_src/lib/localization/tree.ts +++ b/specifyweb/frontend/js_src/lib/localization/tree.ts @@ -771,4 +771,11 @@ export const treeText = createDictionary({ 'uk-ua': 'Якщо це ввімкнено, користувачі можуть додавати дочірні елементи до синонімізованих батьківських елементів та синонімізувати вузол з дочірніми елементами.', }, + utf8EncodingWarning: { + 'en-us': + 'This tree data cannot be imported in your database because it contains UTF-8 characters and this database uses {encoding:string}', + }, + resolveEncoding: { + 'en-us': 'How to resolve:', + }, } as const); From 36533bb66f6a6b3d9982321c896252bad3ffbfc5 Mon Sep 17 00:00:00 2001 From: Caroline Denis Date: Wed, 25 Mar 2026 20:09:52 +0100 Subject: [PATCH 03/39] Fix: Allow non admin to have access to discipline resource Fixes #7848-1 --- specifyweb/backend/delete_blockers/views.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/specifyweb/backend/delete_blockers/views.py b/specifyweb/backend/delete_blockers/views.py index b579dc0defa..d7f06c7ceae 100644 --- a/specifyweb/backend/delete_blockers/views.py +++ b/specifyweb/backend/delete_blockers/views.py @@ -22,8 +22,9 @@ 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') + # commented out to allow non admin to access discipline resource + # 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 From 752bc30e0ee8fd0a15b9d63dd3a67e3d92afdc6d Mon Sep 17 00:00:00 2001 From: Caroline Denis Date: Wed, 25 Mar 2026 19:10:49 +0000 Subject: [PATCH 04/39] Lint code with ESLint and Prettier Triggered by 573f744d6b01b306708942cbf1a862259c3227a5 on branch refs/heads/issue-7842-3 --- .../frontend/js_src/lib/components/TreeView/CreateTree.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specifyweb/frontend/js_src/lib/components/TreeView/CreateTree.tsx b/specifyweb/frontend/js_src/lib/components/TreeView/CreateTree.tsx index ecfe5b0e57c..b8730b4e5da 100644 --- a/specifyweb/frontend/js_src/lib/components/TreeView/CreateTree.tsx +++ b/specifyweb/frontend/js_src/lib/components/TreeView/CreateTree.tsx @@ -15,6 +15,7 @@ import { Progress } from '../Atoms'; import { Button } from '../Atoms/Button'; import { className } from '../Atoms/className'; import { icons } from '../Atoms/Icons'; +import { Link } from '../Atoms/Link'; import { LoadingContext } from '../Core/Contexts'; import type { AnySchema, @@ -31,7 +32,6 @@ import type { TreeInformation } from '../InitialContext/treeRanks'; import { userInformation } from '../InitialContext/userInformation'; import { Dialog } from '../Molecules/Dialog'; import { defaultTreeDefs } from './defaults'; -import { Link } from '../Atoms/Link'; export type TaxonFileDefaultDefinition = { readonly discipline: string; From 18484f6df373d068a766f5dbc36c32139ceae586 Mon Sep 17 00:00:00 2001 From: "Caroline D." <108160931+CarolineDenis@users.noreply.github.com> Date: Wed, 25 Mar 2026 20:16:21 +0100 Subject: [PATCH 05/39] Allow non-admin access to discipline resource Removed admin access restriction for discipline resource. --- specifyweb/backend/delete_blockers/views.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/specifyweb/backend/delete_blockers/views.py b/specifyweb/backend/delete_blockers/views.py index d7f06c7ceae..034a5339b2c 100644 --- a/specifyweb/backend/delete_blockers/views.py +++ b/specifyweb/backend/delete_blockers/views.py @@ -22,9 +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 - # commented out to allow non admin to access discipline resource - # 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 @@ -55,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] From 158d87269b4224c91e702cc6f4de75dd190fb5cb Mon Sep 17 00:00:00 2001 From: Grant Fitzsimmons <37256050+grantfitzsimmons@users.noreply.github.com> Date: Thu, 26 Mar 2026 09:42:06 -0500 Subject: [PATCH 06/39] feat: update CHANGELOG --- CHANGELOG.md | 42 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) 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 From efc16cc7ab35df705a08157a8e4cc1aa36d48b76 Mon Sep 17 00:00:00 2001 From: Caroline Denis Date: Thu, 26 Mar 2026 17:45:53 +0100 Subject: [PATCH 07/39] Fix: Remove login required for encoding route --- specifyweb/backend/trees/views.py | 1 - 1 file changed, 1 deletion(-) diff --git a/specifyweb/backend/trees/views.py b/specifyweb/backend/trees/views.py index 7ac2889c203..bd04ec2522f 100644 --- a/specifyweb/backend/trees/views.py +++ b/specifyweb/backend/trees/views.py @@ -929,7 +929,6 @@ def default_tree_mapping(request) -> http.HttpResponse: return http.JsonResponse(tree_cfg) -@login_maybe_required def get_db_encoding(request): cursor = connection.cursor() From 3c94db0d246876c469c8d33cd638ddd414348463 Mon Sep 17 00:00:00 2001 From: alesan99 Date: Fri, 27 Mar 2026 09:11:06 -0500 Subject: [PATCH 08/39] Check if _class attribute exists first --- specifyweb/backend/stored_queries/format.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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( From 694c51fbcb87a575e961bce380232640754f8d8d Mon Sep 17 00:00:00 2001 From: alesan99 Date: Fri, 27 Mar 2026 13:53:26 -0500 Subject: [PATCH 09/39] Don't de-duplicate leaf nodes in tree imports --- specifyweb/backend/trees/defaults.py | 31 +++++++++++++++++++++------- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/specifyweb/backend/trees/defaults.py b/specifyweb/backend/trees/defaults.py index 105f126f806..3ff5877eb1c 100644 --- a/specifyweb/backend/trees/defaults.py +++ b/specifyweb/backend/trees/defaults.py @@ -133,9 +133,14 @@ class RankMappingConfiguration(TypedDict): fullnameseparator: NotRequired[str] fields: Dict[str, str] +class TreeConfiguration(TypedDict): + all_columns: list[str] + ranks: list[RankMappingConfiguration] + root: NotRequired[dict] + class DefaultTreeContext(): """Context for a default tree creation task""" - def __init__(self, tree_type: str, tree_def, tree_cfg: dict[str, RankMappingConfiguration], create_missing_ranks: bool): + def __init__(self, tree_type: str, tree_def, tree_cfg: TreeConfiguration, create_missing_ranks: bool): self.tree_type = tree_type self.tree_def = tree_def @@ -209,7 +214,7 @@ def flush(self, force=False): self.counter += 1 if not (force or self.counter > self.batch_size): return - logger.debug(f"Batch creating {self.batch_size} rows.") + logger.debug(f"Batch creating {self.counter} rows.") # Go through ranks in ascending order and bulk create nodes ordered_rank_ids = sorted(self.buffers.keys()) @@ -261,7 +266,7 @@ def flush(self, force=False): self.counter = 0 -def add_default_tree_record(context: DefaultTreeContext, row: dict, tree_cfg: dict[str, RankMappingConfiguration], row_id: int): +def add_default_tree_record(context: DefaultTreeContext, row: dict, tree_cfg: TreeConfiguration, row_id: int): """ Given one CSV row and a column mapping / rank configuration dictionary, walk through the 'ranks' in order, creating or updating each tree record and linking @@ -273,14 +278,16 @@ def add_default_tree_record(context: DefaultTreeContext, row: dict, tree_cfg: di parent_id = None rank_id = 10 - for rank_mapping in tree_cfg['ranks']: + rank_count = len(tree_cfg['ranks']) + for index in range(rank_count): + rank_mapping = tree_cfg['ranks'][index] rank_name = rank_mapping['name'] fields_mapping = rank_mapping['fields'] record_name = row.get(rank_mapping.get('column', rank_name)) # Record's name is in the column. if not record_name: - continue # This row doesn't contain a record for this rank. + break # This row doesn't contain a record for this rank. Assume this is the end of the row. defaults = {} for model_field, csv_col in fields_mapping.items(): @@ -301,14 +308,24 @@ def add_default_tree_record(context: DefaultTreeContext, row: dict, tree_cfg: di if tree_def_item is None: continue + + # Check if this is the last node in this row. + # If so, do not attempt to de-duplicate it. Non-parent nodes are allowed to share names. + is_last = (index == rank_count-1) + if not is_last and index < rank_count-1: + next_rank_mapping = tree_cfg['ranks'][index+1] + next_rank_name = next_rank_mapping['name'] + next_record_name = row.get(next_rank_mapping.get('column', next_rank_name)) + if not next_record_name: + is_last = True # Create the node at this rank if it isn't already there. buffered = context.get_node_in_buffer(tree_def_item.rankid, record_name) existing_id = context.get_existing_node_id(tree_def_item.rankid, record_name) - if existing_id is not None: + if not is_last and existing_id is not None: parent_id = existing_id parent = None - elif buffered is not None: + elif not is_last and buffered is not None: parent_id = None parent = buffered else: From bd178148ca991c74993c8c092f06c49df0913e05 Mon Sep 17 00:00:00 2001 From: alesan99 Date: Fri, 27 Mar 2026 13:59:37 -0500 Subject: [PATCH 10/39] Don't skip ranks (to allow optional ranks) --- specifyweb/backend/trees/defaults.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specifyweb/backend/trees/defaults.py b/specifyweb/backend/trees/defaults.py index 3ff5877eb1c..a164bdd33da 100644 --- a/specifyweb/backend/trees/defaults.py +++ b/specifyweb/backend/trees/defaults.py @@ -287,7 +287,7 @@ def add_default_tree_record(context: DefaultTreeContext, row: dict, tree_cfg: Tr record_name = row.get(rank_mapping.get('column', rank_name)) # Record's name is in the column. if not record_name: - break # This row doesn't contain a record for this rank. Assume this is the end of the row. + continue # This row doesn't contain a record for this rank. defaults = {} for model_field, csv_col in fields_mapping.items(): From c3c1ada20802769bf833033084d293edd924a664 Mon Sep 17 00:00:00 2001 From: alesan99 Date: Mon, 30 Mar 2026 14:00:40 -0500 Subject: [PATCH 11/39] Uncomment specifyuser_spprincipal usages --- .../backend/businessrules/rules/user_rules.py | 11 +++++------ .../specify/models_utils/model_extras.py | 19 +++++++++---------- 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/specifyweb/backend/businessrules/rules/user_rules.py b/specifyweb/backend/businessrules/rules/user_rules.py index 60bd3e8455a..c019a7cca5b 100644 --- a/specifyweb/backend/businessrules/rules/user_rules.py +++ b/specifyweb/backend/businessrules/rules/user_rules.py @@ -30,12 +30,11 @@ def added_user(sender, instance, created, raw, **kwargs): grouptype=user.usertype, ) - # TODO: UNCOMMENT THIS. Commented specifically for testing PR https://github.com/specify/specify7/pull/6671 - # for gp in group_principals: - # cursor.execute( - # 'insert into specifyuser_spprincipal(specifyuserid, spprincipalid) values (%s, %s)', - # [user.id, gp.id] - # ) + for gp in group_principals: + cursor.execute( + 'insert into specifyuser_spprincipal(specifyuserid, spprincipalid) values (%s, %s)', + [user.id, gp.id] + ) @receiver(signals.pre_delete, sender=Specifyuser) diff --git a/specifyweb/specify/models_utils/model_extras.py b/specifyweb/specify/models_utils/model_extras.py index d3598b729c2..70c14bb72ea 100644 --- a/specifyweb/specify/models_utils/model_extras.py +++ b/specifyweb/specify/models_utils/model_extras.py @@ -97,16 +97,15 @@ def clear_admin(self): "Make the user not a Specify 6 admin." from django.db import connection, transaction - # TODO: UNCOMMENT THIS. Commented specifically for testing PR https://github.com/specify/specify7/pull/6671 - # cursor = connection.cursor() - # cursor.execute(""" - # DELETE FROM specifyuser_spprincipal - # WHERE SpecifyUserId = %s - # AND SpPrincipalId IN ( - # SELECT SpPrincipalId FROM spprincipal - # WHERE Name = 'Administrator' - # ) - # """, [self.id]) + cursor = connection.cursor() + cursor.execute(""" + DELETE FROM specifyuser_spprincipal + WHERE SpecifyUserId = %s + AND SpPrincipalId IN ( + SELECT SpPrincipalId FROM spprincipal + WHERE Name = 'Administrator' + ) + """, [self.id]) def save(self, *args, **kwargs): # There is a signal handler that updates last_login when From ef4cff847d788a43829ebee138969df35eb2dbd4 Mon Sep 17 00:00:00 2001 From: alesan99 Date: Mon, 30 Mar 2026 16:59:54 -0500 Subject: [PATCH 12/39] WIP Don't look for parents in nodes that aren't being added to --- specifyweb/backend/trees/defaults.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/specifyweb/backend/trees/defaults.py b/specifyweb/backend/trees/defaults.py index a164bdd33da..c6e4478a72d 100644 --- a/specifyweb/backend/trees/defaults.py +++ b/specifyweb/backend/trees/defaults.py @@ -182,20 +182,24 @@ def create_rank_map(self): # Buffers for batches self.rankid_map = {rank.rankid: rank for rank in ranks} self.buffers = {rank.rankid: {} for rank in ranks} + self.buffer_lookups = {rank.rankid: {} for rank in ranks} self.created = {rank.rankid: {} for rank in ranks} + self.highest_rank = 0 def add_node_to_buffer(self, node, rank_id, row_id): """Add node to the current batch of nodes to be created""" if rank_id not in self.buffers: self.buffers[rank_id] = {} + self.buffer_lookups[rank_id] = {} self.created[rank_id] = {} self.buffers[rank_id][row_id] = node + self.buffer_lookups[rank_id][row_id] = node return node def get_node_in_buffer(self, rank_id: int, name: str): """Gets a node if its already in the current batch's buffer. Prevents duplication within a batch.""" # Check for node in buffer, return node - buffer = self.buffers.get(rank_id, {}) + buffer = self.buffer_lookups.get(rank_id, {}) for node in buffer.values(): if node.name == name: return node @@ -263,6 +267,7 @@ def flush(self, force=False): self.created[rank_id].update({n.name: n.id for n in created_nodes}) self.buffers[rank_id] = {} + self.buffer_lookups[rank_id] = {} self.counter = 0 @@ -278,6 +283,7 @@ def add_default_tree_record(context: DefaultTreeContext, row: dict, tree_cfg: Tr parent_id = None rank_id = 10 + highest_rank = 0 rank_count = len(tree_cfg['ranks']) for index in range(rank_count): rank_mapping = tree_cfg['ranks'][index] @@ -318,6 +324,9 @@ def add_default_tree_record(context: DefaultTreeContext, row: dict, tree_cfg: Tr next_record_name = row.get(next_rank_mapping.get('column', next_rank_name)) if not next_record_name: is_last = True + + if is_last: + highest_rank = tree_def_item.rankid # Create the node at this rank if it isn't already there. buffered = context.get_node_in_buffer(tree_def_item.rankid, record_name) @@ -353,6 +362,17 @@ def add_default_tree_record(context: DefaultTreeContext, row: dict, tree_cfg: Tr parent_id = None rank_id += 10 + # Clear all higher-rank buffers, since they are no longer relevant + # This will prevent a node from being parented to an incorrect parent with the same name + # TODO: This still doesn't work, there must be a bug somewhere + if highest_rank < context.highest_rank: + logger.debug(f"Clearing buffers for ranks > {highest_rank}") + for id in list(context.buffer_lookups.keys()): + if id > highest_rank: + context.buffer_lookups[id] = {} + context.highest_rank = highest_rank + context.highest_rank = max(highest_rank, context.highest_rank) + def queue_create_default_tree_task(task_id): """Store queued (and active) default tree creation tasks so they can be reliably tracked later.""" add_to_set(ACTIVE_DEFAULT_TREE_TASK_REDIS_KEY, task_id) From 317a98531ffd240c16cfb80e5622b7ab87364b63 Mon Sep 17 00:00:00 2001 From: alesan99 Date: Wed, 1 Apr 2026 09:43:34 -0500 Subject: [PATCH 13/39] WIP Use a local ID to distinguish parents with the same name --- specifyweb/backend/trees/defaults.py | 91 ++++++++++++++++------------ 1 file changed, 52 insertions(+), 39 deletions(-) diff --git a/specifyweb/backend/trees/defaults.py b/specifyweb/backend/trees/defaults.py index c6e4478a72d..e375a3caca7 100644 --- a/specifyweb/backend/trees/defaults.py +++ b/specifyweb/backend/trees/defaults.py @@ -1,4 +1,4 @@ -from typing import Dict, Optional, TypedDict, NotRequired +from typing import Dict, Optional, TypedDict, NotRequired, Union import json from django.db import transaction @@ -149,6 +149,9 @@ def __init__(self, tree_type: str, tree_def, tree_cfg: TreeConfiguration, create self.tree_cfg = tree_cfg if create_missing_ranks: self.create_missing_ranks() + + self.local_count = 0 + self.local_id_field = 'text1' self.create_rank_map() self.root_parent = self.tree_node_model.objects.filter( @@ -181,8 +184,11 @@ def create_rank_map(self): self.tree_def_item_map = {rank.name: rank for rank in ranks} # Buffers for batches self.rankid_map = {rank.rankid: rank for rank in ranks} + # All node objects to be created in this batch, separated by rank self.buffers = {rank.rankid: {} for rank in ranks} - self.buffer_lookups = {rank.rankid: {} for rank in ranks} + # Contains all nodes that can be parents at the current row. Name -> Object or database ID. + self.parent_lookup = {rank.rankid: {} for rank in ranks} + # IDs of nodes commited to the database. Local ID -> Database ID self.created = {rank.rankid: {} for rank in ranks} self.highest_rank = 0 @@ -190,28 +196,17 @@ def add_node_to_buffer(self, node, rank_id, row_id): """Add node to the current batch of nodes to be created""" if rank_id not in self.buffers: self.buffers[rank_id] = {} - self.buffer_lookups[rank_id] = {} + self.parent_lookup[rank_id] = {} self.created[rank_id] = {} self.buffers[rank_id][row_id] = node - self.buffer_lookups[rank_id][row_id] = node + self.parent_lookup[rank_id][node.name] = node return node - def get_node_in_buffer(self, rank_id: int, name: str): + def get_existing_parent(self, rank_id: int, name: str) -> Union[object, int, None]: """Gets a node if its already in the current batch's buffer. Prevents duplication within a batch.""" # Check for node in buffer, return node - buffer = self.buffer_lookups.get(rank_id, {}) - for node in buffer.values(): - if node.name == name: - return node - return None - - def get_existing_node_id(self, rank_id: int, name: str) -> Optional[int]: - """Gets a node's id if it has already been created. Prevents duplication across an entire import.""" - # Check for existing id, return id - created_in_rank = self.created.get(rank_id) - if created_in_rank: - return created_in_rank.get(name) - return None + lookup = self.parent_lookup.get(rank_id, {}) + return lookup.get(name, None) def flush(self, force=False): """Flushes this batch's buffer if the batch is complete. Bulk creates the nodes in a complete batch.""" @@ -237,7 +232,7 @@ def flush(self, force=False): parent = getattr(node, 'parent', None) parent_id = getattr(node, 'parent_id', None) if parent is not None and getattr(parent, 'pk', None) is None: - saved_parent_id = self.created[parent.rankid].get(parent.name) + saved_parent_id = self.created[parent.rankid].get(getattr(parent, self.local_id_field)) # Handle root if not saved_parent_id and parent.name == getattr(self.root_parent, 'name', None): saved_parent_id = self.root_parent.id @@ -255,19 +250,27 @@ def flush(self, force=False): self.tree_node_model.objects.bulk_create(nodes_to_create, ignore_conflicts=True) # Store the ids of the nodes were created in this batch - created_names = [n.name for n in nodes_to_create] - placeholders = ",".join(["%s"] * len(created_names)) + created_local_ids = [getattr(n, self.local_id_field) for n in nodes_to_create] created_nodes = self.tree_node_model.objects.filter( definition=self.tree_def, definitionitem=rank, - ).extra( - where=[f"BINARY name IN ({placeholders})"], - params=created_names + **{f"{self.local_id_field}__in": created_local_ids} ) - self.created[rank_id].update({n.name: n.id for n in created_nodes}) + self.created[rank_id].update({getattr(n, self.local_id_field): n.id for n in created_nodes}) + + # parent_lookup still contains unsaved objects. Replace them with IDs. + sorted_created_nodes = sorted( + created_nodes, + key=lambda n: getattr(n, self.local_id_field), + reverse=True + ) + for node in sorted_created_nodes: + local_id = getattr(node, self.local_id_field) + name = node.name + self.parent_lookup[rank_id][name] = self.created[rank_id].get(local_id) + self.buffers[rank_id] = {} - self.buffer_lookups[rank_id] = {} self.counter = 0 @@ -281,7 +284,6 @@ def add_default_tree_record(context: DefaultTreeContext, row: dict, tree_cfg: Tr tree_def = context.tree_def parent = context.root_parent parent_id = None - rank_id = 10 highest_rank = 0 rank_count = len(tree_cfg['ranks']) @@ -329,14 +331,16 @@ def add_default_tree_record(context: DefaultTreeContext, row: dict, tree_cfg: Tr highest_rank = tree_def_item.rankid # Create the node at this rank if it isn't already there. - buffered = context.get_node_in_buffer(tree_def_item.rankid, record_name) - existing_id = context.get_existing_node_id(tree_def_item.rankid, record_name) - if not is_last and existing_id is not None: - parent_id = existing_id - parent = None - elif not is_last and buffered is not None: - parent_id = None - parent = buffered + existing = context.get_existing_parent(tree_def_item.rankid, record_name) + if not is_last and existing is not None: + if type(existing) is int: + # Use parent's true id + parent_id = existing + parent = None + else: + # Unsaved parent, use the object directly (It will be replaced with the true id when buffer is flushed) + parent_id = None + parent = existing else: # Add new node to buffer data = { @@ -349,27 +353,36 @@ def add_default_tree_record(context: DefaultTreeContext, row: dict, tree_cfg: Tr if hasattr(tree_node_model, 'isaccepted'): data['isaccepted'] = True data.update(defaults) + + # Add a unique identifier in this import context (to be deleted when tree is finalized) + # This will be used to query this exact node again once its saved + context.local_count += 1 + data[context.local_id_field] = context.local_count if parent is not None: data['parent'] = parent elif parent_id is not None: data['parent_id'] = parent_id + logger.debug(data) + obj = tree_node_model(**data) obj = context.add_node_to_buffer(obj, tree_def_item.rankid, row_id) parent = obj parent_id = None - rank_id += 10 # Clear all higher-rank buffers, since they are no longer relevant # This will prevent a node from being parented to an incorrect parent with the same name - # TODO: This still doesn't work, there must be a bug somewhere + # TODO: This should work in theory, but it still doesn't work, there must be a bug somewhere in the implementation + logger.debug("---------------------------") + logger.debug(highest_rank) + logger.debug(context.parent_lookup) if highest_rank < context.highest_rank: logger.debug(f"Clearing buffers for ranks > {highest_rank}") - for id in list(context.buffer_lookups.keys()): + for id in list(context.parent_lookup.keys()): if id > highest_rank: - context.buffer_lookups[id] = {} + context.parent_lookup[id] = {} context.highest_rank = highest_rank context.highest_rank = max(highest_rank, context.highest_rank) From 6c2e71e2c9c8122546215a810223bed091825bb6 Mon Sep 17 00:00:00 2001 From: alesan99 Date: Wed, 1 Apr 2026 09:54:46 -0500 Subject: [PATCH 14/39] Fix ids not being ints --- specifyweb/backend/trees/defaults.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/specifyweb/backend/trees/defaults.py b/specifyweb/backend/trees/defaults.py index e375a3caca7..6d54e0c5fe6 100644 --- a/specifyweb/backend/trees/defaults.py +++ b/specifyweb/backend/trees/defaults.py @@ -256,7 +256,7 @@ def flush(self, force=False): definitionitem=rank, **{f"{self.local_id_field}__in": created_local_ids} ) - self.created[rank_id].update({getattr(n, self.local_id_field): n.id for n in created_nodes}) + self.created[rank_id].update({int(getattr(n, self.local_id_field)): n.id for n in created_nodes}) # parent_lookup still contains unsaved objects. Replace them with IDs. sorted_created_nodes = sorted( @@ -375,9 +375,6 @@ def add_default_tree_record(context: DefaultTreeContext, row: dict, tree_cfg: Tr # Clear all higher-rank buffers, since they are no longer relevant # This will prevent a node from being parented to an incorrect parent with the same name # TODO: This should work in theory, but it still doesn't work, there must be a bug somewhere in the implementation - logger.debug("---------------------------") - logger.debug(highest_rank) - logger.debug(context.parent_lookup) if highest_rank < context.highest_rank: logger.debug(f"Clearing buffers for ranks > {highest_rank}") for id in list(context.parent_lookup.keys()): From b43605027cb6d0371f00e943b88a5e86842255b5 Mon Sep 17 00:00:00 2001 From: Grant Fitzsimmons <37256050+grantfitzsimmons@users.noreply.github.com> Date: Wed, 1 Apr 2026 10:59:35 -0500 Subject: [PATCH 15/39] fix: remove critters --- .../frontend/static/img/apple-touch-icon.png | Bin 10450 -> 6953 bytes .../frontend/static/img/favicon-512x512.png | Bin 19862 -> 21306 bytes .../frontend/static/img/splash_screen.svg | 32 +++++++++++++++- .../static/img/splash_screen_dark.svg | 36 +++++++++++++++++- 4 files changed, 66 insertions(+), 2 deletions(-) diff --git a/specifyweb/frontend/static/img/apple-touch-icon.png b/specifyweb/frontend/static/img/apple-touch-icon.png index 78c694e824a03a024ab9db6f5b4b17422d689599..2a57c704ea1d028db4bbcf3aa08fee0d275c4989 100644 GIT binary patch literal 6953 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 literal 10450 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 \ No newline at end of file + + \ No newline at end of file diff --git a/specifyweb/frontend/static/img/splash_screen_dark.svg b/specifyweb/frontend/static/img/splash_screen_dark.svg index 84f1ed69915..dfe7ac19ab5 100644 --- a/specifyweb/frontend/static/img/splash_screen_dark.svg +++ b/specifyweb/frontend/static/img/splash_screen_dark.svg @@ -1 +1,35 @@ - \ No newline at end of file + + + + + + + + + + + + + + + + + + \ No newline at end of file From 70f5d4ad66e0e31d76ce2749a97af02cfa2edafc Mon Sep 17 00:00:00 2001 From: Grant Fitzsimmons <37256050+grantfitzsimmons@users.noreply.github.com> Date: Wed, 1 Apr 2026 11:30:12 -0500 Subject: [PATCH 16/39] feat: add critterless splash screen --- .../js_src/lib/components/HomePage/index.tsx | 22 +++++++++--- .../lib/components/Preferences/Renderers.tsx | 5 +++ .../js_src/lib/localization/preferences.ts | 11 +++++- .../static/img/critterless_splash_screen.svg | 31 ++++++++++++++++ .../img/critterless_splash_screen_dark.svg | 35 ++++++++++++++++++ .../frontend/static/img/splash_screen.svg | 32 +---------------- .../static/img/splash_screen_dark.svg | 36 +------------------ 7 files changed, 101 insertions(+), 71 deletions(-) create mode 100644 specifyweb/frontend/static/img/critterless_splash_screen.svg create mode 100644 specifyweb/frontend/static/img/critterless_splash_screen_dark.svg diff --git a/specifyweb/frontend/js_src/lib/components/HomePage/index.tsx b/specifyweb/frontend/js_src/lib/components/HomePage/index.tsx index 0900fd2b086..9494fb0cc12 100644 --- a/specifyweb/frontend/js_src/lib/components/HomePage/index.tsx +++ b/specifyweb/frontend/js_src/lib/components/HomePage/index.tsx @@ -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' ? (