Commit 141d57b
committed
Prevent deletion of
`Codelist` are created with one `Handle`. They may gain more later, but
business logic in `actions.py` does not permit deletion of any of them,
and parts of the application may expect that there is always a current
`Handle` for a `Codelist`. So we ought not permit it in application code.
The only models with deletion in `codelists/actions.py` or `builder/actions.py`
are `Codelist` (when discarding the last draft version), `CodelistVersion` (when
discarding drafts), and `Search` (when deleting a search in the builder).
We don't think that we have application code that does this (see `actions.py`)
but have observered it twice in production in recent months, see parent issue
#2893. This also helps
avoid any use of the developer Django shell to delete `Handles`.
We enforce this constraint only in the Django model layer. That does not
stop `Handles` being deleted by other means such as via direct SQL, such
as via `on_delete=models.CASCADE`, see
https://docs.djangoproject.com/en/5.2/topics/db/queries/#deleting-objects
> Keep in mind that this will, whenever possible, be executed purely in SQL,
> and so the delete() methods of individual object instances will not
> necessarily be called during the process.
That includes `Handle` when deleted via `Codelist` `CASCADE` as they do not
have any dependent models, triggers etc. See
https://github.com/django/django/blob/cb1d2854ed2b13799f2b0cc6e04019df181bacd4/django/db/models/deletion.py#L501
Trying locally via deleting a codelist draft in the web UI led to:
```
[debug ] (0.007) DELETE FROM "codelists_handle" WHERE "codelists_handle"."codelist_id" IN (9979); args=(9979,); alias=default [django.db.backends]
```
... and I can find and delete multiple codelist with multiple handles in the
Django shell, which deletes the handles via cascade:
```
>>> from django.db.models import Count
>>> five_handle=Codelist.objects.annotate(hcount=Count("handles")).filter(hcount=5)
>>> len(five_handle)
2
>>> five_handle.delete()
<SUCCESS MESSAGE>
>>> five_handle=Codelist.objects.annotate(hcount=Count("handles")).filter(hcount=5)
>>> len(five_handle)
0
```
... and I cannot delete via the handle instances or `QuerySet` methods directly in the Django shell:
```
>>> three_handle_cl=Codelist.objects.annotate(hcount=Count("handles")).filter(hcount=3).first()
>>> three_handle_cl.handles.all().delete()
codelists.models.DeleteHandleException: Bulk deletion of Handle instances via the ORM is not permitted. Attempted on QuerySet `<NoDeleteHandleQuerySet [<Handle: Handle object (2800)>, <Handle: Handle object (2811)>, <Handle: Handle object (2908)>]>`
>>> three_handle_cl.handles.all().first().delete()
codelists.models.DeleteHandleException: May not delete handles - attempted on `Handle object (2800)` for codelist `BNF codes for NSAIDs (Medication Safety Indicator GIB01)`
```
Added automated tests for the overriden methods and manager queryset. There are
already automated tests for discarding drafts etc. in
`builder/tests/test_views.py`:
- test_discard_only_draft_version
- test_discard_one_draft_versionHandle instances via ORM.1 parent 93134f1 commit 141d57b
3 files changed
+54
-4
lines changed| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
4 | 4 | | |
5 | 5 | | |
6 | 6 | | |
| 7 | + | |
7 | 8 | | |
8 | 9 | | |
9 | 10 | | |
| |||
217 | 218 | | |
218 | 219 | | |
219 | 220 | | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
220 | 234 | | |
221 | 235 | | |
222 | 236 | | |
| |||
271 | 285 | | |
272 | 286 | | |
273 | 287 | | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
| 302 | + | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
| 308 | + | |
274 | 309 | | |
275 | 310 | | |
276 | 311 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
3 | 3 | | |
4 | 4 | | |
5 | 5 | | |
| 6 | + | |
6 | 7 | | |
7 | 8 | | |
8 | | - | |
| 9 | + | |
9 | 10 | | |
10 | 11 | | |
11 | 12 | | |
| |||
272 | 273 | | |
273 | 274 | | |
274 | 275 | | |
275 | | - | |
| 276 | + | |
276 | 277 | | |
277 | | - | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
278 | 283 | | |
279 | 284 | | |
280 | 285 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
3 | 3 | | |
4 | 4 | | |
5 | 5 | | |
6 | | - | |
| 6 | + | |
7 | 7 | | |
8 | 8 | | |
9 | 9 | | |
| |||
150 | 150 | | |
151 | 151 | | |
152 | 152 | | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
153 | 163 | | |
154 | 164 | | |
155 | 165 | | |
| |||
0 commit comments