In this tutorial, we will:
We will assume you have Merengue installed and that you want to create a Merengue CMS website (check out the merengueprojectorg directory in the repository for a working example).
Let’s start the project:
$ merengue-admin.py startproject merengueprojectorg
This command will create a merengueprojectorg directory, containing these files and directories:
.
|-- ...
|-- settings.py
|-- urls.py
|-- apps/
| `-- website/
|-- fixtures/
|-- locale/
|-- media/
| |-- themes/
| | |-- ...
| | |-- merengue/
| | `-- yaco/
| `-- tinyimages/
|-- merengue/
| |-- ...
|-- plugins/
| |-- ...
`-- templates/
|-- themes/
| |-- ...
| |-- merengue/
| `-- yaco/
`-- website/
Here is a brief description of the files and directories:
The project settings are in the merengueprojectorg/settings.py file, which should be familiar to Django users.
First of all, you need to define your database connection (see database setup in the Django tutorial). Let's assume you are using SQLite (you will also need to install pysqlite) to keep things simple.
DATABASE = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': 'merengueprojectorg.db',
'USER': '',
'PASSWORD': '',
'HOST': '',
'PORT': '',
}
}
Other settings you should define:
ADMINS = (
('Your Name', 'youremail@yourdomain.org'), # put your name and email
)
$ python manage.py syncdb --migrate
Note
This is a django-south command, and is equivalent to executing python manage.py syncdb and then python manage.py migrate.
If there is a problem with this command, please create a ticket in Merengue trac. Please specify your database engine and the version of Merengue you are using. To continue this tutorial without problems, try this quick workaround:
After the project settings are defined and the database created, you can run a development server and see what the default Merengue theme looks like. We hope you will like it!:
$ python manage.py runserver
Django version 1.3, using settings 'merengueprojectorg.settings'
Development server is running at http://127.0.0.1:8000/
Quit the server with CONTROL-C.
Now open your favourite web browser, and point it to http://localhost:8000 . You should see something like this:
For more information about using Merengue, you can read our comprehensive user guide.
In Merengue, the preferred method of changing the look and feel is to create a theme.
First, you need to create both a templates and a media directory for the merengueprojectorg theme:
$ cd merengueprojectorg
$ mkdir templates/themes/merengueprojectorg
$ mkdir media/themes/merengueprojectorg
$ mkdir media/themes/merengueprojectorg/img
$ mkdir media/themes/merengueprojectorg/css
If you go to the Merengue admin application (open http://localhost:8000/admin/ in your browser and use admin/admin as credentials), you will see that Merengue has registered merengueprojectorg as a new theme:
If you activate the merengueprojectorg theme you will see a blank theme (without CSS styles) since we didn't put anything yet in the directories created above:
You can now add stylesheets to define the look and feel. There are several ways to accomplish this but one of the easiest is to create a base.html template in the templates/themes/merengueprojectorg/ directory and then hook your stylesheets from there. But first we will see how this template works by writing our very own hello world template:
<h1>Hello world!</h1>
If you refresh your browser page, you will see a big Hello world! greeting. No tutorial is complete until there is a hello world example, right?
But wait a second... how is this template loaded? Let's talk a little bit about Merengue theming internals... Merengue implements a new Django template loader, so when some part of the code tries to load a template (in a render_to_response call or in an extends or include tag) Merengue will look first into the template directory of the current/active theme.
By placing the base.html template in the merengueprojectorg theme, you are overriding merengue/base/templates/base.html, which extends itself from merengue/base/templates/base/layout.html. That's the reason you see a huge hello world instead of the normal Merengue layout. This pattern is repeated all over during the development cycle of a theme so you better get used to it, huh?
You may be thinking, why this double template inheritance? and I'm glad you asked. The answer is: we usually need to reuse some of the Merengue layout stuff, and if this code would have been located in merengue/base/templates/base.html we could not have extended this template from your theme. In fact, if you try to extend base.html inside your theme base.html template you will get a nice infinite recursion error.
Let's reuse the Merengue base layout in your base.html template:
{% extends "base/layout.html" %}
{% block extrastyles %}
<link href="{{ THEME_MEDIA_URL }}css/layout.css" rel="stylesheet" type="text/css" />
{% endblock %}
Now if you refresh your browser window, you will again see the Merengue default web layout.
Note
THEME_MEDIA_URL will be replaced with the relative media location of the active theme in Merengue admin. Using this context variable instead of hardcoding /media/themes/merengueprojectorg/css/layout.css or {{ MEDIA_URL }}themes/merengueprojectorg/css/layout.css is considered a best practice.
Of course, you need to create the layout.css CSS file, that will be in media/themes/merengueprojectorg/css/ directory, to begin customizing the look and feel of your site.
Next, we will add a couple of images, header_bg.jpg and header_logo.jpg, into the media/themes/merengueprojectorg/img directory, to begin the header layout.
After this, we'll write some CSS rules in the layout.css file. Listed below, is the fragment of this file in which we put the header images and other related stuff:
html {
background-color: #BCB9AD;
}
body {
font: 12px Helvetica, sans-serif;
color: #717171;
}
a {
color: #cfa570;
text-decoration: none;
}
#container {
margin: 0 auto;
padding-bottom: 100px;
width: 960px;
}
#header {
background: url(../img/header_bg.jpg) no-repeat top center;
}
#headerlogo {
height: 98px;
background: url(../img/header_logo.jpg) no-repeat top left;
}
#content {
float: left;
width: 470px;
padding: 0 10px;
}
body div#content-zone {
width: auto;
}
/* more stuff below ... */
If you have done this correctly, then refresh your browser window and you should see a page similar to this screenshot:
Next, we will add a Django based CMS framework for perfectionists with deadlines phrase into header. Feel free to change the wording if you do not consider yourself a perfectionist :-). If you take a look at merengue/base/templates/base/layout.html template, you will find a fragment similar to this:
# more stuff ...
<div id="headerlogo">
{% block headerlogo %}
{% include "inc.headerlogo.html" %}
{% endblock %}
</div>
# more stuff ...
There are two ways to include text near the logo:
In this example, we are going to use the second option. Create a inc.headerlogo.html file in the templates/themes/merengueprojectorg directory with the following content:
<a href="{% url website.views.index %}">
<img src="{{ THEME_MEDIA_URL }}img/header_logo.jpg"/>
</a>
<div class="logophrase">
Django based CMS framework for perfectionists with deadlines
</div>
Finally, add some CSS for the logo phrase:
.logophrase {
font-family: 'HelveticaNeue-UltraLight','Helvetica Neue UltraLight','Helvetica Neue',Arial,Helvetica,sans-serif;
font-weight: 100;
float: right;
font-size: 15px;
padding-top: 60px;
padding-right: 20px;
color: #b3a99d;
}
You should see now a page header with a logo and a phrase to the right:
To see a finished version of the www.merengueproject.org theme, you have to:
More Information
For more information about Merengue theming, you can read working with themes
After finishing the merengueprojectorg theme, you should get an index page like this:
However, the www.merengueproject.org website looks different:
By comparing the two screenshots above, you can see that there is more work to do:
All of the non-programming tasks can be handled in the Merengue admin interface. For more information about how to do this, please read the Merengue user guide
When a Merengue project is created with the startproject command, a website application is created inside the apps directory. This is a skeleton application useful for holding specific project views that do not fit into any plugin.
One example would be the index page. In fact, the website app comes with a default index page view already implemented and included in the root urls.py. This is the default implementation:
from plugins.core.config import PluginConfig as CoreConfig
from merengue.base.models import BaseContent
def index(request):
""" Index page. You can override this as you like """
core_config = CoreConfig().get_config()
main_content_index = core_config['home_initial_content'].get_value()
content = BaseContent.objects.get(pk=main_content_index)
return render_to_response('website/index.html',
{'content': content},
context_instance=RequestContext(request))
By default, this implementation takes some initial content and shows it, rendering a website/index.html template. Let's explain this code fragment line by line:
We do not need to change anything in this view for the www.merengueproject.org index, but we need to change several things in the template.
The template website/index.html has the following default content:
{% extends "content_view.html" %}
{% load block_tags %}
{% block aftercontentbody %}
{% render_blocks "homepage" %}
{% endblock %}
This template extends content_view.html, the generic Merengue template used to show content in a detail view. This base template extends others, in the following order:
content.html and base.html are placeholder templates that only extend base/content.html and base/layout.html respectively. Why are these templates only placeholders and have no unique content of their own? Because they will likely be overriden in the active theme -- custom templates may extend the base/content.html or base/layout.html files (see the infinite recursion problem described earlier).
The only change we need to do in the default index template base/content.html is to render all of the visual blocks placed in a homepage place. See the blocks documentation for more information. The aftercontentbody block is defined in the base/content.html template and is rendered just after the content data.
More Information
We recommend that you explore both the base/content.html and base/layout.html files in order to understand Merengue layout, blocks, actions, etc.
Now we need to remove the breadcrumbs and position an image close to the main content text.
The new template will look like this (including comments to explain new fragments):
{% extends "content_view.html" %}
{% load block_tags %}
{% block breadcrumbswrapper %}{# remove breadcrumbs in the homepage #}{% endblock %}
{% block contentdescription %}
{# put image with description #}
<div class="contentDescriptionIndex">
<img alt="Merengue demo" src="{{ THEME_MEDIA_URL }}img/preview_merengue.jpg" />
{{ content.description|safe }}
</div>
{% endblock %}
{% block aftercontentbody %}
{% render_blocks "homepage" %}
{% endblock %}
Merengue plugins are installable components that add some functionality or change existing Merengue features. See the Plugins documentation for more information.
In this tutorial, we will implement a features plugin that:
Note
If you want to inspect the finished features plugin, you can browse the features plugin code in our Trac system.
A plugin is a Django application with some restrictions:
So, to build the simplest plugin possible, we do the following:
$ cd merengueprojectorg/plugins
$ mkdir features
$ touch features/__init__.py
$ touch features/models.py
$ touch features/config.py
Then, put the following code in plugins/features/config.py (the plugin configuration file):
from merengue.pluggable import Plugin
class PluginConfig(Plugin):
name = 'Features'
description = 'Features Display Plugin'
version = '0.0.1'
This fragment only sets the plugin metadata, without changing functionality. Our features plugin is still a very simple plugin.
Merengue should now recognize this python module as a plugin:
Before installing the plugin, we should create its models:
from django.db import models
from django.utils.translation import ugettext_lazy as _
from merengue.base.models import BaseContent, BaseCategory
from merengue.base.managers import BaseContentManager
class FeatureCategory(BaseCategory):
pass
class Feature(BaseContent):
icon = models.ImageField(verbose_name=_('icon'), upload_to='feature_icons',
null=True, blank=True)
show_in_home = models.BooleanField(verbose_name=_('show in home'),
null=False)
categories = models.ManyToManyField(FeatureCategory,
verbose_name=_('category'),
blank=True, null=True, db_index=True)
objects = BaseContentManager()
BaseCategory is the base model for all categorizations, and BaseContent will be a non-abstract model (see Django model inheritance) for all managed content. If your model inherits from BaseContent it will have all of the Merengue features related to content management for free. BaseContentManager is a special manager with a lot of cool query methods, like getting content by workflow status.
These models will be created automatically when you install the plugin in the admin site. Please, do it now before continuing further. Then, let's check the tables created in our sqlite3 database:
$ python manage.py dbshell
sqlite> .tables feature
features_feature features_featurecategory
features_feature_categories
We can now play with features and feature categories using the Django ORM as follows:
$ python manage.py shell
>>> from plugins.features.models import Feature, FeatureCategory
>>> category = FeatureCategory.objects.create(name_en='Look and feel')
>>> feature = Feature.objects.create(name_en='Skinnable')
>>> feature.categories.add(category)
>>> Feature.objects.published() # testing workflow
[]
>>> feature.status
u'draft'
>>> feature.status = 'published'
>>> feature.save()
>>> Feature.objects.published()
[<Feature: Skinnable>]
>>> feature.name_es = 'Aspecto cambiable' # testing model translation
>>> feature.save()
>>> feature.name # 'name' attribute will retrieve name_en
'Skinnable'
>>> from django.utils.translation import activate
>>> activate('es')
>>> feature.name # 'name' attribute now get 'name_es'
'Aspecto cambiable'
The features, as we said previously, will be managed content, like documents, news items, etc. Let's check:
>>> from merengue.base.models import BaseContent
>>> BaseContent.objects.all()
[<BaseContent: Skinnable>, <BaseContent: Welcome to Merengue>]
>>> BaseContent.objects.all().values('class_name', 'name_en')
[{'class_name': u'feature', 'name_en': u'Skinnable'}, {'class_name': u'document', 'name_en': u'Welcome to Merengue'}]
To enable these models in the Merengue admin site, you will need to define a plugin admin section implementing the get_model_admins method in config.py:
from plugins.features.admin import FeatureAdmin, FeatureCategoryAdmin
from plugins.features.models import Feature, FeatureCategory
class PluginConfig(Plugin):
# stuff ...
@classmethod
def get_model_admins(cls):
return [(Feature, FeatureAdmin),
(FeatureCategory, FeatureCategoryAdmin)]
Both FeatureAdmin and FeatureCategoryAdmin are Django model admins.
Of course, as you can see, these ModelAdmin classes are defined in the plugins.features.admin module. This implies that there is an admin.py file inside the plugin package with this content:
from merengue.base.admin import BaseContentAdmin, BaseCategoryAdmin
from plugins.features.models import Feature, FeatureCategory
class FeatureCategoryAdmin(BaseCategoryAdmin):
pass
class FeatureAdmin(BaseContentAdmin):
pass
Both BaseCategoryAdmin and BaseContentAdmin are Django ModelAdmin subclasses, but with extra features. You can customize the usual ModelAdmin options and add new ones. See merengue.base.admin module for more information.
Now, if you access the admin site, you will see a Features plugin admin site:
If you click on it, you will access both the FeatureAdmin and FeatureCategoryAdmin:
Now, you can add features via the admin site.
This section is about creating new blocks with Merengue. The blocks in Merengue can be placed in various locations. See:ref:Merengue blocks <topics-blocks> for more information about blocks.
We want to implement a features block to render some Merengue features (from the Feature model). This is shown in next screenshot:
To create a Merengue block your code must extend the merengue.block.blocks.Block class and implement the render class method. You can see an example of a features block implementation below:
from merengue.block.blocks import Block
from plugins.features.models import Feature
class FeatureBlock(Block):
name = 'mainfeatures'
default_place = 'homepage'
@classmethod
def render(cls, request, place, context):
features_list = Feature.objects.published().filter(show_in_home=True)
return cls.render_block(request, template_name='features/block_mainfeatures.html',
block_title='Some features',
context={'features_list': features_list})
Usually block code lives in a blocks.py file inside your plugin directory. We will explain the most significant lines of code:
Now, the only thing to do is to create the features/block_mainfeatures.html template:
The content of this template is as follows:
{% extends "block.html" %}
{% block blocktitle %}
<h1>Merengue main features</h1>
{% endblock %}
{% block blockbody %}
<div id="features">
{% for feature in features_list %}
<div>
{% if feature.icon %}
<img src="{{ feature.icon.url }}"/>
{% endif %}
<h2 title="{{ feature }}"><a href="{{ feature.get_absolute_url }}">{{ feature }}</a></h2>
<div class="description">
{{ feature.description|safe|truncatewords_html:20 }}
</div>
</div>
{% endfor %}
</div>
{% endblock %}
This template is self-explanatory. It is important to note that there is a base template for blocks (named block.html) that should be used in your blocks in order to maintain a similar look and feel (as was done in the previous example).
Now the features block is finished. You will need to register it in the plugin itself to inform Merengue that this block exists. This is done inside the plugin config:
from plugins.features.blocks import FeatureBlock
class PluginConfig(Plugin):
# old stuff ...
@classmethod
def get_blocks(cls):
return [FeatureBlock]
Since the features plugin is already installed and activated, the new block is not registered unless we deactivate and reactivate the plugin again in the admin interface.
If you look in the admin screen at the list of blocks (after you reactivate the block), you will see a new block registered:
As you can see, the block is "placed at", by default, on the homepage because we defined this in our block. You can change this location in the blocks.py file, or by using the Merengue block reordering interface.
Now, you can create one or two features, adding text and also an icon, if you want. You will then need to publish those features and mark the show in home flag in order to see them in our features block. After that, if you go to home page, you will see something like this:
Note
You may have noticed that the title of the block Merengue main features is not shown on the homepage. The reason being that it is hidden using CSS in the merengueprojectorg theme.
The last thing you will do in creating the feature plugin is define two views:
The first step consists of registering a URL namespace for your plugin, adding a url_prefixes attribute in the PluginConfig class like this:
from merengue.pluggable import Plugin
# ...
class PluginConfig(Plugin):
# ...
url_prefixes = (
('features', 'plugins.features.urls'),
)
# ...
The code above reserves a /features prefixe in the URLs to be handled by our plugin, in the plugins.features.urls module. Of course, you can add several URL namespaces in each individual plugin, if necessary.
Now, if we try to access the development server, we will get an error, because the urls module and views are not implemented.
So, next, we create an urls.py file inside your plugin, with content just like a normal Django urls module:
from django.conf.urls.defaults import patterns, url
urlpatterns = patterns('plugins.features.views',
url(r'^$', 'features_index', name='features_index'),
url(r'^(?P<features_slug>[\w-]+)/$', 'features_view', name='features_view'),
)
features_index and features_view will be the listing and detail views, respectively, of the Merengue feature plugin. Again, you will get an error if you try to access the development web server, because these views are not implemented. You need to create a views.py file with content similar to this:
from django.shortcuts import get_object_or_404
from merengue.base.views import content_view, content_list
from plugins.features.models import Feature, FeatureCategory
def features_index(request):
categories_list = FeatureCategory.objects.all()
features = Feature.objects.published()
return content_list(request, features,
template_name='features/features_index.html',
paginate_by=12,
extra_context={'categories_list': categories_list})
def features_view(request, features_slug):
features_view = get_object_or_404(Feature, slug=features_slug)
return content_view(request, features_view,
template_name='features/features_view.html')
These two views are typical of views used in the Django world. The only thing to note is the use of the content_list and content_view views from Merengue. The usage of these views (available within Merengue) allows us to reuse code (although their usage is optional).
The last thing to do is to implement the features/features_index.html and features/features_view.html templates. These two templates should be placed in a templates/features directory inside your plugin directory, and never in your theme, because they deal with the content itself, not the appearance of that content.
The features/features_index.html template would look as follows:
{% extends "content_list.html" %}
{% load i18n inlinetrans %}
{% block extrabreadcrumbs %}
<span class="breadcrumbSeparator">→</span> <a href="{% url features_index %}" title="{% trans "Features" %}">{% inline_trans "Features" %}</a>
{% endblock %}
{% block listtitle %}{% trans "Features index" %}{% endblock %}
{% block listing %}
<ul class="contentList">
{% for feature in content_list %}
<li>
<a href="{{ feature.get_absolute_url }}" title="{{ feature }}">
{{ feature }}
</a>
</li>
{% endfor %}
</ul>
{% endblock %}
Additional notes:
The resulting view, if you access the /features/ URL, looks something like this:
If you click on any link to the feature you will see that it works, even if you do not specifically request the feature detail view from Merengue. The reason for this being that the Feature model inherits the BaseContent model which has a default implementation of the URL returned in get_absolute_url.
In fact, get_absolute_url will not return the final content detail URL. It will return an intermediate URL (something like /cms/base/public_redirect/features/feature/4/) which does some checks and then redirects the browser to the URL returned in the first of these two methods implemented in the model:
The first method is tried first because sometimes you need different behaviour depending whether the user is allowed to see the content (permissions need to be checked, or a login is required). If the first method is not implemented, Merengue will call the second one (which is implemented in BaseContent model). That is how you will see a default page displayed, even if you have not implemented method #2.
But, to conform to Django best practices, we have to use features_view as our content link. In Merengue this means we implement a public_link method in our Feature model, instead of a get_absolute_url method:
from django.db.models import permalink
class Feature(BaseContent):
# ...
@permalink
def public_link(self):
return ('features_view', [self.slug])
Now, all of the features links will try to call the features_view view. Before we finish, you will need to implement the last template, features/features_view.html:
{% extends "content_view.html" %}
{% load i18n inlinetrans action_tags %}
{% block beforecontentbreadcrumbs %}
<span class="breadcrumbSeparator">→</span><a href="{% url features_index %}" title="{% trans "Features" %}">{% inline_trans "Features" %}</a>
{% endblock %}
{% block contentbody %}
Categories: {{ content.categories.all|join:"," }}
{% endblock %}
This template extends content_view.html which include some features like a multimedia viewer (you can try to add some images or videos to the feature).
The resulting view looks like this:
Note
Some parts of the templates used in the original features plugin have been ommited for readability reasons.
The tutorial ends here. At this point you should know how to begin a Merengue project, design new themes, and develop your own plugins.
May 23, 2011