MEZZaTHEMing Part 4: To the blog, and beyond

This is the fourth and final part of my tutorial series on creating Mezzanine themes. Throughout the tutorial I have been going over the process of taking static html and using it to develop a Mezzanine theme. I've been working with the html template that is available here, but the methods I discuss could be used to develop a Mezzanine theme based on a PSD to html conversion, or any other number of potential sources of styled html. The first post went over the process of creating base.html which is the foundation of the rest of a Mezzanine theme. Part two taught you to take that foundation and create a backend editable page to be your site's home page. The third post described how to make templates DRY and applied those principals to styling Mezzanine's default pages, including a custom design for the gallery. This post will focus on styling the blog and creating more custom content types, adding Portfolio capabilities to our theme.

I don't think I have any philosophizing, contemplating or other rubbish today, let's jump right to it.

The blog

First up, copy the blog templates into your theme app:

$ python manage.py collecttemplates mezzanine.blog
$ mv templates/blog/ theme/templates/

I generally don't use collecttemplates to copy all of an apps templates because it usually ends up copying a lot of templates that I don't want. In the case of the blog I know from experience that it is only going to copy three templates and that I almost certainly will be editing all three of them.

This is a good time to add some sample blog posts to your dev site (if you haven't set BLOG_USE_FEATURED_IMAGE to True in your settings.py do that now).

This site is a great resource for generating filler text.

blog_post_list.html

With some test blog posts up, head over to the blog section of your development site. It doesn't look bad, but it doesn't look like the template we are working with. Open the downloaded theme's blog.html in both a browser and a text editor and open the newly copied blog_post_list.html in an editor.

Scroll down in the downloaded template to about line 80 where you see <!--page_container-->. As you know if you've read my other posts, this is the main content area and the way we have designed our base.html the blog will easily fit in. There are no extra css classes or markup (except the wrapping <section> which I will remove since it seems to serve no practical purpose) so we can work directly with our main and right_panel blocks. After reviewing the downloaded template I see that each post is a <div class="post"> directly inside the span8 (our main block). I copy one of the post divs and paste it into blog_post_list.html, directly after {% for blog_post in blog_posts.object_list %} inside {% block main %}. I now have:

{% for blog_post in blog_posts.object_list %}
<div class="post">
    <h2 class="title"><span><a href="blog_post.html">Lorem Ipsum is simply dummy text</a></span></h2>
    <img src="img/blog/1.jpg" alt="" />
    <div class="post_info">
        <div class="fleft">On <span>12 Nov 2020</span> / By <a href="#">John Smith</a> / Tags <a href="#">Works</a>, <a href="#">Personal</a></div>
        <div class="fright"><a href="#">25</a> Comments</div>                                    
        <div class="clear"></div>
    </div>
    <p>[redacted because it is a lot filler text]...</p>
    <a href="blog_post.html" class="arrow_link">Read more</a>
</div>
... all the reset of the default Mezzanine markup

From there I cut and paste the default code into the appropriate sections of the new markup, deleting any items that don't apply.

Whether or not you want to keep all of the blocks defined in blog_post_list.html, like {% block blog_post_list_post_title %} is up to you. If you will make use of them, then certainly do, but if not they are not necessary.

I want to correctly size the thumbnail for a blog post's featured image, so I inspect one of the downloaded templates images using firebug. Besides that nothing is really tricky, just a lot of copying, pasting, making sure “ifs” are properly closed and paying attention to detail. After hooking everything up the main block of my blog_post_list.html (the only block I've edited) looks like this:

{% block main %}
{% if tag or category or year or month or author %}
    {% block blog_post_list_filterinfo %}
    <p>
    {% if tag %}
        {% trans "Viewing posts tagged" %} {{ tag }}
    {% else %}{% if category %}
        {% trans "Viewing posts for the category" %} {{ category }}
    {% else %}{% if year or month %}
        {% trans "Viewing posts from" %} {% if month %}{{ month }}, {% endif %}
        {{ year }}
    {% else %}{% if author %}
        {% trans "Viewing posts by" %}
        {{ author.get_full_name|default:author.username }}
    {% endif %}{% endif %}{% endif %}{% endif %}
    {% endblock %}
    </p>
{% else %}
    {% if page %}
    {% block blog_post_list_pagecontent %}
    {% editable page.richtextpage.content %}
    {{ page.richtextpage.content|richtext_filters|safe }}
    {% endeditable %}
    {% endblock %}
    {% endif %}
{% endif %}

{% for blog_post in blog_posts.object_list %}
<div class="post">
    {% block blog_post_list_post_title %}
    {% editable blog_post.title %}
    <h2 class="title"><span><a href="{{ blog_post.get_absolute_url }}">{{ blog_post.title }}</a></span></h2>
    {% endeditable %}
    {% endblock %}


    {% if settings.BLOG_USE_FEATURED_IMAGE and blog_post.featured_image %}
    {% block blog_post_list_post_featured_image %}
    <a href="{{ blog_post.get_absolute_url }}">
        <img src="{{ MEDIA_URL }}{% thumbnail blog_post.featured_image 770 0 %}">
    </a>
    {% endblock %}
    {% endif %}

    {% block blog_post_list_post_metainfo %}
    {% editable blog_post.publish_date %}
    <div class="post_info">
        <div class="fleft">
            {% blocktrans with sometime=blog_post.publish_date|timesince %}{{ sometime }} ago{% endblocktrans %} /
            {% with blog_post.user as author %}
            By <a href="{% url "blog_post_list_author" author %}">{{ author.get_full_name|default:author.username }}</a>
            {% endwith %}

            {% keywords_for blog_post as tags %}
            {% if tags %}
            /
            {% trans "Tags" %}:
            {% for tag in tags %}
            <a href="{% url "blog_post_list_tag" tag.slug %}" class="tag">{{ tag }}</a>{% if not forloop.last %}, {% endif %}
            {% endfor %}
            {% endif %}

            {% with blog_post.categories.all as categories %}
            {% if categories %}
            /
            {% trans "Categories" %}:
            {% for category in categories %}
            <a href="{% url "blog_post_list_category" category.slug %}">{{ category }}</a>{% if not forloop.last %}, {% endif %}
            {% endfor %}
            {% endif %}
            {% endwith %}
        </div>
        <div class="fright">
            {% if settings.COMMENTS_DISQUS_SHORTNAME %}
            <a href="{{ blog_post.get_absolute_url }}#disqus_thread"
                data-disqus-identifier="{% disqus_id_for blog_post %}">
                {% trans "Comments" %}
            </a>
            {% else %}
            <a href="{{ blog_post.get_absolute_url }}#comments">
                {% blocktrans count comments_count=blog_post.comments_count %}{{ comments_count }} comment{% plural %}{{ comments_count }} comments{% endblocktrans %}
            </a>
            {% endif %}
        </div>                                    
        <div class="clear"></div>
    </div>
    {% endeditable %}
    {% endblock %}


    {% block blog_post_list_post_content %}
    {% editable blog_post.content %}
    {{ blog_post.description_from_content|safe }}
    {% endeditable %}
    {% endblock %}    
    <a href="{{ blog_post.get_absolute_url }}" class="arrow_link">Read more</a>
</div>
{% endfor %}

{% pagination_for blog_posts %}

{% if settings.COMMENTS_DISQUS_SHORTNAME %}
{% include "generic/includes/disqus_counts.html" %}
{% endif %}
{% endblock %}

filter_panel.html

Next I'll move on to the the filter panel; its Django template should be at theme/templates/blog/includes/filter_panel.html. Scrolling down through the downloaded template I see that the markup for the sidebar starts around line 137 with the <div class="span4">. The whole sidebar is wrapped in another div with a class of sidebar so let's add that to the top of filter_panel.html and close it at the bottom.

I notice that each section of the templates filter panel is wrapped in a <div class="widget"> and the titles of the section are wrapped in <h2 class="title"><span>. The default filter panel has no wrappers and section headers are h3s. Using “find” and “replace all” it's easy to update every <h3> to <div class="widget"><h2 class="title"><span> and then update every </h3> to </span></h2>. Notice that I didn't close out the widget div: this is because it wraps all the content of a section, not just a title. You will need to go through and manually close out those divs.

After that your filter panel will be looking fairly close to the template. There are some additional sections that Mezzanine doesn't have by default (like a text widget), and you can add or remove those as you like. Some of the sections (tags and recent posts for example) have different markup so if you like you can go through and update them to match.

blog_post_detail.html

The filter panel is done, let's move onto individual blog posts. Open blog_post_detail.html from your theme and blog_post.html from the downloaded template. Find the main content section of the downloaded template and copy the post content like before and paste it into your theme's template. Then update it all to be driven by Mezzanine like you did with the blog list. A lot of the work that you did for the blog post list can be reused because the markup is virtually identical. Mezzanine has a lot of features built into the template including share buttons, ratings, etc…. You can remove those or leave them as you see fit. If you include the ratings it's probably a good idea to copy Mezzanine's ratings css into the theme's stylesheet.

The last part of the blog post detail is the comments section. The template we downloaded does include a design for comments. I'm not going to go into styling comments here but if you do want to give it a shot the Mezzanine templates you would need to modify are generic/includes/comment.html and generic/includes/comments.html. You could also just copy the default Mezzanine css for comments (like I suggested you do above for ratings). The last option for comments, which I have been using lately, is to set up Disqus comments. It's incredibly easy to do with Mezzanine. Login to Disqus, set up the site on Disqus, then go to Site -> Settings in your Mezzanine site's admin and fill in the Disqus shortname setting. There are some other Disqus related setting but they are not needed to get comments working. A potential downside of Disqus is that if Disqus goes down, your comments will not work.

Your blog is now styled and matches the downloaded template. It's time to move on!

...and beyond

The downloaded template includes a portfolio page as well as individual portfolio items. We used parts of the portfolio page to style our gallery, but nothing in Mezzanine maps perfectly to the portfolio. First let's open portfolio_4columns.html and single_portfolio.html in our browser (you could work with one of the pages that has a different number of columns) and figure out some requirements.

Portfolio

This page just shows a bunch of thumbnails with titles and descriptions that are filterable based on categories. All of the content seems to be driven by the portfolio items that it "owns". I will add an optional rich content block. It also appears that the only difference between the different number of column templates is the span size of the individual items so I will add a field to allow selecting how many columns a portfolio should have.

Portfolio Item

The portfolio items have an image slideshow, two content boxes and a back to portfolio button. Based on the portfolio page we are also going to need to create categories (which we should display) and it would be nice to add an optional view project button that would link externally somewhere.

To fit the requirements laid out above I come up with these models:

# these will map to spans
COLUMNS_CHOICES = (
    ('6', 'Two columns'), # two columns use span6
    ('4', 'Three columns'), # three columns use span4
    ('3', 'Four Columns'), # four columns use span3
)


class Portfolio(Page):
    '''
    A collection of individual portfolio items
    '''
    content = RichTextField(blank=True)
    columns = models.CharField(max_length=1, choices=COLUMNS_CHOICES,
        default='3')
    class Meta:
        verbose_name = _("Portfolio")
        verbose_name_plural = _("Portfolios")


class PortfolioItem(Page, RichText):
    '''
    An individual portfolio item, should be nested under a Portfolio
    '''
    featured_image = FileField(verbose_name=_("Featured Image"),
        upload_to=upload_to("theme.PortfolioItem.featured_image", "portfolio"),
        format="Image", max_length=255, null=True, blank=True)
    short_description = RichTextField(blank=True)
    categories = models.ManyToManyField("PortfolioItemCategory",
                                        verbose_name=_("Categories"),
                                        blank=True,
                                        related_name="portfolioitems")
    href = models.CharField(max_length=2000, blank=True,
        help_text="A link to the finished project (optional)")

    class Meta:
        verbose_name = _("Portfolio item")
        verbose_name_plural = _("Portfolio items")


class PortfolioItemImage(Orderable):
    '''
    An image for a PortfolioItem
    '''
    portfolioitem = models.ForeignKey(PortfolioItem, related_name="images")
    file = FileField(_("File"), max_length=200, format="Image",
        upload_to=upload_to("theme.PortfolioItemImage.file", "portfolio items"))

    class Meta:
        verbose_name = _("Image")
        verbose_name_plural = _("Images")


class PortfolioItemCategory(Slugged):
    """
    A category for grouping portfolio items into a series.
    """

    class Meta:
        verbose_name = _("Portfolio Item Category")
        verbose_name_plural = _("Portfolio Item Categories")
        ordering = ("title",)

Keep in mind that you have already created a stub of the Portfolio model when we created the HomePage. Be sure to replace it, we don't want to define it twice.

Let's migrate our models:

$ python manage.py schemamigration theme --auto
$ python manage.py migrate theme

Next we need to update our theme's admin.py to have the new models show in the admin. Import the newly created models and add:

class PortfolioItemImageInline(TabularDynamicInlineAdmin):
    model = PortfolioItemImage


class PortfolioItemAdmin(PageAdmin):
    inlines = (PortfolioItemImageInline,)

admin.site.register(PortfolioItem, PortfolioItemAdmin)
admin.site.register(PortfolioItemCategory)

We now need to create two new templates, pages/portfolio.html and pages/portfolioitem.html. I'm not going to go into detail about those here, the techniques that you have learned throughout these tutorials should enable you to create these. If you get really stuck and have specific questions please do you use the comments to ask.

One more freebie, I set up page processors for the Portfolio and PortfolioItem page types, they look like this:

from mezzanine.pages.page_processors import processor_for
from .models import Portfolio, PortfolioItem, PortfolioItemCategory


@processor_for(Portfolio)
def portfolio_processor(request, page):
    '''
    Adds a portfolio's portfolio items to the context
    '''
    # get the Portfolio's items, prefetching categories for performance
    items = PortfolioItem.objects.published(
        for_user=request.user).prefetch_related('categories')
    items = items.filter(parent=page)
    # filter out only cateogries that are user in the Portfolio's items
    categories = PortfolioItemCategory.objects.filter(
        portfolioitems__in=items).distinct()
    return {'items': items, 'categories': categories}


@processor_for(PortfolioItem)
def portfolioitem_processor(request, page):
    '''
    Adds a portfolio's portfolio items to the context
    '''
    portfolioitem = PortfolioItem.objects.published(
        for_user=request.user).prefetch_related(
        'categories', 'images').get(id=page.portfolioitem.id)
    return {'portfolioitem': portfolioitem}

The last thing to do for portfolios is go back to your homepage, you remember making that right, and display items from the selected portfolio. You could create a page processor for the HomePage to put the right portfolio items in the context. It might look like this:

@processor_for(HomePage)
def home_processor(request, page):
    items = PortfolioItem.objects.published(
        for_user=request.user).prefetch_related('categories')
    items = items.filter(parent=page.homepage.featured_portfolio)
    return {'items': items}

You could also make a templatetag to do it. In either case we are done with portfolios and finished creating our Mezzanine theme!

Thanks for reading and I hope these tutorials have been helpful. If you made it all the way through thanks for sticking with it and congratulations! Sorry if my style of writing was inconsistent across the posts, I haven't blogged much till recently and I'm still figuring out what works for me. If you enjoyed the posts, show some love and follow me on Twitter. Sound off in the comments below with your own tips, errors you noticed, questions or anything else you want to say. Lucid is available on MEZZaTHEME so check it out!

Comments