Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
132 commits
Select commit Hold shift + click to select a range
042411c
feat(dwc): add darwin core occurrence
grantfitzsimmons Sep 1, 2026
cf2eff3
feat(dwc): add audiovisual core
grantfitzsimmons Sep 1, 2026
10d82ac
feat(dwc): add core term pattern matches
grantfitzsimmons Sep 1, 2026
13aa92d
feat(dwca): validate export definitions
grantfitzsimmons Sep 2, 2026
eb0f842
test(dwca): cover export definition validation
grantfitzsimmons Sep 2, 2026
759ae08
feat(dwca): support mapped fields in query builder
grantfitzsimmons Sep 2, 2026
3d98be9
feat(dwca): add default query definitions
grantfitzsimmons Sep 2, 2026
3111fbf
feat(dwca): add editor localization
grantfitzsimmons Sep 2, 2026
b5860bd
feat(dwca): add default export template
grantfitzsimmons Sep 2, 2026
dba5cf5
feat(dwca): add visual definition editor
grantfitzsimmons Sep 2, 2026
b72d921
feat(dwca): register visual app resource editor
grantfitzsimmons Sep 2, 2026
56bb3bc
test(dwca): update app resource coverage
grantfitzsimmons Sep 2, 2026
95865a4
test(dwca): cover definition editor mappings
grantfitzsimmons Sep 2, 2026
4c4c70e
fix(dwca): enforce `occurrenceID` as CO `guid`
grantfitzsimmons Sep 2, 2026
2b481bf
fix(dwca): only auto map terms present in vocab
grantfitzsimmons Sep 2, 2026
e0ec14c
feat(dwca): add more pattern matches
grantfitzsimmons Sep 2, 2026
54302e1
fix(dwca): scope DwCA templates by mapping target
grantfitzsimmons Sep 2, 2026
1fc618e
fix(dwca): clean up term patterns
grantfitzsimmons Sep 3, 2026
40e851e
feat(dwca): add data model alignment
grantfitzsimmons Sep 3, 2026
d10054f
feat(dwca): add new field mappings
grantfitzsimmons Sep 3, 2026
ffb7491
fix(dwca): update mapping
grantfitzsimmons Sep 3, 2026
a5d15e2
refactor(dwca): mapping helpers and add tests
grantfitzsimmons Sep 3, 2026
cf795ea
feat(dwca): add gbif extension mappings
grantfitzsimmons Sep 3, 2026
191add5
fix(dwca): support end date too
grantfitzsimmons Sep 3, 2026
ef5c6a9
refactor(dwca): drop default queries
grantfitzsimmons Sep 3, 2026
e2b18eb
fix(dwca): handle terms with qualifiers
grantfitzsimmons Sep 3, 2026
55ba9b3
fix(dwca): minor mapping updates
grantfitzsimmons Sep 3, 2026
684eba5
feat(dwca): add url routing
grantfitzsimmons Sep 3, 2026
4036299
feat(dwca): improve audiovisual core mapping
grantfitzsimmons Sep 3, 2026
9e4c65f
feat(dwca): add EOL references extension mapping
grantfitzsimmons Sep 3, 2026
845e52b
feat(dwca): add identification history
grantfitzsimmons Sep 3, 2026
07e682c
feat(dwca): add new dwc terms
grantfitzsimmons Sep 3, 2026
13ae73f
feat(dwca): generate GBIF catalogs
grantfitzsimmons Sep 3, 2026
12e72ca
feat(dwca): support core-specific mappings
grantfitzsimmons Sep 3, 2026
fa0ddb3
feat(dwca): use core-specific query tables
grantfitzsimmons Sep 3, 2026
94ff5c2
fix(dwca): reset mappings for custom cores
grantfitzsimmons Sep 3, 2026
5ca4604
refactor(dwca): remove legacy core catalog
grantfitzsimmons Sep 3, 2026
aecdbef
refactor(dwca): isolate definition editor
grantfitzsimmons Sep 3, 2026
67001fc
refactor(dwca): move catalog data with editor
grantfitzsimmons Sep 3, 2026
000677b
fix(dwca): add mimetype for dwca
grantfitzsimmons Sep 14, 2026
c5859ac
chore(dwca): add GitHub Action for generating GBIF catalogs
grantfitzsimmons Sep 14, 2026
3fd94b7
fix(dwca): prevent button flickering
grantfitzsimmons Sep 14, 2026
1a6725c
chore(dwca): simplify templates and queries
grantfitzsimmons Sep 14, 2026
d6e2422
fix(dwca): support multiple cores on backend
grantfitzsimmons Sep 14, 2026
aff70e8
fix(dwca): make screenreader accessible
grantfitzsimmons Sep 14, 2026
85929c9
fix(dwca): localize errors
grantfitzsimmons Sep 14, 2026
5a70606
chore: updaet frontend dependencies
grantfitzsimmons Sep 14, 2026
6a9561f
fix(dwca): fix bad test
grantfitzsimmons Sep 14, 2026
0af7dc0
fix(dwca): localize errors
grantfitzsimmons Sep 15, 2026
a87f407
Potential fix for pull request finding 'CodeQL / Information exposure…
grantfitzsimmons Sep 17, 2026
20a8a79
fix(dwca): tests
grantfitzsimmons Sep 20, 2026
42a447e
fix: update packages
grantfitzsimmons Sep 20, 2026
652ed69
feat(dwca): add examples
grantfitzsimmons Sep 27, 2026
b12858d
fix(dwca): decode term descriptions
grantfitzsimmons Sep 27, 2026
a3a02e1
fix(dwca): format aligned term descriptions
grantfitzsimmons Sep 27, 2026
5d40710
fix(dwca): correct shared term matching
grantfitzsimmons Sep 27, 2026
1bb8f57
fix(dwca): harden mapping and export validation
grantfitzsimmons Sep 27, 2026
c16ef25
fix(dwca): report core-specific duplicate identifiers
grantfitzsimmons Sep 27, 2026
6a777d3
fix(dwca): identify core fields by stable mapping
grantfitzsimmons Sep 27, 2026
8daf2a9
fix(dwca): scope georeferencedBy mapping
grantfitzsimmons Sep 27, 2026
afd450b
fix(dwca): remove bare terms from identification template
grantfitzsimmons Sep 27, 2026
ee517ce
fix(query-builder): align basic view columns
grantfitzsimmons Sep 27, 2026
989f91b
fix(localization): correct Bokmal schema labels
grantfitzsimmons Sep 27, 2026
fb837df
fix(dwca): restore georeferencedBy field mapping
grantfitzsimmons Sep 27, 2026
f971712
fix(dwca): retain current determination in identification template
grantfitzsimmons Sep 27, 2026
60cb7ac
test(dwca): cover current determination template field
grantfitzsimmons Sep 27, 2026
1e2fb43
fix(dwca): include current determination in occurrence default
grantfitzsimmons Sep 27, 2026
12d1ff0
fix(query-builder): preserve generated grid classes
grantfitzsimmons Sep 27, 2026
ad41fd8
feat(dwca): add Event core default mapping
grantfitzsimmons Sep 27, 2026
ff4b799
feat(dwca): add Taxon core default mapping
grantfitzsimmons Sep 27, 2026
b960492
fix(upload): type serialized upload table payload
grantfitzsimmons Sep 28, 2026
e2fab99
fix(accounts): normalize external user name
grantfitzsimmons Sep 28, 2026
eb425b3
fix(query-builder): protect read-only field order
grantfitzsimmons Oct 1, 2026
6e0a45f
fix(dwca): preserve saved export filenames
grantfitzsimmons Oct 1, 2026
950c6a8
fix(query-builder): lock mapped read-only fields
grantfitzsimmons Oct 1, 2026
ed04667
fix(dwca): decode generated catalog metadata
grantfitzsimmons Oct 1, 2026
cddaf31
fix(dwca): preserve and constrain visual mappings
grantfitzsimmons Oct 1, 2026
2036e53
fix(dwca): omit terms from hidden query fields
grantfitzsimmons Oct 1, 2026
46739a4
Merge remote-tracking branch 'origin/main' into issue-286
grantfitzsimmons Oct 1, 2026
fc00ab6
fix(frontend): scope brace expansion overrides
grantfitzsimmons Oct 1, 2026
abf2f5a
fix(dwca): normalize template discipline types
grantfitzsimmons Oct 1, 2026
14862cb
fix(query-builder): block sorting over locked fields
grantfitzsimmons Oct 1, 2026
ff08b47
fix(dwca): identify extensions by row type
grantfitzsimmons Oct 1, 2026
0168bcf
fix(dwca): decode HTML entities once
grantfitzsimmons Oct 1, 2026
4102179
fix(dwca): return export validation details
grantfitzsimmons Oct 1, 2026
3c7a020
fix(dwca): map EOL DOI to reference field
grantfitzsimmons Oct 1, 2026
db2e3fc
fix(dwca): fail catalog generation on errors
grantfitzsimmons Oct 1, 2026
26d9b22
fix(dwca): preserve relationship GUID mappings
grantfitzsimmons Oct 1, 2026
1abcc4f
fix(dwca): block incomplete mapping saves
grantfitzsimmons Oct 1, 2026
91149b5
fix(schema-config): match alignment field suffixes
grantfitzsimmons Oct 1, 2026
adc460f
fix(localization): use complete DWCA prompts
grantfitzsimmons Oct 1, 2026
498b4e8
style(dwca): format review fixes
grantfitzsimmons Oct 1, 2026
7588c85
Merge branch 'main' into issue-286
grantfitzsimmons Oct 2, 2026
17cf1fa
fix(dwca): fix default filename encoding
grantfitzsimmons Oct 20, 2026
e62403e
fix(dwca): keep current tab selected
grantfitzsimmons Oct 20, 2026
47c36ed
fix(dwca): improve error presentation
grantfitzsimmons Oct 20, 2026
dd86aa2
refactor(dwca): improve alerts
grantfitzsimmons Oct 20, 2026
0254314
fix(dwca): check before overriding mapping
grantfitzsimmons Oct 20, 2026
1455f75
fix(dwca): handle invalid table ID mapping
grantfitzsimmons Oct 20, 2026
52db784
feat(dwca): make extension name clickable
grantfitzsimmons Oct 20, 2026
e4b83bb
fix(dwca): reformat invalid base table error
grantfitzsimmons Oct 20, 2026
691b2ea
feat(dwca): add switch to XML editor option
grantfitzsimmons Oct 20, 2026
cb66c91
fix(dwca): condense alignment list
grantfitzsimmons Oct 20, 2026
08e4fd5
Merge branch 'main' of https://github.com/specify/specify7 into issue…
grantfitzsimmons Oct 20, 2026
9f63a77
fix(dwca): omit query values from validation errors
grantfitzsimmons Oct 21, 2026
cf920ca
fix(dwca): clear visual editor save blockers on unmount
grantfitzsimmons Oct 21, 2026
7f31efa
fix(schema-config): include discipline-specific alignments
grantfitzsimmons Oct 21, 2026
7722725
fix: correct ORCID and Wikidata defaults
grantfitzsimmons Oct 23, 2026
e044c2f
feat: add Agentidentifier and CollectorID
grantfitzsimmons Oct 23, 2026
d7bff32
fix: use proper wikidata logo
grantfitzsimmons Oct 23, 2026
685721e
feat: add determiner ID
grantfitzsimmons Oct 23, 2026
cb2a045
feat: add Determiner aggregations
grantfitzsimmons Oct 23, 2026
03f4e8b
fix: minor name change
grantfitzsimmons Oct 23, 2026
67102b1
Merge branch 'main' into issue-286
grantfitzsimmons Oct 23, 2026
0c5cccb
fix(query): preserve distinct formatted fields
grantfitzsimmons Oct 23, 2026
c3d4b72
Lint code with ESLint and Prettier
grantfitzsimmons Oct 23, 2026
1921050
fix(query): omit separators for empty formatted values
grantfitzsimmons Oct 23, 2026
86c6583
fix(query): show format choices for new columns
grantfitzsimmons Oct 23, 2026
5aa00b3
fix(query): distinguish visible result format columns
grantfitzsimmons Oct 23, 2026
de40500
fix(query): keep first relationship column on implicit default
grantfitzsimmons Oct 23, 2026
a323d87
fix(query): keep initial format selector closed
grantfitzsimmons Oct 23, 2026
dc03625
Lint code with ESLint and Prettier
grantfitzsimmons Oct 23, 2026
1f6fb8e
Merge branch 'issue-286' into issue-8692
grantfitzsimmons Oct 23, 2026
3f9e7cb
Merge pull request #8695 from specify/issue-8692
grantfitzsimmons Oct 23, 2026
552f95c
feat(dwca): add default mappings for agent identifiers
grantfitzsimmons Oct 23, 2026
52a5260
refactor(query): name agent identifier aggregations clearly
grantfitzsimmons Oct 23, 2026
1689f26
fix(query): revalidate formatter resource on initial load
grantfitzsimmons Oct 23, 2026
2a977a7
fix(dwca): move selector out of settings dialog
grantfitzsimmons Oct 23, 2026
a696fd8
fix(dwca): fix selector
grantfitzsimmons Oct 23, 2026
d4c05ca
Merge branch 'main' of https://github.com/specify/specify7 into issue…
grantfitzsimmons Oct 26, 2026
201b100
Lint code with ESLint and Prettier
grantfitzsimmons Oct 26, 2026
fa411c4
Merge branch 'main' into issue-286
grantfitzsimmons Oct 30, 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
77 changes: 77 additions & 0 deletions .github/workflows/update-gbif-catalogs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
name: Update GBIF catalogs

on:
workflow_dispatch:
schedule:
- cron: '0 6 1 * *'

permissions:
contents: write
pull-requests: write

concurrency:
group: update-gbif-catalogs
cancel-in-progress: true

jobs:
update:
name: Generate GBIF catalogs
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version-file: specifyweb/frontend/js_src/package.json
cache: npm
cache-dependency-path: specifyweb/frontend/js_src/package-lock.json

- name: Install frontend dependencies
working-directory: specifyweb/frontend/js_src
run: npm ci

- name: Generate GBIF catalogs
working-directory: specifyweb/frontend/js_src
run: npm run dwca:catalog

- name: Verify GBIF catalog examples
working-directory: specifyweb/frontend/js_src
run: |
node --input-type=module -e '
import cores from "./lib/components/DwcaDefinition/data/gbifCores.json" with { type: "json" };
import extensions from "./lib/components/DwcaDefinition/data/gbifExtensions.json" with { type: "json" };
const fields = [...cores, ...extensions].flatMap(({ fields }) => fields);
if (!fields.some(({ examples }) => examples !== undefined && examples !== "")) {
throw new Error("GBIF catalogs do not contain any field examples");
}
'

- name: Check for catalog changes
id: changes
run: |
if git diff --quiet -- \
specifyweb/frontend/js_src/lib/components/DwcaDefinition/data/gbifCores.json \
specifyweb/frontend/js_src/lib/components/DwcaDefinition/data/gbifExtensions.json; then
echo 'changed=false' >> "$GITHUB_OUTPUT"
else
echo 'changed=true' >> "$GITHUB_OUTPUT"
fi

- name: Open update pull request if catalogs changed
if: steps.changes.outputs.changed == 'true'
uses: peter-evans/create-pull-request@v7
with:
commit-message: 'chore(dwca): update GBIF catalogs'
title: 'Update GBIF catalogs'
body: |
Updates the generated GBIF core and extension catalogs from the GBIF registry.

This pull request was created automatically by the monthly catalog refresh.
branch: automation/update-gbif-catalogs
delete-branch: true
add-paths: |
specifyweb/frontend/js_src/lib/components/DwcaDefinition/data/gbifCores.json
specifyweb/frontend/js_src/lib/components/DwcaDefinition/data/gbifExtensions.json
46 changes: 38 additions & 8 deletions config/backstop/dataobj_formatters.xml
Original file line number Diff line number Diff line change
Expand Up @@ -549,13 +549,6 @@
</fields>
</switch>
</format>
<format name="AgentIdentifier" title="Agent Identifier" class="edu.ku.brc.specify.datamodel.AgentIdentifier" default="true">
<switch single="true">
<fields>
<field sep=" ">identifier</field>
</fields>
</switch>
</format>
<format name="TectonicUnit" title="Tectonic Unit" class="edu.ku.brc.specify.datamodel.TectonicUnit" default="true">
<switch single="true">
<fields>
Expand Down Expand Up @@ -584,6 +577,40 @@
</fields>
</switch>
</format>
<format name="AgentIdentifier" title="Agent Identifier" class="edu.ku.brc.specify.datamodel.AgentIdentifier" default="true">
<switch single="false" field="identifierType">
<fields>
<field>identifier</field>
</fields>
<fields value="ORCID">
<field sep="https://orcid.org/">identifier</field>
</fields>
<fields value="Wikidata">
<field sep="https://www.wikidata.org/entity/">identifier</field>
</fields>
</switch>
</format>
<format name="CollectorID" title="CollectorID" class="edu.ku.brc.specify.datamodel.Collector" default="">
<switch single="true">
<fields>
<field>agent.identifiers</field>
</fields>
</switch>
</format>
<format name="Determiner" title="Determiner" class="edu.ku.brc.specify.datamodel.Determiner" default="true">
<switch single="true">
<fields>
<field sep=" ">agent</field>
</fields>
</switch>
</format>
<format name="DeterminerID" title="DeterminerID" class="edu.ku.brc.specify.datamodel.Determiner" default="">
<switch single="true">
<fields>
<field>agent.identifiers</field>
</fields>
</switch>
</format>
<aggregators>
<aggregator name="AccessionAgent" title="AccessionAgent" class="edu.ku.brc.specify.datamodel.AccessionAgent" default="true" separator="; " ending="" format="AccessionAgent"/>
<aggregator name="AccessionAttachment" title="AccessionAttachment" class="edu.ku.brc.specify.datamodel.AccessionAttachment" default="true" separator="; " ending="" format="identity" orderfieldname="ordinal"/>
Expand Down Expand Up @@ -629,12 +656,15 @@
<aggregator name="PcrPerson" title="PcrPerson" class="edu.ku.brc.specify.datamodel.PcrPerson" default="true" separator="; " ending="" format="PcrPerson" orderfieldname="orderNumber"/>
<aggregator name="TaxonAttachment" title="TaxonAttachment" class="edu.ku.brc.specify.datamodel.TaxonAttachment" default="true" separator="; " ending="" format="identity" orderfieldname="ordinal"/>
<aggregator name="CollectionObjectGroupJoin" title="Collection Object Group Join" class="edu.ku.brc.specify.datamodel.CollectionObjectGroupJoin" default="true" separator="; " ending="" format=""/>
<aggregator name="AgentIdentifier" title="Agent Identifier" class="edu.ku.brc.specify.datamodel.AgentIdentifier" default="true" separator="; " ending="" format=""/>
<aggregator name="Collection" title="Collection" class="edu.ku.brc.specify.datamodel.Collection" default="true" separator=" | " ending="" format=""/>
<aggregator name="Discipline" title="Discipline" class="edu.ku.brc.specify.datamodel.Discipline" default="true" separator=" | " ending="" format=""/>
<aggregator name="Division" title="Division" class="edu.ku.brc.specify.datamodel.Division" default="true" separator=" | " ending="" format=""/>
<aggregator name="Taxon" title="Taxon" class="edu.ku.brc.specify.datamodel.Taxon" default="true" separator="; " ending="" format=""/>
<aggregator name="Geography" title="Geography" class="edu.ku.brc.specify.datamodel.Geography" default="true" separator="; " ending="" format=""/>
<aggregator name="Storage" title="Storage" class="edu.ku.brc.specify.datamodel.Storage" default="true" separator="; " ending="" format=""/>
<aggregator name="AgentIdentifier" title="Agent Identifier" class="edu.ku.brc.specify.datamodel.AgentIdentifier" default="true" separator=" | " ending="" format=""/>
<aggregator name="Collector Identifiers" title="Collector Identifiers" class="edu.ku.brc.specify.datamodel.Collector" default="" separator=" | " ending="" format="CollectorID" orderfieldname="orderNumber"/>
<aggregator name="Determiner" title="Determiner" class="edu.ku.brc.specify.datamodel.Determiner" default="true" separator="; " ending="" format=""/>
<aggregator name="Determiner Identifiers" title="Determiner Identifiers" class="edu.ku.brc.specify.datamodel.Determiner" default="" separator=" | " ending="" format="DeterminerID" orderfieldname="orderNumber"/>
</aggregators>
</formatters>
2 changes: 1 addition & 1 deletion config/common/common.views.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5579,7 +5579,7 @@
<!-- Shown if the Identifier Type is Wikidata -->
<rows condition="identifierType=Wikidata">
<row>
<cell type="field" id="Wikidata" name="this" uitype="plugin" initialize="name=WebLinkButton;weblink=Wikidata;icon=https://upload.wikimedia.org/wikipedia/commons/thumb/f/ff/Wikidata-logo.svg/2560px-Wikidata-logo.svg.png"/>
<cell type="field" id="Wikidata" name="this" uitype="plugin" initialize="name=WebLinkButton;weblink=Wikidata;icon=https://upload.wikimedia.org/wikipedia/commons/b/be/Wikidata-logo.png"/>
</row>
</rows>
</cell>
Expand Down
8 changes: 4 additions & 4 deletions config/common/weblinks.xml
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@
</weblinkdef>
<weblinkdef>
<name>ORCID</name>
<tableName>Agent</tableName>
<baseURLStr>orcid.com/&lt;this&gt;</baseURLStr>
<tableName>AgentIdentifier</tableName>
<baseURLStr>https://orcid.org/&lt;this&gt;</baseURLStr>
<args>
<weblinkdefarg>
<name>this</name>
Expand All @@ -39,8 +39,8 @@
</weblinkdef>
<weblinkdef>
<name>Wikidata</name>
<tableName>Agent</tableName>
<baseURLStr>wikidata.com/&lt;this&gt;</baseURLStr>
<tableName>AgentIdentifier</tableName>
<baseURLStr>https://www.wikidata.org/entity/&lt;this&gt;</baseURLStr>
<args>
<weblinkdefarg>
<name>this</name>
Expand Down
2 changes: 1 addition & 1 deletion specifyweb/backend/accounts/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ def oic_callback(request: http.HttpRequest) -> http.HttpResponse:
'id': str(ext_user['sub']),
'provider': provider,
'provider_title': provider_info['title'],
'name': ext_user.get('name', ext_user.get('email', None)),
'name': ext_user.get('name') or ext_user.get('email') or '',
'idtoken': ext_user,
}
request.session['external_user'] = external_user
Expand Down
55 changes: 38 additions & 17 deletions specifyweb/backend/context/app_resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -323,27 +323,48 @@ def get_app_resource_dirs_for_level(collection, user, level):
return Spappresourcedir.objects.filter(**filters)

def merge_formatters(custom_xml, default_xml):
"""Merge custom and default XML formatters.
This function takes two XML strings, one for custom formatters and one for default formatters,
and merges them by adding any formatters from the default XML that are not already present in
the custom XML. It returns the merged XML as a string.
"""
"""Keep custom definitions while supplying required backstop ID formats."""

custom_root = ElementTree.fromstring(custom_xml)
default_root = ElementTree.fromstring(default_xml)

# Create a set of existing classes in the custom XML to avoid duplicates
custom_classes = set()
for format_elem in custom_root.findall('format'):
class_attr = format_elem.get('class')
if class_attr:
custom_classes.add(class_attr)

# Add formatters from default that don't exist in custom
for format_elem in default_root.findall('format'):
class_attr = format_elem.get('class')
if class_attr and class_attr not in custom_classes:
custom_root.append(format_elem)
# Custom resources override a table's ordinary formats. The named ID
# formats are also needed by the shipped DwC-A templates for that table.
required_formats = {'AgentIdentifier', 'CollectorID', 'DeterminerID'}
required_aggregators = {'AgentIdentifier', 'Collector Identifiers', 'Determiner Identifiers'}
custom_formats = custom_root.findall('format')
custom_classes = {element.get('class') for element in custom_formats}
custom_keys = {
(element.get('class'), element.get('name')) for element in custom_formats
}
for element in default_root.findall('format'):
class_name = element.get('class')
key = (class_name, element.get('name'))
if class_name and key not in custom_keys and (
class_name not in custom_classes or element.get('name') in required_formats
):
custom_root.append(element)
custom_keys.add(key)

default_aggregators = default_root.find('aggregators')
if default_aggregators is not None:
custom_aggregators = custom_root.find('aggregators')
existing = (
{
(element.get('class'), element.get('name'))
for element in custom_aggregators.findall('aggregator')
}
if custom_aggregators is not None else set()
)
for element in default_aggregators.findall('aggregator'):
if element.get('name') not in required_aggregators or (
element.get('class'), element.get('name')
) in existing:
continue
if custom_aggregators is None:
custom_aggregators = ElementTree.SubElement(custom_root, 'aggregators')
custom_aggregators.append(element)
existing.add((element.get('class'), element.get('name')))

# Convert the merged XML tree back to a string
return ElementTree.tostring(custom_root, encoding='utf-8').decode('utf-8')
112 changes: 108 additions & 4 deletions specifyweb/backend/context/tests/test_app_resource.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,120 @@
from django.test import Client

from specifyweb.specify.tests.test_api import ApiTests
from pathlib import Path
from unittest.mock import Mock, patch
from xml.etree import ElementTree

from specifyweb.backend.context.app_resource import get_data_view_queries_resource
from django.conf import settings
from django.test import Client, SimpleTestCase

from specifyweb.backend.context.app_resource import (
get_app_resource,
get_data_view_queries_resource,
merge_formatters,
)
from specifyweb.specify.tests.test_api import ApiTests


FOUND_RESOURCE = ('<resource><text>"Value"</text></resource>', "text/xml", 4)


class TestMergeFormatters(SimpleTestCase):
def test_inherits_named_formats_and_aggregators_without_replacing_custom_ones(self):
custom = '''<formatters>
<format class="Collector" name="Collector" default="true" title="Custom"/>
<aggregators>
<aggregator class="Collector" name="Collector" default="true" title="Custom"/>
</aggregators>
</formatters>'''
defaults = '''<formatters>
<format class="Collector" name="Collector" default="true" title="Default"/>
<format class="Collector" name="CollectorID" default="false"/>
<format class="Collector" name="Other" default="false"/>
<aggregators>
<aggregator class="Collector" name="Collector" default="true" title="Default"/>
<aggregator class="Collector" name="Collector Identifiers" default="false" format="CollectorID"/>
<aggregator class="Collector" name="Other" default="false"/>
</aggregators>
</formatters>'''

merged = ElementTree.fromstring(merge_formatters(custom, defaults))
self.assertEqual(
[(node.get('name'), node.get('title')) for node in merged.findall('format')],
[('Collector', 'Custom'), ('CollectorID', None)],
)
self.assertEqual(
[
(node.get('name'), node.get('title'))
for node in merged.findall('aggregators/aggregator')
],
[('Collector', 'Custom'), ('Collector Identifiers', None)],
)
self.assertEqual(
[node.get('name') for node in merged.findall('format[@default="true"]')],
['Collector'],
)
without_aggregators = '''<formatters>
<format class="Collector" name="Collector" default="true"/>
</formatters>'''
inherited = ElementTree.fromstring(
merge_formatters(without_aggregators, defaults)
)
self.assertIsNotNone(
inherited.find('aggregators/aggregator[@name="Collector Identifiers"]')
)

def test_backstop_id_formats_survive_custom_formatters(self):
custom = '''<formatters>
<format class="edu.ku.brc.specify.datamodel.Collector" name="Collector" default="true"/>
<format class="edu.ku.brc.specify.datamodel.Determiner" name="Determiner" default="true"/>
<format class="edu.ku.brc.specify.datamodel.AgentIdentifier" name="CustomIdentifier" default="true"/>
<aggregators>
<aggregator class="edu.ku.brc.specify.datamodel.Collector" name="Collector" default="true"/>
<aggregator class="edu.ku.brc.specify.datamodel.Determiner" name="Determiner" default="true"/>
</aggregators>
</formatters>'''
backstop = (
Path(settings.SPECIFY_CONFIG_DIR) / 'backstop' / 'dataobj_formatters.xml'
).read_text()
merged = ElementTree.fromstring(merge_formatters(custom, backstop))

for name in ('AgentIdentifier', 'CollectorID', 'DeterminerID'):
self.assertIsNotNone(merged.find(f'format[@name="{name}"]'))
for name in ('AgentIdentifier', 'Collector Identifiers', 'Determiner Identifiers'):
self.assertIsNotNone(merged.find(f'aggregators/aggregator[@name="{name}"]'))


class TestAppResource(ApiTests):

@patch("specifyweb.backend.context.app_resource.load_resource_at_level")
@patch("specifyweb.backend.context.app_resource.get_app_resource_from_db")
def test_custom_formatters_inherit_backstop_aggregations(
self, get_from_db: Mock, load_from_filesystem: Mock
):
custom = '''<formatters>
<format class="edu.ku.brc.specify.datamodel.Collector" name="Collector" default="true"/>
<aggregators>
<aggregator class="edu.ku.brc.specify.datamodel.Collector" name="Collector" default="true"/>
</aggregators>
</formatters>'''
backstop = (
Path(settings.SPECIFY_CONFIG_DIR) / 'backstop' / 'dataobj_formatters.xml'
).read_text()
get_from_db.side_effect = lambda _collection, _user, level, _name: (
(custom, 'text/xml', 1) if level == 'Collection' else None
)
load_from_filesystem.side_effect = lambda _collection, _user, level, _name: (
(backstop, 'text/xml', 2) if level == 'Backstop' else None
)

result = get_app_resource(self.collection, self.specifyuser, 'DataObjFormatters')
self.assertIsNotNone(result)
merged = ElementTree.fromstring(result[0])
self.assertIsNotNone(
merged.find('aggregators/aggregator[@name="Collector Identifiers"]')
)
self.assertIsNotNone(
merged.find('aggregators/aggregator[@name="Determiner Identifiers"]')
)

@patch("specifyweb.backend.context.app_resource.load_resource_at_level")
@patch("specifyweb.backend.context.app_resource.get_app_resource_from_db")
def test_data_view_queries_inherit_by_table(
Expand Down
Loading
Loading