Repository: jazzband/django-simple-history

Issue:
Can't Access the History Detail View of /app/model/<pk>/history/<history_id>
We have this setup (Django **1.8.18**, SimpleHistory **1.8.2**):
```
# core/models.py
class VersionedAbstractModel(models.Model):
    class Meta:
        abstract = True

    history = HistoricalRecords(inherit=True)


# accounts/models.py
class Brand(VersionedAbstractModel):
    ...

```

So, when I run the `makemigrations`, SimpleHistory migration files will be created in **Accounts** (contains the VersionedAbstractModel child models) and **Core** apps (contains the VersionedAbstractModel parent model). Everything is working fine except when going to the Detail View of particular history objects (e.g. `/accounts/brand/4/history/3/`) since the breadcrumbs in `object_history_form.html` are not resolving properly. It will reference the `/core` admin URL for the Core app (which is non-existent in our case).

Moreover, breadcrumb will switch from `Home › Accounts › Brands › Brand-1 › History` to `Home › Core › Historical Brands › Brand-1 › History › Revert Brand` as you go from history list to history details/revert pages. Hence, it switches scope from **Accounts** app to **Core** app. Unfortunately, we have **NO** non-abstract model registered in our **Core** app and it only contains services/utility/helper functions, plus the migration files for every model due to the inherited VersionedAbstractModel. This issue probably happens when `inherit=True` and/or the involved `parent-child` models are in separate apps/namespaces.

Error Trace:
```
NoReverseMatch at /accounts/brand/4/history/3/
Reverse for 'app_list' with arguments '(u'core',)' and keyword arguments '{}' not found. 
1 pattern(s) tried: [u'(?P<app_label>accounts|events)/$']

In template ~/.virtualenvs/eventcms/eventcms/templates/simple_history/object_history_form.html,
error at line 8:
<a href="{% url "admin:app_list" app_label %}">{{app_label\|capfirst\|escape}}</a> &rsaquo;
```

Possible Workarounds:
1. Create a dummy model in our **Core** app and register it via `admin.site.register()` so that `/core` URL will be registered. However, the dummy model will show in Admin dashboard.
2. Override the `object_history_form.html` and remove the line referencing the **Core** app or the app containing the parent `SimpleHistory` model (i.e. the `VersionedAbstractModel`).

Eventually, we did the item 2 since we have no need for the `/core` URL as of now. It would be great/interesting if you could find a cleaner/simpler solution.

Cheers for this great app! :)



Discussion:
I'm guessing this also means that the historical tables have a 'core_' prefix? It looks like our app label testing is a bit light with regards to inherited history tracking:

https://github.com/treyhunner/django-simple-history/blob/1.9.0/simple_history/tests/tests/test_models.py#L378

I'm thinking it's a bug in how the app label is being set. I would expect the historical models to match the models they are registered with.
Possibly related to #269 

Resolving patch:
diff --git a/simple_history/models.py b/simple_history/models.py
--- a/simple_history/models.py
+++ b/simple_history/models.py
@@ -73,13 +73,15 @@ def save_without_historical_record(self, *args, **kwargs):
                 save_without_historical_record)
 
     def finalize(self, sender, **kwargs):
+        inherited = False
         try:
             hint_class = self.cls
         except AttributeError:  # called via `register`
             pass
         else:
             if hint_class is not sender:  # set in concrete
-                if not (self.inherit and issubclass(sender, hint_class)):
+                inherited = (self.inherit and issubclass(sender, hint_class))
+                if not inherited:
                     return  # set in abstract
         if hasattr(sender._meta, 'simple_history_manager_attribute'):
             raise exceptions.MultipleRegistrationsError(
@@ -88,8 +90,12 @@ def finalize(self, sender, **kwargs):
                     sender._meta.object_name,
                 )
             )
-        history_model = self.create_history_model(sender)
-        module = importlib.import_module(self.module)
+        history_model = self.create_history_model(sender, inherited)
+        if inherited:
+            # Make sure history model is in same module as concrete model
+            module = importlib.import_module(history_model.__module__)
+        else:
+            module = importlib.import_module(self.module)
         setattr(module, history_model.__name__, history_model)
 
         # The HistoricalRecords object will be discarded,
@@ -103,14 +109,18 @@ def finalize(self, sender, **kwargs):
         setattr(sender, self.manager_name, descriptor)
         sender._meta.simple_history_manager_attribute = self.manager_name
 
-    def create_history_model(self, model):
+    def create_history_model(self, model, inherited):
         """
         Creates a historical model to associate with the model provided.
         """
         attrs = {'__module__': self.module}
 
         app_module = '%s.models' % model._meta.app_label
-        if model.__module__ != self.module:
+
+        if inherited:
+            # inherited use models module
+            attrs['__module__'] = model.__module__
+        elif model.__module__ != self.module:
             # registered under different app
             attrs['__module__'] = self.module
         elif app_module != self.module:


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
@@ -71,6 +71,21 @@ def test_accessor_override(self):
         assert hasattr(User, 'my_history_model_accessor')
 
 
+class TestInheritedModule(TestCase):
+
+    def test_using_app_label(self):
+        try:
+            from ..tests.models import HistoricalConcreteExternal
+        except ImportError:
+            self.fail("HistoricalConcreteExternal is in wrong module")
+
+    def test_default(self):
+        try:
+            from ..tests.models import HistoricalConcreteExternal2
+        except ImportError:
+            self.fail("HistoricalConcreteExternal2 is in wrong module")
+
+
 class TestTrackingInheritance(TestCase):
 
     def test_tracked_abstract_base(self):
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
@@ -355,6 +355,13 @@ class Meta:
         app_label = 'tests'
 
 
+class ConcreteExternal2(AbstractExternal):
+    name = models.CharField(max_length=50)
+
+    class Meta:
+        pass    # Don't set app_label to test inherited module path
+
+
 class TrackedWithAbstractBase(TrackedAbstractBaseA):
     pass
 
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
@@ -4,18 +4,18 @@
 import warnings
 from datetime import datetime, timedelta
 
-import django
+from django.apps import apps
 from django.contrib.auth import get_user_model
 from django.core.files.base import ContentFile
 from django.db import models
 from django.db.models.fields.proxy import OrderWrt
 from django.test import TestCase
+
 from simple_history.models import HistoricalRecords, convert_auto_field
 from simple_history.utils import update_change_reason
-
 from ..external.models import ExternalModel2, ExternalModel4
 from ..models import (AbstractBase, AdminProfile, Book, Bookcase, Choice, City,
-                      ConcreteAttr, ConcreteUtil, Contact, ContactRegister,
+                      ConcreteAttr, ConcreteExternal, ConcreteUtil, Contact, ContactRegister,
                       Country, Document, Employee, ExternalModel1,
                       ExternalModel3, FileModel, HistoricalChoice,
                       HistoricalCustomFKError, HistoricalPoll, HistoricalState,
@@ -23,7 +23,6 @@
                       PollWithExcludeFields, Province, Restaurant, SelfFK,
                       Series, SeriesWork, State, Temperature,
                       UnicodeVerboseName, WaterLevel)
-from django.apps import apps
 
 get_model = apps.get_model
 User = get_user_model()
@@ -345,7 +344,7 @@ def test_create_history_model_with_one_to_one_field_to_integer_field(self):
         records = HistoricalRecords()
         records.module = AdminProfile.__module__
         try:
-            records.create_history_model(AdminProfile)
+            records.create_history_model(AdminProfile, False)
         except:
             self.fail("SimpleHistory should handle foreign keys to one to one"
                       "fields to integer fields without throwing an exception")
@@ -354,7 +353,7 @@ def test_create_history_model_with_one_to_one_field_to_char_field(self):
         records = HistoricalRecords()
         records.module = Bookcase.__module__
         try:
-            records.create_history_model(Bookcase)
+            records.create_history_model(Bookcase, False)
         except:
             self.fail("SimpleHistory should handle foreign keys to one to one"
                       "fields to char fields without throwing an exception.")
@@ -363,7 +362,7 @@ def test_create_history_model_with_multiple_one_to_ones(self):
         records = HistoricalRecords()
         records.module = MultiOneToOne.__module__
         try:
-            records.create_history_model(MultiOneToOne)
+            records.create_history_model(MultiOneToOne, False)
         except:
             self.fail("SimpleHistory should handle foreign keys to one to one"
                       "fields to one to one fields without throwing an "
@@ -396,6 +395,10 @@ def test_register_app_label(self):
                          'external_externalmodel4')
         self.assertEqual(self.get_table_name(ExternalModel4.histories),
                          'tests_historicalexternalmodel4')
+        self.assertEqual(self.get_table_name(ConcreteExternal.objects),
+                         'tests_concreteexternal')
+        self.assertEqual(self.get_table_name(ConcreteExternal.history),
+                         'tests_historicalconcreteexternal')
 
     def test_get_model(self):
         self.assertEqual(get_model('external', 'ExternalModel1'),
@@ -418,6 +421,13 @@ def test_get_model(self):
         self.assertEqual(get_model('tests', 'HistoricalExternalModel4'),
                          ExternalModel4.histories.model)
 
+        # Test that historical model is defined within app of concrete
+        # model rather than abstract base model
+        self.assertEqual(get_model('tests', 'ConcreteExternal'),
+                         ConcreteExternal)
+        self.assertEqual(get_model('tests', 'HistoricalConcreteExternal'),
+                         ConcreteExternal.history.model)
+
 
 class HistoryManagerTest(TestCase):
     def test_most_recent(self):