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