diff --git a/mezzanine/accounts/__init__.py b/mezzanine/accounts/__init__.py index eca7151b95..7456f98090 100644 --- a/mezzanine/accounts/__init__.py +++ b/mezzanine/accounts/__init__.py @@ -7,12 +7,12 @@ """ from __future__ import unicode_literals -from django.apps import apps from django.conf import settings from django.contrib.auth import get_user_model from django.core.exceptions import ImproperlyConfigured from mezzanine.utils.importing import import_dotted_path +from mezzanine.utils.models import get_swappable_model class ProfileNotConfigured(Exception): @@ -25,19 +25,10 @@ def get_profile_model(): ``settings.ACCOUNTS_PROFILE_MODEL``, or ``None`` if no profile model is configured. """ - if not getattr(settings, "ACCOUNTS_PROFILE_MODEL", None): raise ProfileNotConfigured - try: - return apps.get_model(settings.ACCOUNTS_PROFILE_MODEL) - except ValueError: - raise ImproperlyConfigured("ACCOUNTS_PROFILE_MODEL must be of " - "the form 'app_label.model_name'") - except LookupError: - raise ImproperlyConfigured("ACCOUNTS_PROFILE_MODEL refers to " - "model '%s' that has not been installed" - % settings.ACCOUNTS_PROFILE_MODEL) + return get_swappable_model("ACCOUNTS_PROFILE_MODEL") def get_profile_for_user(user): 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 59a1506424..07514f694c 100644 --- a/mezzanine/blog/feeds.py +++ b/mezzanine/blog/feeds.py @@ -8,12 +8,14 @@ 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.pages 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 @@ -43,8 +45,8 @@ 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(): + Page = get_page_model() try: page = Page.objects.published().get(slug=settings.BLOG_SLUG) except Page.DoesNotExist: @@ -79,12 +81,14 @@ def link(self): def items(self): if not self._public: return [] + BlogPost = get_post_model() blog_posts = BlogPost.objects.published().select_related("user" ).prefetch_related("categories") if self.tag: tag = get_object_or_404(Keyword, slug=self.tag) blog_posts = blog_posts.filter(keywords__keyword=tag) if self.category: + BlogCategory = get_category_model() category = get_object_or_404(BlogCategory, slug=self.category) blog_posts = blog_posts.filter(categories=category) if self.username: @@ -105,6 +109,7 @@ def item_description(self, item): def categories(self): if not self._public: return [] + BlogCategory = get_category_model() return BlogCategory.objects.all() def feed_url(self): diff --git a/mezzanine/blog/forms.py b/mezzanine/blog/forms.py index 1db7c638d9..7ed707a795 100644 --- a/mezzanine/blog/forms.py +++ b/mezzanine/blog/forms.py @@ -2,10 +2,11 @@ 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..b4a6e67113 100644 --- a/mezzanine/blog/management/base.py +++ b/mezzanine/blog/management/base.py @@ -12,15 +12,19 @@ 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 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..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 ad958c3bba..4934e6f3d9 100644 --- a/mezzanine/blog/templatetags/blog_tags.py +++ b/mezzanine/blog/templatetags/blog_tags.py @@ -4,12 +4,14 @@ from django.contrib.auth import get_user_model from django.db.models import Count, Q +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 f6d7fec340..3a565efb22 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 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.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..9e32ad2d22 100644 --- a/mezzanine/blog/translation.py +++ b/mezzanine/blog/translation.py @@ -12,5 +12,7 @@ class TranslatedBlogPost(TranslatedDisplayable, TranslatedRichText): class TranslatedBlogCategory(TranslatedSlugged): fields = () + +# XXX What 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..b9dcc07761 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 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/management/commands/createdb.py b/mezzanine/core/management/commands/createdb.py index a6557f5713..abd7b99dfe 100644 --- a/mezzanine/core/management/commands/createdb.py +++ b/mezzanine/core/management/commands/createdb.py @@ -10,6 +10,7 @@ from django.db import connection from mezzanine.conf import settings +from mezzanine.galleries import get_gallery_model from mezzanine.utils.tests import copy_test_to_media @@ -115,7 +116,7 @@ def create_pages(self): if self.verbosity >= 1: print("\nCreating demo pages: About us, Contact form, " "Gallery ...\n") - from mezzanine.galleries.models import Gallery + 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..c1f4fe651b 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 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..86f1bfeb13 100644 --- a/mezzanine/core/tests.py +++ b/mezzanine/core/tests.py @@ -38,15 +38,21 @@ 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.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 +Form = get_form_model() +Page = get_page_model() +RichTextPage = get_rich_text_page_model() + + class CoreTests(TestCase): def test_tagcloser(self): @@ -60,7 +66,7 @@ def test_tagcloser(self): "Line break
") @skipUnless("mezzanine.mobile" in settings.INSTALLED_APPS and - "mezzanine.pages" in settings.INSTALLED_APPS, + pages_installed(), "mobile and pages apps required") def test_device_specific_template(self): """ @@ -118,8 +124,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 +135,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 +158,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 +220,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 +318,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 +492,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..cf7b783e59 100644 --- a/mezzanine/core/views.py +++ b/mezzanine/core/views.py @@ -30,10 +30,12 @@ from mezzanine.conf import settings from mezzanine.core.forms import get_edit_form from mezzanine.core.models import Displayable, SitePermission +from mezzanine.pages import get_page_model 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 +189,8 @@ def displayable_links_js(request): TinyMCE. """ links = [] - if "mezzanine.pages" in settings.INSTALLED_APPS: - from mezzanine.pages.models import Page + if pages_installed(): + 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 915aef958e..f9a1fa3a5c 100644 --- a/mezzanine/forms/admin.py +++ b/mezzanine/forms/admin.py @@ -18,13 +18,18 @@ from mezzanine.conf import settings from mezzanine.core.admin import TabularDynamicInlineAdmin +from mezzanine.forms import get_field_model, get_form_entry_model +from mezzanine.forms import get_field_entry_model from mezzanine.forms.forms import EntriesForm -from mezzanine.forms.models import Form, Field, FormEntry, FieldEntry +from mezzanine.forms.models import Form 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..d43d8b3edf 100644 --- a/mezzanine/forms/forms.py +++ b/mezzanine/forms/forms.py @@ -15,11 +15,13 @@ from django.utils.timezone import now from mezzanine.conf import settings +from mezzanine.forms import get_form_entry_model, get_field_entry_model from mezzanine.forms import fields -from mezzanine.forms.models import FormEntry, FieldEntry 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..7b53110559 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.. @@ -108,36 +115,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(settings.FORM_MODEL, 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(settings.FORM_MODEL, 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(settings.FORM_ENTRY_MODEL, 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..cf619670e4 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 @@ -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..27004d021c 100644 --- a/mezzanine/forms/tests.py +++ b/mezzanine/forms/tests.py @@ -6,12 +6,15 @@ from django import forms from mezzanine.conf import settings from mezzanine.core.models import CONTENT_STATUS_PUBLISHED +from mezzanine.forms import get_form_model from mezzanine.forms import fields 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..555345893c 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 What about swapped models? 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..573c8bc0cd 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_image_model +from mezzanine.galleries.models import Gallery 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..ca1deba1a7 100644 --- a/mezzanine/galleries/models.py +++ b/mezzanine/galleries/models.py @@ -35,13 +35,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 +55,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 +113,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(settings.GALLERY_MODEL, 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 +151,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..1fa4159130 100644 --- a/mezzanine/galleries/translation.py +++ b/mezzanine/galleries/translation.py @@ -10,5 +10,7 @@ class TranslatedGallery(TranslatedRichText): class TranslatedGalleryImage(TranslationOptions): fields = ('description',) + +# XXX What 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..6425ec8657 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.conf import settings +from mezzanine.blog import get_post_model 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 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/__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..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..5fc4a8efab 100644 --- a/mezzanine/pages/middleware.py +++ b/mezzanine/pages/middleware.py @@ -5,8 +5,7 @@ from django.http import HttpResponse, Http404 from mezzanine.conf import settings -from mezzanine.pages import context_processors, page_processors -from mezzanine.pages.models import Page +from mezzanine.pages import context_processors, page_processors, 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 +70,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..eeb7c49147 100644 --- a/mezzanine/pages/models.py +++ b/mezzanine/pages/models.py @@ -20,34 +20,24 @@ from mezzanine.utils.urls import path_to_slug -class BasePage(Orderable, Displayable): - """ - 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. - """ - - objects = PageManager() - - class Meta: - abstract = True - - @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(settings.PAGE_MODEL, 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 +74,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 +83,12 @@ def description_from_content(self): ``Page`` instance, so that all fields defined on the subclass are available for generating the description. """ + from mezzanine.pages import get_page_model + 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 +108,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 @@ -135,7 +129,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 +140,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):] @@ -269,28 +265,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..21a386018f 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,12 +34,13 @@ 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 " - "or string form (app.model), or a valid slug" % + "which should be a model subclass of AbstractPage " + "in class or string form (app.model), " + "or a valid slug" % content_model_or_slug) def decorator(func): diff --git a/mezzanine/pages/templatetags/pages_tags.py b/mezzanine/pages/templatetags/pages_tags.py index fbc06ab435..428e4b173a 100644 --- a/mezzanine/pages/templatetags/pages_tags.py +++ b/mezzanine/pages/templatetags/pages_tags.py @@ -8,9 +8,9 @@ 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 register = template.Library() @@ -23,6 +23,7 @@ def page_menu(context, token): pages in a dict in the context when first called using parents as keys for retrieval on subsequent recursive calls from the menu template. """ + Page = get_page_model() # First arg could be the menu template file name, or the parent page. # Also allow for both to be used. template_name = None diff --git a/mezzanine/pages/tests.py b/mezzanine/pages/tests.py index 1b9f9dba8a..16e9a223e9 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 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..d22cfdbd43 100644 --- a/mezzanine/pages/translation.py +++ b/mezzanine/pages/translation.py @@ -15,6 +15,8 @@ class TranslatedRichTextPage(TranslatedRichText): class TranslatedLink(TranslationOptions): fields = () + +# XXX What 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..a70b10f074 100644 --- a/mezzanine/pages/views.py +++ b/mezzanine/pages/views.py @@ -8,7 +8,8 @@ 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 @@ -21,6 +22,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..41a79d8781 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" +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 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..ee020449c1 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,45 @@ def get_user_model_name(): return getattr(settings, "AUTH_USER_MODEL", "auth.User") +def get_model(model_name, setting_name): + """ + Returns the model by its "app_label.object_name" reference. + """ + try: + return apps.get_model(model_name) + 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 + return get_model(model_name, setting_name) + + +def pages_installed(): + """Detects any of the vanilla pages app or a complete replacement.""" + # XXX Cannot import models yet + return "mezzanine.pages" in settings.INSTALLED_APPS + + +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 + # XXX Cannot import models yet + return "mezzanine.blog" in settings.INSTALLED_APPS + + def _base_concrete_model(abstract, klass): for kls in reversed(klass.__mro__): if issubclass(kls, abstract) and not kls._meta.abstract: