You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
As a repo maintainer, I'm always nervous around bulk actions being introduced which don't capture history alongside
Describe the solution you'd like
It'd be nice to have the register function handle enforcement for externally managed models,
as well as a QuerySet and Manager for ones created in the repo.
Describe alternatives you've considered
I've implemented workarounds in my own repos, but this is the second time I'm doing this, so it seems like a good pattern to handle.
Additional context
Here are snippets of code I've used:
For externally defined models (e.g. User):
models.py:
BOUND_EXTERNAL_MODELS= []
defbind_external_model(model):
# Bind history for model, https://django-simple-history.readthedocs.io/en/latest/quick_start.html#track-history-for-a-third-party-model # noqa:E501# DEV: Due to User coming from Django, `makemigrations` will create its migration inside virtualenv directory (e.g. `/root/.virtualenvs/...` (not git trackable)) without `app=` # noqa:E501register(model, app="MY_APP")
BOUND_EXTERNAL_MODELS.append(model)
# DEV: We expect no bulk mechanisms to be called on external models# If we do want this, then please add support to run relevant `full_clean` calls alwaysassertmodel._default_manager==model.objects, f"Expected {model}._default_manager to be {model}.objects"assert (
model._default_manager._queryset_class==QuerySet
), f"Expected {model}._default_manager._queryset_class to be QuerySet"defproxy_queryset_method(klass, method_name):
original_method=getattr(klass, method_name)
@wraps(original_method)defnew_method_to_reject_bound_models(self, *args, **kwargs):
ifself.modelinBOUND_EXTERNAL_MODELS:
raiseRuntimeError(
# Same message as in `signals.py`"Attempting to run a bulk action (intentional or not) outside of django-simple-history utilities. ""django-simple-history is not attached to bulk actions due to no signals. ""Please use `bulk_create_with_history` or `bulk_update_with_history` utilities, "f"or the history-less `objects.unsafe_{method_name}` instead."
)
returnoriginal_method(self, *args, **kwargs)
setattr(klass, method_name, new_method_to_reject_bound_models)
setattr(klass, f"unsafe_{method_name}", original_method)
# DEV: We initially bound at `Manager` level but `.filter()` dropped to `QuerySet` level# We could've monkey patched at `Manager.get_queryset()` but this feels even stronger (i.e. all scenarios)proxy_queryset_method(QuerySet, "bulk_create")
proxy_queryset_method(QuerySet, "bulk_update")
proxy_queryset_method(QuerySet, "update")
For repo-defined models
models.py:
classHistoryEnforcingQuerySet(QuerySet):
def_enforce_bulk_action_with_history(self):
forframe_infoininspect.stack():
# Inner frame_info: FrameInfo(# frame=<frame at 0x32bce50, file '***/simple_history/utils.py', line 98, code bulk_create_with_history>,# filename='***/simple_history/utils.py',# lineno=98, function='bulk_create_with_history',# code_context=[' objs_with_id = model_manager.bulk_create(\n'],# index=0# )# Outer frame_info: FrameInfo(# frame=<frame at 0x7fb177db8420, file 'MY_APP/models.py', ***, # noqa:E501# filename='MY_APP/models.py',# lineno=374, function='***',# code_context=[' bulk_create_with_history(obj_list, model=Model)\n'],# index=0# )if (
frame_info.functionin ["bulk_create_with_history", "bulk_update_with_history"]
and"site-packages/simple_history"inframe_info.filename
):
return# WARNING: If we ever need to check against `loaddata`, then this makes relevant tests stop working# i.e. Fixtures use `loaddata` so tests will just start passing =/raiseRuntimeError(
# Same message as in `models.py`"Attempting to run a bulk action (intentional or not) outside of django-simple-history utilities. ""django-simple-history is not attached to bulk actions due to no signals. ""Please use `bulk_create_with_history` or `bulk_update_with_history` utilities, ""or the history-leass `objects.unsafe_*` methods instead."
)
# https://docs.djangoproject.com/en/4.2/ref/models/querysets/#bulk-createdefbulk_create(self, objs, *args, **kwargs):
self._enforce_bulk_action_with_history()
returnself.unsafe_bulk_create(objs, *args, **kwargs)
# https://docs.djangoproject.com/en/4.2/ref/models/querysets/#bulk-updatedefbulk_update(self, objs, fields, *args, **kwargs):
self._enforce_bulk_action_with_history()
returnself.unsafe_bulk_update(objs, fields, *args, **kwargs)
defupdate(self, *args, **kwargs):
raiseRuntimeError(
"QuerySet#update not permitted due to bypassing django-simple-history. ""Please use `bulk_create_with_history` or `bulk_update_with_history` utilities, ""or the history-leass `objects.unsafe_*` methods instead."
)
defunsafe_bulk_create(self, objs, *args, **kwargs):
returnsuper().bulk_create(objs, *args, **kwargs)
defunsafe_bulk_update(self, objs, fields, *args, **kwargs):
returnsuper().bulk_update(objs, fields, *args, **kwargs)
defunsafe_update(self, objs, fields, *args, **kwargs):
returnsuper().bulk_update(objs, fields, *args, **kwargs)
classHistoryEnforcingManager(models.Manager.from_queryset(HistoryEnforcingQuerySet)):
use_in_migrations=False
Alternative: The original was split up with signals.py for enforcement (played nicely to have such signals for other mechanisms)
Note: The original also never used objects directly as its first manager, so the implementation feels a little iffy -- instead it used:
Issue
Problem Statement
As a repo maintainer, I'm always nervous around bulk actions being introduced which don't capture history alongside
Describe the solution you'd like
It'd be nice to have the
register
function handle enforcement for externally managed models,as well as a
QuerySet
andManager
for ones created in the repo.Describe alternatives you've considered
I've implemented workarounds in my own repos, but this is the second time I'm doing this, so it seems like a good pattern to handle.
Additional context
Here are snippets of code I've used:
For externally defined models (e.g.
User
):models.py:
For repo-defined models
models.py:
Alternative: The original was split up with
signals.py
for enforcement (played nicely to have such signals for other mechanisms)Note: The original also never used
objects
directly as its first manager, so the implementation feels a little iffy -- instead it used:but I'm confident that what I've written above should work well
The text was updated successfully, but these errors were encountered: