Repository: stephenmcd/mezzanine

Issue:
SingletonAdmin can't be imported before models are loaded.
When I import `SingletonAdmin` in a file that gets sourced at startup (models.py) I get the following exception:

``` py
...
  File ".../cartridge-example/app_name/models.py", line 3, in <module>
    from mezzanine.core.admin import SingletonAdmin
  File ".../python3.5/site-packages/mezzanine/core/admin.py", line 54, in <module>
    User = get_user_model()
  File ".../python3.5/site-packages/django/contrib/auth/__init__.py", line 150, in get_user_model
    return django_apps.get_model(settings.AUTH_USER_MODEL)
  File ".../python3.5/site-packages/django/apps/registry.py", line 199, in get_model
    self.check_models_ready()
  File ".../python3.5/site-packages/django/apps/registry.py", line 131, in check_models_ready
    raise AppRegistryNotReady("Models aren't loaded yet.")
django.core.exceptions.AppRegistryNotReady: Models aren't loaded yet.
```

It can easily be worked around by doing the registration in a model that isn't sourced at startup such as `admin.py`, but it seems less than ideal for a documented API to have that limitation.

This could probably be fixed with a try/except block in mezzanine/core/admin, but I wonder if it wouldn't be better to move `SingletonAdmin` to a new file or even a new app (contrib?) since it isn't used at all internally. I'd be willing to try implementing a solution if one were agreed upon.



Discussion:
I guess we could move it into a new `mezzanine.utils.admin` module, what do you think?

@stephenmcd Seems reasonable to me.


Resolving patch:
diff --git a/mezzanine/core/admin.py b/mezzanine/core/admin.py
--- a/mezzanine/core/admin.py
+++ b/mezzanine/core/admin.py
@@ -4,17 +4,15 @@
 from django.contrib.auth import get_user_model
 from django.contrib.auth.admin import UserAdmin
 from django.forms import ValidationError, ModelForm
-from django.http import HttpResponseRedirect
-from django.shortcuts import redirect
 from django.utils.translation import ugettext_lazy as _
 from django.contrib.auth.models import User as AuthUser
 
 from mezzanine.conf import settings
+from mezzanine.utils.admin import SingletonAdmin  # Deprecated import path.
 from mezzanine.core.forms import DynamicInlineAdminForm
 from mezzanine.core.models import (Orderable, SitePermission,
                                    CONTENT_STATUS_PUBLISHED)
 from mezzanine.utils.static import static_lazy as static
-from mezzanine.utils.urls import admin_url
 
 if settings.USE_MODELTRANSLATION:
     from django.utils.datastructures import SortedDict
@@ -237,62 +235,6 @@ class has been added to the sequence in the setting
         return qs.filter(user__id=request.user.id)
 
 
-class SingletonAdmin(admin.ModelAdmin):
-    """
-    Admin class for models that should only contain a single instance
-    in the database. Redirect all views to the change view when the
-    instance exists, and to the add view when it doesn't.
-    """
-
-    def handle_save(self, request, response):
-        """
-        Handles redirect back to the dashboard when save is clicked
-        (eg not save and continue editing), by checking for a redirect
-        response, which only occurs if the form is valid.
-        """
-        form_valid = isinstance(response, HttpResponseRedirect)
-        if request.POST.get("_save") and form_valid:
-            return redirect("admin:index")
-        return response
-
-    def add_view(self, *args, **kwargs):
-        """
-        Redirect to the change view if the singleton instance exists.
-        """
-        try:
-            singleton = self.model.objects.get()
-        except (self.model.DoesNotExist, self.model.MultipleObjectsReturned):
-            kwargs.setdefault("extra_context", {})
-            kwargs["extra_context"]["singleton"] = True
-            response = super(SingletonAdmin, self).add_view(*args, **kwargs)
-            return self.handle_save(args[0], response)
-        return redirect(admin_url(self.model, "change", singleton.id))
-
-    def changelist_view(self, *args, **kwargs):
-        """
-        Redirect to the add view if no records exist or the change
-        view if the singleton instance exists.
-        """
-        try:
-            singleton = self.model.objects.get()
-        except self.model.MultipleObjectsReturned:
-            return super(SingletonAdmin, self).changelist_view(*args, **kwargs)
-        except self.model.DoesNotExist:
-            return redirect(admin_url(self.model, "add"))
-        return redirect(admin_url(self.model, "change", singleton.id))
-
-    def change_view(self, *args, **kwargs):
-        """
-        If only the singleton instance exists, pass ``True`` for
-        ``singleton`` into the template which will use CSS to hide
-        the "save and add another" button.
-        """
-        kwargs.setdefault("extra_context", {})
-        kwargs["extra_context"]["singleton"] = self.model.objects.count() == 1
-        response = super(SingletonAdmin, self).change_view(*args, **kwargs)
-        return self.handle_save(args[0], response)
-
-
 ###########################################
 # Site Permissions Inlines for User Admin #
 ###########################################
diff --git a/mezzanine/utils/admin.py b/mezzanine/utils/admin.py
new file mode 100644
--- /dev/null
+++ b/mezzanine/utils/admin.py
@@ -0,0 +1,63 @@
+from __future__ import unicode_literals
+
+from django.contrib import admin
+from django.shortcuts import redirect
+from django.http import HttpResponseRedirect
+
+from mezzanine.utils.urls import admin_url
+
+
+class SingletonAdmin(admin.ModelAdmin):
+    """
+    Admin class for models that should only contain a single instance
+    in the database. Redirect all views to the change view when the
+    instance exists, and to the add view when it doesn't.
+    """
+
+    def handle_save(self, request, response):
+        """
+        Handles redirect back to the dashboard when save is clicked
+        (eg not save and continue editing), by checking for a redirect
+        response, which only occurs if the form is valid.
+        """
+        form_valid = isinstance(response, HttpResponseRedirect)
+        if request.POST.get("_save") and form_valid:
+            return redirect("admin:index")
+        return response
+
+    def add_view(self, *args, **kwargs):
+        """
+        Redirect to the change view if the singleton instance exists.
+        """
+        try:
+            singleton = self.model.objects.get()
+        except (self.model.DoesNotExist, self.model.MultipleObjectsReturned):
+            kwargs.setdefault("extra_context", {})
+            kwargs["extra_context"]["singleton"] = True
+            response = super(SingletonAdmin, self).add_view(*args, **kwargs)
+            return self.handle_save(args[0], response)
+        return redirect(admin_url(self.model, "change", singleton.id))
+
+    def changelist_view(self, *args, **kwargs):
+        """
+        Redirect to the add view if no records exist or the change
+        view if the singleton instance exists.
+        """
+        try:
+            singleton = self.model.objects.get()
+        except self.model.MultipleObjectsReturned:
+            return super(SingletonAdmin, self).changelist_view(*args, **kwargs)
+        except self.model.DoesNotExist:
+            return redirect(admin_url(self.model, "add"))
+        return redirect(admin_url(self.model, "change", singleton.id))
+
+    def change_view(self, *args, **kwargs):
+        """
+        If only the singleton instance exists, pass ``True`` for
+        ``singleton`` into the template which will use CSS to hide
+        the "save and add another" button.
+        """
+        kwargs.setdefault("extra_context", {})
+        kwargs["extra_context"]["singleton"] = self.model.objects.count() == 1
+        response = super(SingletonAdmin, self).change_view(*args, **kwargs)
+        return self.handle_save(args[0], response)


Test patch:
diff --git a/mezzanine/utils/tests.py b/mezzanine/utils/tests.py
--- a/mezzanine/utils/tests.py
+++ b/mezzanine/utils/tests.py
@@ -52,6 +52,8 @@
     # lambdas are OK.
     "do not assign a lambda",
 
+    # Imported during deprecation grace period after moving to utils.admin.
+    "'SingletonAdmin' imported but unused",
 )