Repository: jazzband/django-simple-history

Issue:
Inadequate length for the 'history_change_reason' field
Hi, thanks for the great library! 

I hit a snag when I was trying to set the 'history_change_reason' value after doing some manual historical model diff'ing and then formatting the results into a human readable string. The model currently uses a CharField of only length 100 characters which is really not enough to display something useful.

Is it possible to make this field wider by default?
Would it make sense to make this value configurable?
Would it make sense to change this from a CharField to a TextField without max_length set?



Discussion:
#346 also asked about this. I'd be happy to make it configurable. Thinking that `CharField` with max_length=100 is a good default, but you should be able to pass an argument into `HistoricalRecords` that could specify `TextField` or `CharField` with a different max_length. The `CharField` is currently defined [here](https://github.com/treyhunner/django-simple-history/blob/master/simple_history/models.py#L256) for your reference. 
Oops, sorry. Didn't see that issue before posting.

I'll take a stab at it.

Resolving patch:
diff --git a/simple_history/models.py b/simple_history/models.py
--- a/simple_history/models.py
+++ b/simple_history/models.py
@@ -29,13 +29,15 @@ class HistoricalRecords(object):
 
     def __init__(self, verbose_name=None, bases=(models.Model,),
                  user_related_name='+', table_name=None, inherit=False,
+                 excluded_fields=None,
                  history_id_field=None,
-                 excluded_fields=None):
+                 history_change_reason_field=None):
         self.user_set_verbose_name = verbose_name
         self.user_related_name = user_related_name
         self.table_name = table_name
         self.inherit = inherit
         self.history_id_field = history_id_field
+        self.history_change_reason_field = history_change_reason_field
         if excluded_fields is None:
             excluded_fields = []
         self.excluded_fields = excluded_fields
@@ -250,11 +252,25 @@ def get_prev_record(self):
         else:
             history_id_field = models.AutoField(primary_key=True)
 
+        if self.history_change_reason_field:
+            # User specific field from init
+            history_change_reason_field = self.history_change_reason_field
+        elif getattr(
+            settings, 'SIMPLE_HISTORY_HISTORY_CHANGE_REASON_USE_TEXT_FIELD',
+            False
+        ):
+            # Use text field with no max length, not enforced by DB anyways
+            history_change_reason_field = models.TextField(null=True)
+        else:
+            # Current default, with max length
+            history_change_reason_field = models.CharField(
+                max_length=100, null=True
+            )
+
         return {
             'history_id': history_id_field,
             'history_date': models.DateTimeField(),
-            'history_change_reason': models.CharField(max_length=100,
-                                                      null=True),
+            'history_change_reason': history_change_reason_field,
             'history_user': models.ForeignKey(
                 user_model, null=True, related_name=self.user_related_name,
                 on_delete=models.SET_NULL),
@@ -306,6 +322,7 @@ def create_historical_record(self, instance, history_type):
         history_date = getattr(instance, '_history_date', now())
         history_user = self.get_history_user(instance)
         history_change_reason = getattr(instance, 'changeReason', None)
+
         manager = getattr(instance, self.manager_name)
         attrs = {}
         for field in self.fields_included(instance):


Test patch:
diff --git a/simple_history/registry_tests/tests.py b/simple_history/registry_tests/tests.py
--- a/simple_history/registry_tests/tests.py
+++ b/simple_history/registry_tests/tests.py
@@ -101,8 +101,8 @@ def test_tracked_abstract_base(self):
                 for f in TrackedWithAbstractBase.history.model._meta.fields
             ],
             [
-                'id', 'history_id', 'history_date',
-                'history_change_reason', 'history_user_id',
+                'id', 'history_id',
+                'history_change_reason', 'history_date', 'history_user_id',
                 'history_type',
             ],
         )
@@ -115,7 +115,7 @@ def test_tracked_concrete_base(self):
             ],
             [
                 'id', 'trackedconcretebase_ptr_id', 'history_id',
-                'history_date', 'history_change_reason', 'history_user_id',
+                'history_change_reason', 'history_date', 'history_user_id',
                 'history_type',
             ],
         )
@@ -131,7 +131,7 @@ def test_tracked_abstract_and_untracked_concrete_base(self):
             [f.attname for f in InheritTracking1.history.model._meta.fields],
             [
                 'id', 'untrackedconcretebase_ptr_id', 'history_id',
-                'history_date', 'history_change_reason',
+                'history_change_reason', 'history_date',
                 'history_user_id', 'history_type',
             ],
         )
@@ -141,7 +141,7 @@ def test_indirect_tracked_abstract_base(self):
             [f.attname for f in InheritTracking2.history.model._meta.fields],
             [
                 'id', 'baseinherittracking2_ptr_id', 'history_id',
-                'history_date', 'history_change_reason',
+                'history_change_reason', 'history_date',
                 'history_user_id', 'history_type',
             ],
         )
@@ -151,7 +151,7 @@ def test_indirect_tracked_concrete_base(self):
             [f.attname for f in InheritTracking3.history.model._meta.fields],
             [
                 'id', 'baseinherittracking3_ptr_id', 'history_id',
-                'history_date', 'history_change_reason',
+                'history_change_reason', 'history_date',
                 'history_user_id', 'history_type',
             ],
         )
diff --git a/simple_history/tests/models.py b/simple_history/tests/models.py
--- a/simple_history/tests/models.py
+++ b/simple_history/tests/models.py
@@ -432,3 +432,28 @@ class UUIDDefaultModel(models.Model):
 
 # Clear the SIMPLE_HISTORY_HISTORY_ID_USE_UUID
 delattr(settings, 'SIMPLE_HISTORY_HISTORY_ID_USE_UUID')
+
+
+# Set the SIMPLE_HISTORY_HISTORY_CHANGE_REASON_FIELD
+setattr(settings, 'SIMPLE_HISTORY_HISTORY_CHANGE_REASON_USE_TEXT_FIELD', True)
+
+
+class TextFieldChangeReasonModel1(models.Model):
+    greeting = models.CharField(max_length=100)
+    history = HistoricalRecords()
+
+
+# Clear the SIMPLE_HISTORY_HISTORY_CHANGE_REASON_FIELD
+delattr(settings, 'SIMPLE_HISTORY_HISTORY_CHANGE_REASON_USE_TEXT_FIELD')
+
+
+class TextFieldChangeReasonModel2(models.Model):
+    greeting = models.CharField(max_length=100)
+    history = HistoricalRecords(
+        history_change_reason_field=models.TextField(null=True)
+    )
+
+
+class CharFieldChangeReasonModel(models.Model):
+    greeting = models.CharField(max_length=100)
+    history = HistoricalRecords()
diff --git a/simple_history/tests/tests/test_models.py b/simple_history/tests/tests/test_models.py
--- a/simple_history/tests/tests/test_models.py
+++ b/simple_history/tests/tests/test_models.py
@@ -53,7 +53,10 @@
     UnicodeVerboseName,
     UUIDModel,
     UUIDDefaultModel,
-    WaterLevel
+    WaterLevel,
+    TextFieldChangeReasonModel1,
+    TextFieldChangeReasonModel2,
+    CharFieldChangeReasonModel,
 )
 
 get_model = apps.get_model
@@ -382,6 +385,57 @@ def test_uuid_default_history_id(self):
         history = entry.history.all()[0]
         self.assertTrue(isinstance(history.history_id, uuid.UUID))
 
+    def test_default_history_change_reason(self):
+        entry = CharFieldChangeReasonModel.objects.create(
+            greeting="what's up?"
+        )
+        history = entry.history.get()
+
+        self.assertEqual(history.history_change_reason, None)
+
+    def test_charfield_history_change_reason(self):
+        # Default CharField and length
+        entry = CharFieldChangeReasonModel.objects.create(
+            greeting="what's up?"
+        )
+        entry.greeting = "what is happening?"
+        entry.save()
+        update_change_reason(entry, 'Change greeting.')
+
+        history = entry.history.all()[0]
+        field = history._meta.get_field('history_change_reason')
+
+        self.assertTrue(isinstance(field, models.CharField))
+        self.assertTrue(field.max_length, 100)
+
+    def test_textfield_history_change_reason1(self):
+        # TextField usage is determined by settings
+        entry = TextFieldChangeReasonModel1.objects.create(
+            greeting="what's up?"
+        )
+        entry.greeting = "what is happening?"
+        entry.save()
+        update_change_reason(entry, 'Change greeting.')
+
+        history = entry.history.all()[0]
+        field = history._meta.get_field('history_change_reason')
+
+        self.assertTrue(isinstance(field, models.TextField))
+
+    def test_textfield_history_change_reason2(self):
+        # TextField instance is passed in init
+        entry = TextFieldChangeReasonModel2.objects.create(
+            greeting="what's up?"
+        )
+        entry.greeting = "what is happening?"
+        entry.save()
+        update_change_reason(entry, 'Change greeting.')
+
+        history = entry.history.all()[0]
+        field = history._meta.get_field('history_change_reason')
+
+        self.assertTrue(isinstance(field, models.TextField))
+
     def test_get_prev_record(self):
         poll = Poll(question="what's up?", pub_date=today)
         poll.save()