Skip to content

Commit 3df2061

Browse files
uhurusurfaRoss Mechanic
authored andcommitted
Support passing a callable in "custom_model_name" feature (#490)
* Move paragraph on "custom_moel_name" to advanced.rst Add extensive documetnation on additional functionality for "custom_moel_name" usage. * Document Issue 489 change * Support callable for the "custom_moel_name" option * Additional tests for "custom_model_name" option * Formatting fixes * Add additional tests for "custom_model_name" feature * Refactor the create_history_model method to simplify * Formatting fixes * Further refactoring to reduce complexity of method * Fix errors in documentation per PR review * Fix the exception raise * If using custom_model_name results in a history class name that is the same as the models class it is tracking then throw an error. * Update the "custom_model_name" documentation * Merge upstream master * Split into multiple tests * Fix formatting issue * Fix duplication of the class * Change "clazz" to "cls" * Change logic per review request * Fix the rebase to use the new document structure.
1 parent 400eec7 commit 3df2061

File tree

6 files changed

+194
-17
lines changed

6 files changed

+194
-17
lines changed

CHANGES.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ Changes
77
- Fix router backward-compatibility issue with 2.7.0 (gh-539, gh-547)
88
- Fix hardcoded history manager (gh-542)
99
- Replace deprecated `django.utils.six` with `six` (gh-526)
10+
- Allow `custom_model_name` parameter to be a callable (gh-489)
1011

1112
2.7.0 (2019-01-16)
1213
------------------

docs/historical_model.rst

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,13 +122,42 @@ Users can specify a custom model name via the constructor on
122122
``HistoricalRecords``. The common use case for this is avoiding naming conflict
123123
if the user already defined a model named as 'Historical' + model name.
124124

125+
This feature provides the ability to override the default model name used for the generated history model.
126+
127+
To configure history models to use a different name for the history model class, use an option named ``custom_model_name``.
128+
The value for this option can be a `string` or a `callable`.
129+
A simple string replaces the default name of `'Historical' + model name` with the defined string.
130+
The most simple use case is illustrated below using a simple string:
131+
125132
.. code-block:: python
126133
127134
class ModelNameExample(models.Model):
128135
history = HistoricalRecords(
129136
custom_model_name='SimpleHistoricalModelNameExample'
130137
)
131138
139+
If you are using a base class for your models and want to apply a name change for the historical model
140+
for all models using the base class then a callable can be used.
141+
The callable is passed the name of the model for which the history model will be created.
142+
As an example using the callable mechanism, the below changes the default prefix `Historical` to `Audit`:
143+
144+
.. code-block:: python
145+
146+
class Poll(models.Model):
147+
question = models.CharField(max_length=200)
148+
history = HistoricalRecords(custom_model_name=lambda x:f'Audit{x}')
149+
150+
class Opinion(models.Model):
151+
opinion = models.CharField(max_length=2000)
152+
153+
register(Opinion, custom_model_name=lambda x:f'Audit{x}')
154+
155+
The resulting history class names would be `AuditPoll` and `AuditOpinion`.
156+
If the app the models are defined in is `yoda` then the corresponding history table names would be `yoda_auditpoll` and `yoda_auditopinion`
157+
158+
IMPORTANT: Setting `custom_model_name` to `lambda x:f'{x}'` is not permitted.
159+
An error will be generated and no history model created if they are the same.
160+
132161

133162
TextField as `history_change_reason`
134163
------------------------------------

simple_history/models.py

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,28 @@ def finalize(self, sender, **kwargs):
168168
setattr(sender, self.manager_name, descriptor)
169169
sender._meta.simple_history_manager_attribute = self.manager_name
170170

171+
def get_history_model_name(self, model):
172+
if not self.custom_model_name:
173+
return "Historical{}".format(model._meta.object_name)
174+
# Must be trying to use a custom history model name
175+
if callable(self.custom_model_name):
176+
name = self.custom_model_name(model._meta.object_name)
177+
else:
178+
# simple string
179+
name = self.custom_model_name
180+
# Desired class name cannot be same as the model it is tracking
181+
if not (
182+
name.lower() == model._meta.object_name.lower()
183+
and model.__module__ == self.module
184+
):
185+
return name
186+
raise ValueError(
187+
"The 'custom_model_name' option '{}' evaluates to a name that is the same "
188+
"as the model it is tracking. This is not permitted.".format(
189+
self.custom_model_name
190+
)
191+
)
192+
171193
def create_history_model(self, model, inherited):
172194
"""
173195
Creates a historical model to associate with the model provided.
@@ -198,11 +220,10 @@ def create_history_model(self, model, inherited):
198220
attrs.update(Meta=type(str("Meta"), (), self.get_meta_options(model)))
199221
if self.table_name is not None:
200222
attrs["Meta"].db_table = self.table_name
201-
name = (
202-
self.custom_model_name
203-
if self.custom_model_name is not None
204-
else "Historical%s" % model._meta.object_name
205-
)
223+
224+
# Set as the default then check for overrides
225+
name = self.get_history_model_name(model)
226+
206227
registered_models[model._meta.db_table] = model
207228
return python_2_unicode_compatible(type(str(name), self.bases, attrs))
208229

simple_history/tests/external/models.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,26 @@ class Meta:
1414
abstract = True
1515

1616

17+
class AbstractExternal2(models.Model):
18+
history = HistoricalRecords(
19+
inherit=True, custom_model_name=lambda x: "Audit{}".format(x)
20+
)
21+
22+
class Meta:
23+
abstract = True
24+
app_label = "external"
25+
26+
27+
class AbstractExternal3(models.Model):
28+
history = HistoricalRecords(
29+
inherit=True, app="external", custom_model_name=lambda x: "Audit{}".format(x)
30+
)
31+
32+
class Meta:
33+
abstract = True
34+
app_label = "external"
35+
36+
1737
class ExternalModel(models.Model):
1838
name = models.CharField(max_length=100)
1939
history = HistoricalRecords()

simple_history/tests/models.py

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@
99
from simple_history import register
1010
from simple_history.models import HistoricalRecords
1111
from .custom_user.models import CustomUser as User
12+
1213
from .external.models import AbstractExternal
14+
from .external.models import AbstractExternal2
15+
from .external.models import AbstractExternal3
1316

1417
get_model = apps.get_model
1518

@@ -566,14 +569,59 @@ class CharFieldChangeReasonModel(models.Model):
566569
history = HistoricalRecords()
567570

568571

569-
class CustomNameModel(models.Model):
572+
class CustomManagerNameModel(models.Model):
573+
name = models.CharField(max_length=15)
574+
log = HistoricalRecords()
575+
576+
577+
"""
578+
Following classes test the "custom_model_name" option
579+
"""
580+
581+
582+
class OverrideModelNameAsString(models.Model):
570583
name = models.CharField(max_length=15, unique=True)
571584
history = HistoricalRecords(custom_model_name="MyHistoricalCustomNameModel")
572585

573586

574-
class CustomManagerNameModel(models.Model):
575-
name = models.CharField(max_length=15)
576-
log = HistoricalRecords()
587+
class OverrideModelNameAsCallable(models.Model):
588+
name = models.CharField(max_length=15, unique=True)
589+
history = HistoricalRecords(custom_model_name=lambda x: "Audit{}".format(x))
590+
591+
592+
class AbstractModelCallable1(models.Model):
593+
history = HistoricalRecords(
594+
inherit=True, custom_model_name=lambda x: "Audit{}".format(x)
595+
)
596+
597+
class Meta:
598+
abstract = True
599+
600+
601+
class OverrideModelNameUsingBaseModel1(AbstractModelCallable1):
602+
name = models.CharField(max_length=15, unique=True)
603+
604+
605+
class OverrideModelNameUsingExternalModel1(AbstractExternal2):
606+
name = models.CharField(max_length=15, unique=True)
607+
608+
609+
class OverrideModelNameUsingExternalModel2(AbstractExternal3):
610+
name = models.CharField(max_length=15, unique=True)
611+
612+
613+
class OverrideModelNameRegisterMethod1(models.Model):
614+
name = models.CharField(max_length=15, unique=True)
615+
616+
617+
register(
618+
OverrideModelNameRegisterMethod1,
619+
custom_model_name="MyOverrideModelNameRegisterMethod1",
620+
)
621+
622+
623+
class OverrideModelNameRegisterMethod2(models.Model):
624+
name = models.CharField(max_length=15, unique=True)
577625

578626

579627
class ForeignKeyToSelfModel(models.Model):

simple_history/tests/tests/test_models.py

Lines changed: 66 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,9 @@
6262
HistoricalPoll,
6363
HistoricalPollWithHistoricalIPAddress,
6464
HistoricalState,
65+
OverrideModelNameAsCallable,
66+
OverrideModelNameUsingBaseModel1,
67+
MyOverrideModelNameRegisterMethod1,
6568
Library,
6669
ModelWithFkToModelWithHistoryUsingBaseModelDb,
6770
ModelWithHistoryInDifferentDb,
@@ -711,17 +714,72 @@ def test_create_history_model_with_multiple_one_to_ones(self):
711714
"exception."
712715
)
713716

714-
def test_instantiate_history_model_with_custom_model_name(self):
717+
718+
class CustomModelNameTests(unittest.TestCase):
719+
def verify_custom_model_name_feature(
720+
self, model, expected_class_name, expected_table_name
721+
):
722+
history_model = model.history.model
723+
self.assertEqual(history_model.__name__, expected_class_name)
724+
self.assertEqual(history_model._meta.db_table, expected_table_name)
725+
726+
def test_instantiate_history_model_with_custom_model_name_as_string(self):
715727
try:
716-
from ..models import MyHistoricalCustomNameModel
728+
from ..models import OverrideModelNameAsString
717729
except ImportError:
718-
self.fail("MyHistoricalCustomNameModel is in wrong module")
719-
historical_model = MyHistoricalCustomNameModel()
720-
self.assertEqual(
721-
historical_model.__class__.__name__, "MyHistoricalCustomNameModel"
730+
self.fail("{}OverrideModelNameAsString is in wrong module")
731+
expected_cls_name = "MyHistoricalCustomNameModel"
732+
self.verify_custom_model_name_feature(
733+
OverrideModelNameAsString(),
734+
expected_cls_name,
735+
"tests_{}".format(expected_cls_name.lower()),
722736
)
723-
self.assertEqual(
724-
historical_model._meta.db_table, "tests_myhistoricalcustomnamemodel"
737+
738+
def test_register_history_model_with_custom_model_name_override(self):
739+
try:
740+
from ..models import OverrideModelNameRegisterMethod1
741+
except ImportError:
742+
self.fail("OverrideModelNameRegisterMethod1 is in wrong module")
743+
744+
cls = OverrideModelNameRegisterMethod1()
745+
expected_cls_name = "MyOverrideModelNameRegisterMethod1"
746+
self.verify_custom_model_name_feature(
747+
cls, expected_cls_name, "tests_{}".format(expected_cls_name.lower())
748+
)
749+
750+
from simple_history import register
751+
from ..models import OverrideModelNameRegisterMethod2
752+
753+
try:
754+
register(
755+
OverrideModelNameRegisterMethod2,
756+
custom_model_name=lambda x: "{}".format(x),
757+
)
758+
except ValueError:
759+
self.assertRaises(ValueError)
760+
761+
def test_register_history_model_with_custom_model_name_from_abstract_model(self):
762+
cls = OverrideModelNameUsingBaseModel1
763+
expected_cls_name = "Audit{}".format(cls.__name__)
764+
self.verify_custom_model_name_feature(
765+
cls, expected_cls_name, "tests_" + expected_cls_name.lower()
766+
)
767+
768+
def test_register_history_model_with_custom_model_name_from_external_model(self):
769+
from ..models import OverrideModelNameUsingExternalModel1
770+
771+
cls = OverrideModelNameUsingExternalModel1
772+
expected_cls_name = "Audit{}".format(cls.__name__)
773+
self.verify_custom_model_name_feature(
774+
cls, expected_cls_name, "tests_" + expected_cls_name.lower()
775+
)
776+
777+
from ..models import OverrideModelNameUsingExternalModel2
778+
779+
cls = OverrideModelNameUsingExternalModel2
780+
expected_cls_name = "Audit{}".format(cls.__name__)
781+
self.verify_custom_model_name_feature(
782+
cls, expected_cls_name, "external_" + expected_cls_name.lower()
725783
)
726784

727785

0 commit comments

Comments
 (0)