From c1a989dacf7b7a2ed2632b6635b465019da0eef9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Herv=C3=A9=20Cauwelier?= Date: Sat, 27 Jul 2019 12:25:30 +0200 Subject: [PATCH] WIP make content models swappable, closes #1160 (It's marked as WIP because tests are far from passing, and we can always discuss the coding style and naming.) Any of the following models can now be swapped by your own version: * Page * RichTextPage * Link * BlogPost * BlogCategory * Form * FormEntry * Field * 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. without having to resort to model inheritance. Just make sure you inherit from each respective abstract base class. Contrary to my first submission, I kept the BaseXXX classes for a smoother transition path. 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 we provide default values in the template project settings or in the code. 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/__init__.py | 9 ++++ mezzanine/blog/feeds.py | 10 +++-- mezzanine/blog/forms.py | 4 +- mezzanine/blog/management/base.py | 7 +++- mezzanine/blog/migrations/0001_initial.py | 2 + mezzanine/blog/models.py | 20 +++++++-- mezzanine/blog/templatetags/blog_tags.py | 4 +- mezzanine/blog/tests.py | 14 ++++--- mezzanine/blog/translation.py | 7 +++- mezzanine/blog/views.py | 4 +- mezzanine/boot/lazy_admin.py | 3 +- mezzanine/conf/translation.py | 1 + .../core/management/commands/createdb.py | 3 +- mezzanine/core/models.py | 9 ++-- mezzanine/core/sitemaps.py | 8 ++-- mezzanine/core/tests.py | 28 ++++++------- mezzanine/core/views.py | 8 ++-- mezzanine/forms/__init__.py | 17 ++++++++ mezzanine/forms/admin.py | 8 +++- mezzanine/forms/forms.py | 5 ++- mezzanine/forms/migrations/0001_initial.py | 4 ++ mezzanine/forms/models.py | 42 +++++++++++++++---- mezzanine/forms/page_processors.py | 5 ++- mezzanine/forms/tests.py | 6 ++- mezzanine/forms/translation.py | 7 +++- mezzanine/galleries/__init__.py | 9 ++++ mezzanine/galleries/admin.py | 6 ++- .../galleries/migrations/0001_initial.py | 2 + mezzanine/galleries/models.py | 22 ++++++++-- mezzanine/galleries/tests.py | 6 ++- mezzanine/galleries/translation.py | 7 +++- mezzanine/generic/models.py | 3 +- mezzanine/generic/tests.py | 22 +++++----- mezzanine/pages/__init__.py | 13 ++++++ mezzanine/pages/admin.py | 10 ++++- mezzanine/pages/context_processors.py | 4 +- mezzanine/pages/middleware.py | 5 ++- mezzanine/pages/migrations/0001_initial.py | 3 ++ mezzanine/pages/models.py | 41 ++++++++++++++---- mezzanine/pages/page_processors.py | 6 +-- mezzanine/pages/templatetags/pages_tags.py | 6 ++- mezzanine/pages/tests.py | 12 +++--- mezzanine/pages/translation.py | 8 +++- mezzanine/pages/views.py | 6 ++- .../project_template/project_name/settings.py | 14 +++++++ mezzanine/urls.py | 10 ++--- mezzanine/utils/apps.py | 26 ++++++++++++ mezzanine/utils/models.py | 26 ++++++++++-- mezzanine/utils/urls.py | 3 +- 49 files changed, 392 insertions(+), 113 deletions(-) create mode 100644 mezzanine/utils/apps.py diff --git a/mezzanine/blog/__init__.py b/mezzanine/blog/__init__.py index 03fc4334ee..0822bd8f66 100644 --- a/mezzanine/blog/__init__.py +++ b/mezzanine/blog/__init__.py @@ -5,3 +5,12 @@ from __future__ import unicode_literals from mezzanine import __version__ # noqa +from mezzanine.utils.models import get_swappable_model + + +def get_post_model(): + return get_swappable_model("BLOG_POST_MODEL") + + +def get_category_model(): + return get_swappable_model("BLOG_CATEGORY_MODEL") diff --git a/mezzanine/blog/feeds.py b/mezzanine/blog/feeds.py index 85f3408f50..097f1c73fc 100644 --- a/mezzanine/blog/feeds.py +++ b/mezzanine/blog/feeds.py @@ -8,16 +8,19 @@ from django.utils.feedgenerator import Atom1Feed from django.utils.html import strip_tags -from mezzanine.blog.models import BlogPost, BlogCategory +from mezzanine.blog 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.utils.apps import pages_installed from mezzanine.utils.html import absolute_urls from mezzanine.utils.sites import current_site_id User = get_user_model() +BlogPost = get_post_model() +BlogCategory = get_category_model() try: unicode @@ -43,8 +46,9 @@ 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(): + from mezzanine.pages import get_page_model + Page = get_page_model() 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..3406231222 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 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..f7871e3378 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 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 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 045c98b9f1..49c0de0f70 100644 --- a/mezzanine/blog/migrations/0001_initial.py +++ b/mezzanine/blog/migrations/0001_initial.py @@ -24,6 +24,7 @@ class Migration(migrations.Migration): ('site', models.ForeignKey(editable=False, to='sites.Site', on_delete=models.CASCADE)), ], options={ + 'swappable': 'BLOG_CATEGORY_MODEL', 'ordering': ('title',), 'verbose_name': 'Blog Category', 'verbose_name_plural': 'Blog Categories', @@ -60,6 +61,7 @@ class Migration(migrations.Migration): ('user', models.ForeignKey(related_name='blogposts', verbose_name='Author', to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE)), ], 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..2892968fa6 100644 --- a/mezzanine/blog/models.py +++ b/mezzanine/blog/models.py @@ -12,12 +12,12 @@ from mezzanine.utils.models import AdminThumbMixin, upload_to -class BlogPost(Displayable, Ownable, RichText, AdminThumbMixin): +class AbstractBlogPost(Displayable, Ownable, RichText, AdminThumbMixin): """ A blog post. """ - categories = models.ManyToManyField("BlogCategory", + categories = models.ManyToManyField(settings.BLOG_CATEGORY_MODEL, verbose_name=_("Categories"), blank=True, related_name="blogposts") allow_comments = models.BooleanField(verbose_name=_("Allow comments"), @@ -33,6 +33,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 +65,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 +85,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 8d352536e9..a1decf0bd4 100644 --- a/mezzanine/blog/templatetags/blog_tags.py +++ b/mezzanine/blog/templatetags/blog_tags.py @@ -5,12 +5,14 @@ from django.db.models import Count, Q from django.utils import timezone +from mezzanine.blog import get_post_model, get_category_model from mezzanine.blog.forms import BlogPostForm -from mezzanine.blog.models import BlogPost, BlogCategory 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 0f76818d5f..4790b7b0f3 100644 --- a/mezzanine/blog/tests.py +++ b/mezzanine/blog/tests.py @@ -14,13 +14,19 @@ from django.template import Context, Template from django.test import override_settings -from mezzanine.blog.models import BlogPost +from mezzanine.blog 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 import get_page_model, get_rich_text_page_model +from mezzanine.utils.apps import accounts_installed, 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): @@ -38,9 +44,7 @@ def test_blog_views(self): response = self.client.get(blog_post.get_absolute_url()) self.assertEqual(response.status_code, 200) - @skipUnless("mezzanine.accounts" in settings.INSTALLED_APPS and - "mezzanine.pages" in settings.INSTALLED_APPS, - "accounts and pages apps required") + @skipUnless(accounts_installed() and pages_installed(), "accounts and pages apps required") def test_login_protected_blog(self): """ Test the blog is login protected if its page has login_required diff --git a/mezzanine/blog/translation.py b/mezzanine/blog/translation.py index dfb9e828d7..bbe84c1c0d 100644 --- a/mezzanine/blog/translation.py +++ b/mezzanine/blog/translation.py @@ -2,7 +2,11 @@ from mezzanine.core.translation import (TranslatedSlugged, TranslatedDisplayable, TranslatedRichText) -from mezzanine.blog.models import BlogCategory, BlogPost +from mezzanine.blog import get_post_model, get_category_model + + +BlogPost = get_post_model() +BlogCategory = get_category_model() class TranslatedBlogPost(TranslatedDisplayable, TranslatedRichText): @@ -12,5 +16,6 @@ class TranslatedBlogPost(TranslatedDisplayable, TranslatedRichText): class TranslatedBlogCategory(TranslatedSlugged): fields = () + translator.register(BlogCategory, TranslatedBlogCategory) translator.register(BlogPost, TranslatedBlogPost) diff --git a/mezzanine/blog/views.py b/mezzanine/blog/views.py index 3e41361e9f..07feca3d86 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 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..5a26b0eba7 100644 --- a/mezzanine/boot/lazy_admin.py +++ b/mezzanine/boot/lazy_admin.py @@ -7,6 +7,7 @@ NotRegistered, AlreadyRegistered) from django.shortcuts import redirect +from mezzanine.utils.apps import pages_installed from mezzanine.utils.importing import import_dotted_path @@ -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/conf/translation.py b/mezzanine/conf/translation.py index d3b075e52f..4e5ad92d47 100644 --- a/mezzanine/conf/translation.py +++ b/mezzanine/conf/translation.py @@ -5,4 +5,5 @@ class TranslatedSetting(TranslationOptions): fields = ('value',) + translator.register(Setting, TranslatedSetting) diff --git a/mezzanine/core/management/commands/createdb.py b/mezzanine/core/management/commands/createdb.py index 47ae7b09b4..1cecfdf803 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 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/models.py b/mezzanine/core/models.py index b4c3a08c89..e70ec78a37 100644 --- a/mezzanine/core/models.py +++ b/mezzanine/core/models.py @@ -25,14 +25,11 @@ from mezzanine.core.managers import DisplayableManager, CurrentSiteManager from mezzanine.generic.fields import KeywordsField from mezzanine.utils.html import TagCloser -from mezzanine.utils.models import base_concrete_model, get_user_model_name +from mezzanine.utils.models import base_concrete_model from mezzanine.utils.sites import current_site_id, current_request from mezzanine.utils.urls import admin_url, slugify, unique_slug -user_model_name = get_user_model_name() - - def wrapped_manager(klass): if settings.USE_MODELTRANSLATION: from modeltranslation.manager import MultilingualManager @@ -507,7 +504,7 @@ class Ownable(models.Model): Abstract model that provides ownership of an object for a user. """ - user = models.ForeignKey(user_model_name, on_delete=models.CASCADE, + user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, verbose_name=_("Author"), related_name="%(class)ss") class Meta: @@ -577,7 +574,7 @@ class SitePermission(models.Model): access. """ - user = models.OneToOneField(user_model_name, on_delete=models.CASCADE, + user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, verbose_name=_("Author"), related_name="%(class)ss") sites = models.ManyToManyField("sites.Site", blank=True, verbose_name=_("Sites")) diff --git a/mezzanine/core/sitemaps.py b/mezzanine/core/sitemaps.py index 8c4f24fb4f..e0220cbb53 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.apps 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 import get_post_model + BlogPost = get_post_model() class DisplayableSitemap(Sitemap): diff --git a/mezzanine/core/tests.py b/mezzanine/core/tests.py index 1cc89f899f..e71020a2c9 100644 --- a/mezzanine/core/tests.py +++ b/mezzanine/core/tests.py @@ -41,9 +41,10 @@ from mezzanine.core.managers import DisplayableManager from mezzanine.core.models import (CONTENT_STATUS_DRAFT, CONTENT_STATUS_PUBLISHED) +from mezzanine.forms import get_form_model from mezzanine.forms.admin import FieldAdmin -from mezzanine.forms.models import Form -from mezzanine.pages.models import Page, RichTextPage +from mezzanine.pages import get_page_model, get_rich_text_page_model +from mezzanine.utils.apps import pages_installed from mezzanine.utils.deprecation import (get_middleware_setting, get_middleware_setting_name) from mezzanine.utils.importing import import_dotted_path @@ -52,6 +53,11 @@ from mezzanine.utils.html import TagCloser, escape +Form = get_form_model() +Page = get_page_model() +RichTextPage = get_rich_text_page_model() + + class CoreTests(TestCase): def test_tagcloser(self): @@ -93,8 +99,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 @@ -105,8 +110,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. @@ -129,8 +133,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. @@ -218,8 +221,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 @@ -317,8 +319,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): """ @@ -503,8 +504,7 @@ def middleware(request): return middleware -@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 494597c342..a3c189857b 100644 --- a/mezzanine/core/views.py +++ b/mezzanine/core/views.py @@ -30,9 +30,10 @@ from mezzanine.conf import settings from mezzanine.core.forms import get_edit_form from mezzanine.core.models import Displayable, SitePermission -from mezzanine.utils.views import is_editable, paginate +from mezzanine.utils.apps 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 mimetypes.init() @@ -176,8 +177,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 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/__init__.py b/mezzanine/forms/__init__.py index bf441dce5d..d748337e80 100644 --- a/mezzanine/forms/__init__.py +++ b/mezzanine/forms/__init__.py @@ -5,3 +5,20 @@ from __future__ import unicode_literals from mezzanine import __version__ # noqa +from mezzanine.utils.models import get_swappable_model + + +def get_form_model(): + return get_swappable_model("FORM_MODEL") + + +def get_field_model(): + return get_swappable_model("FIELD_MODEL") + + +def get_form_entry_model(): + return get_swappable_model("FORM_ENTRY_MODEL") + + +def get_field_entry_model(): + return get_swappable_model("FIELD_ENTRY_MODEL") diff --git a/mezzanine/forms/admin.py b/mezzanine/forms/admin.py index d59f6ad963..0a680d0455 100644 --- a/mezzanine/forms/admin.py +++ b/mezzanine/forms/admin.py @@ -20,12 +20,18 @@ from mezzanine.core.admin import TabularDynamicInlineAdmin from mezzanine.core.forms import DynamicInlineAdminForm from mezzanine.forms.forms import EntriesForm -from mezzanine.forms.models import Form, Field, FormEntry, FieldEntry +from mezzanine.forms import get_form_model, get_form_entry_model +from mezzanine.forms import get_field_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 +Form = get_form_model() +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 af8baa4c23..bfa93e58bd 100644 --- a/mezzanine/forms/forms.py +++ b/mezzanine/forms/forms.py @@ -21,10 +21,13 @@ from mezzanine.conf import settings from mezzanine.forms import fields -from mezzanine.forms.models import FormEntry, FieldEntry +from mezzanine.forms 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/migrations/0001_initial.py b/mezzanine/forms/migrations/0001_initial.py index 8044a7f753..b450b12326 100644 --- a/mezzanine/forms/migrations/0001_initial.py +++ b/mezzanine/forms/migrations/0001_initial.py @@ -28,6 +28,7 @@ class Migration(migrations.Migration): ('help_text', models.CharField(max_length=100, verbose_name='Help text', blank=True)), ], options={ + 'swappable': 'FIELD_MODEL', 'ordering': ('_order',), 'verbose_name': 'Field', 'verbose_name_plural': 'Fields', @@ -42,6 +43,7 @@ class Migration(migrations.Migration): ('value', models.CharField(max_length=2000, null=True)), ], options={ + 'swappable': 'FIELD_ENTRY_MODEL', 'verbose_name': 'Form field entry', 'verbose_name_plural': 'Form field entries', }, @@ -61,6 +63,7 @@ class Migration(migrations.Migration): ('email_message', models.TextField(help_text='Emails sent based on the above options will contain each of the form fields entered. You can also enter a message here that will be included in the email.', verbose_name='Message', blank=True)), ], options={ + 'swappable': 'FORM_MODEL', 'ordering': ('_order',), 'verbose_name': 'Form', 'verbose_name_plural': 'Forms', @@ -75,6 +78,7 @@ class Migration(migrations.Migration): ('form', models.ForeignKey(related_name='entries', to='forms.Form', on_delete=models.CASCADE)), ], options={ + 'swappable': 'FORM_ENTRY_MODEL', 'verbose_name': 'Form entry', 'verbose_name_plural': 'Form entries', }, diff --git a/mezzanine/forms/models.py b/mezzanine/forms/models.py index 6e34c42124..463ca65dbe 100644 --- a/mezzanine/forms/models.py +++ b/mezzanine/forms/models.py @@ -11,7 +11,7 @@ from mezzanine.pages.models import Page -class Form(Page, RichText): +class AbstractForm(Page, RichText): """ A user-built form. """ @@ -35,10 +35,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.. @@ -107,39 +114,60 @@ def is_a(self, *args): return self.field_type in args -class Field(AbstractBaseField): - form = models.ForeignKey("Form", on_delete=models.CASCADE, +class AbstractField(AbstractBaseField): + form = models.ForeignKey(settings.FORM_MODEL, on_delete=models.CASCADE, 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", on_delete=models.CASCADE, + form = models.ForeignKey(settings.FORM_MODEL, on_delete=models.CASCADE, 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", on_delete=models.CASCADE, + entry = models.ForeignKey(settings.FORM_ENTRY_MODEL, on_delete=models.CASCADE, 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..d4db3316a0 100644 --- a/mezzanine/forms/page_processors.py +++ b/mezzanine/forms/page_processors.py @@ -4,14 +4,17 @@ from django.template import RequestContext from mezzanine.conf import settings +from mezzanine.forms import get_form_model from mezzanine.forms.forms import FormForForm -from mezzanine.forms.models import Form 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 diff --git a/mezzanine/forms/tests.py b/mezzanine/forms/tests.py index 3ca7e2632b..9cb0d7aafb 100644 --- a/mezzanine/forms/tests.py +++ b/mezzanine/forms/tests.py @@ -6,12 +6,14 @@ from django import forms from mezzanine.conf import settings from mezzanine.core.models import CONTENT_STATUS_PUBLISHED -from mezzanine.forms import fields +from mezzanine.forms import fields, get_form_model from mezzanine.forms.forms import FormForForm -from mezzanine.forms.models import Form 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..f7316a5419 100644 --- a/mezzanine/forms/translation.py +++ b/mezzanine/forms/translation.py @@ -1,6 +1,10 @@ from modeltranslation.translator import translator, TranslationOptions from mezzanine.core.translation import TranslatedRichText -from mezzanine.forms.models import Form, Field +from mezzanine.forms import get_form_model, get_field_model + + +Form = get_form_model() +Field = get_field_model() class TranslatedForm(TranslatedRichText): @@ -10,5 +14,6 @@ class TranslatedForm(TranslatedRichText): class TranslatedField(TranslationOptions): fields = ('label', 'choices', 'default', 'placeholder_text', 'help_text',) + translator.register(Form, TranslatedForm) translator.register(Field, TranslatedField) diff --git a/mezzanine/galleries/__init__.py b/mezzanine/galleries/__init__.py index 0884d14f7b..ee27e28da0 100644 --- a/mezzanine/galleries/__init__.py +++ b/mezzanine/galleries/__init__.py @@ -4,3 +4,12 @@ from __future__ import unicode_literals from mezzanine import __version__ # noqa +from mezzanine.utils.models import get_swappable_model + + +def get_gallery_model(): + return get_swappable_model("GALLERY_MODEL") + + +def get_gallery_image_model(): + return get_swappable_model("GALLERY_IMAGE_MODEL") diff --git a/mezzanine/galleries/admin.py b/mezzanine/galleries/admin.py index ff98052e92..73a93a5ec7 100644 --- a/mezzanine/galleries/admin.py +++ b/mezzanine/galleries/admin.py @@ -4,10 +4,14 @@ from mezzanine.core.admin import TabularDynamicInlineAdmin from mezzanine.pages.admin import PageAdmin -from mezzanine.galleries.models import Gallery, GalleryImage +from mezzanine.galleries import get_gallery_model, get_gallery_image_model from mezzanine.utils.static import static_lazy as static +Gallery = get_gallery_model() +GalleryImage = get_gallery_image_model() + + class GalleryImageInline(TabularDynamicInlineAdmin): model = GalleryImage diff --git a/mezzanine/galleries/migrations/0001_initial.py b/mezzanine/galleries/migrations/0001_initial.py index 2f9bbdc1cf..37f94b71d6 100644 --- a/mezzanine/galleries/migrations/0001_initial.py +++ b/mezzanine/galleries/migrations/0001_initial.py @@ -20,6 +20,7 @@ class Migration(migrations.Migration): ('zip_import', models.FileField(help_text="Upload a zip file containing images, and they'll be imported into this gallery.", upload_to='galleries', verbose_name='Zip import', blank=True)), ], options={ + 'swappable': 'GALLERY_MODEL', 'ordering': ('_order',), 'verbose_name': 'Gallery', 'verbose_name_plural': 'Galleries', @@ -36,6 +37,7 @@ class Migration(migrations.Migration): ('gallery', models.ForeignKey(related_name='images', to='galleries.Gallery', on_delete=models.CASCADE)), ], options={ + 'swappable': 'GALLERY_IMAGE_MODEL', 'ordering': ('_order',), 'verbose_name': 'Image', 'verbose_name_plural': 'Images', diff --git a/mezzanine/galleries/models.py b/mezzanine/galleries/models.py index 1637cb1747..835112203b 100644 --- a/mezzanine/galleries/models.py +++ b/mezzanine/galleries/models.py @@ -111,20 +111,27 @@ def save(self, delete_zip_import=True, *args, **kwargs): self.zip_import.delete(save=True) -class Gallery(Page, RichText, BaseGallery): +class AbstractGallery(Page, RichText, BaseGallery): """ Page bucket for gallery photos. """ class Meta: + abstract = True verbose_name = _("Gallery") verbose_name_plural = _("Galleries") +class Gallery(AbstractGallery): + + class Meta(AbstractGallery.Meta): + swappable = 'GALLERY_MODEL' + + @python_2_unicode_compatible -class GalleryImage(Orderable): +class AbstractGalleryImage(Orderable): - gallery = models.ForeignKey("Gallery", on_delete=models.CASCADE, + gallery = models.ForeignKey(settings.GALLERY_MODEL, on_delete=models.CASCADE, related_name="images") file = FileField(_("File"), max_length=200, format="Image", upload_to=upload_to("galleries.GalleryImage.file", "galleries")) @@ -132,6 +139,7 @@ class GalleryImage(Orderable): blank=True) class Meta: + abstract = True verbose_name = _("Image") verbose_name_plural = _("Images") @@ -153,4 +161,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..7b2e829a3f 100644 --- a/mezzanine/galleries/tests.py +++ b/mezzanine/galleries/tests.py @@ -8,10 +8,14 @@ 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 import get_gallery_model +from mezzanine.galleries.models import 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..99dc4d14d8 100644 --- a/mezzanine/galleries/translation.py +++ b/mezzanine/galleries/translation.py @@ -1,6 +1,10 @@ from modeltranslation.translator import translator, TranslationOptions from mezzanine.core.translation import TranslatedRichText -from mezzanine.galleries.models import GalleryImage, Gallery +from mezzanine.galleries import get_gallery_model, get_gallery_image_model + + +Gallery = get_gallery_model() +GalleryImage = get_gallery_image_model() class TranslatedGallery(TranslatedRichText): @@ -10,5 +14,6 @@ class TranslatedGallery(TranslatedRichText): class TranslatedGalleryImage(TranslationOptions): fields = ('description',) + translator.register(Gallery, TranslatedGallery) translator.register(GalleryImage, TranslatedGalleryImage) diff --git a/mezzanine/generic/models.py b/mezzanine/generic/models.py index 64a55fc676..74da44d2db 100644 --- a/mezzanine/generic/models.py +++ b/mezzanine/generic/models.py @@ -14,7 +14,6 @@ from mezzanine.generic.managers import CommentManager, KeywordManager from mezzanine.core.models import Slugged, Orderable from mezzanine.conf import settings -from mezzanine.utils.models import get_user_model_name from mezzanine.utils.sites import current_site_id @@ -132,7 +131,7 @@ class Rating(models.Model): on_delete=models.CASCADE) object_pk = models.IntegerField() content_object = GenericForeignKey("content_type", "object_pk") - user = models.ForeignKey(get_user_model_name(), on_delete=models.CASCADE, + user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, verbose_name=_("Rater"), null=True, related_name="%(class)ss") class Meta: diff --git a/mezzanine/generic/tests.py b/mezzanine/generic/tests.py index 22923fb1cd..bb54e87341 100644 --- a/mezzanine/generic/tests.py +++ b/mezzanine/generic/tests.py @@ -9,21 +9,24 @@ from django.contrib.contenttypes.models import ContentType from django.core.urlresolvers import reverse -from mezzanine.blog.models import BlogPost +from mezzanine.blog import get_post_model from mezzanine.conf import settings - from mezzanine.core.models import CONTENT_STATUS_PUBLISHED from mezzanine.generic.forms import RatingForm, KeywordsWidget from mezzanine.generic.models import AssignedKeyword, Keyword, ThreadedComment from mezzanine.generic.views import comment -from mezzanine.pages.models import RichTextPage +from mezzanine.pages import get_rich_text_page_model +from mezzanine.utils.apps 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 +57,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 +80,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 +104,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/__init__.py b/mezzanine/pages/__init__.py index 5e8d9cf897..a606b28452 100644 --- a/mezzanine/pages/__init__.py +++ b/mezzanine/pages/__init__.py @@ -5,6 +5,19 @@ from __future__ import unicode_literals from mezzanine import __version__ # noqa +from mezzanine.utils.models import get_swappable_model default_app_config = 'mezzanine.pages.apps.PagesConfig' + + +def get_page_model(): + return get_swappable_model("PAGE_MODEL") + + +def get_rich_text_page_model(): + return get_swappable_model("RICH_TEXT_PAGE_MODEL") + + +def get_link_model(): + return get_swappable_model("LINK_MODEL") diff --git a/mezzanine/pages/admin.py b/mezzanine/pages/admin.py index eb4e313ef3..d128be12ed 100644 --- a/mezzanine/pages/admin.py +++ b/mezzanine/pages/admin.py @@ -10,10 +10,16 @@ from mezzanine.conf import settings from mezzanine.core.admin import ( ContentTypedAdmin, DisplayableAdmin, DisplayableAdminForm) -from mezzanine.pages.models import Page, RichTextPage, Link +from mezzanine.pages import get_page_model, get_rich_text_page_model, get_link_model +from mezzanine.pages.models import AbstractLink from mezzanine.utils.urls import clean_slashes +Page = get_page_model() +RichTextPage = get_rich_text_page_model() +Link = get_link_model() + + # Add extra fields for pages to the Displayable fields. # We only add the menu field if PAGE_MENU_TEMPLATES has values. page_fieldsets = deepcopy(DisplayableAdmin.fieldsets) @@ -32,7 +38,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 a15db7203d..70a1099ec3 100644 --- a/mezzanine/pages/middleware.py +++ b/mezzanine/pages/middleware.py @@ -6,13 +6,16 @@ from mezzanine.conf import settings from mezzanine.pages import context_processors, page_processors -from mezzanine.pages.models import Page +from mezzanine.pages import get_page_model from mezzanine.pages.views import page as page_view from mezzanine.utils.conf import middlewares_or_subclasses_installed from mezzanine.utils.deprecation import (MiddlewareMixin, is_authenticated) from mezzanine.utils.urls import path_to_slug +Page = get_page_model() + + class PageMiddleware(MiddlewareMixin): """ Adds a page to the template context for the current response. diff --git a/mezzanine/pages/migrations/0001_initial.py b/mezzanine/pages/migrations/0001_initial.py index eb1bbcfe60..ddf33cef44 100644 --- a/mezzanine/pages/migrations/0001_initial.py +++ b/mezzanine/pages/migrations/0001_initial.py @@ -37,6 +37,7 @@ class Migration(migrations.Migration): ('login_required', models.BooleanField(default=False, help_text='If checked, only logged in users can view this page', verbose_name='Login required')), ], options={ + 'swappable': 'PAGE_MODEL', 'ordering': ('titles',), 'verbose_name': 'Page', 'verbose_name_plural': 'Pages', @@ -49,6 +50,7 @@ class Migration(migrations.Migration): ('page_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, serialize=False, to='pages.Page', on_delete=models.CASCADE)), ], options={ + 'swappable': 'LINK_MODEL', 'ordering': ('_order',), 'verbose_name': 'Link', 'verbose_name_plural': 'Links', @@ -62,6 +64,7 @@ class Migration(migrations.Migration): ('content', mezzanine.core.fields.RichTextField(verbose_name='Content')), ], options={ + 'swappable': 'RICH_TEXT_PAGE_MODEL', 'ordering': ('_order',), 'verbose_name': 'Rich text page', 'verbose_name_plural': 'Rich text pages', diff --git a/mezzanine/pages/models.py b/mezzanine/pages/models.py index e778be73b4..85a7a55862 100644 --- a/mezzanine/pages/models.py +++ b/mezzanine/pages/models.py @@ -35,13 +35,13 @@ class Meta: @python_2_unicode_compatible -class Page(BasePage, ContentTyped): +class AbstractPage(BasePage): """ A page in the page tree. This is the base class that custom content types need to subclass. """ - parent = models.ForeignKey("Page", on_delete=models.CASCADE, + parent = models.ForeignKey(settings.PAGE_MODEL, on_delete=models.CASCADE, 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) @@ -85,7 +85,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): """ @@ -94,10 +94,11 @@ def description_from_content(self): ``Page`` instance, so that all fields defined on the subclass are available for generating the description. """ - if self.__class__ == Page: + from mezzanine.pages import get_page_model + if self.__class__ == get_page_model(): 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): """ @@ -117,6 +118,8 @@ def get_ascendants(self, for_user=None): if self.slug: kwargs = {"for_user": for_user} with override_current_site_id(self.site_id): + from mezzanine.pages import get_page_model + Page = get_page_model() pages = Page.objects.with_ascendants_for_slug(self.slug, **kwargs) self._ascendants = pages[0]._ascendants @@ -136,7 +139,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 @@ -147,6 +150,8 @@ def set_slug(self, new_slug): start with this page's slug. """ slug_prefix = "%s/" % self.slug + from mezzanine.pages import get_page_model + 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):] @@ -271,28 +276,48 @@ def get_template_name(self): return None -class RichTextPage(Page, RichText): +class Page(AbstractPage): + + class Meta(AbstractPage.Meta): + swappable = 'PAGE_MODEL' + + +class AbstractRichTextPage(AbstractPage, RichText): """ Implements the default type of page with a single Rich Text content field. """ class 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(AbstractPage): """ A general content type for creating external links in the page menu. """ class 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 9692d6a6c6..acdf093dc4 100644 --- a/mezzanine/pages/templatetags/pages_tags.py +++ b/mezzanine/pages/templatetags/pages_tags.py @@ -8,10 +8,12 @@ from django.template.loader import get_template from django.utils.translation import ugettext_lazy as _ -from mezzanine.pages.models import Page -from mezzanine.utils.urls import home_slug from mezzanine import template +from mezzanine.pages import get_page_model +from mezzanine.utils.urls import home_slug + +Page = get_page_model() register = template.Library() diff --git a/mezzanine/pages/tests.py b/mezzanine/pages/tests.py index 61e1ab6e43..a0c32e8d40 100644 --- a/mezzanine/pages/tests.py +++ b/mezzanine/pages/tests.py @@ -20,16 +20,19 @@ 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 import get_page_model, get_rich_text_page_model from mezzanine.pages.admin import PageAdminForm from mezzanine.pages.fields import MenusField from mezzanine.pages.checks import check_context_processor from mezzanine.urls import PAGES_SLUG +from mezzanine.utils.apps import accounts_installed from mezzanine.utils.sites import override_current_site_id from mezzanine.utils.tests import TestCase User = get_user_model() +Page = get_page_model() +RichTextPage = get_rich_text_page_model() class PagesTests(TestCase): @@ -176,7 +179,6 @@ def test_login_required(self): title="Public", slug="public", login_required=False) private, _ = RichTextPage.objects.get_or_create( title="Private", slug="private", login_required=True) - accounts_installed = ("mezzanine.accounts" in settings.INSTALLED_APPS) args = {"for_user": AnonymousUser()} self.assertTrue(public in RichTextPage.objects.published(**args)) @@ -208,7 +210,7 @@ def test_login_required(self): # a second redirect that encodes the next parameter. login_next = urlquote_plus(login_next) login = "%s%s?next=%s" % (login_prefix, login_url, login_next) - if accounts_installed: + if accounts_installed(): # For an inaccessible page with mezzanine.accounts we should # see a login page, without it 404 is more appropriate than an # admin login. @@ -220,7 +222,7 @@ def test_login_required(self): response = self.client.get(public_url, follow=True) self.assertEqual(response.status_code, 200) - if accounts_installed: + if accounts_installed(): # View / pattern name redirect properly, without encoding next. login = "%s%s?next=%s" % (login_prefix, login_url, private_url) with override_settings(LOGIN_URL="login"): @@ -236,7 +238,7 @@ def test_login_required(self): response = self.client.get(public_url, follow=True) self.assertEqual(response.status_code, 200) - if accounts_installed: + if accounts_installed(): with override_settings(LOGIN_URL="mezzanine.accounts.views.login"): response = self.client.get(public_url, follow=True) self.assertEqual(response.status_code, 200) diff --git a/mezzanine/pages/translation.py b/mezzanine/pages/translation.py index e8b565e396..b911f45e26 100644 --- a/mezzanine/pages/translation.py +++ b/mezzanine/pages/translation.py @@ -1,7 +1,12 @@ from modeltranslation.translator import translator, TranslationOptions from mezzanine.core.translation import (TranslatedDisplayable, TranslatedRichText) -from mezzanine.pages.models import Page, RichTextPage, Link +from mezzanine.pages import get_page_model, get_rich_text_page_model, get_link_model + + +Page = get_page_model() +RichTextPage = get_rich_text_page_model() +Link = get_link_model() class TranslatedPage(TranslatedDisplayable): @@ -15,6 +20,7 @@ class TranslatedRichTextPage(TranslatedRichText): class TranslatedLink(TranslationOptions): fields = () + 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..ba73196073 100644 --- a/mezzanine/pages/views.py +++ b/mezzanine/pages/views.py @@ -8,10 +8,14 @@ from django.contrib import messages from django.template.response import TemplateResponse -from mezzanine.pages.models import Page, PageMoveException +from mezzanine.pages import get_page_model +from mezzanine.pages.models import PageMoveException from mezzanine.utils.urls import home_slug +Page = get_page_model() + + @staff_member_required def admin_page_ordering(request): """ diff --git a/mezzanine/project_template/project_name/settings.py b/mezzanine/project_template/project_name/settings.py index 5625f12c99..9f0c846e43 100644 --- a/mezzanine/project_template/project_name/settings.py +++ b/mezzanine/project_template/project_name/settings.py @@ -77,6 +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" +FORM_ENTRY_MODEL = "forms.FormEntry" +FIELD_MODEL = "forms.Field" +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 diff --git a/mezzanine/urls.py b/mezzanine/urls.py index 28da77a491..99c995dcef 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.apps import accounts_installed, blog_installed, pages_installed urlpatterns = [] @@ -56,7 +57,7 @@ ] # Mezzanine's Accounts app -if "mezzanine.accounts" in settings.INSTALLED_APPS: +if accounts_installed(): # We don't define a URL prefix here such as /account/ since we want # to honour the LOGIN_* settings, which Django has prefixed with # /account/ by default. So those settings are used in accounts.urls @@ -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/apps.py b/mezzanine/utils/apps.py new file mode 100644 index 0000000000..81ab1e07ab --- /dev/null +++ b/mezzanine/utils/apps.py @@ -0,0 +1,26 @@ +from django.conf import settings +from django.core.exceptions import ImproperlyConfigured + +from mezzanine.utils.models import get_model + + +def accounts_installed(): + return "mezzanine.accounts" in settings.INSTALLED_APPS + + +def pages_installed(): + """Detects any of the vanilla pages app or a complete replacement.""" + try: + get_model(settings.PAGE_MODEL) + except ImproperlyConfigured: + return False + return True + + +def blog_installed(): + """Detects any of the vanilla blog app or a complete replacement.""" + try: + get_model(settings.POST_MODEL) + except ImproperlyConfigured: + return False + return True diff --git a/mezzanine/utils/models.py b/mezzanine/utils/models.py index f791549abf..f3e5bcc352 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 @@ -22,11 +23,30 @@ def get_user_model(): return django_get_user_model() -def get_user_model_name(): +def get_model(model_name, setting_name): """ - Returns the app_label.object_name string for the user model. + Returns the model by its "app_label.object_name" reference. """ - return getattr(settings, "AUTH_USER_MODEL", "auth.User") + try: + return apps.get_model(model_name, require_ready=False) + except ValueError: + raise ImproperlyConfigured( + "%s must be of the form 'app_label.model_name'" % setting_name + ) + except LookupError: + raise ImproperlyConfigured( + "%s refers to model '%s' that has not been installed" % ( + setting_name, model_name + ) + ) + + +def get_swappable_model(setting_name): + try: + model_name = getattr(settings, setting_name) + except AttributeError: + raise ImproperlyConfigured("settings have no %s" % setting_name) + return get_model(model_name, setting_name) def _base_concrete_model(abstract, klass): diff --git a/mezzanine/utils/urls.py b/mezzanine/utils/urls.py index b55da70304..b3466383d1 100644 --- a/mezzanine/utils/urls.py +++ b/mezzanine/utils/urls.py @@ -14,6 +14,7 @@ from django.utils import translation from mezzanine.conf import settings +from mezzanine.utils.apps import accounts_installed from mezzanine.utils.importing import import_dotted_path @@ -105,7 +106,7 @@ def login_redirect(request): - homepage """ ignorable_nexts = ("",) - if "mezzanine.accounts" in settings.INSTALLED_APPS: + if accounts_installed(): from mezzanine.accounts import urls ignorable_nexts += (urls.SIGNUP_URL, urls.LOGIN_URL, urls.LOGOUT_URL) next = next_url(request) or ""