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 ""