Skip to content

Commit

Permalink
WIP make content models swappable, closes stephenmcd#1160
Browse files Browse the repository at this point in the history
(It's marked as WIP because I left some XXX with questions.)

Any of the following models can now be swapped by your own version:

* Page
* RichTextPage
* Link
* BlogPost
* BlogCategory
* Form
* FormEntry
* FieldEntry
* Gallery
* GalleryImage

So you can keep the same features and API but add fields, methods, inherit from
other classes (geo models for geo fields, third-party models...), etc. Just
make sure you inherit from each respective abstract base class.

BasePage was merged into AbstractPage with the same side effect on inheriting
the object manager on Page, Link, etc.

BaseGallery was merged into AbstractGallery, no point in keeping it.

I didn't change imports or references in the admin modules, I followed
UserAdmin conventions here. You'll have to explicitly register, e.g.  PageAdmin
to your swapped Page model.

No docs update yet, let's first agree on code and naming conventions, what will
become the public API for users. This includes whether the docstring goes on
the abstract model or the vanilla model.

I also guess this deprecates the field injection feature, before removing it in
a future major version.

Besides that, it must be 100% compatible, no test changes apart from imports.
  • Loading branch information
Hervé Cauwelier committed Jan 6, 2017
1 parent e574490 commit a8a1863
Show file tree
Hide file tree
Showing 38 changed files with 408 additions and 109 deletions.
10 changes: 7 additions & 3 deletions mezzanine/blog/feeds.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand Down
4 changes: 3 additions & 1 deletion mezzanine/blog/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 5 additions & 2 deletions mezzanine/blog/management/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
1 change: 1 addition & 0 deletions mezzanine/blog/migrations/0001_initial.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
35 changes: 32 additions & 3 deletions mezzanine/blog/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand All @@ -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",)
Expand Down Expand Up @@ -64,16 +80,29 @@ 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",)

@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'
4 changes: 3 additions & 1 deletion mezzanine/blog/templatetags/blog_tags.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
12 changes: 9 additions & 3 deletions mezzanine/blog/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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):
"""
Expand Down
2 changes: 2 additions & 0 deletions mezzanine/blog/translation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
4 changes: 3 additions & 1 deletion mezzanine/blog/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
3 changes: 2 additions & 1 deletion mezzanine/boot/lazy_admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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"))
Expand Down
3 changes: 2 additions & 1 deletion mezzanine/core/defaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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",),
Expand Down
3 changes: 2 additions & 1 deletion mezzanine/core/management/commands/createdb.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
8 changes: 4 additions & 4 deletions mezzanine/core/sitemaps.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
31 changes: 15 additions & 16 deletions mezzanine/core/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -59,8 +65,7 @@ def test_tagcloser(self):
self.assertEqual(TagCloser("Line break<br>").html,
"Line break<br>")

@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):
"""
Expand Down Expand Up @@ -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
Expand All @@ -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.
Expand All @@ -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.
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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):
"""
Expand Down Expand Up @@ -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):
Expand Down
8 changes: 5 additions & 3 deletions mezzanine/core/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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
Expand Down
Loading

0 comments on commit a8a1863

Please sign in to comment.