|
6 | 6 | from collections.abc import Callable |
7 | 7 | from django.db import transaction |
8 | 8 | from django.core.exceptions import FieldError, FieldDoesNotExist |
9 | | -from django.db.models import Model, F |
| 9 | +from django.db.models import Model, F, Q, Subquery |
10 | 10 | from django.http import (HttpResponseServerError, Http404) |
11 | 11 | from django.apps import apps |
12 | 12 |
|
13 | 13 | from specifyweb.backend.permissions.permissions import check_field_permissions, check_table_permissions |
| 14 | +from specifyweb.backend.businessrules.exceptions import BusinessRuleException |
| 15 | +from specifyweb.backend.trees.utils import DISCIPLINE_TREE_MODELS |
14 | 16 | from specifyweb.backend.workbench.upload.auditlog import auditlog |
15 | 17 | from specifyweb.specify import models |
16 | 18 | from specifyweb.specify.api.api_utils import objs_to_data_, CollectionPayload |
@@ -188,6 +190,146 @@ def _deleter(obj, parent_obj): |
188 | 190 | auditlog.remove(obj, agent, parent_obj) |
189 | 191 | return _deleter |
190 | 192 |
|
| 193 | +def is_discipline(obj) -> bool: |
| 194 | + return getattr(getattr(obj, "_meta", None), "model_name", "") == "discipline" |
| 195 | + |
| 196 | +def get_discipline_delete_guard_blockers(discipline) -> list[dict[str, Any]]: |
| 197 | + """ |
| 198 | + Returns discipline blockers for deleting disciplines in the configuration tool. |
| 199 | + A discipline can be deleted only when it has no associated collections and no associated users. |
| 200 | + """ |
| 201 | + if not is_discipline(discipline): |
| 202 | + return [] |
| 203 | + |
| 204 | + collection_ids = sorted(discipline.collections.values_list("id", flat=True)) |
| 205 | + |
| 206 | + blockers: list[dict[str, Any]] = [] |
| 207 | + if collection_ids: |
| 208 | + blockers.append( |
| 209 | + { |
| 210 | + "table": "Collection", |
| 211 | + "field": "discipline", |
| 212 | + "ids": collection_ids, |
| 213 | + } |
| 214 | + ) |
| 215 | + return blockers |
| 216 | + |
| 217 | + discipline_scoped_user_blockers = ( |
| 218 | + ( |
| 219 | + "Spappresourcedir", |
| 220 | + "specifyuser", |
| 221 | + sorted( |
| 222 | + models.Spappresourcedir.objects.filter( |
| 223 | + discipline=discipline, |
| 224 | + specifyuser__isnull=False, |
| 225 | + ).values_list("id", flat=True) |
| 226 | + ), |
| 227 | + ), |
| 228 | + ( |
| 229 | + "Sptasksemaphore", |
| 230 | + "owner", |
| 231 | + sorted( |
| 232 | + models.Sptasksemaphore.objects.filter( |
| 233 | + discipline=discipline, |
| 234 | + owner__isnull=False, |
| 235 | + ).values_list("id", flat=True) |
| 236 | + ), |
| 237 | + ), |
| 238 | + ) |
| 239 | + for table_name, field_name, blocker_ids in discipline_scoped_user_blockers: |
| 240 | + if blocker_ids: |
| 241 | + blockers.append( |
| 242 | + { |
| 243 | + "table": table_name, |
| 244 | + "field": field_name, |
| 245 | + "ids": blocker_ids, |
| 246 | + } |
| 247 | + ) |
| 248 | + return blockers |
| 249 | + |
| 250 | +def _raw_delete_queryset(queryset) -> None: |
| 251 | + queryset._raw_delete(queryset.db) |
| 252 | + |
| 253 | +def delete_discipline_owned_setup_data(obj) -> None: |
| 254 | + """ |
| 255 | + Remove discipline-scoped setup/config rows in bulk before the final |
| 256 | + discipline delete so Django doesn't need to build one large collector graph. |
| 257 | + """ |
| 258 | + if not is_discipline(obj): |
| 259 | + return |
| 260 | + |
| 261 | + from specifyweb.backend.businessrules.models import ( |
| 262 | + UniquenessRule, |
| 263 | + UniquenessRuleField, |
| 264 | + ) |
| 265 | + |
| 266 | + schema_ids = models.Spexportschema.objects.filter( |
| 267 | + discipline_id=obj.id |
| 268 | + ).values("id") |
| 269 | + export_item_ids = models.Spexportschemaitem.objects.filter( |
| 270 | + spexportschema_id__in=Subquery(schema_ids) |
| 271 | + ).values("id") |
| 272 | + _raw_delete_queryset( |
| 273 | + models.Spexportschemaitemmapping.objects.filter( |
| 274 | + exportschemaitem_id__in=Subquery(export_item_ids) |
| 275 | + ) |
| 276 | + ) |
| 277 | + _raw_delete_queryset( |
| 278 | + models.Spexportschema_exportmapping.objects.filter( |
| 279 | + spexportschema_id__in=Subquery(schema_ids) |
| 280 | + ) |
| 281 | + ) |
| 282 | + _raw_delete_queryset( |
| 283 | + models.Spexportschemaitem.objects.filter( |
| 284 | + spexportschema_id__in=Subquery(schema_ids) |
| 285 | + ) |
| 286 | + ) |
| 287 | + _raw_delete_queryset(models.Spexportschema.objects.filter(discipline_id=obj.id)) |
| 288 | + |
| 289 | + container_ids = models.Splocalecontainer.objects.filter( |
| 290 | + discipline_id=obj.id |
| 291 | + ).values("id") |
| 292 | + item_ids = models.Splocalecontaineritem.objects.filter( |
| 293 | + container_id__in=Subquery(container_ids) |
| 294 | + ).values("id") |
| 295 | + _raw_delete_queryset( |
| 296 | + models.Splocaleitemstr.objects.filter( |
| 297 | + Q(containerdesc_id__in=Subquery(container_ids)) |
| 298 | + | Q(containername_id__in=Subquery(container_ids)) |
| 299 | + | Q(itemdesc_id__in=Subquery(item_ids)) |
| 300 | + | Q(itemname_id__in=Subquery(item_ids)) |
| 301 | + ) |
| 302 | + ) |
| 303 | + _raw_delete_queryset( |
| 304 | + models.Splocalecontaineritem.objects.filter( |
| 305 | + container_id__in=Subquery(container_ids) |
| 306 | + ) |
| 307 | + ) |
| 308 | + _raw_delete_queryset(models.Splocalecontainer.objects.filter(discipline_id=obj.id)) |
| 309 | + |
| 310 | + rule_ids = UniquenessRule.objects.filter(discipline_id=obj.id).values("id") |
| 311 | + _raw_delete_queryset( |
| 312 | + UniquenessRuleField.objects.filter( |
| 313 | + uniquenessrule_id__in=Subquery(rule_ids) |
| 314 | + ) |
| 315 | + ) |
| 316 | + _raw_delete_queryset(UniquenessRule.objects.filter(discipline_id=obj.id)) |
| 317 | + |
| 318 | + _raw_delete_queryset(models.Spappresourcedir.objects.filter(discipline_id=obj.id)) |
| 319 | + _raw_delete_queryset(models.Sptasksemaphore.objects.filter(discipline_id=obj.id)) |
| 320 | + _raw_delete_queryset(models.Autonumschdsp.objects.filter(discipline_id=obj.id)) |
| 321 | + |
| 322 | +def prepare_discipline_for_delete(obj) -> None: |
| 323 | + """ |
| 324 | + Detach discipline owned tree defs and bulk delete discipline-scoped setup |
| 325 | + data before deletion so empty disciplines delete quickly. |
| 326 | + """ |
| 327 | + if not is_discipline(obj): |
| 328 | + return |
| 329 | + |
| 330 | + for tree_def_model in DISCIPLINE_TREE_MODELS: |
| 331 | + tree_def_model.objects.filter(discipline_id=obj.id).update(discipline_id=None) |
| 332 | + delete_discipline_owned_setup_data(obj) |
191 | 333 |
|
192 | 334 | @transaction.atomic |
193 | 335 | def put_resource(collection, agent, name: str, id, version, data: dict[str, Any]): |
@@ -308,8 +450,22 @@ def delete_resource(collection, agent, name, id, version) -> None: |
308 | 450 | locking 'version'. |
309 | 451 | """ |
310 | 452 | obj = get_object_or_404(name, id=int(id)) |
311 | | - return delete_obj(obj, (make_default_deleter(collection, agent)), version) |
312 | | - |
| 453 | + if is_discipline(obj): |
| 454 | + guard_blockers = get_discipline_delete_guard_blockers(obj) |
| 455 | + if guard_blockers: |
| 456 | + raise BusinessRuleException( |
| 457 | + "Discipline cannot be deleted while it has associated users or collections." |
| 458 | + ) |
| 459 | + clean_predelete = prepare_discipline_for_delete |
| 460 | + else: |
| 461 | + clean_predelete = None |
| 462 | + |
| 463 | + return delete_obj( |
| 464 | + obj, |
| 465 | + make_default_deleter(collection, agent), |
| 466 | + version, |
| 467 | + clean_predelete=clean_predelete, |
| 468 | + ) |
313 | 469 |
|
314 | 470 | def get_collection(logged_in_collection, model, checker: ReadPermChecker, control_params=GetCollectionForm.defaults, params={}) -> CollectionPayload: |
315 | 471 | from specifyweb.specify.api.serializers import _obj_to_data |
|
0 commit comments