diff --git a/mezzanine/blog/feeds.py b/mezzanine/blog/feeds.py
index 59a1506424..4d33119ec1 100644
--- a/mezzanine/blog/feeds.py
+++ b/mezzanine/blog/feeds.py
@@ -8,16 +8,21 @@
from django.utils.feedgenerator import Atom1Feed
from django.utils.html import strip_tags
-from mezzanine.blog.models import BlogPost, BlogCategory
+from mezzanine.blog.models import get_post_model, get_category_model
from mezzanine.conf import settings
from mezzanine.core.templatetags.mezzanine_tags import richtext_filters
from mezzanine.core.request import current_request
from mezzanine.generic.models import Keyword
+from mezzanine.pages.models import get_page_model
from mezzanine.utils.html import absolute_urls
+from mezzanine.utils.models import pages_installed
from mezzanine.utils.sites import current_site_id
User = get_user_model()
+BlogPost = get_post_model()
+BlogCategory = get_category_model()
+Page = get_page_model()
try:
unicode
@@ -43,8 +48,7 @@ def __init__(self, *args, **kwargs):
super(PostsRSS, self).__init__(*args, **kwargs)
self._public = True
page = None
- if "mezzanine.pages" in settings.INSTALLED_APPS:
- from mezzanine.pages.models import Page
+ if pages_installed():
try:
page = Page.objects.published().get(slug=settings.BLOG_SLUG)
except Page.DoesNotExist:
diff --git a/mezzanine/blog/forms.py b/mezzanine/blog/forms.py
index 1db7c638d9..adef63225c 100644
--- a/mezzanine/blog/forms.py
+++ b/mezzanine/blog/forms.py
@@ -2,10 +2,12 @@
from django import forms
-from mezzanine.blog.models import BlogPost
+from mezzanine.blog.models import get_post_model
from mezzanine.core.models import CONTENT_STATUS_DRAFT
+BlogPost = get_post_model()
+
# These fields need to be in the form, hidden, with default values,
# since it posts to the blog post admin, which includes these fields
# and will use empty values instead of the model defaults, without
diff --git a/mezzanine/blog/management/base.py b/mezzanine/blog/management/base.py
index b88278b4ac..c8586404e3 100644
--- a/mezzanine/blog/management/base.py
+++ b/mezzanine/blog/management/base.py
@@ -12,15 +12,18 @@
from django.utils.encoding import force_text
from django.utils.html import strip_tags
-from mezzanine.blog.models import BlogPost, BlogCategory
+from mezzanine.blog.models import get_post_model, get_category_model
from mezzanine.conf import settings
from mezzanine.core.models import CONTENT_STATUS_DRAFT
from mezzanine.core.models import CONTENT_STATUS_PUBLISHED
from mezzanine.generic.models import Keyword, ThreadedComment
-from mezzanine.pages.models import RichTextPage
+from mezzanine.pages.models import get_rich_text_page_model
from mezzanine.utils.html import decode_entities
User = get_user_model()
+BlogPost = get_post_model()
+BlogCategory = get_category_model()
+RichTextPage = get_rich_text_page_model()
class BaseImporterCommand(BaseCommand):
diff --git a/mezzanine/blog/migrations/0001_initial.py b/mezzanine/blog/migrations/0001_initial.py
index 1124f8b496..3d44eb4174 100644
--- a/mezzanine/blog/migrations/0001_initial.py
+++ b/mezzanine/blog/migrations/0001_initial.py
@@ -60,6 +60,7 @@ class Migration(migrations.Migration):
('user', models.ForeignKey(related_name='blogposts', verbose_name='Author', to=settings.AUTH_USER_MODEL)),
],
options={
+ 'swappable': 'BLOG_POST_MODEL',
'ordering': ('-publish_date',),
'verbose_name': 'Blog post',
'verbose_name_plural': 'Blog posts',
diff --git a/mezzanine/blog/models.py b/mezzanine/blog/models.py
index 0c3fbd8115..c2a97ea75f 100644
--- a/mezzanine/blog/models.py
+++ b/mezzanine/blog/models.py
@@ -10,14 +10,29 @@
from mezzanine.core.models import Displayable, Ownable, RichText, Slugged
from mezzanine.generic.fields import CommentsField, RatingField
from mezzanine.utils.models import AdminThumbMixin, upload_to
+from mezzanine.utils.models import get_post_model_name, get_category_model_name, get_model
-class BlogPost(Displayable, Ownable, RichText, AdminThumbMixin):
+def get_post_model():
+ """
+ Returns the BlogPost model that is active in this project.
+ """
+ return get_model(get_post_model_name())
+
+
+def get_category_model():
+ """
+ Returns the BlogCategory model that is active in this project.
+ """
+ return get_model(get_category_model_name())
+
+
+class AbstractBlogPost(Displayable, Ownable, RichText, AdminThumbMixin):
"""
A blog post.
"""
- categories = models.ManyToManyField("BlogCategory",
+ categories = models.ManyToManyField(get_category_model_name(),
verbose_name=_("Categories"),
blank=True, related_name="blogposts")
allow_comments = models.BooleanField(verbose_name=_("Allow comments"),
@@ -33,6 +48,7 @@ class BlogPost(Displayable, Ownable, RichText, AdminThumbMixin):
admin_thumb_field = "featured_image"
class Meta:
+ abstract = True
verbose_name = _("Blog post")
verbose_name_plural = _("Blog posts")
ordering = ("-publish_date",)
@@ -64,12 +80,19 @@ def get_absolute_url(self):
return reverse(url_name, kwargs=kwargs)
-class BlogCategory(Slugged):
+class BlogPost(AbstractBlogPost):
+
+ class Meta(AbstractBlogPost.Meta):
+ swappable = 'BLOG_POST_MODEL'
+
+
+class AbstractBlogCategory(Slugged):
"""
A category for grouping blog posts into a series.
"""
class Meta:
+ abstract = True
verbose_name = _("Blog Category")
verbose_name_plural = _("Blog Categories")
ordering = ("title",)
@@ -77,3 +100,9 @@ class Meta:
@models.permalink
def get_absolute_url(self):
return ("blog_post_list_category", (), {"category": self.slug})
+
+
+class BlogCategory(AbstractBlogCategory):
+
+ class Meta(AbstractBlogCategory.Meta):
+ swappable = 'BLOG_CATEGORY_MODEL'
diff --git a/mezzanine/blog/templatetags/blog_tags.py b/mezzanine/blog/templatetags/blog_tags.py
index ad958c3bba..eb02d11a47 100644
--- a/mezzanine/blog/templatetags/blog_tags.py
+++ b/mezzanine/blog/templatetags/blog_tags.py
@@ -5,11 +5,13 @@
from django.db.models import Count, Q
from mezzanine.blog.forms import BlogPostForm
-from mezzanine.blog.models import BlogPost, BlogCategory
+from mezzanine.blog.models import get_post_model, get_category_model
from mezzanine.generic.models import Keyword
from mezzanine import template
User = get_user_model()
+BlogPost = get_post_model()
+BlogCategory = get_category_model()
register = template.Library()
diff --git a/mezzanine/blog/tests.py b/mezzanine/blog/tests.py
index f6d7fec340..1bda7ee0ee 100644
--- a/mezzanine/blog/tests.py
+++ b/mezzanine/blog/tests.py
@@ -9,13 +9,19 @@
from django.core.urlresolvers import reverse
-from mezzanine.blog.models import BlogPost
+from mezzanine.blog.models import get_post_model
from mezzanine.conf import settings
from mezzanine.core.models import CONTENT_STATUS_PUBLISHED
-from mezzanine.pages.models import Page, RichTextPage
+from mezzanine.pages.models import get_page_model, get_rich_text_page_model
+from mezzanine.utils.models import pages_installed
from mezzanine.utils.tests import TestCase
+BlogPost = get_post_model()
+Page = get_page_model()
+RichTextPage = get_rich_text_page_model()
+
+
class BlogTests(TestCase):
def test_blog_views(self):
@@ -34,7 +40,7 @@ def test_blog_views(self):
self.assertEqual(response.status_code, 200)
@skipUnless("mezzanine.accounts" in settings.INSTALLED_APPS and
- "mezzanine.pages" in settings.INSTALLED_APPS,
+ pages_installed(),
"accounts and pages apps required")
def test_login_protected_blog(self):
"""
diff --git a/mezzanine/blog/translation.py b/mezzanine/blog/translation.py
index dfb9e828d7..5c45291b3e 100644
--- a/mezzanine/blog/translation.py
+++ b/mezzanine/blog/translation.py
@@ -12,5 +12,7 @@ class TranslatedBlogPost(TranslatedDisplayable, TranslatedRichText):
class TranslatedBlogCategory(TranslatedSlugged):
fields = ()
+
+# XXX How about swapped models?
translator.register(BlogCategory, TranslatedBlogCategory)
translator.register(BlogPost, TranslatedBlogPost)
diff --git a/mezzanine/blog/views.py b/mezzanine/blog/views.py
index 3e41361e9f..78b7ec5874 100644
--- a/mezzanine/blog/views.py
+++ b/mezzanine/blog/views.py
@@ -9,13 +9,15 @@
from django.template.response import TemplateResponse
from django.utils.translation import ugettext_lazy as _
-from mezzanine.blog.models import BlogPost, BlogCategory
+from mezzanine.blog.models import get_post_model, get_category_model
from mezzanine.blog.feeds import PostsRSS, PostsAtom
from mezzanine.conf import settings
from mezzanine.generic.models import Keyword
from mezzanine.utils.views import paginate
User = get_user_model()
+BlogPost = get_post_model()
+BlogCategory = get_category_model()
def blog_post_list(request, tag=None, year=None, month=None, username=None,
diff --git a/mezzanine/boot/lazy_admin.py b/mezzanine/boot/lazy_admin.py
index a375d3eed6..e8b96b052d 100644
--- a/mezzanine/boot/lazy_admin.py
+++ b/mezzanine/boot/lazy_admin.py
@@ -8,6 +8,7 @@
from django.shortcuts import redirect
from mezzanine.utils.importing import import_dotted_path
+from mezzanine.utils.models import pages_installed
class LazyAdminSite(AdminSite):
@@ -104,7 +105,7 @@ def urls(self):
url("^displayable_links.js$", displayable_links_js,
name="displayable_links_js"),
]
- if "mezzanine.pages" in settings.INSTALLED_APPS:
+ if pages_installed():
from mezzanine.pages.views import admin_page_ordering
urls.append(url("^admin_page_ordering/$", admin_page_ordering,
name="admin_page_ordering"))
diff --git a/mezzanine/core/defaults.py b/mezzanine/core/defaults.py
index aa9fd88a3a..6785c3eeb1 100644
--- a/mezzanine/core/defaults.py
+++ b/mezzanine/core/defaults.py
@@ -15,6 +15,7 @@
from django.utils.translation import ugettext_lazy as _
from mezzanine.conf import register_setting
+from mezzanine.utils.models import blog_installed
register_setting(
@@ -87,7 +88,7 @@
default=30,
)
-if "mezzanine.blog" in settings.INSTALLED_APPS:
+if blog_installed():
dashboard_tags = (
("blog_tags.quick_blog", "mezzanine_tags.app_list"),
("comment_tags.recent_comments",),
diff --git a/mezzanine/core/management/commands/createdb.py b/mezzanine/core/management/commands/createdb.py
index a6557f5713..486dd824f9 100644
--- a/mezzanine/core/management/commands/createdb.py
+++ b/mezzanine/core/management/commands/createdb.py
@@ -115,7 +115,8 @@ def create_pages(self):
if self.verbosity >= 1:
print("\nCreating demo pages: About us, Contact form, "
"Gallery ...\n")
- from mezzanine.galleries.models import Gallery
+ from mezzanine.galleries.models import get_gallery_model
+ Gallery = get_gallery_model()
call_command("loaddata", "mezzanine_optional.json")
zip_name = "gallery.zip"
copy_test_to_media("mezzanine.core", zip_name)
diff --git a/mezzanine/core/sitemaps.py b/mezzanine/core/sitemaps.py
index 8c4f24fb4f..bdd54cd0a0 100644
--- a/mezzanine/core/sitemaps.py
+++ b/mezzanine/core/sitemaps.py
@@ -3,14 +3,14 @@
from django.contrib.sitemaps import Sitemap
from django.contrib.sites.models import Site
-from mezzanine.conf import settings
from mezzanine.core.models import Displayable
+from mezzanine.utils.models import blog_installed
from mezzanine.utils.sites import current_site_id
-blog_installed = "mezzanine.blog" in settings.INSTALLED_APPS
-if blog_installed:
- from mezzanine.blog.models import BlogPost
+if blog_installed():
+ from mezzanine.blog.models import get_post_model
+ BlogPost = get_post_model()
class DisplayableSitemap(Sitemap):
diff --git a/mezzanine/core/tests.py b/mezzanine/core/tests.py
index 290021d673..88b1fdb1f6 100644
--- a/mezzanine/core/tests.py
+++ b/mezzanine/core/tests.py
@@ -39,14 +39,20 @@
from mezzanine.core.models import (CONTENT_STATUS_DRAFT,
CONTENT_STATUS_PUBLISHED)
from mezzanine.forms.admin import FieldAdmin
-from mezzanine.forms.models import Form
-from mezzanine.pages.models import Page, RichTextPage
+from mezzanine.forms.models import get_form_model
+from mezzanine.pages.models import get_page_model, get_rich_text_page_model
from mezzanine.utils.importing import import_dotted_path
+from mezzanine.utils.models import pages_installed
from mezzanine.utils.tests import (TestCase, run_pyflakes_for_package,
run_pep8_for_package)
from mezzanine.utils.html import TagCloser
+Page = get_page_model()
+RichTextPage = get_rich_text_page_model()
+Form = get_form_model()
+
+
class CoreTests(TestCase):
def test_tagcloser(self):
@@ -59,8 +65,7 @@ def test_tagcloser(self):
self.assertEqual(TagCloser("Line break
").html,
"Line break
")
- @skipUnless("mezzanine.mobile" in settings.INSTALLED_APPS and
- "mezzanine.pages" in settings.INSTALLED_APPS,
+ @skipUnless("mezzanine.mobile" in settings.INSTALLED_APPS and pages_installed(),
"mobile and pages apps required")
def test_device_specific_template(self):
"""
@@ -118,8 +123,7 @@ def test_utils(self):
self.fail("mezzanine.utils.imports.import_dotted_path"
"could not import \"mezzanine.core\"")
- @skipUnless("mezzanine.pages" in settings.INSTALLED_APPS,
- "pages app required")
+ @skipUnless(pages_installed(), "pages app required")
def test_description(self):
"""
Test generated description is text version of the first line
@@ -130,8 +134,7 @@ def test_description(self):
content=description * 3)
self.assertEqual(page.description, strip_tags(description))
- @skipUnless("mezzanine.pages" in settings.INSTALLED_APPS,
- "pages app required")
+ @skipUnless(pages_installed(), "pages app required")
def test_draft(self):
"""
Test a draft object as only being viewable by a staff member.
@@ -154,8 +157,7 @@ def test_searchable_manager_search_fields(self):
manager = DisplayableManager(search_fields={'foo': 10})
self.assertTrue(manager._search_fields)
- @skipUnless("mezzanine.pages" in settings.INSTALLED_APPS,
- "pages app required")
+ @skipUnless(pages_installed(), "pages app required")
def test_search(self):
"""
Objects with status "Draft" should not be within search results.
@@ -217,8 +219,7 @@ def _test_site_pages(self, title, status, count):
response = self.client.get(pages[0].get_absolute_url(), follow=True)
self.assertEqual(response.status_code, code)
- @skipUnless("mezzanine.pages" in settings.INSTALLED_APPS,
- "pages app required")
+ @skipUnless(pages_installed(), "pages app required")
def test_multisite(self):
from django.conf import settings
@@ -316,8 +317,7 @@ def _get_formurl(self, response):
action = response.request['PATH_INFO']
return action
- @skipUnless('mezzanine.pages' in settings.INSTALLED_APPS,
- 'pages app required')
+ @skipUnless(pages_installed(), "pages app required")
@override_settings(LANGUAGE_CODE="en")
def test_password_reset(self):
"""
@@ -491,8 +491,7 @@ class SubclassMiddleware(FetchFromCacheMiddleware):
pass
-@skipUnless("mezzanine.pages" in settings.INSTALLED_APPS,
- "pages app required")
+@skipUnless(pages_installed(), "pages app required")
class SiteRelatedTestCase(TestCase):
def test_update_site(self):
diff --git a/mezzanine/core/views.py b/mezzanine/core/views.py
index 545e772b15..8122c42560 100644
--- a/mezzanine/core/views.py
+++ b/mezzanine/core/views.py
@@ -31,9 +31,10 @@
from mezzanine.core.forms import get_edit_form
from mezzanine.core.models import Displayable, SitePermission
from mezzanine.utils.cache import add_cache_bypass
-from mezzanine.utils.views import is_editable, paginate, set_cookie
+from mezzanine.utils.models import pages_installed
from mezzanine.utils.sites import has_site_permission
from mezzanine.utils.urls import next_url
+from mezzanine.utils.views import is_editable, paginate, set_cookie
mimetypes.init()
@@ -187,8 +188,9 @@ def displayable_links_js(request):
TinyMCE.
"""
links = []
- if "mezzanine.pages" in settings.INSTALLED_APPS:
- from mezzanine.pages.models import Page
+ if pages_installed():
+ from mezzanine.pages.models import get_page_model
+ Page = get_page_model()
is_page = lambda obj: isinstance(obj, Page)
else:
is_page = lambda obj: False
diff --git a/mezzanine/forms/admin.py b/mezzanine/forms/admin.py
index 915aef958e..1bd0a89e2e 100644
--- a/mezzanine/forms/admin.py
+++ b/mezzanine/forms/admin.py
@@ -19,12 +19,15 @@
from mezzanine.conf import settings
from mezzanine.core.admin import TabularDynamicInlineAdmin
from mezzanine.forms.forms import EntriesForm
-from mezzanine.forms.models import Form, Field, FormEntry, FieldEntry
+from mezzanine.forms.models import Form, get_field_model, get_form_entry_model, get_field_entry_model
from mezzanine.pages.admin import PageAdmin
from mezzanine.utils.static import static_lazy as static
from mezzanine.utils.urls import admin_url, slugify
+Field = get_field_model()
+FormEntry = get_form_entry_model()
+FieldEntry = get_field_entry_model()
fs = FileSystemStorage(location=settings.FORMS_UPLOAD_ROOT)
# Copy the fieldsets for PageAdmin and add the extra fields for FormAdmin.
diff --git a/mezzanine/forms/forms.py b/mezzanine/forms/forms.py
index 6cd13fb852..eeef6dfa53 100644
--- a/mezzanine/forms/forms.py
+++ b/mezzanine/forms/forms.py
@@ -16,10 +16,12 @@
from mezzanine.conf import settings
from mezzanine.forms import fields
-from mezzanine.forms.models import FormEntry, FieldEntry
+from mezzanine.forms.models import get_form_entry_model, get_field_entry_model
from mezzanine.utils.email import split_addresses as split_choices
+FormEntry = get_form_entry_model()
+FieldEntry = get_field_entry_model()
fs = FileSystemStorage(location=settings.FORMS_UPLOAD_ROOT)
##############################
diff --git a/mezzanine/forms/models.py b/mezzanine/forms/models.py
index f6766ae255..0678a11abe 100644
--- a/mezzanine/forms/models.py
+++ b/mezzanine/forms/models.py
@@ -9,9 +9,27 @@
from mezzanine.core.models import Orderable, RichText
from mezzanine.forms import fields
from mezzanine.pages.models import Page
+from mezzanine.utils.models import get_model, get_form_model_name, get_field_model_name
+from mezzanine.utils.models import get_form_entry_model_name, get_field_entry_model_name
-class Form(Page, RichText):
+def get_form_model():
+ return get_model(get_form_model_name())
+
+
+def get_field_model():
+ return get_model(get_field_model_name())
+
+
+def get_form_entry_model():
+ return get_model(get_form_entry_model_name())
+
+
+def get_field_entry_model():
+ return get_model(get_field_entry_model_name())
+
+
+class AbstractForm(Page, RichText):
"""
A user-built form.
"""
@@ -35,10 +53,17 @@ class Form(Page, RichText):
"a message here that will be included in the email."))
class Meta:
+ abstract = True
verbose_name = _("Form")
verbose_name_plural = _("Forms")
+class Form(AbstractForm):
+
+ class Meta(AbstractForm.Meta):
+ swappable = 'FORM_MODEL'
+
+
class FieldManager(models.Manager):
"""
Only show visible fields when displaying actual form..
@@ -108,36 +133,57 @@ def is_a(self, *args):
return self.field_type in args
-class Field(AbstractBaseField):
- form = models.ForeignKey("Form", related_name="fields")
+class AbstractField(AbstractBaseField):
+ form = models.ForeignKey(get_form_model_name(), related_name="fields")
class Meta(AbstractBaseField.Meta):
+ abstract = True
order_with_respect_to = "form"
-class FormEntry(models.Model):
+class Field(AbstractField):
+
+ class Meta(AbstractField.Meta):
+ swappable = 'FIELD_MODEL'
+
+
+class AbstractFormEntry(models.Model):
"""
An entry submitted via a user-built form.
"""
- form = models.ForeignKey("Form", related_name="entries")
+ form = models.ForeignKey(get_form_model_name(), related_name="entries")
entry_time = models.DateTimeField(_("Date/time"))
class Meta:
+ abstract = True
verbose_name = _("Form entry")
verbose_name_plural = _("Form entries")
-class FieldEntry(models.Model):
+class FormEntry(AbstractFormEntry):
+
+ class Meta(AbstractFormEntry.Meta):
+ swappable = 'FORM_ENTRY_MODEL'
+
+
+class AbstractFieldEntry(models.Model):
"""
A single field value for a form entry submitted via a user-built form.
"""
- entry = models.ForeignKey("FormEntry", related_name="fields")
+ entry = models.ForeignKey(get_form_entry_model_name(), related_name="fields")
field_id = models.IntegerField()
value = models.CharField(max_length=settings.FORMS_FIELD_MAX_LENGTH,
null=True)
class Meta:
+ abstract = True
verbose_name = _("Form field entry")
verbose_name_plural = _("Form field entries")
+
+
+class FieldEntry(AbstractFieldEntry):
+
+ class Meta(AbstractFieldEntry.Meta):
+ swappable = 'FIELD_ENTRY_MODEL'
diff --git a/mezzanine/forms/page_processors.py b/mezzanine/forms/page_processors.py
index 04e300ca00..a5be3e839f 100644
--- a/mezzanine/forms/page_processors.py
+++ b/mezzanine/forms/page_processors.py
@@ -5,13 +5,16 @@
from mezzanine.conf import settings
from mezzanine.forms.forms import FormForForm
-from mezzanine.forms.models import Form
+from mezzanine.forms.models import get_form_model
from mezzanine.forms.signals import form_invalid, form_valid
from mezzanine.pages.page_processors import processor_for
from mezzanine.utils.email import split_addresses, send_mail_template
from mezzanine.utils.views import is_spam
+Form = get_form_model()
+
+
def format_value(value):
"""
Convert a list into a comma separated string, for displaying
@@ -22,6 +25,7 @@ def format_value(value):
return value
+# XXX The swapped model may not be ready at this level
@processor_for(Form)
def form_processor(request, page):
"""
diff --git a/mezzanine/forms/tests.py b/mezzanine/forms/tests.py
index 3ca7e2632b..6beca9347d 100644
--- a/mezzanine/forms/tests.py
+++ b/mezzanine/forms/tests.py
@@ -8,10 +8,13 @@
from mezzanine.core.models import CONTENT_STATUS_PUBLISHED
from mezzanine.forms import fields
from mezzanine.forms.forms import FormForForm
-from mezzanine.forms.models import Form
+from mezzanine.forms.models import get_form_model
from mezzanine.utils.tests import TestCase
+Form = get_form_model()
+
+
class TestsForm(TestCase):
def test_forms(self):
diff --git a/mezzanine/forms/translation.py b/mezzanine/forms/translation.py
index 75214291f4..dbe8a7a843 100644
--- a/mezzanine/forms/translation.py
+++ b/mezzanine/forms/translation.py
@@ -10,5 +10,7 @@ class TranslatedForm(TranslatedRichText):
class TranslatedField(TranslationOptions):
fields = ('label', 'choices', 'default', 'placeholder_text', 'help_text',)
+
+# XXX How about swapped models?
translator.register(Form, TranslatedForm)
translator.register(Field, TranslatedField)
diff --git a/mezzanine/galleries/admin.py b/mezzanine/galleries/admin.py
index ff98052e92..83fa16097a 100644
--- a/mezzanine/galleries/admin.py
+++ b/mezzanine/galleries/admin.py
@@ -4,10 +4,13 @@
from mezzanine.core.admin import TabularDynamicInlineAdmin
from mezzanine.pages.admin import PageAdmin
-from mezzanine.galleries.models import Gallery, GalleryImage
+from mezzanine.galleries.models import Gallery, get_gallery_image_model
from mezzanine.utils.static import static_lazy as static
+GalleryImage = get_gallery_image_model()
+
+
class GalleryImageInline(TabularDynamicInlineAdmin):
model = GalleryImage
diff --git a/mezzanine/galleries/models.py b/mezzanine/galleries/models.py
index fcf54188a9..7a8723cd0a 100644
--- a/mezzanine/galleries/models.py
+++ b/mezzanine/galleries/models.py
@@ -20,7 +20,16 @@
from mezzanine.core.models import Orderable, RichText
from mezzanine.pages.models import Page
from mezzanine.utils.importing import import_dotted_path
-from mezzanine.utils.models import upload_to
+from mezzanine.utils.models import upload_to, get_model
+from mezzanine.utils.models import get_gallery_model_name, get_gallery_image_model_name
+
+
+def get_gallery_model():
+ return get_model(get_gallery_model_name())
+
+
+def get_gallery_image_model():
+ return get_model(get_gallery_image_model_name())
# Set the directory where gallery images are uploaded to,
@@ -35,13 +44,15 @@
pass
-class BaseGallery(models.Model):
+class AbstractGallery(Page, RichText):
"""
- Base gallery functionality.
+ Page bucket for gallery photos.
"""
class Meta:
abstract = True
+ verbose_name = _("Gallery")
+ verbose_name_plural = _("Galleries")
zip_import = models.FileField(verbose_name=_("Zip import"), blank=True,
upload_to=upload_to("galleries.Gallery.zip_import", "galleries"),
@@ -53,7 +64,7 @@ def save(self, delete_zip_import=True, *args, **kwargs):
If a zip file is uploaded, extract any images from it and add
them to the gallery, before removing the zip file.
"""
- super(BaseGallery, self).save(*args, **kwargs)
+ super(AbstractGallery, self).save(*args, **kwargs)
if self.zip_import:
zip_file = ZipFile(self.zip_import)
for name in zip_file.namelist():
@@ -111,26 +122,23 @@ def save(self, delete_zip_import=True, *args, **kwargs):
self.zip_import.delete(save=True)
-class Gallery(Page, RichText, BaseGallery):
- """
- Page bucket for gallery photos.
- """
+class Gallery(AbstractGallery):
- class Meta:
- verbose_name = _("Gallery")
- verbose_name_plural = _("Galleries")
+ class Meta(AbstractGallery.Meta):
+ swappable = 'GALLERY_MODEL'
@python_2_unicode_compatible
-class GalleryImage(Orderable):
+class AbstractGalleryImage(Orderable):
- gallery = models.ForeignKey("Gallery", related_name="images")
+ gallery = models.ForeignKey(get_gallery_model_name(), related_name="images")
file = FileField(_("File"), max_length=200, format="Image",
upload_to=upload_to("galleries.GalleryImage.file", "galleries"))
description = models.CharField(_("Description"), max_length=1000,
blank=True)
class Meta:
+ abstract = True
verbose_name = _("Image")
verbose_name_plural = _("Images")
@@ -152,4 +160,10 @@ def save(self, *args, **kwargs):
name = "".join([s.upper() if i == 0 or name[i - 1] == " " else s
for i, s in enumerate(name)])
self.description = name
- super(GalleryImage, self).save(*args, **kwargs)
+ super(AbstractGalleryImage, self).save(*args, **kwargs)
+
+
+class GalleryImage(AbstractGalleryImage):
+
+ class Meta(AbstractGalleryImage.Meta):
+ swappable = 'GALLERY_IMAGE_MODEL'
diff --git a/mezzanine/galleries/tests.py b/mezzanine/galleries/tests.py
index 5dc5ca007b..5c3b7b3c5d 100644
--- a/mezzanine/galleries/tests.py
+++ b/mezzanine/galleries/tests.py
@@ -8,10 +8,13 @@
from mezzanine.conf import settings
from mezzanine.core.templatetags.mezzanine_tags import thumbnail
-from mezzanine.galleries.models import Gallery, GALLERIES_UPLOAD_DIR
+from mezzanine.galleries.models import get_gallery_model, GALLERIES_UPLOAD_DIR
from mezzanine.utils.tests import TestCase, copy_test_to_media
+Gallery = get_gallery_model()
+
+
class GalleriesTests(TestCase):
def test_gallery_import(self):
diff --git a/mezzanine/galleries/translation.py b/mezzanine/galleries/translation.py
index 5026979136..01bcd3189f 100644
--- a/mezzanine/galleries/translation.py
+++ b/mezzanine/galleries/translation.py
@@ -10,5 +10,7 @@ class TranslatedGallery(TranslatedRichText):
class TranslatedGalleryImage(TranslationOptions):
fields = ('description',)
+
+# XXX How about swapped models?
translator.register(Gallery, TranslatedGallery)
translator.register(GalleryImage, TranslatedGalleryImage)
diff --git a/mezzanine/generic/tests.py b/mezzanine/generic/tests.py
index 153209179a..f4c35db0cf 100644
--- a/mezzanine/generic/tests.py
+++ b/mezzanine/generic/tests.py
@@ -9,21 +9,25 @@
from django.contrib.contenttypes.models import ContentType
from django.core.urlresolvers import reverse
-from mezzanine.blog.models import BlogPost
+from mezzanine.blog.models import get_post_model
from mezzanine.conf import settings
from mezzanine.core.models import CONTENT_STATUS_PUBLISHED
from mezzanine.generic.forms import RatingForm
from mezzanine.generic.models import AssignedKeyword, Keyword, ThreadedComment
from mezzanine.generic.views import comment
-from mezzanine.pages.models import RichTextPage
+from mezzanine.pages.models import get_rich_text_page_model
+from mezzanine.utils.models import blog_installed, pages_installed
from mezzanine.utils.tests import TestCase
+BlogPost = get_post_model()
+RichTextPage = get_rich_text_page_model()
+
+
class GenericTests(TestCase):
- @skipUnless("mezzanine.blog" in settings.INSTALLED_APPS,
- "blog app required")
+ @skipUnless(blog_installed(), "blog app required")
def test_rating(self):
"""
Test that ratings can be posted and avarage/count are calculated.
@@ -54,8 +58,7 @@ def test_rating(self):
self.assertEqual(blog_post.rating_sum, _sum)
self.assertEqual(blog_post.rating_average, average)
- @skipUnless("mezzanine.blog" in settings.INSTALLED_APPS,
- "blog app required")
+ @skipUnless(blog_installed(), "blog app required")
def test_comment_ratings(self):
"""
Test that a generic relation defined on one of Mezzanine's generic
@@ -78,8 +81,7 @@ def test_comment_ratings(self):
comment.rating_average,
(settings.RATINGS_RANGE[0] + settings.RATINGS_RANGE[-1]) / 2)
- @skipUnless("mezzanine.blog" in settings.INSTALLED_APPS,
- "blog app required")
+ @skipUnless(blog_installed(), "blog app required")
def test_comment_queries(self):
"""
Test that rendering comments executes the same number of
@@ -103,8 +105,7 @@ def test_comment_queries(self):
after = self.queries_used_for_template(template, **context)
self.assertEqual(before, after)
- @skipUnless("mezzanine.pages" in settings.INSTALLED_APPS,
- "pages app required")
+ @skipUnless(pages_installed(), "pages app required")
def test_keywords(self):
"""
Test that the keywords_string field is correctly populated.
diff --git a/mezzanine/pages/admin.py b/mezzanine/pages/admin.py
index eb4e313ef3..89856df99d 100644
--- a/mezzanine/pages/admin.py
+++ b/mezzanine/pages/admin.py
@@ -10,7 +10,7 @@
from mezzanine.conf import settings
from mezzanine.core.admin import (
ContentTypedAdmin, DisplayableAdmin, DisplayableAdminForm)
-from mezzanine.pages.models import Page, RichTextPage, Link
+from mezzanine.pages.models import Page, RichTextPage, Link, AbstractLink
from mezzanine.utils.urls import clean_slashes
@@ -32,7 +32,7 @@ def clean_slug(self):
"""
self.instance._old_slug = self.instance.slug
new_slug = self.cleaned_data['slug']
- if not isinstance(self.instance, Link) and new_slug != "/":
+ if not isinstance(self.instance, AbstractLink) and new_slug != "/":
new_slug = clean_slashes(self.cleaned_data['slug'])
return new_slug
diff --git a/mezzanine/pages/context_processors.py b/mezzanine/pages/context_processors.py
index bb16a7f8dd..34d93afb86 100644
--- a/mezzanine/pages/context_processors.py
+++ b/mezzanine/pages/context_processors.py
@@ -1,5 +1,5 @@
-from mezzanine.pages.models import Page
+from mezzanine.pages.models import AbstractPage
def page(request):
@@ -12,7 +12,7 @@ def page(request):
"""
context = {}
page = getattr(request, "page", None)
- if isinstance(page, Page):
+ if isinstance(page, AbstractPage):
# set_helpers has always expected the current template context,
# but here we're just passing in our context dict with enough
# variables to satisfy it.
diff --git a/mezzanine/pages/middleware.py b/mezzanine/pages/middleware.py
index fa3355561a..b9a756f5d5 100644
--- a/mezzanine/pages/middleware.py
+++ b/mezzanine/pages/middleware.py
@@ -6,7 +6,7 @@
from mezzanine.conf import settings
from mezzanine.pages import context_processors, page_processors
-from mezzanine.pages.models import Page
+from mezzanine.pages.models import get_page_model
from mezzanine.pages.views import page as page_view
from mezzanine.utils.deprecation import MiddlewareMixin, get_middleware_setting
from mezzanine.utils.importing import import_dotted_path
@@ -71,6 +71,7 @@ def process_view(self, request, view_func, view_args, view_kwargs):
# Load the closest matching page by slug, and assign it to the
# request object. If none found, skip all further processing.
slug = path_to_slug(request.path_info)
+ Page = get_page_model()
pages = Page.objects.with_ascendants_for_slug(slug,
for_user=request.user, include_login_required=True)
if pages:
diff --git a/mezzanine/pages/models.py b/mezzanine/pages/models.py
index bb20528994..f6836e80fb 100644
--- a/mezzanine/pages/models.py
+++ b/mezzanine/pages/models.py
@@ -17,37 +17,50 @@
ContentTyped, Displayable, Orderable, RichText)
from mezzanine.pages.fields import MenusField
from mezzanine.pages.managers import PageManager
+from mezzanine.utils.models import get_page_model_name, get_rich_text_page_model_name, get_link_model_name
+from mezzanine.utils.models import get_model
from mezzanine.utils.urls import path_to_slug
-class BasePage(Orderable, Displayable):
+def get_page_model():
"""
- Exists solely to store ``PageManager`` as the main manager.
- If it's defined on ``Page``, a concrete model, then each
- ``Page`` subclass loses the custom manager.
+ Returns the Page model that is active in this project.
"""
+ return get_model(get_page_model_name())
- objects = PageManager()
- class Meta:
- abstract = True
+def get_rich_text_page_model():
+ """
+ Returns the RichTextPage model that is active in this project.
+ """
+ return get_model(get_rich_text_page_model_name())
+
+
+def get_link_model():
+ """
+ Returns the Link model that is active in this project.
+ """
+ return get_model(get_link_model_name())
@python_2_unicode_compatible
-class Page(BasePage, ContentTyped):
+class AbstractPage(Orderable, Displayable, ContentTyped):
"""
A page in the page tree. This is the base class that custom content types
need to subclass.
"""
- parent = models.ForeignKey("Page", blank=True, null=True,
+ parent = models.ForeignKey(get_page_model_name(), blank=True, null=True,
related_name="children")
in_menus = MenusField(_("Show in menus"), blank=True, null=True)
titles = models.CharField(editable=False, max_length=1000, null=True)
login_required = models.BooleanField(_("Login required"), default=False,
help_text=_("If checked, only logged in users can view this page"))
+ objects = PageManager()
+
class Meta:
+ abstract = True
verbose_name = _("Page")
verbose_name_plural = _("Pages")
ordering = ("titles",)
@@ -84,7 +97,7 @@ def save(self, *args, **kwargs):
titles.insert(0, parent.title)
parent = parent.parent
self.titles = " / ".join(titles)
- super(Page, self).save(*args, **kwargs)
+ super(AbstractPage, self).save(*args, **kwargs)
def description_from_content(self):
"""
@@ -93,10 +106,11 @@ def description_from_content(self):
``Page`` instance, so that all fields defined on the subclass
are available for generating the description.
"""
+ Page = get_page_model()
if self.__class__ == Page:
if self.content_model:
return self.get_content_model().description_from_content()
- return super(Page, self).description_from_content()
+ return super(AbstractPage, self).description_from_content()
def get_ascendants(self, for_user=None):
"""
@@ -116,6 +130,7 @@ def get_ascendants(self, for_user=None):
if self.slug:
kwargs = {"for_user": for_user}
with override_current_site_id(self.site_id):
+ Page = get_page_model()
pages = Page.objects.with_ascendants_for_slug(self.slug,
**kwargs)
self._ascendants = pages[0]._ascendants
@@ -135,7 +150,7 @@ def get_slug(self):
"""
Recursively build the slug from the chain of parents.
"""
- slug = super(Page, self).get_slug()
+ slug = super(AbstractPage, self).get_slug()
if self.parent is not None:
return "%s/%s" % (self.parent.slug, slug)
return slug
@@ -146,6 +161,7 @@ def set_slug(self, new_slug):
start with this page's slug.
"""
slug_prefix = "%s/" % self.slug
+ Page = get_page_model()
for page in Page.objects.filter(slug__startswith=slug_prefix):
if not page.overridden():
page.slug = new_slug + page.slug[len(self.slug):]
@@ -269,28 +285,48 @@ def get_template_name(self):
return None
-class RichTextPage(Page, RichText):
+class Page(AbstractPage):
+
+ class Meta(AbstractPage.Meta):
+ swappable = 'PAGE_MODEL'
+
+
+class AbstractRichTextPage(Page, RichText):
"""
Implements the default type of page with a single Rich Text
content field.
"""
- class Meta:
+ class Meta(Page.Meta):
+ abstract = True
verbose_name = _("Rich text page")
verbose_name_plural = _("Rich text pages")
-class Link(Page):
+class RichTextPage(AbstractRichTextPage):
+
+ class Meta(AbstractRichTextPage.Meta):
+ swappable = 'RICH_TEXT_PAGE_MODEL'
+
+
+class AbstractLink(Page):
"""
A general content type for creating external links in the page
menu.
"""
- class Meta:
+ class Meta(Page.Meta):
+ abstract = True
verbose_name = _("Link")
verbose_name_plural = _("Links")
+class Link(AbstractLink):
+
+ class Meta(AbstractLink.Meta):
+ swappable = 'LINK_MODEL'
+
+
class PageMoveException(Exception):
"""
Raised by ``can_move()`` when the move permission is denied. Takes
diff --git a/mezzanine/pages/page_processors.py b/mezzanine/pages/page_processors.py
index 4b6385b198..f6108ef939 100644
--- a/mezzanine/pages/page_processors.py
+++ b/mezzanine/pages/page_processors.py
@@ -7,7 +7,7 @@
from django.apps import apps
from django.utils.module_loading import module_has_submodule
-from mezzanine.pages.models import Page
+from mezzanine.pages.models import AbstractPage
from mezzanine.utils.importing import get_app_name_list
@@ -34,11 +34,11 @@ def processor_for(content_model_or_slug, exact_page=False):
content_model = apps.get_model(*parts)
except (TypeError, ValueError, LookupError):
slug = content_model_or_slug
- elif issubclass(content_model_or_slug, Page):
+ elif issubclass(content_model_or_slug, AbstractPage):
content_model = content_model_or_slug
else:
raise TypeError("%s is not a valid argument for page_processor, "
- "which should be a model subclass of Page in class "
+ "which should be a model subclass of AbstractPage in class "
"or string form (app.model), or a valid slug" %
content_model_or_slug)
diff --git a/mezzanine/pages/templatetags/pages_tags.py b/mezzanine/pages/templatetags/pages_tags.py
index fbc06ab435..1f5cc0e80f 100644
--- a/mezzanine/pages/templatetags/pages_tags.py
+++ b/mezzanine/pages/templatetags/pages_tags.py
@@ -8,11 +8,13 @@
from django.template.loader import get_template
from django.utils.translation import ugettext_lazy as _
-from mezzanine.pages.models import Page
+from mezzanine.pages.models import get_page_model
from mezzanine.utils.urls import home_slug
from mezzanine import template
+Page = get_page_model()
+
register = template.Library()
diff --git a/mezzanine/pages/tests.py b/mezzanine/pages/tests.py
index 1b9f9dba8a..5e54d59da7 100644
--- a/mezzanine/pages/tests.py
+++ b/mezzanine/pages/tests.py
@@ -19,7 +19,7 @@
from mezzanine.conf import settings
from mezzanine.core.models import CONTENT_STATUS_PUBLISHED
from mezzanine.core.request import current_request
-from mezzanine.pages.models import Page, RichTextPage
+from mezzanine.pages.models import get_page_model, get_rich_text_page_model
from mezzanine.pages.admin import PageAdminForm
from mezzanine.urls import PAGES_SLUG
from mezzanine.utils.sites import override_current_site_id
@@ -27,6 +27,8 @@
User = get_user_model()
+Page = get_page_model()
+RichTextPage = get_rich_text_page_model()
class PagesTests(TestCase):
diff --git a/mezzanine/pages/translation.py b/mezzanine/pages/translation.py
index e8b565e396..abffba1ab4 100644
--- a/mezzanine/pages/translation.py
+++ b/mezzanine/pages/translation.py
@@ -15,6 +15,8 @@ class TranslatedRichTextPage(TranslatedRichText):
class TranslatedLink(TranslationOptions):
fields = ()
+
+# XXX How about swapped models?
translator.register(Page, TranslatedPage)
translator.register(RichTextPage, TranslatedRichTextPage)
translator.register(Link, TranslatedLink)
diff --git a/mezzanine/pages/views.py b/mezzanine/pages/views.py
index 86d1667969..6553e05cf5 100644
--- a/mezzanine/pages/views.py
+++ b/mezzanine/pages/views.py
@@ -8,7 +8,7 @@
from django.contrib import messages
from django.template.response import TemplateResponse
-from mezzanine.pages.models import Page, PageMoveException
+from mezzanine.pages.models import get_page_model, PageMoveException
from mezzanine.utils.urls import home_slug
@@ -21,6 +21,7 @@ def admin_page_ordering(request):
def get_id(s):
s = s.split("_")[-1]
return int(s) if s.isdigit() else None
+ Page = get_page_model()
page = get_object_or_404(Page, id=get_id(request.POST['id']))
old_parent_id = page.parent_id
new_parent_id = get_id(request.POST['parent_id'])
diff --git a/mezzanine/project_template/project_name/settings.py b/mezzanine/project_template/project_name/settings.py
index b1f615098d..0b790ccb79 100644
--- a/mezzanine/project_template/project_name/settings.py
+++ b/mezzanine/project_template/project_name/settings.py
@@ -77,8 +77,20 @@
# ),
# )
+# Settings to override the content models
+# PAGE_MODEL = "pages.Page"
+# RICH_TEXT_PAGE_MODEL = "pages.RichTextPage"
+# LINK_MODEL = "pages.Link"
+# BLOG_POST_MODEL = "blog.BlogPost"
+# BLOG_CATEGORY_MODEL = "blog.BlogCategory"
+# FORM_MODEL = "forms.Form"
+# FIELD_MODEL = "forms.Field"
+# FORM_ENTRY_MODEL = "forms.FormEntry"
+# FIELD_ENTRY_MODEL = "forms.FieldEntry"
+# GALLERY_MODEL = "galleries.Gallery"
+# GALLERY_IMAGE_MODEL = "galleries.GalleryImage"
+
# Setting to turn on featured images for blog posts. Defaults to False.
-#
# BLOG_USE_FEATURED_IMAGE = True
# If True, the django-modeltranslation will be added to the
diff --git a/mezzanine/urls.py b/mezzanine/urls.py
index 28da77a491..f4672ec103 100644
--- a/mezzanine/urls.py
+++ b/mezzanine/urls.py
@@ -14,6 +14,7 @@
from mezzanine.conf import settings
from mezzanine.core.sitemaps import DisplayableSitemap
+from mezzanine.utils.models import blog_installed, pages_installed
urlpatterns = []
@@ -65,8 +66,7 @@
]
# Mezzanine's Blog app.
-blog_installed = "mezzanine.blog" in settings.INSTALLED_APPS
-if blog_installed:
+if blog_installed():
BLOG_SLUG = settings.BLOG_SLUG.rstrip("/")
if BLOG_SLUG:
BLOG_SLUG += "/"
@@ -77,11 +77,11 @@
# Mezzanine's Pages app.
PAGES_SLUG = ""
-if "mezzanine.pages" in settings.INSTALLED_APPS:
+if pages_installed():
# No BLOG_SLUG means catch-all patterns belong to the blog,
# so give pages their own prefix and inject them before the
# blog urlpatterns.
- if blog_installed and not BLOG_SLUG.rstrip("/"):
+ if blog_installed() and not BLOG_SLUG.rstrip("/"):
PAGES_SLUG = getattr(settings, "PAGES_SLUG", "pages").strip("/") + "/"
blog_patterns_start = urlpatterns.index(blog_patterns[0])
urlpatterns[blog_patterns_start:len(blog_patterns)] = [
diff --git a/mezzanine/utils/models.py b/mezzanine/utils/models.py
index f791549abf..214d9453a0 100644
--- a/mezzanine/utils/models.py
+++ b/mezzanine/utils/models.py
@@ -4,6 +4,7 @@
from future.utils import with_metaclass
+from django.apps import apps
from django.conf import settings
from django.contrib.auth import get_user_model as django_get_user_model
from django.core.exceptions import ImproperlyConfigured
@@ -29,6 +30,110 @@ def get_user_model_name():
return getattr(settings, "AUTH_USER_MODEL", "auth.User")
+def get_model(model_name):
+ """
+ Returns the model by its "app_label.object_name" reference.
+ """
+ try:
+ return apps.get_model(model_name)
+ except ValueError:
+ # XXX Displaying the name of the setting would provide a more helpful message
+ raise ImproperlyConfigured("model_name must be of the form 'app_label.model_name'")
+ except LookupError:
+ raise ImproperlyConfigured(
+ "model_name refers to model '%s' that has not been installed" % model_name
+ )
+
+
+def get_page_model_name():
+ """
+ Returns the app_label.object_name string for the page model.
+ """
+ return getattr(settings, "PAGE_MODEL", "pages.Page")
+
+
+def get_rich_text_page_model_name():
+ """
+ Returns the app_label.object_name string for the rich text page model.
+ """
+ return getattr(settings, "RICH_TEXT_PAGE_MODEL", "pages.RichTextPage")
+
+
+def get_link_model_name():
+ """
+ Returns the app_label.object_name string for the link model.
+ """
+ return getattr(settings, "LINK_MODEL", "pages.Link")
+
+
+def get_post_model_name():
+ """
+ Returns the app_label.object_name string for the blog post model.
+ """
+ return getattr(settings, "BLOG_POST_MODEL", "blog.BlogPost")
+
+
+def get_category_model_name():
+ """
+ Returns the app_label.object_name string for the blog category model.
+ """
+ return getattr(settings, "BLOG_CATEGORY_MODEL", "blog.BlogCategory")
+
+
+def get_form_model_name():
+ """
+ Returns the app_label.object_name string for the form model.
+ """
+ return getattr(settings, "FORM_MODEL", "forms.Form")
+
+
+def get_field_model_name():
+ """
+ Returns the app_label.object_name string for the field model.
+ """
+ return getattr(settings, "FIELD_MODEL", "forms.Field")
+
+
+def get_form_entry_model_name():
+ """
+ Returns the app_label.object_name string for the form entry model.
+ """
+ return getattr(settings, "FORM_ENTRY_MODEL", "forms.FormEntry")
+
+
+def get_field_entry_model_name():
+ """
+ Returns the app_label.object_name string for the field entry model.
+ """
+ return getattr(settings, "FIELD_ENTRY_MODEL", "forms.FieldEntry")
+
+
+def get_gallery_model_name():
+ """
+ Returns the app_label.object_name string for the gallery model.
+ """
+ return getattr(settings, "GALLERY_MODEL", "galleries.Gallery")
+
+
+def get_gallery_image_model_name():
+ """
+ Returns the app_label.object_name string for the gallery image model.
+ """
+ return getattr(settings, "GALLERY_IMAGE_MODEL", "galleries.GalleryImage")
+
+
+def pages_installed():
+ """Detects any of the vanilla pages app or a complete replacement."""
+ # Assumes you want the "blog" feature if you customized the models
+ return "mezzanine.pages" in settings.INSTALLED_APPS or getattr(settings, "PAGES_MODEL", None)
+
+
+def blog_installed():
+ """Detects any of the vanilla blog app or a complete replacement."""
+ # Assumes you want the "blog" feature if you customized the models
+ return "mezzanine.blog" in settings.INSTALLED_APPS or getattr(settings, "BLOG_POST_MODEL", None)
+
+
def _base_concrete_model(abstract, klass):
for kls in reversed(klass.__mro__):
if issubclass(kls, abstract) and not kls._meta.abstract: