Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
730b84d
Cache catalognumber uniqueness preferences during bulk operations
acwhite211 May 4, 2026
fb7880c
Skip component duplicate checks when CO catalog fields are unchanged
acwhite211 May 4, 2026
fa7a6ba
Reuse uniqueness preference lookup across bulk copy requests
acwhite211 May 4, 2026
c12d76e
Cache uniqueness preference lookup for bulk create endpoints
acwhite211 May 4, 2026
b3b0b2c
Cache catalognumber uniqueness preference during data set upload
acwhite211 May 4, 2026
b558145
Merge branch 'v7_12_0_6' into issue-8055
grantfitzsimmons May 6, 2026
3313343
Cache Component catalog lookups during bulk CO saves
acwhite211 May 6, 2026
f23437c
Cache CO business rule lookups during bulk saves
acwhite211 May 7, 2026
60ea311
Avoid repeated FK object loads in CO pre-save checks
acwhite211 May 7, 2026
0633bbe
Cache uniqueness rule metadata during bulk operations
acwhite211 May 7, 2026
8dbcfd4
Cache repeated permission queries within bulk operations
acwhite211 May 7, 2026
1b53e6d
Use bulk validation caches during dataset commit
acwhite211 May 7, 2026
1480b87
Apply validation caches to bulk copy endpoints
acwhite211 May 7, 2026
26b4b77
Apply validation caches to API bulk endpoints
acwhite211 May 7, 2026
71b3ef7
Track dirty model fields during scoped saves
acwhite211 May 7, 2026
59e8b3b
Save only dirty fields during Batch Edit updates
acwhite211 May 7, 2026
b54f7b0
Skip CO catalog checks for unrelated field updates
acwhite211 May 7, 2026
cba9e12
Skip unchanged uniqueness rules during updates
acwhite211 May 7, 2026
ecf3277
Add bulk Batch Edit update and audit flush path
acwhite211 May 8, 2026
969a568
Try bulk Batch Edit commits with row-by-row fallback
acwhite211 May 8, 2026
f4b2d99
Carry bulk update context through WB auditing
acwhite211 May 8, 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
56 changes: 34 additions & 22 deletions specifyweb/backend/bulk_copy/bulk_copy.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import json

from specifyweb.backend.permissions.permissions import table_permissions_checker
from specifyweb.backend.permissions.permissions import cache_permission_queries, table_permissions_checker
from django.http import (HttpResponse, HttpResponseNotAllowed)

from specifyweb.specify.api.crud import post_resource
from specifyweb.specify.api.dispatch import HttpResponseCreated
from specifyweb.specify.api.serializers import _obj_to_data, toJson
from specifyweb.backend.businessrules.utils import cache_unique_catnum_preferences
from specifyweb.backend.businessrules.uniqueness_rules import cache_uniqueness_rules


def collection_dispatch_bulk_copy(request, model, copies) -> HttpResponse:
Expand All @@ -17,15 +19,20 @@ def collection_dispatch_bulk_copy(request, model, copies) -> HttpResponse:
data = json.loads(request.body)
data = dict(filter(lambda item: item[0] != 'id', data.items())) # Remove ID field before making copies
resp_objs = []
for _ in range(int(copies)):
obj = post_resource(
request.specify_collection,
request.specify_user_agent,
model,
data,
request.GET.get("recordsetid", None),
)
resp_objs.append(_obj_to_data(obj, checker))
with (
cache_unique_catnum_preferences(),
cache_uniqueness_rules(),
cache_permission_queries(),
):
for _ in range(int(copies)):
obj = post_resource(
request.specify_collection,
request.specify_user_agent,
model,
data,
request.GET.get("recordsetid", None),
)
resp_objs.append(_obj_to_data(obj, checker))

return HttpResponseCreated(toJson(resp_objs), content_type='application/json')

Expand All @@ -39,17 +46,22 @@ def collection_dispatch_bulk(request, model) -> HttpResponse:

if request.method != 'POST':
return HttpResponseNotAllowed(['POST'])

data = json.loads(request.body)
resp_objs = []
for obj_data in data:
obj = post_resource(
request.specify_collection,
request.specify_user_agent,
model,
obj_data,
request.GET.get("recordsetid", None),
)
resp_objs.append(_obj_to_data(obj, checker))

return HttpResponseCreated(toJson(resp_objs), content_type='application/json')
with (
cache_unique_catnum_preferences(),
cache_uniqueness_rules(),
cache_permission_queries(),
):
for obj_data in data:
obj = post_resource(
request.specify_collection,
request.specify_user_agent,
model,
obj_data,
request.GET.get("recordsetid", None),
)
resp_objs.append(_obj_to_data(obj, checker))

return HttpResponseCreated(toJson(resp_objs), content_type='application/json')
64 changes: 45 additions & 19 deletions specifyweb/backend/businessrules/rules/collectionobject_rules.py
Original file line number Diff line number Diff line change
@@ -1,28 +1,54 @@
from specifyweb.backend.businessrules.orm_signal_handler import orm_signal_handler

from specifyweb.backend.businessrules.exceptions import BusinessRuleException
from specifyweb.backend.businessrules.utils import get_unique_catnum_across_comp_co_coll_pref
from specifyweb.specify.models import Component

from specifyweb.backend.businessrules.utils import (
changed_fields_include,
collection_has_component_catalog_number,
get_default_collectionobjecttype_id,
get_unique_catnum_across_comp_co_coll_pref_by_ids,
)

def _collection_object_catalog_check_needed(co) -> bool:
if co.catalognumber is None:
return False
if co.pk is None:
return True
if not changed_fields_include(co, ("catalognumber", "collection")):
return False

return not type(co).objects.filter(
pk=co.pk,
catalognumber=co.catalognumber,
collection_id=co.collection_id,
).exists()

@orm_signal_handler('pre_save', 'Collectionobject')
def collectionobject_pre_save(co):
if co.collectionmemberid is None:
co.collectionmemberid = co.collection_id

if co.collectionobjecttype is None:
co.collectionobjecttype = co.collection.collectionobjecttype

agent = co.createdbyagent
if agent is not None and agent.specifyuser is not None:

unique_catnum_across_comp_co_coll_pref = get_unique_catnum_across_comp_co_coll_pref(co.collection, co.createdbyagent.specifyuser)

if unique_catnum_across_comp_co_coll_pref:
if co.catalognumber is not None:
contains_component_duplicates = Component.objects.filter(
catalognumber=co.catalognumber).exclude(pk=co.pk).exists()

if contains_component_duplicates:
raise BusinessRuleException(
'Catalog Number is already in use for another Component in this collection.')
if co.collectionobjecttype_id is None:
co.collectionobjecttype_id = get_default_collectionobjecttype_id(
co.collection_id
)

if (
co.createdbyagent_id is not None
and _collection_object_catalog_check_needed(co)
):

unique_catnum_across_comp_co_coll_pref = (
get_unique_catnum_across_comp_co_coll_pref_by_ids(
co.collection_id,
co.createdbyagent_id,
)
)

if unique_catnum_across_comp_co_coll_pref:
contains_component_duplicates = collection_has_component_catalog_number(
co.collection_id,
co.catalognumber,
)

if contains_component_duplicates:
raise BusinessRuleException("Catalog Number is already in use for another Component in this collection.")
18 changes: 16 additions & 2 deletions specifyweb/backend/businessrules/rules/component_rules.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,20 @@
from specifyweb.backend.businessrules.orm_signal_handler import orm_signal_handler
from specifyweb.backend.businessrules.exceptions import BusinessRuleException
from specifyweb.backend.businessrules.utils import get_unique_catnum_across_comp_co_coll_pref
from specifyweb.backend.businessrules.utils import (
clear_component_catalog_number_cache,
component_catalog_number_cache_is_active,
get_unique_catnum_across_comp_co_coll_pref,
)
from specifyweb.specify.models import Collectionobject, Component

def _clear_component_catalog_number_cache_if_needed(comp):
if component_catalog_number_cache_is_active():
clear_component_catalog_number_cache(comp.collectionobject.collection_id)

@orm_signal_handler('pre_save', 'Component')
def component_pre_save(comp):
_clear_component_catalog_number_cache_if_needed(comp)

agent = comp.createdbyagent
if agent is not None and agent.specifyuser is not None:
unique_catnum_across_comp_co_coll_pref = get_unique_catnum_across_comp_co_coll_pref(comp.collectionobject.collection, comp.createdbyagent.specifyuser)
Expand All @@ -19,4 +29,8 @@ def component_pre_save(comp):

if contains_co_duplicates or contains_component_duplicates:
raise BusinessRuleException(
'Catalog Number is already in use for another Collection Object or Component in this collection.')
'Catalog Number is already in use for another Collection Object or Component in this collection.')

@orm_signal_handler('pre_delete', 'Component')
def component_pre_delete(comp):
_clear_component_catalog_number_cache_if_needed(comp)
Loading
Loading