From a8a1863c60a337fdbb31dca498e9033ca2dbd621 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Herv=C3=A9=20Cauwelier?= Date: Fri, 6 Jan 2017 15:06:20 +0100 Subject: [PATCH] WIP make content models swappable, closes #1160 (It's marked as WIP because I left some XXX with questions.) Any of the following models can now be swapped by your own version: * Page * RichTextPage * Link * BlogPost * BlogCategory * Form * FormEntry * FieldEntry * Gallery * GalleryImage So you can keep the same features and API but add fields, methods, inherit from other classes (geo models for geo fields, third-party models...), etc. Just make sure you inherit from each respective abstract base class. BasePage was merged into AbstractPage with the same side effect on inheriting the object manager on Page, Link, etc. BaseGallery was merged into AbstractGallery, no point in keeping it. I didn't change imports or references in the admin modules, I followed UserAdmin conventions here. You'll have to explicitly register, e.g. PageAdmin to your swapped Page model. No docs update yet, let's first agree on code and naming conventions, what will become the public API for users. This includes whether the docstring goes on the abstract model or the vanilla model. I also guess this deprecates the field injection feature, before removing it in a future major version. Besides that, it must be 100% compatible, no test changes apart from imports. --- mezzanine/blog/feeds.py | 10 +- mezzanine/blog/forms.py | 4 +- mezzanine/blog/management/base.py | 7 +- mezzanine/blog/migrations/0001_initial.py | 1 + mezzanine/blog/models.py | 35 +++++- mezzanine/blog/templatetags/blog_tags.py | 4 +- mezzanine/blog/tests.py | 12 +- mezzanine/blog/translation.py | 2 + mezzanine/blog/views.py | 4 +- mezzanine/boot/lazy_admin.py | 3 +- mezzanine/core/defaults.py | 3 +- .../core/management/commands/createdb.py | 3 +- mezzanine/core/sitemaps.py | 8 +- mezzanine/core/tests.py | 31 +++--- mezzanine/core/views.py | 8 +- mezzanine/forms/admin.py | 5 +- mezzanine/forms/forms.py | 4 +- mezzanine/forms/models.py | 60 ++++++++-- mezzanine/forms/page_processors.py | 6 +- mezzanine/forms/tests.py | 5 +- mezzanine/forms/translation.py | 2 + mezzanine/galleries/admin.py | 5 +- mezzanine/galleries/models.py | 42 ++++--- mezzanine/galleries/tests.py | 5 +- mezzanine/galleries/translation.py | 2 + mezzanine/generic/tests.py | 21 ++-- mezzanine/pages/admin.py | 4 +- mezzanine/pages/context_processors.py | 4 +- mezzanine/pages/middleware.py | 3 +- mezzanine/pages/models.py | 68 +++++++++--- mezzanine/pages/page_processors.py | 6 +- mezzanine/pages/templatetags/pages_tags.py | 4 +- mezzanine/pages/tests.py | 4 +- mezzanine/pages/translation.py | 2 + mezzanine/pages/views.py | 3 +- .../project_template/project_name/settings.py | 14 ++- mezzanine/urls.py | 8 +- mezzanine/utils/models.py | 105 ++++++++++++++++++ 38 files changed, 408 insertions(+), 109 deletions(-) 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: